Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Undefined Variable Access in ThinkPy Interpreter #33

Open
lwgray opened this issue Nov 19, 2024 · 0 comments
Open

Fix Undefined Variable Access in ThinkPy Interpreter #33

lwgray opened this issue Nov 19, 2024 · 0 comments

Comments

@lwgray
Copy link
Owner

lwgray commented Nov 19, 2024

Description

The ThinkPy interpreter currently fails silently when accessing undefined variables in certain contexts, particularly within enumerate loops. Instead of raising an error, it treats undefined variable names as string literals, leading to confusing runtime errors.

Current Behavior

for score, weight in enumerate(weights):
    total = total + (scores[index] * weight)  # 'index' is undefined but doesn't raise error

Currently produces a TypeError: list indices must be integers or slices, not str because index is treated as a string literal rather than raising an undefined variable error.

Expected Behavior

The interpreter should raise a clear RuntimeError: Undefined variable: index when attempting to access undefined variables.

Proposed Solution

Implement proper variable scoping in the interpreter:

  1. Add a scope stack to track variables in different contexts
  2. Implement scope push/pop for loops and other block structures
  3. Add proper variable lookup through the scope chain
  4. Maintain ability to handle string literals while catching undefined variables

Implementation Details

class ThinkPyInterpreter:
    def __init__(self, explain_mode=False, format_style="default", max_iterations_shown=5):
        self.state = {}  # Global variable storage
        self.scopes = []  # Stack of local scopes
        # ... rest of __init__ remains the same ...

    def push_scope(self):
        """Create a new local scope"""
        self.scopes.append({})

    def pop_scope(self):
        """Remove the current local scope"""
        if self.scopes:
            self.scopes.pop()

    def get_variable(self, name):
        """Look up a variable in the current scope chain"""
        # Check local scopes from innermost to outermost
        for scope in reversed(self.scopes):
            if name in scope:
                return scope[name]
        # Check global scope
        if name in self.state:
            return self.state[name]
        raise RuntimeError(f"Undefined variable: {name}")

    def set_variable(self, name, value, is_global=False):
        """Set a variable in the appropriate scope"""
        if is_global or not self.scopes:
            self.state[name] = value
        else:
            self.scopes[-1][name] = value

    def evaluate_expression(self, expr):
        """Evaluate an expression and return its value"""
        if isinstance(expr, (int, float, bool)):
            return expr
            
        if isinstance(expr, str):
            try:
                return self.get_variable(expr)
            except RuntimeError:
                # If it's not found as a variable, treat it as a string literal
                if expr in self.builtins:
                    return self.builtins[expr]
                return expr
            
        if isinstance(expr, dict):
            # ... rest of complex expression handling remains the same ...
            pass
        
        return expr

    def execute_enumerate_loop(self, loop_stmt):
        """Execute an enumerate loop with proper variable scoping"""
        index_var = loop_stmt['index']
        value_var = loop_stmt['element']
        iterable_name = loop_stmt['iterable']

        iterable = self.get_variable(iterable_name)
        if not hasattr(iterable, '__iter__'):
            raise RuntimeError(f"{iterable_name} is not a collection we can enumerate")
        
        self.explain_print("LOOP", f"Starting an enumerate loop over {iterable_name}")
        self.explain_print("INFO", f"Total number of items to process: {len(iterable)}")
        self.indent_level += 1

        self.push_scope()  # Create new scope for loop variables
        try:
            for i, value in enumerate(iterable):
                self.set_variable(index_var, i)
                self.set_variable(value_var, value)

                if self.explain_mode:
                    if i < self.max_iterations_shown:
                        self.explain_print("ITERATION", 
                            f"Loop #{i + 1}: {index_var} = {i}, {value_var} = {value}")
                    elif i == self.max_iterations_shown:
                        remaining = len(iterable) - self.max_iterations_shown
                        self.explain_print("INFO", 
                            f"... {remaining} more iterations will be processed ...")
                
                for statement in loop_stmt['body']:
                    result = self.execute_statement(statement)
                    if isinstance(result, dict) and result.get('type') == 'return':
                        return result
        finally:
            self.pop_scope()  # Always clean up the scope
            self.indent_level -= 1

        if self.explain_mode:
            self.explain_print("COMPLETE", f"Loop finished after processing {len(iterable)} items")

Test Cases

  1. Basic undefined variable access
  2. Enumerate loop variable scope
  3. String literal handling
  4. Built-in function access
  5. Nested scope handling

Impact

This change will improve error messaging and catch programming errors earlier in the development process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant