#!/usr/bin/env python3
"""
Purp Programming Language - Bootstrap Interpreter
A Python-based interpreter that can run Purp programs directly.
This allows running Purp without needing to compile the C interpreter first.

Usage:
    python purp.py                    # Start REPL
    python purp.py file.purp          # Run a file
    python purp.py --version          # Show version
"""

import sys
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Callable, Union
from enum import Enum, auto

VERSION = "1.0.0"

# ============================================================================
# TOKEN TYPES
# ============================================================================

class TokenType(Enum):
    # Literals
    NUMBER = auto()
    STRING = auto()
    TRUE = auto()
    FALSE = auto()
    NOTHING = auto()
    IDENTIFIER = auto()
    
    # Keywords
    IS = auto()
    ALWAYS = auto()
    END = auto()
    IF = auto()
    ELIF = auto()
    ELSE = auto()
    FOR = auto()
    IN = auto()
    FROM = auto()
    TO = auto()
    BY = auto()
    WHILE = auto()
    BREAK = auto()
    CONTINUE = auto()
    DEFINE = auto()
    RETURN = auto()
    CLASS = auto()
    EXTENDS = auto()
    HAS = auto()
    DOES = auto()
    STATIC = auto()
    SUPER = auto()
    NEW = auto()
    AND = auto()
    OR = auto()
    NOT = auto()
    TRY = auto()
    CATCH = auto()
    FINALLY = auto()
    RAISE = auto()
    MATCH = auto()
    WHEN = auto()
    USE = auto()
    AS = auto()
    PRIVATE = auto()
    LAMBDA = auto()
    
    # Operators
    PLUS = auto()
    MINUS = auto()
    STAR = auto()
    SLASH = auto()
    PERCENT = auto()
    CARET = auto()
    EQ = auto()
    NEQ = auto()
    LT = auto()
    GT = auto()
    LTE = auto()
    GTE = auto()
    ARROW = auto()
    PLUSEQ = auto()
    MINUSEQ = auto()
    STAREQ = auto()
    SLASHEQ = auto()
    DOTDOT = auto()
    
    # Delimiters
    LPAREN = auto()
    RPAREN = auto()
    LBRACKET = auto()
    RBRACKET = auto()
    LBRACE = auto()
    RBRACE = auto()
    COMMA = auto()
    COLON = auto()
    DOT = auto()
    NEWLINE = auto()
    
    # Special
    EOF = auto()

@dataclass
class Token:
    type: TokenType
    value: Any
    line: int
    column: int

# ============================================================================
# LEXER
# ============================================================================

KEYWORDS = {
    'is': TokenType.IS,
    'always': TokenType.ALWAYS,
    'end': TokenType.END,
    'if': TokenType.IF,
    'elif': TokenType.ELIF,
    'else': TokenType.ELSE,
    'for': TokenType.FOR,
    'in': TokenType.IN,
    'from': TokenType.FROM,
    'to': TokenType.TO,
    'by': TokenType.BY,
    'while': TokenType.WHILE,
    'break': TokenType.BREAK,
    'continue': TokenType.CONTINUE,
    'define': TokenType.DEFINE,
    'return': TokenType.RETURN,
    'class': TokenType.CLASS,
    'extends': TokenType.EXTENDS,
    'has': TokenType.HAS,
    'does': TokenType.DOES,
    'static': TokenType.STATIC,
    'super': TokenType.SUPER,
    'new': TokenType.NEW,
    'and': TokenType.AND,
    'or': TokenType.OR,
    'not': TokenType.NOT,
    'true': TokenType.TRUE,
    'false': TokenType.FALSE,
    'nothing': TokenType.NOTHING,
    'try': TokenType.TRY,
    'catch': TokenType.CATCH,
    'finally': TokenType.FINALLY,
    'raise': TokenType.RAISE,
    'match': TokenType.MATCH,
    'when': TokenType.WHEN,
    'use': TokenType.USE,
    'as': TokenType.AS,
    'private': TokenType.PRIVATE,
    'lambda': TokenType.LAMBDA,
}

class Lexer:
    def __init__(self, source: str):
        self.source = source
        self.pos = 0
        self.line = 1
        self.column = 1
        self.tokens: List[Token] = []
    
    def peek(self, offset: int = 0) -> str:
        pos = self.pos + offset
        if pos >= len(self.source):
            return '\0'
        return self.source[pos]
    
    def advance(self) -> str:
        ch = self.peek()
        self.pos += 1
        if ch == '\n':
            self.line += 1
            self.column = 1
        else:
            self.column += 1
        return ch
    
    def skip_whitespace(self):
        while self.peek() in ' \t\r':
            self.advance()
    
    def skip_comment(self):
        if self.peek() == '/' and self.peek(1) == '/':
            while self.peek() not in '\n\0':
                self.advance()
        elif self.peek() == '-' and self.peek(1) == '-':
            while self.peek() not in '\n\0':
                self.advance()
    
    def read_string(self) -> Token:
        quote = self.advance()  # consume opening quote
        start_line, start_col = self.line, self.column
        value = ""
        
        while self.peek() != quote and self.peek() != '\0':
            if self.peek() == '\\':
                self.advance()
                ch = self.advance()
                if ch == 'n': value += '\n'
                elif ch == 't': value += '\t'
                elif ch == 'r': value += '\r'
                elif ch == '\\': value += '\\'
                elif ch == quote: value += quote
                else: value += ch
            else:
                value += self.advance()
        
        if self.peek() == quote:
            self.advance()  # consume closing quote
        
        return Token(TokenType.STRING, value, start_line, start_col)
    
    def read_number(self) -> Token:
        start_line, start_col = self.line, self.column
        value = ""
        
        while self.peek().isdigit():
            value += self.advance()
        
        if self.peek() == '.' and self.peek(1).isdigit():
            value += self.advance()  # consume '.'
            while self.peek().isdigit():
                value += self.advance()
        
        return Token(TokenType.NUMBER, float(value), start_line, start_col)
    
    def read_identifier(self) -> Token:
        start_line, start_col = self.line, self.column
        value = ""
        
        while self.peek().isalnum() or self.peek() == '_':
            value += self.advance()
        
        token_type = KEYWORDS.get(value, TokenType.IDENTIFIER)
        return Token(token_type, value, start_line, start_col)
    
    def tokenize(self) -> List[Token]:
        while self.pos < len(self.source):
            self.skip_whitespace()
            self.skip_comment()
            
            if self.pos >= len(self.source):
                break
            
            ch = self.peek()
            start_line, start_col = self.line, self.column
            
            # Newline
            if ch == '\n':
                self.advance()
                self.tokens.append(Token(TokenType.NEWLINE, '\n', start_line, start_col))
                continue
            
            # Skip whitespace after comments
            if ch in ' \t\r':
                continue
            
            # End of input
            if ch == '\0':
                break
            
            # String
            if ch in '"\'':
                self.tokens.append(self.read_string())
                continue
            
            # Number
            if ch.isdigit():
                self.tokens.append(self.read_number())
                continue
            
            # Identifier or keyword
            if ch.isalpha() or ch == '_':
                self.tokens.append(self.read_identifier())
                continue
            
            # Two-character operators
            if ch == '=' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.EQ, '==', start_line, start_col))
                continue
            if ch == '!' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.NEQ, '!=', start_line, start_col))
                continue
            if ch == '<' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.LTE, '<=', start_line, start_col))
                continue
            if ch == '>' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.GTE, '>=', start_line, start_col))
                continue
            if ch == '+' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.PLUSEQ, '+=', start_line, start_col))
                continue
            if ch == '-' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.MINUSEQ, '-=', start_line, start_col))
                continue
            if ch == '*' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.STAREQ, '*=', start_line, start_col))
                continue
            if ch == '/' and self.peek(1) == '=':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.SLASHEQ, '/=', start_line, start_col))
                continue
            if ch == '-' and self.peek(1) == '>':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.ARROW, '->', start_line, start_col))
                continue
            if ch == '.' and self.peek(1) == '.':
                self.advance(); self.advance()
                self.tokens.append(Token(TokenType.DOTDOT, '..', start_line, start_col))
                continue
            
            # Single-character tokens
            single_char_tokens = {
                '+': TokenType.PLUS, '-': TokenType.MINUS,
                '*': TokenType.STAR, '/': TokenType.SLASH,
                '%': TokenType.PERCENT, '^': TokenType.CARET,
                '<': TokenType.LT, '>': TokenType.GT,
                '(': TokenType.LPAREN, ')': TokenType.RPAREN,
                '[': TokenType.LBRACKET, ']': TokenType.RBRACKET,
                '{': TokenType.LBRACE, '}': TokenType.RBRACE,
                ',': TokenType.COMMA, ':': TokenType.COLON,
                '.': TokenType.DOT,
            }
            
            if ch in single_char_tokens:
                self.advance()
                self.tokens.append(Token(single_char_tokens[ch], ch, start_line, start_col))
                continue
            
            # Unknown character - skip it
            self.advance()
        
        self.tokens.append(Token(TokenType.EOF, None, self.line, self.column))
        return self.tokens

# ============================================================================
# AST NODES
# ============================================================================

@dataclass
class NumberNode:
    value: float

@dataclass
class StringNode:
    value: str

@dataclass
class BoolNode:
    value: bool

@dataclass
class NothingNode:
    pass

@dataclass
class IdentifierNode:
    name: str

@dataclass
class BinaryNode:
    left: Any
    op: TokenType
    right: Any

@dataclass
class UnaryNode:
    op: TokenType
    operand: Any

@dataclass
class CallNode:
    callee: Any
    args: List[Any]

@dataclass
class IndexNode:
    obj: Any
    index: Any

@dataclass
class MemberNode:
    obj: Any
    member: str

@dataclass
class ListNode:
    elements: List[Any]

@dataclass
class MapNode:
    pairs: List[tuple]

@dataclass
class VarDeclNode:
    name: str
    value: Any
    is_const: bool = False

@dataclass
class AssignNode:
    target: Any
    value: Any

@dataclass
class IfNode:
    condition: Any
    then_branch: List[Any]
    elif_branches: List[tuple]
    else_branch: Optional[List[Any]]

@dataclass
class WhileNode:
    condition: Any
    body: List[Any]

@dataclass
class ForInNode:
    var_name: str
    iterable: Any
    body: List[Any]

@dataclass
class FunctionNode:
    name: str
    params: List[str]
    body: List[Any]

@dataclass
class ReturnNode:
    value: Any

@dataclass
class BreakNode:
    pass

@dataclass
class ContinueNode:
    pass

@dataclass
class ClassNode:
    name: str
    fields: List[str]
    methods: List[FunctionNode]
    parent: Optional[str] = None
    static_fields: Optional[List[tuple]] = None
    static_methods: Optional[List[FunctionNode]] = None

@dataclass
class NewNode:
    class_name: str
    args: List[Any]

@dataclass
class BlockNode:
    statements: List[Any]

@dataclass
class TryNode:
    try_body: List[Any]
    catch_var: Optional[str]
    catch_body: Optional[List[Any]]
    finally_body: Optional[List[Any]]

@dataclass
class RaiseNode:
    value: Any

@dataclass
class MatchNode:
    value: Any
    cases: List[tuple]  # List of (patterns, body)
    default_body: Optional[List[Any]]

@dataclass
class LambdaNode:
    params: List[str]
    body: Any

@dataclass
class ForRangeNode:
    var_name: str
    start: Any
    end: Any
    step: Any
    body: List[Any]

@dataclass
class CompoundAssignNode:
    target: Any
    op: TokenType
    value: Any

@dataclass
class SuperNode:
    method: Optional[str]
    args: List[Any]

@dataclass
class RangeNode:
    start: Any
    end: Any

# ============================================================================
# PARSER
# ============================================================================

class Parser:
    def __init__(self, tokens: List[Token]):
        self.tokens = tokens
        self.pos = 0
    
    def peek(self, offset: int = 0) -> Token:
        pos = self.pos + offset
        if pos >= len(self.tokens):
            return self.tokens[-1]
        return self.tokens[pos]
    
    def advance(self) -> Token:
        token = self.peek()
        if self.pos < len(self.tokens) - 1:
            self.pos += 1
        return token
    
    def check(self, *types: TokenType) -> bool:
        return self.peek().type in types
    
    def match(self, *types: TokenType) -> Optional[Token]:
        if self.check(*types):
            return self.advance()
        return None
    
    def expect(self, type: TokenType, message: str) -> Token:
        if self.check(type):
            return self.advance()
        raise SyntaxError(f"Line {self.peek().line}: {message}")
    
    def skip_newlines(self):
        while self.match(TokenType.NEWLINE):
            pass
    
    def parse(self) -> List[Any]:
        statements = []
        self.skip_newlines()
        
        while not self.check(TokenType.EOF):
            stmt = self.parse_statement()
            if stmt is not None:
                statements.append(stmt)
            self.skip_newlines()
        
        return statements
    
    def parse_statement(self) -> Any:
        self.skip_newlines()
        
        # Variable declaration: name is value
        if self.check(TokenType.IDENTIFIER) and self.peek(1).type == TokenType.IS:
            name = self.advance().value
            self.advance()  # consume 'is'
            value = self.parse_expression()
            return VarDeclNode(name, value)
        
        # Constant: name always is value
        if self.check(TokenType.IDENTIFIER) and self.peek(1).type == TokenType.ALWAYS:
            name = self.advance().value
            self.advance()  # consume 'always'
            self.expect(TokenType.IS, "Expected 'is' after 'always'")
            value = self.parse_expression()
            return VarDeclNode(name, value, is_const=True)
        
        # Compound assignment: name += value, name -= value, etc.
        if self.check(TokenType.IDENTIFIER) and self.peek(1).type in (TokenType.PLUSEQ, TokenType.MINUSEQ, TokenType.STAREQ, TokenType.SLASHEQ):
            name = IdentifierNode(self.advance().value)
            op = self.advance().type
            value = self.parse_expression()
            return CompoundAssignNode(name, op, value)
        
        # Function definition
        if self.match(TokenType.DEFINE):
            return self.parse_function()
        
        # Class definition
        if self.match(TokenType.CLASS):
            return self.parse_class()
        
        # If statement
        if self.match(TokenType.IF):
            return self.parse_if()
        
        # While loop
        if self.match(TokenType.WHILE):
            return self.parse_while()
        
        # For loop
        if self.match(TokenType.FOR):
            return self.parse_for()
        
        # Try/catch/finally
        if self.match(TokenType.TRY):
            return self.parse_try()
        
        # Raise
        if self.match(TokenType.RAISE):
            value = self.parse_expression()
            return RaiseNode(value)
        
        # Match/when
        if self.match(TokenType.MATCH):
            return self.parse_match()
        
        # Return
        if self.match(TokenType.RETURN):
            value = None
            if not self.check(TokenType.NEWLINE, TokenType.EOF, TokenType.END):
                value = self.parse_expression()
            return ReturnNode(value)
        
        # Break
        if self.match(TokenType.BREAK):
            return BreakNode()
        
        # Continue
        if self.match(TokenType.CONTINUE):
            return ContinueNode()
        
        # Expression statement (including assignments)
        return self.parse_expression_statement()
    
    def parse_expression_statement(self) -> Any:
        # Check for bare function call: identifier followed by expression (not 'is', operator, or newline)
        # But NOT if followed by LPAREN (that's a normal function call like func())
        if self.check(TokenType.IDENTIFIER):
            next_tok = self.peek(1)
            # If followed by an expression-starting token (not is, operator, newline, EOF, etc.)
            # Exclude LPAREN since that's handled by normal call expression parsing
            if next_tok.type in (TokenType.STRING, TokenType.NUMBER, TokenType.IDENTIFIER, 
                                 TokenType.TRUE, TokenType.FALSE, TokenType.NOTHING,
                                 TokenType.LBRACKET, TokenType.LBRACE,
                                 TokenType.MINUS, TokenType.NOT):
                # But not if next is "identifier is ..." (variable declaration)
                # Check if it's not a declaration or compound assign pattern
                if next_tok.type == TokenType.IDENTIFIER:
                    tok_after = self.peek(2)
                    if tok_after.type in (TokenType.IS, TokenType.ALWAYS, TokenType.PLUSEQ, 
                                           TokenType.MINUSEQ, TokenType.STAREQ, TokenType.SLASHEQ):
                        # This is "say x is y" or "say x += y", don't treat as bare call
                        pass
                    else:
                        # It's a bare call like "say x"
                        func_name = self.advance()
                        args = []
                        while not self.check(TokenType.NEWLINE, TokenType.EOF, TokenType.END):
                            args.append(self.parse_expression())
                            if not self.match(TokenType.COMMA):
                                break
                        return CallNode(IdentifierNode(func_name.value), args)
                else:
                    # It's a bare call like "say 42" or "say [1,2,3]"
                    func_name = self.advance()
                    args = []
                    while not self.check(TokenType.NEWLINE, TokenType.EOF, TokenType.END):
                        args.append(self.parse_expression())
                        if not self.match(TokenType.COMMA):
                            break
                    return CallNode(IdentifierNode(func_name.value), args)
        
        expr = self.parse_expression()
        
        # Check for assignment
        if self.match(TokenType.IS):
            value = self.parse_expression()
            return AssignNode(expr, value)
        
        return expr
    
    def parse_function(self) -> FunctionNode:
        name = self.expect(TokenType.IDENTIFIER, "Expected function name").value
        self.expect(TokenType.LPAREN, "Expected '(' after function name")
        
        params = []
        if not self.check(TokenType.RPAREN):
            params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
            while self.match(TokenType.COMMA):
                params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
        
        self.expect(TokenType.RPAREN, "Expected ')' after parameters")
        self.skip_newlines()
        
        body = self.parse_block()
        self.expect(TokenType.END, "Expected 'end' after function body")
        
        return FunctionNode(name, params, body)
    
    def parse_class(self) -> ClassNode:
        name = self.expect(TokenType.IDENTIFIER, "Expected class name").value
        
        # Check for inheritance
        parent = None
        if self.match(TokenType.EXTENDS):
            parent = self.expect(TokenType.IDENTIFIER, "Expected parent class name").value
        
        self.skip_newlines()
        
        fields = []
        methods = []
        static_fields = []
        static_methods = []
        
        while not self.check(TokenType.END, TokenType.EOF):
            self.skip_newlines()
            
            if self.check(TokenType.END):
                break
            
            if self.match(TokenType.HAS):
                # Field declaration
                fields.append(self.expect(TokenType.IDENTIFIER, "Expected field name").value)
                while self.match(TokenType.COMMA):
                    fields.append(self.expect(TokenType.IDENTIFIER, "Expected field name").value)
            elif self.match(TokenType.STATIC):
                # Static field or method
                if self.match(TokenType.HAS):
                    field_name = self.expect(TokenType.IDENTIFIER, "Expected field name").value
                    value = None
                    if self.match(TokenType.IS):
                        value = self.parse_expression()
                    static_fields.append((field_name, value))
                elif self.match(TokenType.DOES):
                    method_name = self.expect(TokenType.IDENTIFIER, "Expected method name").value
                    self.expect(TokenType.LPAREN, "Expected '(' after method name")
                    params = []
                    if not self.check(TokenType.RPAREN):
                        params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
                        while self.match(TokenType.COMMA):
                            params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
                    self.expect(TokenType.RPAREN, "Expected ')' after parameters")
                    self.skip_newlines()
                    body = self.parse_block()
                    self.expect(TokenType.END, "Expected 'end' after method body")
                    static_methods.append(FunctionNode(method_name, params, body))
            elif self.match(TokenType.DOES):
                # Method declaration
                method_name = self.expect(TokenType.IDENTIFIER, "Expected method name").value
                self.expect(TokenType.LPAREN, "Expected '(' after method name")
                
                params = []
                if not self.check(TokenType.RPAREN):
                    params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
                    while self.match(TokenType.COMMA):
                        params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
                
                self.expect(TokenType.RPAREN, "Expected ')' after parameters")
                self.skip_newlines()
                
                body = self.parse_block()
                self.expect(TokenType.END, "Expected 'end' after method body")
                
                methods.append(FunctionNode(method_name, params, body))
            else:
                self.advance()  # Skip unknown token
            
            self.skip_newlines()
        
        self.expect(TokenType.END, "Expected 'end' after class body")
        
        return ClassNode(name, fields, methods, parent, static_fields if static_fields else None, static_methods if static_methods else None)
    
    def parse_if(self) -> IfNode:
        condition = self.parse_expression()
        self.skip_newlines()
        
        then_branch = self.parse_block()
        elif_branches = []
        else_branch = None
        
        while self.match(TokenType.ELIF):
            elif_condition = self.parse_expression()
            self.skip_newlines()
            elif_body = self.parse_block()
            elif_branches.append((elif_condition, elif_body))
        
        if self.match(TokenType.ELSE):
            self.skip_newlines()
            else_branch = self.parse_block()
        
        self.expect(TokenType.END, "Expected 'end' after if statement")
        
        return IfNode(condition, then_branch, elif_branches, else_branch)
    
    def parse_while(self) -> WhileNode:
        condition = self.parse_expression()
        self.skip_newlines()
        body = self.parse_block()
        self.expect(TokenType.END, "Expected 'end' after while loop")
        return WhileNode(condition, body)
    
    def parse_for(self):
        var_name = self.expect(TokenType.IDENTIFIER, "Expected variable name").value
        
        # Check for 'from X to Y' syntax vs 'in iterable' syntax
        if self.match(TokenType.FROM):
            start = self.parse_expression()
            self.expect(TokenType.TO, "Expected 'to' in for loop")
            end = self.parse_expression()
            step = None
            if self.match(TokenType.BY):
                step = self.parse_expression()
            self.skip_newlines()
            body = self.parse_block()
            self.expect(TokenType.END, "Expected 'end' after for loop")
            return ForRangeNode(var_name, start, end, step, body)
        else:
            self.expect(TokenType.IN, "Expected 'in' or 'from' after variable name")
            iterable = self.parse_expression()
            self.skip_newlines()
            body = self.parse_block()
            self.expect(TokenType.END, "Expected 'end' after for loop")
            return ForInNode(var_name, iterable, body)
    
    def parse_try(self) -> TryNode:
        self.skip_newlines()
        try_body = self.parse_try_block()
        
        catch_var = None
        catch_body = None
        finally_body = None
        
        if self.match(TokenType.CATCH):
            if self.check(TokenType.IDENTIFIER):
                catch_var = self.advance().value
            self.skip_newlines()
            catch_body = self.parse_try_block()
        
        if self.match(TokenType.FINALLY):
            self.skip_newlines()
            finally_body = self.parse_try_block()
        
        self.expect(TokenType.END, "Expected 'end' after try statement")
        return TryNode(try_body, catch_var, catch_body, finally_body)
    
    def parse_try_block(self) -> List[Any]:
        statements = []
        self.skip_newlines()
        while not self.check(TokenType.END, TokenType.CATCH, TokenType.FINALLY, TokenType.EOF):
            stmt = self.parse_statement()
            if stmt is not None:
                statements.append(stmt)
            self.skip_newlines()
        return statements
    
    def parse_match(self) -> MatchNode:
        value = self.parse_expression()
        self.skip_newlines()
        
        cases = []
        default_body = None
        
        while not self.check(TokenType.END, TokenType.EOF):
            self.skip_newlines()
            if self.check(TokenType.END):
                break
            
            if self.match(TokenType.WHEN):
                # Parse pattern(s)
                patterns = [self.parse_expression()]
                while self.match(TokenType.COMMA):
                    patterns.append(self.parse_expression())
                self.skip_newlines()
                body = self.parse_match_block()
                cases.append((patterns, body))
            elif self.match(TokenType.ELSE):
                self.skip_newlines()
                default_body = self.parse_match_block()
                break
            else:
                break
        
        self.expect(TokenType.END, "Expected 'end' after match statement")
        return MatchNode(value, cases, default_body)
    
    def parse_match_block(self) -> List[Any]:
        statements = []
        self.skip_newlines()
        while not self.check(TokenType.END, TokenType.WHEN, TokenType.ELSE, TokenType.EOF):
            stmt = self.parse_statement()
            if stmt is not None:
                statements.append(stmt)
            self.skip_newlines()
        return statements
    
    def parse_block(self) -> List[Any]:
        statements = []
        self.skip_newlines()
        
        while not self.check(TokenType.END, TokenType.ELSE, TokenType.ELIF, TokenType.EOF):
            stmt = self.parse_statement()
            if stmt is not None:
                statements.append(stmt)
            self.skip_newlines()
        
        return statements
    
    def parse_expression(self) -> Any:
        return self.parse_or()
    
    def parse_or(self) -> Any:
        left = self.parse_and()
        
        while self.match(TokenType.OR):
            right = self.parse_and()
            left = BinaryNode(left, TokenType.OR, right)
        
        return left
    
    def parse_and(self) -> Any:
        left = self.parse_not()
        
        while self.match(TokenType.AND):
            right = self.parse_not()
            left = BinaryNode(left, TokenType.AND, right)
        
        return left
    
    def parse_not(self) -> Any:
        if self.match(TokenType.NOT):
            operand = self.parse_not()
            return UnaryNode(TokenType.NOT, operand)
        return self.parse_comparison()
    
    def parse_comparison(self) -> Any:
        left = self.parse_additive()
        
        while True:
            if self.match(TokenType.EQ):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.EQ, right)
            elif self.match(TokenType.NEQ):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.NEQ, right)
            elif self.match(TokenType.LT):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.LT, right)
            elif self.match(TokenType.GT):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.GT, right)
            elif self.match(TokenType.LTE):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.LTE, right)
            elif self.match(TokenType.GTE):
                right = self.parse_additive()
                left = BinaryNode(left, TokenType.GTE, right)
            else:
                break
        
        return left
    
    def parse_additive(self) -> Any:
        left = self.parse_multiplicative()
        
        while True:
            if self.match(TokenType.PLUS):
                right = self.parse_multiplicative()
                left = BinaryNode(left, TokenType.PLUS, right)
            elif self.match(TokenType.MINUS):
                right = self.parse_multiplicative()
                left = BinaryNode(left, TokenType.MINUS, right)
            else:
                break
        
        return left
    
    def parse_multiplicative(self) -> Any:
        left = self.parse_power()
        
        while True:
            if self.match(TokenType.STAR):
                right = self.parse_power()
                left = BinaryNode(left, TokenType.STAR, right)
            elif self.match(TokenType.SLASH):
                right = self.parse_power()
                left = BinaryNode(left, TokenType.SLASH, right)
            elif self.match(TokenType.PERCENT):
                right = self.parse_power()
                left = BinaryNode(left, TokenType.PERCENT, right)
            else:
                break
        
        return left
    
    def parse_power(self) -> Any:
        left = self.parse_unary()
        
        if self.match(TokenType.CARET):
            right = self.parse_power()  # Right associative
            left = BinaryNode(left, TokenType.CARET, right)
        
        return left
    
    def parse_unary(self) -> Any:
        if self.match(TokenType.MINUS):
            operand = self.parse_unary()
            return UnaryNode(TokenType.MINUS, operand)
        return self.parse_postfix()
    
    def parse_postfix(self) -> Any:
        expr = self.parse_primary()
        
        while True:
            if self.match(TokenType.LPAREN):
                # Function call
                args = []
                if not self.check(TokenType.RPAREN):
                    args.append(self.parse_expression())
                    while self.match(TokenType.COMMA):
                        args.append(self.parse_expression())
                self.expect(TokenType.RPAREN, "Expected ')' after arguments")
                expr = CallNode(expr, args)
            elif self.match(TokenType.LBRACKET):
                # Index access
                index = self.parse_expression()
                self.expect(TokenType.RBRACKET, "Expected ']' after index")
                expr = IndexNode(expr, index)
            elif self.match(TokenType.DOT):
                # Member access
                member = self.expect(TokenType.IDENTIFIER, "Expected member name").value
                expr = MemberNode(expr, member)
            else:
                break
        
        return expr
    
    def parse_primary(self) -> Any:
        # Number
        if self.check(TokenType.NUMBER):
            return NumberNode(self.advance().value)
        
        # String
        if self.check(TokenType.STRING):
            return StringNode(self.advance().value)
        
        # Boolean
        if self.match(TokenType.TRUE):
            return BoolNode(True)
        if self.match(TokenType.FALSE):
            return BoolNode(False)
        
        # Nothing
        if self.match(TokenType.NOTHING):
            return NothingNode()
        
        # Super call
        if self.match(TokenType.SUPER):
            self.expect(TokenType.DOT, "Expected '.' after 'super'")
            method = self.expect(TokenType.IDENTIFIER, "Expected method name").value
            self.expect(TokenType.LPAREN, "Expected '(' after method name")
            args = []
            if not self.check(TokenType.RPAREN):
                args.append(self.parse_expression())
                while self.match(TokenType.COMMA):
                    args.append(self.parse_expression())
            self.expect(TokenType.RPAREN, "Expected ')' after arguments")
            return SuperNode(method, args)
        
        # New expression
        if self.match(TokenType.NEW):
            class_name = self.expect(TokenType.IDENTIFIER, "Expected class name").value
            self.expect(TokenType.LPAREN, "Expected '(' after class name")
            args = []
            if not self.check(TokenType.RPAREN):
                args.append(self.parse_expression())
                while self.match(TokenType.COMMA):
                    args.append(self.parse_expression())
            self.expect(TokenType.RPAREN, "Expected ')' after arguments")
            return NewNode(class_name, args)
        
        # Lambda: (params) -> expression or lambda (params) -> expression
        if self.match(TokenType.LAMBDA):
            self.expect(TokenType.LPAREN, "Expected '(' after 'lambda'")
            params = []
            if not self.check(TokenType.RPAREN):
                params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
                while self.match(TokenType.COMMA):
                    params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter name").value)
            self.expect(TokenType.RPAREN, "Expected ')' after lambda parameters")
            self.expect(TokenType.ARROW, "Expected '->' after lambda parameters")
            body = self.parse_expression()
            return LambdaNode(params, body)
        
        # Identifier
        if self.check(TokenType.IDENTIFIER):
            return IdentifierNode(self.advance().value)
        
        # Parenthesized expression or lambda
        if self.match(TokenType.LPAREN):
            # Check if it's a lambda: (x) -> expr or (x, y) -> expr
            # Save position for backtracking
            if self.check(TokenType.RPAREN):
                # Empty params lambda: () -> expr
                self.advance()  # consume ')'
                if self.match(TokenType.ARROW):
                    body = self.parse_expression()
                    return LambdaNode([], body)
                # Otherwise it was just ()
                raise SyntaxError(f"Line {self.peek().line}: Empty parentheses")
            
            # Could be (expr) or (param, param) -> expr
            first = self.parse_expression()
            
            if self.match(TokenType.COMMA):
                # Multiple items - likely a lambda
                if isinstance(first, IdentifierNode):
                    params = [first.name]
                    params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter").value)
                    while self.match(TokenType.COMMA):
                        params.append(self.expect(TokenType.IDENTIFIER, "Expected parameter").value)
                    self.expect(TokenType.RPAREN, "Expected ')' after parameters")
                    if self.match(TokenType.ARROW):
                        body = self.parse_expression()
                        return LambdaNode(params, body)
                    raise SyntaxError(f"Line {self.peek().line}: Expected '->' for lambda")
                else:
                    raise SyntaxError(f"Line {self.peek().line}: Invalid lambda parameters")
            
            self.expect(TokenType.RPAREN, "Expected ')' after expression")
            
            # Check for arrow (single param lambda)
            if self.match(TokenType.ARROW):
                if isinstance(first, IdentifierNode):
                    body = self.parse_expression()
                    return LambdaNode([first.name], body)
                raise SyntaxError(f"Line {self.peek().line}: Invalid lambda parameter")
            
            return first
        
        # List literal
        if self.match(TokenType.LBRACKET):
            elements = []
            if not self.check(TokenType.RBRACKET):
                elements.append(self.parse_expression())
                while self.match(TokenType.COMMA):
                    elements.append(self.parse_expression())
            self.expect(TokenType.RBRACKET, "Expected ']' after list")
            return ListNode(elements)
        
        # Map literal
        if self.match(TokenType.LBRACE):
            pairs = []
            self.skip_newlines()
            if not self.check(TokenType.RBRACE):
                key = self.parse_expression()
                self.expect(TokenType.COLON, "Expected ':' after map key")
                value = self.parse_expression()
                pairs.append((key, value))
                
                while self.match(TokenType.COMMA):
                    self.skip_newlines()
                    if self.check(TokenType.RBRACE):
                        break
                    key = self.parse_expression()
                    self.expect(TokenType.COLON, "Expected ':' after map key")
                    value = self.parse_expression()
                    pairs.append((key, value))
            
            self.skip_newlines()
            self.expect(TokenType.RBRACE, "Expected '}' after map")
            return MapNode(pairs)
        
        raise SyntaxError(f"Line {self.peek().line}: Unexpected token {self.peek().type}")

# ============================================================================
# RUNTIME VALUES
# ============================================================================

class PurpNothing:
    def __repr__(self):
        return "nothing"

NOTHING = PurpNothing()

class PurpFunction:
    def __init__(self, name: str, params: List[str], body: List[Any], closure: 'Environment'):
        self.name = name
        self.params = params
        self.body = body
        self.closure = closure
    
    def __repr__(self):
        return f"<fn {self.name}>"

class PurpClass:
    def __init__(self, name: str, fields: List[str], methods: Dict[str, PurpFunction], 
                 parent: Optional['PurpClass'] = None, static_fields: Optional[Dict[str, Any]] = None,
                 static_methods: Optional[Dict[str, PurpFunction]] = None):
        self.name = name
        self.fields = fields
        self.methods = methods
        self.parent = parent
        self.static_fields = static_fields or {}
        self.static_methods = static_methods or {}
        
        # Inherit parent fields and methods
        if parent:
            self.fields = list(parent.fields) + [f for f in fields if f not in parent.fields]
            for name, method in parent.methods.items():
                if name not in self.methods:
                    self.methods[name] = method
    
    def __repr__(self):
        return f"<class {self.name}>"

class PurpInstance:
    def __init__(self, klass: PurpClass):
        self.klass = klass
        self.fields: Dict[str, Any] = {}
        # Initialize fields to nothing
        for field in klass.fields:
            self.fields[field] = NOTHING
    
    def __repr__(self):
        return f"<{self.klass.name} instance>"

class PurpNative:
    def __init__(self, name: str, func: Callable):
        self.name = name
        self.func = func
    
    def __repr__(self):
        return f"<native fn {self.name}>"

class PurpLambda:
    def __init__(self, params: List[str], body: Any, closure: 'Environment'):
        self.params = params
        self.body = body
        self.closure = closure
    
    def __repr__(self):
        return f"<lambda ({len(self.params)} params)>"

class PurpError(Exception):
    def __init__(self, message: str):
        self.message = message
        super().__init__(message)

# ============================================================================
# ENVIRONMENT
# ============================================================================

class Environment:
    def __init__(self, parent: Optional['Environment'] = None):
        self.variables: Dict[str, Any] = {}
        self.constants: set = set()
        self.parent = parent
    
    def define(self, name: str, value: Any, is_const: bool = False):
        self.variables[name] = value
        if is_const:
            self.constants.add(name)
    
    def get(self, name: str) -> Any:
        if name in self.variables:
            return self.variables[name]
        if self.parent:
            return self.parent.get(name)
        raise NameError(f"Undefined variable '{name}'")
    
    def set(self, name: str, value: Any):
        if name in self.variables:
            if name in self.constants:
                raise RuntimeError(f"Cannot reassign constant '{name}'")
            self.variables[name] = value
            return
        if self.parent:
            self.parent.set(name, value)
            return
        raise NameError(f"Undefined variable '{name}'")

# ============================================================================
# INTERPRETER
# ============================================================================

class BreakException(Exception):
    pass

class ContinueException(Exception):
    pass

class ReturnException(Exception):
    def __init__(self, value: Any):
        self.value = value

class Interpreter:
    def __init__(self):
        self.global_env = Environment()
        self.env = self.global_env
        self._setup_builtins()
    
    def _setup_builtins(self):
        # I/O
        self.global_env.define('say', PurpNative('say', self._builtin_say))
        self.global_env.define('write', PurpNative('write', self._builtin_write))
        self.global_env.define('ask', PurpNative('ask', self._builtin_ask))
        
        # Type functions
        self.global_env.define('type', PurpNative('type', self._builtin_type))
        self.global_env.define('number', PurpNative('number', self._builtin_number))
        self.global_env.define('text', PurpNative('text', self._builtin_text))
        self.global_env.define('bool', PurpNative('bool', self._builtin_bool))
        self.global_env.define('length', PurpNative('length', self._builtin_length))
        
        # Math
        self.global_env.define('abs', PurpNative('abs', lambda args: abs(args[0]) if args else 0))
        self.global_env.define('round', PurpNative('round', lambda args: round(args[0]) if args else 0))
        self.global_env.define('floor', PurpNative('floor', lambda args: int(args[0]) if args else 0))
        self.global_env.define('ceil', PurpNative('ceil', lambda args: int(args[0]) + (1 if args[0] % 1 else 0) if args else 0))
        self.global_env.define('sqrt', PurpNative('sqrt', lambda args: args[0] ** 0.5 if args else 0))
        self.global_env.define('pow', PurpNative('pow', lambda args: args[0] ** args[1] if len(args) >= 2 else 0))
        self.global_env.define('min', PurpNative('min', lambda args: min(args[0]) if args and isinstance(args[0], list) else min(args) if args else 0))
        self.global_env.define('max', PurpNative('max', lambda args: max(args[0]) if args and isinstance(args[0], list) else max(args) if args else 0))
        self.global_env.define('sum', PurpNative('sum', lambda args: sum(args[0]) if args and isinstance(args[0], list) else sum(args)))
        
        # Collections
        self.global_env.define('range', PurpNative('range', self._builtin_range))
        self.global_env.define('push', PurpNative('push', self._builtin_push))
        self.global_env.define('pop', PurpNative('pop', self._builtin_pop))
        self.global_env.define('insert', PurpNative('insert', self._builtin_insert))
        self.global_env.define('remove', PurpNative('remove', self._builtin_remove))
        self.global_env.define('first', PurpNative('first', lambda args: args[0][0] if args and args[0] else NOTHING))
        self.global_env.define('last', PurpNative('last', lambda args: args[0][-1] if args and args[0] else NOTHING))
        self.global_env.define('reverse', PurpNative('reverse', lambda args: list(reversed(args[0])) if args else []))
        self.global_env.define('sort', PurpNative('sort', lambda args: sorted(args[0]) if args else []))
        self.global_env.define('keys', PurpNative('keys', lambda args: list(args[0].keys()) if args and isinstance(args[0], dict) else []))
        self.global_env.define('values', PurpNative('values', lambda args: list(args[0].values()) if args and isinstance(args[0], dict) else []))
        self.global_env.define('contains', PurpNative('contains', self._builtin_contains))
        self.global_env.define('index_of', PurpNative('index_of', self._builtin_index_of))
        self.global_env.define('join', PurpNative('join', self._builtin_join))
        self.global_env.define('split', PurpNative('split', self._builtin_split))
        self.global_env.define('slice', PurpNative('slice', self._builtin_slice))
        
        # String functions
        self.global_env.define('upper', PurpNative('upper', lambda args: args[0].upper() if args and isinstance(args[0], str) else ""))
        self.global_env.define('lower', PurpNative('lower', lambda args: args[0].lower() if args and isinstance(args[0], str) else ""))
        self.global_env.define('trim', PurpNative('trim', lambda args: args[0].strip() if args and isinstance(args[0], str) else ""))
        self.global_env.define('replace', PurpNative('replace', self._builtin_replace))
        self.global_env.define('starts_with', PurpNative('starts_with', lambda args: args[0].startswith(args[1]) if len(args) >= 2 else False))
        self.global_env.define('ends_with', PurpNative('ends_with', lambda args: args[0].endswith(args[1]) if len(args) >= 2 else False))
        self.global_env.define('char_at', PurpNative('char_at', lambda args: args[0][int(args[1])] if len(args) >= 2 and isinstance(args[0], str) else ""))
        self.global_env.define('substring', PurpNative('substring', self._builtin_substring))
        
        # Functional
        self.global_env.define('map', PurpNative('map', self._builtin_map))
        self.global_env.define('filter', PurpNative('filter', self._builtin_filter))
        self.global_env.define('reduce', PurpNative('reduce', self._builtin_reduce))
        self.global_env.define('foreach', PurpNative('foreach', self._builtin_foreach))
        
        # File I/O
        self.global_env.define('read_file', PurpNative('read_file', self._builtin_read_file))
        self.global_env.define('write_file', PurpNative('write_file', self._builtin_write_file))
        self.global_env.define('append_file', PurpNative('append_file', self._builtin_append_file))
        self.global_env.define('file_exists', PurpNative('file_exists', self._builtin_file_exists))
        
        # Utility
        self.global_env.define('print_type', PurpNative('print_type', self._builtin_print_type))
        self.global_env.define('random', PurpNative('random', self._builtin_random))
        self.global_env.define('time', PurpNative('time', lambda args: __import__('time').time()))
        
        # GUI (Tkinter) bindings
        self.global_env.define('gui_window', PurpNative('gui_window', self._builtin_gui_window))
        self.global_env.define('gui_button', PurpNative('gui_button', self._builtin_gui_button))
        self.global_env.define('gui_label', PurpNative('gui_label', self._builtin_gui_label))
        self.global_env.define('gui_entry', PurpNative('gui_entry', self._builtin_gui_entry))
        self.global_env.define('gui_text', PurpNative('gui_text', self._builtin_gui_text))
        self.global_env.define('gui_frame', PurpNative('gui_frame', self._builtin_gui_frame))
        self.global_env.define('gui_pack', PurpNative('gui_pack', self._builtin_gui_pack))
        self.global_env.define('gui_grid', PurpNative('gui_grid', self._builtin_gui_grid))
        self.global_env.define('gui_config', PurpNative('gui_config', self._builtin_gui_config))
        self.global_env.define('gui_get', PurpNative('gui_get', self._builtin_gui_get))
        self.global_env.define('gui_set', PurpNative('gui_set', self._builtin_gui_set))
        self.global_env.define('gui_run', PurpNative('gui_run', self._builtin_gui_run))
        self.global_env.define('gui_messagebox', PurpNative('gui_messagebox', self._builtin_gui_messagebox))
        self.global_env.define('gui_bind', PurpNative('gui_bind', self._builtin_gui_bind))
    
    def _builtin_say(self, args):
        print(' '.join(self._to_string(a) for a in args))
        return NOTHING
    
    def _builtin_write(self, args):
        print(''.join(self._to_string(a) for a in args), end='')
        return NOTHING
    
    def _builtin_ask(self, args):
        prompt = self._to_string(args[0]) if args else ""
        return input(prompt)
    
    def _builtin_type(self, args):
        if not args:
            return "nothing"
        val = args[0]
        if val is NOTHING or val is None:
            return "nothing"
        if isinstance(val, bool):
            return "bool"
        if isinstance(val, (int, float)):
            return "number"
        if isinstance(val, str):
            return "string"
        if isinstance(val, list):
            return "list"
        if isinstance(val, dict):
            return "map"
        if isinstance(val, PurpFunction):
            return "function"
        if isinstance(val, PurpClass):
            return "class"
        if isinstance(val, PurpInstance):
            return "instance"
        if isinstance(val, PurpLambda):
            return "lambda"
        return "unknown"
    
    def _builtin_number(self, args):
        if not args:
            return 0
        try:
            return float(args[0])
        except:
            return 0
    
    def _builtin_text(self, args):
        if not args:
            return ""
        return self._to_string(args[0])
    
    def _builtin_bool(self, args):
        if not args:
            return False
        return self._is_truthy(args[0])
    
    def _builtin_length(self, args):
        if not args:
            return 0
        val = args[0]
        if isinstance(val, (str, list)):
            return len(val)
        if isinstance(val, dict):
            return len(val)
        return 0
    
    def _builtin_range(self, args):
        if len(args) == 1:
            return list(range(int(args[0])))
        elif len(args) == 2:
            return list(range(int(args[0]), int(args[1])))
        elif len(args) >= 3:
            return list(range(int(args[0]), int(args[1]), int(args[2])))
        return []
    
    def _builtin_push(self, args):
        if len(args) >= 2 and isinstance(args[0], list):
            args[0].append(args[1])
        return NOTHING
    
    def _builtin_pop(self, args):
        if args and isinstance(args[0], list) and args[0]:
            return args[0].pop()
        return NOTHING
    
    def _builtin_insert(self, args):
        if len(args) >= 3 and isinstance(args[0], list):
            args[0].insert(int(args[1]), args[2])
        return NOTHING
    
    def _builtin_remove(self, args):
        if len(args) >= 2 and isinstance(args[0], list):
            try:
                args[0].remove(args[1])
            except ValueError:
                pass
        return NOTHING
    
    def _builtin_contains(self, args):
        if len(args) >= 2:
            container = args[0]
            item = args[1]
            if isinstance(container, (list, str)):
                return item in container
            if isinstance(container, dict):
                return item in container
        return False
    
    def _builtin_index_of(self, args):
        if len(args) >= 2:
            container = args[0]
            item = args[1]
            if isinstance(container, list):
                try:
                    return container.index(item)
                except ValueError:
                    return -1
            if isinstance(container, str):
                return container.find(item)
        return -1
    
    def _builtin_join(self, args):
        if len(args) >= 2 and isinstance(args[0], list):
            return str(args[1]).join(self._to_string(x) for x in args[0])
        if args and isinstance(args[0], list):
            return "".join(self._to_string(x) for x in args[0])
        return ""
    
    def _builtin_split(self, args):
        if len(args) >= 2 and isinstance(args[0], str):
            return args[0].split(args[1])
        if args and isinstance(args[0], str):
            return args[0].split()
        return []
    
    def _builtin_slice(self, args):
        if len(args) >= 3:
            return args[0][int(args[1]):int(args[2])]
        if len(args) >= 2:
            return args[0][int(args[1]):]
        return args[0] if args else []
    
    def _builtin_replace(self, args):
        if len(args) >= 3 and isinstance(args[0], str):
            return args[0].replace(args[1], args[2])
        return args[0] if args else ""
    
    def _builtin_substring(self, args):
        if len(args) >= 3 and isinstance(args[0], str):
            return args[0][int(args[1]):int(args[2])]
        if len(args) >= 2 and isinstance(args[0], str):
            return args[0][int(args[1]):]
        return args[0] if args else ""
    
    def _builtin_map(self, args):
        if len(args) >= 2:
            lst, func = args[0], args[1]
            if isinstance(lst, list) and callable(getattr(func, 'func', None)) or isinstance(func, (PurpFunction, PurpLambda)):
                result = []
                for item in lst:
                    result.append(self._call_callable(func, [item]))
                return result
        return []
    
    def _builtin_filter(self, args):
        if len(args) >= 2:
            lst, func = args[0], args[1]
            if isinstance(lst, list):
                result = []
                for item in lst:
                    if self._is_truthy(self._call_callable(func, [item])):
                        result.append(item)
                return result
        return []
    
    def _builtin_reduce(self, args):
        if len(args) >= 3:
            lst, func, initial = args[0], args[1], args[2]
            if isinstance(lst, list):
                acc = initial
                for item in lst:
                    acc = self._call_callable(func, [acc, item])
                return acc
        return NOTHING
    
    def _builtin_foreach(self, args):
        if len(args) >= 2:
            lst, func = args[0], args[1]
            if isinstance(lst, list):
                for item in lst:
                    self._call_callable(func, [item])
        return NOTHING
    
    def _builtin_read_file(self, args):
        if args:
            try:
                with open(args[0], 'r', encoding='utf-8') as f:
                    return f.read()
            except:
                return NOTHING
        return NOTHING
    
    def _builtin_write_file(self, args):
        if len(args) >= 2:
            try:
                with open(args[0], 'w', encoding='utf-8') as f:
                    f.write(self._to_string(args[1]))
                return True
            except:
                return False
        return False
    
    def _builtin_append_file(self, args):
        if len(args) >= 2:
            try:
                with open(args[0], 'a', encoding='utf-8') as f:
                    f.write(self._to_string(args[1]))
                return True
            except:
                return False
        return False
    
    def _builtin_file_exists(self, args):
        if args:
            import os
            return os.path.exists(args[0])
        return False
    
    def _builtin_print_type(self, args):
        if args:
            print(f"Type: {type(args[0])}, Value: {args[0]}")
        return NOTHING
    
    def _builtin_random(self, args):
        import random
        if len(args) >= 2:
            return random.randint(int(args[0]), int(args[1]))
        if len(args) == 1:
            return random.randint(0, int(args[0]))
        return random.random()
    
    # GUI Builtin Functions (Tkinter bindings)
    def _builtin_gui_window(self, args):
        """Create a new GUI window. Args: [title, width, height]"""
        import tkinter as tk
        window = tk.Tk()
        if args:
            window.title(str(args[0]))
        if len(args) >= 3:
            window.geometry(f"{int(args[1])}x{int(args[2])}")
        return window
    
    def _builtin_gui_button(self, args):
        """Create a button. Args: [parent, text, callback]"""
        import tkinter as tk
        if len(args) >= 2:
            parent = args[0]
            text = str(args[1])
            callback = args[2] if len(args) >= 3 else None
            
            def on_click():
                if callback:
                    self._call_callable(callback, [])
            
            button = tk.Button(parent, text=text, command=on_click)
            return button
        return NOTHING
    
    def _builtin_gui_label(self, args):
        """Create a label. Args: [parent, text]"""
        import tkinter as tk
        if len(args) >= 2:
            parent = args[0]
            text = str(args[1])
            label = tk.Label(parent, text=text)
            return label
        return NOTHING
    
    def _builtin_gui_entry(self, args):
        """Create a text entry field. Args: [parent]"""
        import tkinter as tk
        if args:
            parent = args[0]
            entry = tk.Entry(parent)
            return entry
        return NOTHING
    
    def _builtin_gui_text(self, args):
        """Create a multi-line text area. Args: [parent, width, height]"""
        import tkinter as tk
        if args:
            parent = args[0]
            width = int(args[1]) if len(args) >= 2 else 40
            height = int(args[2]) if len(args) >= 3 else 10
            text = tk.Text(parent, width=width, height=height)
            return text
        return NOTHING
    
    def _builtin_gui_frame(self, args):
        """Create a frame container. Args: [parent]"""
        import tkinter as tk
        if args:
            parent = args[0]
            frame = tk.Frame(parent)
            return frame
        return NOTHING
    
    def _builtin_gui_pack(self, args):
        """Pack a widget. Args: [widget, side, fill, expand, padx, pady]"""
        if args:
            widget = args[0]
            kwargs = {}
            if len(args) >= 2 and args[1]:
                kwargs['side'] = str(args[1])
            if len(args) >= 3 and args[2]:
                kwargs['fill'] = str(args[2])
            if len(args) >= 4:
                kwargs['expand'] = bool(args[3])
            if len(args) >= 5:
                kwargs['padx'] = int(args[4])
            if len(args) >= 6:
                kwargs['pady'] = int(args[5])
            widget.pack(**kwargs)
        return NOTHING
    
    def _builtin_gui_grid(self, args):
        """Grid a widget. Args: [widget, row, column, padx, pady]"""
        if len(args) >= 3:
            widget = args[0]
            row = int(args[1])
            col = int(args[2])
            kwargs = {'row': row, 'column': col}
            if len(args) >= 4:
                kwargs['padx'] = int(args[3])
            if len(args) >= 5:
                kwargs['pady'] = int(args[4])
            widget.grid(**kwargs)
        return NOTHING
    
    def _builtin_gui_config(self, args):
        """Configure widget options. Args: [widget, option, value]"""
        if len(args) >= 3:
            widget = args[0]
            option = str(args[1])
            value = args[2]
            widget.configure(**{option: value})
        return NOTHING
    
    def _builtin_gui_get(self, args):
        """Get value from entry/text widget. Args: [widget]"""
        import tkinter as tk
        if args:
            widget = args[0]
            if isinstance(widget, tk.Entry):
                return widget.get()
            elif isinstance(widget, tk.Text):
                return widget.get("1.0", tk.END).strip()
        return ""
    
    def _builtin_gui_set(self, args):
        """Set value for entry/text/label widget. Args: [widget, value]"""
        import tkinter as tk
        if len(args) >= 2:
            widget = args[0]
            value = str(args[1])
            if isinstance(widget, tk.Entry):
                widget.delete(0, tk.END)
                widget.insert(0, value)
            elif isinstance(widget, tk.Text):
                widget.delete("1.0", tk.END)
                widget.insert("1.0", value)
            elif isinstance(widget, tk.Label):
                widget.configure(text=value)
        return NOTHING
    
    def _builtin_gui_run(self, args):
        """Start the GUI main loop. Args: [window]"""
        if args:
            window = args[0]
            window.mainloop()
        return NOTHING
    
    def _builtin_gui_messagebox(self, args):
        """Show a message box. Args: [title, message, type]"""
        from tkinter import messagebox
        if len(args) >= 2:
            title = str(args[0])
            message = str(args[1])
            box_type = str(args[2]) if len(args) >= 3 else "info"
            
            if box_type == "info":
                messagebox.showinfo(title, message)
            elif box_type == "warning":
                messagebox.showwarning(title, message)
            elif box_type == "error":
                messagebox.showerror(title, message)
            elif box_type == "question":
                return messagebox.askyesno(title, message)
        return NOTHING
    
    def _builtin_gui_bind(self, args):
        """Bind an event to a widget. Args: [widget, event, callback]"""
        if len(args) >= 3:
            widget = args[0]
            event = str(args[1])
            callback = args[2]
            
            def on_event(e):
                self._call_callable(callback, [])
            
            widget.bind(event, on_event)
        return NOTHING
    
    def _call_callable(self, func, args):
        if isinstance(func, PurpNative):
            return func.func(args)
        if isinstance(func, PurpFunction):
            return self._call_function(func, args)
        if isinstance(func, PurpLambda):
            return self._call_lambda(func, args)
        return NOTHING
    
    def _to_string(self, value: Any) -> str:
        if value is NOTHING or value is None:
            return "nothing"
        if isinstance(value, bool):
            return "true" if value else "false"
        if isinstance(value, float):
            if value == int(value):
                return str(int(value))
            return str(value)
        if isinstance(value, list):
            return "[" + ", ".join(self._to_string(v) for v in value) + "]"
        if isinstance(value, dict):
            pairs = [f"{self._to_string(k)}: {self._to_string(v)}" for k, v in value.items()]
            return "{" + ", ".join(pairs) + "}"
        return str(value)
    
    def run(self, statements: List[Any]) -> Any:
        result = NOTHING
        for stmt in statements:
            result = self.execute(stmt)
        return result
    
    def execute(self, node: Any) -> Any:
        if isinstance(node, NumberNode):
            return node.value
        
        if isinstance(node, StringNode):
            return node.value
        
        if isinstance(node, BoolNode):
            return node.value
        
        if isinstance(node, NothingNode):
            return NOTHING
        
        if isinstance(node, IdentifierNode):
            return self.env.get(node.name)
        
        if isinstance(node, BinaryNode):
            return self._eval_binary(node)
        
        if isinstance(node, UnaryNode):
            return self._eval_unary(node)
        
        if isinstance(node, CallNode):
            return self._eval_call(node)
        
        if isinstance(node, IndexNode):
            obj = self.execute(node.obj)
            index = self.execute(node.index)
            if isinstance(obj, (list, str)):
                return obj[int(index)]
            if isinstance(obj, dict):
                return obj.get(index, NOTHING)
            raise RuntimeError(f"Cannot index {type(obj)}")
        
        if isinstance(node, MemberNode):
            obj = self.execute(node.obj)
            if isinstance(obj, PurpInstance):
                if node.member in obj.fields:
                    return obj.fields[node.member]
                if node.member in obj.klass.methods:
                    return (obj, obj.klass.methods[node.member])
                # Check parent class methods
                parent = obj.klass.parent
                while parent:
                    if node.member in parent.methods:
                        return (obj, parent.methods[node.member])
                    parent = parent.parent
            if isinstance(obj, PurpClass):
                # Static field or method access
                if node.member in obj.static_fields:
                    return obj.static_fields[node.member]
                if node.member in obj.static_methods:
                    return obj.static_methods[node.member]
            if isinstance(obj, dict):
                return obj.get(node.member, NOTHING)
            raise RuntimeError(f"'{obj}' has no member '{node.member}'")
        
        if isinstance(node, ListNode):
            return [self.execute(e) for e in node.elements]
        
        if isinstance(node, MapNode):
            result = {}
            for key, value in node.pairs:
                k = self.execute(key)
                v = self.execute(value)
                result[k] = v
            return result
        
        if isinstance(node, VarDeclNode):
            value = self.execute(node.value) if node.value else NOTHING
            self.env.define(node.name, value, node.is_const)
            return NOTHING
        
        if isinstance(node, AssignNode):
            value = self.execute(node.value)
            
            if isinstance(node.target, IdentifierNode):
                self.env.set(node.target.name, value)
            elif isinstance(node.target, IndexNode):
                obj = self.execute(node.target.obj)
                index = self.execute(node.target.index)
                if isinstance(obj, list):
                    obj[int(index)] = value
                elif isinstance(obj, dict):
                    obj[index] = value
            elif isinstance(node.target, MemberNode):
                obj = self.execute(node.target.obj)
                if isinstance(obj, PurpInstance):
                    obj.fields[node.target.member] = value
                elif isinstance(obj, dict):
                    obj[node.target.member] = value
            
            return value
        
        if isinstance(node, IfNode):
            if self._is_truthy(self.execute(node.condition)):
                return self.run(node.then_branch)
            
            for condition, body in node.elif_branches:
                if self._is_truthy(self.execute(condition)):
                    return self.run(body)
            
            if node.else_branch:
                return self.run(node.else_branch)
            
            return NOTHING
        
        if isinstance(node, WhileNode):
            result = NOTHING
            while self._is_truthy(self.execute(node.condition)):
                try:
                    result = self.run(node.body)
                except BreakException:
                    break
                except ContinueException:
                    continue
            return result
        
        if isinstance(node, ForInNode):
            iterable = self.execute(node.iterable)
            result = NOTHING
            
            for item in iterable:
                self.env.define(node.var_name, item)
                try:
                    result = self.run(node.body)
                except BreakException:
                    break
                except ContinueException:
                    continue
            
            return result
        
        if isinstance(node, ForRangeNode):
            start = int(self.execute(node.start))
            end = int(self.execute(node.end))
            step = int(self.execute(node.step)) if node.step else (1 if end >= start else -1)
            result = NOTHING
            
            i = start
            while (step > 0 and i <= end) or (step < 0 and i >= end):
                self.env.define(node.var_name, i)
                try:
                    result = self.run(node.body)
                except BreakException:
                    break
                except ContinueException:
                    pass
                i += step
            
            return result
        
        if isinstance(node, FunctionNode):
            func = PurpFunction(node.name, node.params, node.body, self.env)
            self.env.define(node.name, func)
            return func
        
        if isinstance(node, LambdaNode):
            return PurpLambda(node.params, node.body, self.env)
        
        if isinstance(node, ReturnNode):
            value = self.execute(node.value) if node.value else NOTHING
            raise ReturnException(value)
        
        if isinstance(node, BreakNode):
            raise BreakException()
        
        if isinstance(node, ContinueNode):
            raise ContinueException()
        
        if isinstance(node, TryNode):
            try:
                return self.run(node.try_body)
            except (ReturnException, BreakException, ContinueException):
                # Re-raise control flow exceptions
                raise
            except PurpError as e:
                if node.catch_body:
                    if node.catch_var:
                        self.env.define(node.catch_var, e.message)
                    return self.run(node.catch_body)
            except Exception as e:
                if node.catch_body:
                    if node.catch_var:
                        self.env.define(node.catch_var, str(e))
                    return self.run(node.catch_body)
            finally:
                if node.finally_body:
                    self.run(node.finally_body)
            return NOTHING
        
        if isinstance(node, RaiseNode):
            value = self.execute(node.value)
            raise PurpError(self._to_string(value))
        
        if isinstance(node, MatchNode):
            value = self.execute(node.value)
            for patterns, body in node.cases:
                for pattern in patterns:
                    pattern_val = self.execute(pattern)
                    if value == pattern_val:
                        return self.run(body)
            if node.default_body:
                return self.run(node.default_body)
            return NOTHING
        
        if isinstance(node, CompoundAssignNode):
            current = self.execute(node.target)
            value = self.execute(node.value)
            
            if node.op == TokenType.PLUSEQ:
                new_val = current + value
            elif node.op == TokenType.MINUSEQ:
                new_val = current - value
            elif node.op == TokenType.STAREQ:
                new_val = current * value
            elif node.op == TokenType.SLASHEQ:
                new_val = current / value
            else:
                new_val = current
            
            if isinstance(node.target, IdentifierNode):
                self.env.set(node.target.name, new_val)
            return new_val
        
        if isinstance(node, ClassNode):
            # Handle inheritance
            parent_class = None
            if node.parent:
                parent_class = self.env.get(node.parent)
                if not isinstance(parent_class, PurpClass):
                    raise RuntimeError(f"'{node.parent}' is not a class")
            
            methods = {}
            for method in node.methods:
                methods[method.name] = PurpFunction(method.name, method.params, method.body, self.env)
            
            static_fields_dict = {}
            static_methods_dict = {}
            if node.static_fields:
                for name, value_node in node.static_fields:
                    static_fields_dict[name] = self.execute(value_node) if value_node else NOTHING
            if node.static_methods:
                for method in node.static_methods:
                    static_methods_dict[method.name] = PurpFunction(method.name, method.params, method.body, self.env)
            
            klass = PurpClass(node.name, node.fields, methods, parent_class, static_fields_dict, static_methods_dict)
            self.env.define(node.name, klass)
            return klass
        
        if isinstance(node, SuperNode):
            # Get current 'this' and its parent class
            instance = self.env.get('this')
            if not isinstance(instance, PurpInstance):
                raise RuntimeError("'super' can only be used inside a method")
            parent = instance.klass.parent
            if not parent:
                raise RuntimeError("No parent class found")
            if node.method in parent.methods:
                method = parent.methods[node.method]
                args = [self.execute(a) for a in node.args]
                return self._call_function(method, args, instance)
            raise RuntimeError(f"Parent class has no method '{node.method}'")
        
        if isinstance(node, NewNode):
            klass = self.env.get(node.class_name)
            if not isinstance(klass, PurpClass):
                raise RuntimeError(f"'{node.class_name}' is not a class")
            
            instance = PurpInstance(klass)
            # Copy parent fields if inheritance
            if klass.parent:
                for field in klass.parent.fields:
                    if field not in instance.fields:
                        instance.fields[field] = NOTHING
            
            # Call constructor if exists
            args = [self.execute(a) for a in node.args]
            if 'init' in klass.methods:
                self._call_function(klass.methods['init'], args, instance)
            elif 'create' in klass.methods:
                self._call_function(klass.methods['create'], args, instance)
            
            return instance
        
        return NOTHING
    
    def _eval_binary(self, node: BinaryNode) -> Any:
        left = self.execute(node.left)
        
        # Short-circuit evaluation for and/or
        if node.op == TokenType.AND:
            if not self._is_truthy(left):
                return left
            return self.execute(node.right)
        
        if node.op == TokenType.OR:
            if self._is_truthy(left):
                return left
            return self.execute(node.right)
        
        right = self.execute(node.right)
        
        if node.op == TokenType.PLUS:
            if isinstance(left, str) or isinstance(right, str):
                return self._to_string(left) + self._to_string(right)
            return left + right
        if node.op == TokenType.MINUS:
            return left - right
        if node.op == TokenType.STAR:
            return left * right
        if node.op == TokenType.SLASH:
            return left / right
        if node.op == TokenType.PERCENT:
            return left % right
        if node.op == TokenType.CARET:
            return left ** right
        if node.op == TokenType.EQ:
            return left == right
        if node.op == TokenType.NEQ:
            return left != right
        if node.op == TokenType.LT:
            return left < right
        if node.op == TokenType.GT:
            return left > right
        if node.op == TokenType.LTE:
            return left <= right
        if node.op == TokenType.GTE:
            return left >= right
        
        return NOTHING
    
    def _eval_unary(self, node: UnaryNode) -> Any:
        operand = self.execute(node.operand)
        
        if node.op == TokenType.MINUS:
            return -operand
        if node.op == TokenType.NOT:
            return not self._is_truthy(operand)
        
        return NOTHING
    
    def _eval_call(self, node: CallNode) -> Any:
        callee = self.execute(node.callee)
        args = [self.execute(a) for a in node.args]
        
        # Native function
        if isinstance(callee, PurpNative):
            return callee.func(args)
        
        # Bound method
        if isinstance(callee, tuple) and len(callee) == 2:
            instance, method = callee
            return self._call_function(method, args, instance)
        
        # User-defined function
        if isinstance(callee, PurpFunction):
            return self._call_function(callee, args)
        
        # Lambda
        if isinstance(callee, PurpLambda):
            return self._call_lambda(callee, args)
        
        # Class instantiation
        if isinstance(callee, PurpClass):
            instance = PurpInstance(callee)
            # Call constructor if exists
            if 'init' in callee.methods:
                self._call_function(callee.methods['init'], args, instance)
            elif 'create' in callee.methods:
                self._call_function(callee.methods['create'], args, instance)
            return instance
        
        raise RuntimeError(f"Cannot call {callee}")
    
    def _call_function(self, func: PurpFunction, args: List[Any], instance: Any = None) -> Any:
        # Create new environment with closure as parent
        new_env = Environment(func.closure)
        
        # Bind 'this' if method call
        if instance:
            new_env.define('this', instance)
        
        # Bind parameters
        for i, param in enumerate(func.params):
            value = args[i] if i < len(args) else NOTHING
            new_env.define(param, value)
        
        # Execute body
        old_env = self.env
        self.env = new_env
        
        try:
            self.run(func.body)
            return NOTHING
        except ReturnException as ret:
            return ret.value
        finally:
            self.env = old_env
    
    def _call_lambda(self, func: PurpLambda, args: List[Any]) -> Any:
        # Create new environment with closure as parent
        new_env = Environment(func.closure)
        
        # Bind parameters
        for i, param in enumerate(func.params):
            value = args[i] if i < len(args) else NOTHING
            new_env.define(param, value)
        
        # Execute body (expression)
        old_env = self.env
        self.env = new_env
        
        try:
            return self.execute(func.body)
        finally:
            self.env = old_env
    
    def _is_truthy(self, value: Any) -> bool:
        if value is NOTHING or value is None:
            return False
        if isinstance(value, bool):
            return value
        if isinstance(value, (int, float)):
            return value != 0
        if isinstance(value, str):
            return len(value) > 0
        if isinstance(value, list):
            return len(value) > 0
        return True

# ============================================================================
# MAIN
# ============================================================================

def run_file(path: str):
    try:
        with open(path, 'r', encoding='utf-8') as f:
            source = f.read()
    except FileNotFoundError:
        print(f"Error: Could not find file '{path}'")
        return 1
    except Exception as e:
        print(f"Error reading file: {e}")
        return 1
    
    try:
        lexer = Lexer(source)
        tokens = lexer.tokenize()
        
        parser = Parser(tokens)
        ast = parser.parse()
        
        interpreter = Interpreter()
        interpreter.run(ast)
        return 0
    except SyntaxError as e:
        print(f"Syntax Error: {e}")
        return 1
    except Exception as e:
        print(f"Error: {e}")
        return 1

def run_repl():
    print(f"Purp {VERSION} - Interactive Mode")
    print("Type 'exit' or press Ctrl+C to quit\n")
    
    interpreter = Interpreter()
    
    while True:
        try:
            line = input(">>> ")
            
            if line.strip() in ('exit', 'quit'):
                break
            
            if not line.strip():
                continue
            
            lexer = Lexer(line)
            tokens = lexer.tokenize()
            
            parser = Parser(tokens)
            ast = parser.parse()
            
            result = interpreter.run(ast)
            
            if result is not NOTHING:
                print(interpreter._to_string(result))
                
        except KeyboardInterrupt:
            print("\n")
            break
        except EOFError:
            break
        except Exception as e:
            print(f"Error: {e}")
    
    print("Goodbye!")

def main():
    if len(sys.argv) < 2:
        run_repl()
        return 0
    
    arg = sys.argv[1]
    
    if arg in ('--version', '-v'):
        print(f"Purp Programming Language v{VERSION}")
        return 0
    
    if arg in ('--help', '-h'):
        print(f"Purp Programming Language v{VERSION}\n")
        print("Usage:")
        print("  purp                       Start interactive REPL")
        print("  purp <file.purp>           Run a Purp program")
        print("  purp --version, -v         Show version")
        print("  purp --help, -h            Show this help")
        print("\nExamples:")
        print("  purp hello.purp            Run a program")
        print("  purp                       Start REPL mode")
        return 0
    
    return run_file(arg)

if __name__ == "__main__":
    sys.exit(main())
