lwgray / think

MIT License
0 stars 1 forks source link

Fix Undefined Variable Access in ThinkPy Interpreter #33

Open lwgray opened 6 days ago

lwgray commented 6 days ago

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.