#!/usr/bin/env python3
"""
Chotulang - Enhanced Python-Compatible Language (Version 2.5.1)

New in 2.5.1:
1. FIXED: Removed all Syntax Warnings by implementing a robust sub-expression parser (chaining for '.', '()', '[]').
2. ENHANCED: ChotuSyntaxError now includes the source code line and a pointer to the error location.
3. ENHANCED: ChotuRuntimeError reporting for binary operations now includes the line number for better debugging.
4. Includes Lambda Functions (=>) from 2.5.0.
5. FIXED: Added 'FN' to Parser.primary() to allow anonymous function expressions like 'var x = fn(y) => y * 2'.
"""

import sys
import os
import math
import random
import time
import json
import importlib
import inspect
from typing import Any, List, Dict, Callable

# Error classes
class ChotuError(Exception):
    pass

class ChotuSyntaxError(ChotuError):
    def __init__(self, message, line=None, column=None, code_line=None):
        self.line = line
        self.column = column
        self.code_line = code_line
        full_msg = f"SyntaxError at line {line}, column {column}: {message}"
        if code_line:
            indicator = ' ' * (column - 1) + '^'
            full_msg += f"\nCode: {code_line.strip()}\n      {indicator}"
        super().__init__(full_msg)

class ChotuRuntimeError(ChotuError):
    def __init__(self, message, line=None):
        self.line = line
        if line is not None:
            super().__init__(f"RuntimeError at line {line}: {message}")
        else:
            super().__init__(f"RuntimeError: {message}")

# Flow Control Exceptions
class ReturnException(Exception):
    def __init__(self, value):
        self.value = value

class BreakException(Exception):
    pass

class ContinueException(Exception):
    pass

# Function/Closure Class
class ChotuFunction:
    """Represents a first-class function and its captured lexical scope (closure)."""
    def __init__(self, name, parameters, body, closure):
        self.name = name
        self.parameters = parameters
        self.body = body
        self.closure = closure  # The environment (dict) captured at definition time

    def __repr__(self):
        return f"<fn {self.name}({','.join(self.parameters)})>"

# --- AST NODES ---
class ASTNode: 
    def __init__(self, token=None):
        # Store the token that started this node for line number reporting
        self.token = token 
    def get_line(self):
        return self.token.line if self.token else None

class Program(ASTNode):
    def __init__(self, statements): super().__init__(); self.statements = statements
    def __repr__(self): return f"Program({self.statements})"

class PrintStatement(ASTNode):
    def __init__(self, expressions): super().__init__(); self.expressions = expressions
    def __repr__(self): return f"Print({self.expressions})"

class Assignment(ASTNode):
    def __init__(self, token, variable, value): super().__init__(token); self.variable = variable; self.value = value
    def __repr__(self): return f"Assign({self.variable} = {self.value})"

class Variable(ASTNode):
    def __init__(self, token, name): super().__init__(token); self.name = name
    def __repr__(self): return f"Var({self.name})"

class Number(ASTNode):
    def __init__(self, token, value): super().__init__(token); self.value = value
    def __repr__(self): return f"Num({self.value})"

class String(ASTNode):
    def __init__(self, token, value): super().__init__(token); self.value = value
    def __repr__(self): return f"Str({self.value})"

class Boolean(ASTNode):
    def __init__(self, token, value): super().__init__(token); self.value = value
    def __repr__(self): return f"Bool({self.value})"

class BinaryOp(ASTNode):
    def __init__(self, token, left, op, right): super().__init__(token); self.left = left; self.op = op; self.right = right
    def __repr__(self): return f"BinOp({self.left} {self.op} {self.right})"

class IfStatement(ASTNode):
    def __init__(self, condition, then_branch, else_branch=None): super().__init__(); self.condition = condition; self.then_branch = then_branch; self.else_branch = else_branch
    def __repr__(self): return f"If({self.condition}, then={self.then_branch}, else={self.else_branch})"

class WhileLoop(ASTNode):
    def __init__(self, condition, body): super().__init__(); self.condition = condition; self.body = body
    def __repr__(self): return f"While({self.condition}, {self.body})"

class ForLoop(ASTNode):
    def __init__(self, variable, iterable, body): super().__init__(); self.variable = variable; self.iterable = iterable; self.body = body
    def __repr__(self): return f"For({self.variable} in {self.iterable}, {self.body})"

class FunctionDef(ASTNode):
    def __init__(self, name, parameters, body): super().__init__(); self.name = name; self.parameters = parameters; self.body = body
    def __repr__(self): return f"Function({self.name}, params={self.parameters}, body={self.body})"

class FunctionCall(ASTNode):
    def __init__(self, token, name, arguments): super().__init__(token); self.name = name; self.arguments = arguments
    def __repr__(self): return f"Call({self.name}, args={self.arguments})"

class ReturnStatement(ASTNode):
    def __init__(self, value): super().__init__(); self.value = value
    def __repr__(self): return f"Return({self.value})"

class List(ASTNode):
    def __init__(self, token, elements): super().__init__(token); self.elements = elements
    def __repr__(self): return f"List({self.elements})"

class Dict(ASTNode):
    def __init__(self, token, pairs): super().__init__(token); self.pairs = pairs
    def __repr__(self): return f"Dict({self.pairs})"

class Index(ASTNode):
    def __init__(self, token, obj, index): super().__init__(token); self.obj = obj; self.index = index
    def __repr__(self): return f"Index({self.obj}[{self.index}])"

class MethodCall(ASTNode):
    def __init__(self, token, obj, method, arguments): super().__init__(token); self.obj = obj; self.method = method; self.arguments = arguments
    def __repr__(self): return f"MethodCall({self.obj}.{self.method}({self.arguments}))"

class AttributeAccess(ASTNode):
    def __init__(self, token, obj, attribute): super().__init__(token); self.obj = obj; self.attribute = attribute
    def __repr__(self): return f"AttributeAccess({self.obj}.{self.attribute})"

class TypeConversion(ASTNode):
    def __init__(self, token, type_name, expression): super().__init__(token); self.type_name = type_name; self.expression = expression
    def __repr__(self): return f"TypeConv({self.type_name}({self.expression}))"

class ImportStatement(ASTNode):
    def __init__(self, module, alias=None): super().__init__(); self.module = module; self.alias = alias
    def __repr__(self): return f"Import({self.module} as {self.alias})"

class TryCatch(ASTNode):
    def __init__(self, try_block, catch_block, error_var=None): super().__init__(); self.try_block = try_block; self.catch_block = catch_block; self.error_var = error_var
    def __repr__(self): return f"TryCatch(try={self.try_block}, catch={self.catch_block}, error_var={self.error_var})"

class PipeExpression(ASTNode):
    def __init__(self, token, left, right): super().__init__(token); self.left = left; self.right = right
    def __repr__(self): return f"Pipe({self.left} |> {self.right})"

class RangeExpression(ASTNode):
    def __init__(self, token, start, end, step=None): super().__init__(token); self.start = start; self.end = end; self.step = step
    def __repr__(self): return f"Range({self.start}..{self.end})"

class NullCoalesce(ASTNode):
    def __init__(self, token, left, right): super().__init__(token); self.left = left; self.right = right
    def __repr__(self): return f"NullCoalesce({self.left} ?? {self.right})"

class StrictStatement(ASTNode):
    def __init__(self, token, value): super().__init__(token); self.value = value
    def __repr__(self): return f"Strict({self.value})"

class Lambda(ASTNode):
    def __init__(self, token, parameters, body): super().__init__(token); self.parameters = parameters; self.body = body
    def __repr__(self): return f"Lambda(params={self.parameters}, body={self.body})"

# --- LEXER ---
class Token:
    def __init__(self, type, value, line, column):
        self.type = type
        self.value = value
        self.line = line
        self.column = column
    def __repr__(self): return f"Token({self.type}, {repr(self.value)}, line={self.line}, col={self.column})"

class Lexer:
    KEYWORDS = {
        'out': 'OUT', 'var': 'VAR', 'if': 'IF', 'else': 'ELSE', 'do': 'DO',
        'for': 'FOR', 'in': 'IN', 'fn': 'FN', 'ret': 'RET',
        'true': 'TRUE', 'false': 'FALSE', 'and': 'AND', 'or': 'OR', 'not': 'NOT',
        'use': 'USE', 'as': 'AS', 'try': 'TRY', 'catch': 'CATCH',
        'null': 'NULL', 'break': 'BREAK', 'next': 'NEXT',
        'strict': 'STRICT' 
    }
    
    def __init__(self, code):
        self.code = code
        self.tokens = []
    
    def tokenize(self):
        lines = self.code.split('\n')
        line_num = 1
        
        for line in lines:
            column = 1
            i = 0
            while i < len(line):
                char = line[i]
                if char in ' \t':
                    i += 1; column += 1; continue
                if char == '#': break
                
                if char in ('"', "'"):
                    quote_char = char
                    string_content = ''
                    start_column = column
                    i += 1; column += 1
                    while i < len(line) and line[i] != quote_char:
                        if line[i] == '\\':
                            i += 1; column += 1
                            if i < len(line):
                                if line[i] == 'n': string_content += '\n'
                                elif line[i] == 't': string_content += '\t'
                                elif line[i] == '\\': string_content += '\\'
                                elif line[i] == quote_char: string_content += quote_char
                                else: string_content += '\\' + line[i]
                        else:
                            string_content += line[i]
                        i += 1; column += 1
                    if i < len(line) and line[i] == quote_char:
                        i += 1; column += 1
                    elif i == len(line) and line_num == len(lines):
                         pass
                    elif i == len(line):
                         pass
                    
                    self.tokens.append(Token('STRING', string_content, line_num, start_column))
                    continue
                
                if char.isdigit():
                    num_str = ''
                    start_column = column
                    while i < len(line) and (line[i].isdigit() or line[i] == '.'):
                        if line[i] == '.' and i + 1 < len(line) and line[i+1] == '.': break
                        num_str += line[i]
                        i += 1; column += 1
                    self.tokens.append(Token('NUMBER', num_str, line_num, start_column))
                    continue
                
                if char.isalpha() or char == '_':
                    id_str = ''
                    start_column = column
                    while i < len(line) and (line[i].isalnum() or line[i] == '_'):
                        id_str += line[i]
                        i += 1; column += 1
                    if id_str in self.KEYWORDS:
                        self.tokens.append(Token(self.KEYWORDS[id_str], id_str, line_num, start_column))
                    else:
                        self.tokens.append(Token('ID', id_str, line_num, start_column))
                    continue
                
                if char in '+-*/%=<>!&|?.':
                    if i + 1 < len(line):
                        # FIX: Check for C++ style single-line comment (//)
                        if char == '/' and line[i+1] == '/':
                            break # Skip the rest of the line
                            
                        two_char = char + line[i+1]
                        if two_char in ('|>', '??', '..', '=>'):
                            type_map = {'|>': 'PIPE', '??': 'NULL_COALESCE', '..': 'RANGE', '=>': 'LAMBDA_ARROW'}
                            self.tokens.append(Token(type_map[two_char], two_char, line_num, column))
                            i += 2; column += 2; continue
                    
                    if char in '+-*/%=<>!&|':
                        op_str = char
                        start_column = column
                        i += 1; column += 1
                        while i < len(line) and line[i] in '=<>':
                            op_str += line[i]; i += 1; column += 1
                        self.tokens.append(Token('OP', op_str, line_num, start_column))
                        continue
                    elif char == '?':
                        self.tokens.append(Token('OP', char, line_num, column))
                        i += 1; column += 1; continue
                    elif char == '.':
                        self.tokens.append(Token('DOT', char, line_num, column))
                        i += 1; column += 1; continue
                
                char_map = {'(': 'LPAREN', ')': 'RPAREN', '[': 'LBRACKET', ']': 'RBRACKET',
                            '{': 'LBRACE', '}': 'RBRACE', ',': 'COMMA', ':': 'COLON', ';': 'SEMICOLON'}
                if char in char_map:
                    self.tokens.append(Token(char_map[char], char, line_num, column))
                else:
                    raise ChotuSyntaxError(f"Unexpected character: {char}", line_num, column)
                i += 1; column += 1
            
            if line.strip() and not line.strip().startswith('#'):
                self.tokens.append(Token('NEWLINE', '\n', line_num, column))
            line_num += 1
        
        self.tokens.append(Token('EOF', '', line_num, 1))
        return self.tokens

# --- PARSER ---
class Parser:
    # MODIFIED: Takes code_string to enable contextual error reporting
    def __init__(self, tokens, code_string):
        self.tokens = tokens
        self.code_lines = code_string.split('\n')
        self.pos = 0
        self.current_token = self.tokens[0] if self.tokens else None
    
    def peek(self, offset=1):
        target_pos = self.pos + offset
        if target_pos < len(self.tokens): return self.tokens[target_pos]
        return Token('EOF', '', -1, -1)

    # MODIFIED: Uses new ChotuSyntaxError with code context
    def eat(self, token_type):
        if self.current_token and self.current_token.type == token_type:
            token = self.current_token
            self.pos += 1
            if self.pos < len(self.tokens): self.current_token = self.tokens[self.pos]
            else: self.current_token = None
            return token
        else:
            expected = token_type
            got = self.current_token.type if self.current_token else "EOF"
            line = self.current_token.line if self.current_token and self.current_token.line != -1 else "unknown"
            column = self.current_token.column if self.current_token and self.current_token.column != -1 else "unknown"
            
            code_line = self.code_lines[line - 1] if isinstance(line, int) and line > 0 and line <= len(self.code_lines) else None
            
            raise ChotuSyntaxError(f"Expected {expected}, got {got}", line, column, code_line)
    
    def skip_newlines(self):
        while self.current_token and self.current_token.type == 'NEWLINE': self.eat('NEWLINE')

    def parse(self):
        if not self.tokens: return Program([])
        return self.program()
    
    # MODIFIED: Removed warning print in favor of only printing the actual error and trying to recover.
    def program(self):
        statements = []
        while self.current_token and self.current_token.type != 'EOF':
            try:
                stmt = self.statement()
                if stmt: statements.append(stmt)
            except ChotuSyntaxError as e:
                # Print the full contextual error message only once
                print(f"{e}")
                
                # Token skipping for error recovery
                while self.current_token and self.current_token.type not in ['NEWLINE', 'EOF', 'RBRACE', 'SEMICOLON']:
                    try:
                        self.eat(self.current_token.type)
                    except ChotuSyntaxError:
                        break # Stop if we can't even eat the next token
                if self.current_token and self.current_token.type in ['NEWLINE', 'SEMICOLON']: self.eat(self.current_token.type)
                if self.current_token and self.current_token.type == 'RBRACE': self.eat('RBRACE')
        return Program(statements)
    
    def statement(self):
        if not self.current_token: return None
        if self.current_token.type in ['NEWLINE', 'SEMICOLON']:
            self.eat(self.current_token.type); return None
        if self.current_token.type == 'RBRACE': return None

        if self.current_token.type == 'OUT': return self.print_statement()
        elif self.current_token.type == 'VAR': return self.assignment_statement()
        elif self.current_token.type == 'ID':
            next_token = self.peek()
            assignment_ops = ['=', '+=', '-=', '*=', '/=', '%=']
            if next_token and next_token.type == 'OP' and next_token.value in assignment_ops:
                return self.assignment_or_reassignment()
            return self.expression_statement()
        elif self.current_token.type == 'IF': return self.if_statement()
        elif self.current_token.type == 'DO': return self.while_statement()
        elif self.current_token.type == 'FOR': return self.for_statement()
        elif self.current_token.type == 'FN': return self.function_definition()
        elif self.current_token.type == 'RET': return self.return_statement()
        elif self.current_token.type == 'USE': return self.import_statement()
        elif self.current_token.type == 'TRY': return self.try_catch_statement()
        elif self.current_token.type == 'BREAK': return self.break_statement()
        elif self.current_token.type == 'NEXT': return self.continue_statement()
        elif self.current_token.type == 'STRICT': return self.strict_statement()
        else: return self.expression_statement()
    
    def print_statement(self):
        self.eat('OUT')
        expressions = []
        while self.current_token and self.current_token.type not in ['NEWLINE', 'EOF', 'RBRACE', 'SEMICOLON']:
            expressions.append(self.expression())
            if self.current_token.type == 'COMMA': self.eat('COMMA')
        return PrintStatement(expressions)
    
    def assignment_statement(self):
        start_token = self.eat('VAR')
        var_name = self.current_token.value; self.eat('ID')
        self.eat('OP'); value = self.expression() # Should be '=' for 'var'
        return Assignment(start_token, Variable(start_token, var_name), value)
    
    def assignment_or_reassignment(self):
        start_token = self.current_token
        var_name = self.current_token.value; self.eat('ID')
        op_token = self.eat('OP') # Consumes '=', '+=', '-=', etc.
        value = self.expression()

        if op_token.value != '=':
            binary_op = op_token.value[:-1]
            new_value = BinaryOp(op_token, Variable(start_token, var_name), binary_op, value)
            return Assignment(op_token, Variable(start_token, var_name), new_value)
        
        return Assignment(op_token, Variable(start_token, var_name), value)

    def return_statement(self):
        self.eat('RET')
        value = None
        if self.current_token and self.current_token.type not in ['NEWLINE', 'RBRACE', 'EOF', 'SEMICOLON']:
            value = self.expression()
        return ReturnStatement(value)
    
    def break_statement(self): self.eat('BREAK'); return BreakException() 
    def continue_statement(self): self.eat('NEXT'); return ContinueException()
    
    def strict_statement(self):
        start_token = self.eat('STRICT')
        value = self.expression() # Expects 'true' or 'false'
        return StrictStatement(start_token, value)

    def _parse_block(self):
        self.eat('LBRACE'); self.skip_newlines()
        statements = []
        while self.current_token and self.current_token.type != 'RBRACE':
            stmt = self.statement()
            if stmt: statements.append(stmt)
            if self.current_token and self.current_token.type == 'EOF':
                raise ChotuSyntaxError("Unexpected end of file inside block")
        self.eat('RBRACE')
        return statements

    def if_statement(self):
        self.eat('IF'); condition = self.expression()
        then_branch = self._parse_block() if self.current_token.type == 'LBRACE' else [self.statement()]
        else_branch = None; self.skip_newlines()
        if self.current_token and self.current_token.type == 'ELSE':
            self.eat('ELSE')
            else_branch = self._parse_block() if self.current_token.type == 'LBRACE' else [self.statement()]
        return IfStatement(condition, then_branch, else_branch)
    
    def while_statement(self):
        self.eat('DO'); condition = self.expression()
        body = self._parse_block() if self.current_token.type == 'LBRACE' else [self.statement()]
        return WhileLoop(condition, body)
    
    def for_statement(self):
        self.eat('FOR'); var_token = self.current_token; var_name = self.current_token.value; self.eat('ID')
        self.eat('IN'); iterable = self.expression()
        body = self._parse_block() if self.current_token.type == 'LBRACE' else [self.statement()]
        return ForLoop(Variable(var_token, var_name), iterable, body)
    
    def function_definition(self):
        self.eat('FN'); func_name = self.current_token.value; self.eat('ID')
        parameters = []
        if self.current_token.type == 'LPAREN':
            self.eat('LPAREN')
            if self.current_token.type != 'RPAREN':
                parameters.append(self.current_token.value); self.eat('ID')
                while self.current_token.type == 'COMMA':
                    self.eat('COMMA'); parameters.append(self.current_token.value); self.eat('ID')
            self.eat('RPAREN')
        body = self._parse_block() if self.current_token.type == 'LBRACE' else [self.statement()]
        return FunctionDef(func_name, parameters, body)
    
    def import_statement(self):
        self.eat('USE'); module = self.current_token.value; self.eat(self.current_token.type)
        alias = None
        if self.current_token and self.current_token.type == 'AS':
            self.eat('AS'); alias = self.current_token.value; self.eat('ID')
        return ImportStatement(module, alias)
    
    def try_catch_statement(self):
        self.eat('TRY'); try_block = self._parse_block(); self.skip_newlines()
        self.eat('CATCH'); error_var = None
        if self.current_token.type == 'ID': error_var = self.current_token.value; self.eat('ID')
        catch_block = self._parse_block()
        return TryCatch(try_block, catch_block, error_var)
    
    def expression_statement(self): return self.expression()
    def expression(self): return None if not self.current_token else self.null_coalesce()
    
    def null_coalesce(self):
        node = self.pipe()
        while self.current_token and self.current_token.type == 'NULL_COALESCE':
            token = self.eat('NULL_COALESCE'); node = NullCoalesce(token, node, self.pipe())
        return node
    
    def pipe(self):
        node = self.logical_or()
        while self.current_token and self.current_token.type == 'PIPE':
            token = self.eat('PIPE'); node = PipeExpression(token, node, self.logical_or())
        return node
    
    def logical_or(self):
        node = self.logical_and()
        while self.current_token and self.current_token.type == 'OR':
            token = self.eat('OR'); node = BinaryOp(token, node, 'or', self.logical_and())
        return node
    
    def logical_and(self):
        node = self.comparison()
        while self.current_token and self.current_token.type == 'AND':
            token = self.eat('AND'); node = BinaryOp(token, node, 'and', self.comparison())
        return node
    
    def comparison(self):
        node = self.range_expr()
        while self.current_token and self.current_token.type == 'OP' and self.current_token.value in ['<', '>', '<=', '>=', '==', '!=']:
            op_token = self.eat('OP'); node = BinaryOp(op_token, node, op_token.value, self.range_expr())
        return node
    
    def range_expr(self):
        node = self.term()
        if self.current_token and self.current_token.type == 'RANGE':
            range_token = self.eat('RANGE'); end = self.term(); step = None
            if self.current_token and self.current_token.type == 'RANGE':
                self.eat('RANGE'); step = self.term()
            return RangeExpression(range_token, node, end, step)
        return node
    
    def term(self):
        node = self.factor()
        while self.current_token and self.current_token.type == 'OP' and self.current_token.value in ['+', '-']:
            op_token = self.eat('OP'); node = BinaryOp(op_token, node, op_token.value, self.factor())
        return node
    
    def factor(self):
        node = self.unary()
        while self.current_token and self.current_token.type == 'OP' and self.current_token.value in ['*', '/', '%']:
            op_token = self.eat('OP'); node = BinaryOp(op_token, node, op_token.value, self.unary())
        return node
    
    def unary(self):
        if self.current_token and self.current_token.type == 'NOT':
            not_token = self.eat('NOT'); return BinaryOp(not_token, Boolean(not_token, False), '==', self.primary())
        elif self.current_token and self.current_token.type == 'OP' and self.current_token.value in ['+', '-']:
            op_token = self.eat('OP'); return BinaryOp(op_token, Number(op_token, 0), op_token.value, self.primary())
        return self.primary()
    
    # REUSED: This function handles parsing from the starting LPAREN of a lambda definition
    def lambda_expression(self, start_token):
        # We assume primary() has handled the initial token (FN or LPAREN)
        self.eat('LPAREN')
        parameters = []
        if self.current_token.type != 'RPAREN':
            parameters.append(self.current_token.value); self.eat('ID')
            while self.current_token.type == 'COMMA':
                self.eat('COMMA'); parameters.append(self.current_token.value); self.eat('ID')
        self.eat('RPAREN')
        
        self.eat('LAMBDA_ARROW') # Consume the '=>' token
        
        body = self.expression() 
        
        return Lambda(start_token, parameters, body)
    
    # NEW: Robust handler for method calls, attribute access, and indexing chains
    def sub_expression(self, node):
        """Handle chain of indexing, method calls, and attribute access."""
        while self.current_token and self.current_token.type in ['DOT', 'LBRACKET']:
            token = self.current_token
            
            if token.type == 'DOT':
                dot_token = self.eat('DOT'); attr_name = self.current_token.value; self.eat('ID')
                
                if self.current_token.type == 'LPAREN':
                    self.eat('LPAREN'); arguments = []
                    if self.current_token.type != 'RPAREN':
                        arguments.append(self.expression())
                        while self.current_token.type == 'COMMA':
                            self.eat('COMMA'); arguments.append(self.expression())
                    self.eat('RPAREN')
                    node = MethodCall(dot_token, node, attr_name, arguments)
                else:
                    node = AttributeAccess(dot_token, node, attr_name)
            
            elif token.type == 'LBRACKET':
                lbracket_token = self.eat('LBRACKET'); index = self.expression(); self.eat('RBRACKET')
                node = Index(lbracket_token, node, index)
        return node

    def primary(self):
        if not self.current_token: raise ChotuSyntaxError("Unexpected end of input")
        token = self.current_token
        
        if token.type == 'NUMBER':
            value = token.value; self.eat('NUMBER')
            if value.endswith('.') and self.current_token.type == 'RANGE': return Number(token, int(value[:-1]))
            node = Number(token, float(value) if '.' in value else int(value))
        elif token.type == 'STRING':
            self.eat('STRING'); node = String(token, token.value)
        elif token.type == 'TRUE': self.eat('TRUE'); node = Boolean(token, True)
        elif token.type == 'FALSE': self.eat('FALSE'); node = Boolean(token, False)
        elif token.type == 'NULL': self.eat('NULL'); return None
        
        # --- FIX START: Allow 'fn' to start an anonymous function expression ---
        elif token.type == 'FN':
            self.eat('FN')
            if self.current_token.type != 'LPAREN':
                 raise ChotuSyntaxError("Expected '(' after 'fn' for an anonymous function expression.", line=token.line, column=token.column)
            return self.lambda_expression(token)
        # --- FIX END ---
        
        elif token.type == 'ID':
            name = token.value; self.eat('ID')
            
            # Type Conversion (num(expr), str(expr), etc.)
            if name in ['num', 'str', 'bool', 'list', 'dict'] and self.current_token.type == 'LPAREN':
                self.eat('LPAREN'); expr = self.expression(); self.eat('RPAREN')
                node = TypeConversion(token, name, expr)
            
            # Function Call (ID(args))
            elif self.current_token.type == 'LPAREN':
                self.eat('LPAREN'); arguments = []
                if self.current_token.type != 'RPAREN':
                    arguments.append(self.expression())
                    while self.current_token.type == 'COMMA':
                        self.eat('COMMA'); arguments.append(self.expression())
                self.eat('RPAREN'); node = FunctionCall(token, name, arguments)
            
            else:
                node = Variable(token, name)
            
            # Use the robust handler for all subsequent chaining
            return self.sub_expression(node)
                
        elif token.type == 'LPAREN':
            is_lambda = False
            if self.peek(1).type == 'ID' or self.peek(1).type == 'RPAREN':
                peek_pos = 1
                while self.peek(peek_pos).type in ['ID', 'COMMA', 'RPAREN']:
                    if self.peek(peek_pos).type == 'RPAREN':
                        if self.peek(peek_pos + 1).type == 'LAMBDA_ARROW':
                            is_lambda = True
                            break
                    peek_pos += 1

            if is_lambda:
                # Standard lambda: (params) => body
                return self.lambda_expression(token)

            # Standard grouping: (expression)
            self.eat('LPAREN'); node = self.expression(); self.eat('RPAREN'); return node

        elif token.type == 'LBRACKET': node = self.list(token)
        elif token.type == 'LBRACE': node = self.dict(token)
        else: raise ChotuSyntaxError(f"Unexpected token: {token.type}")
        
        # Ensures that a list or dict is also checked for chaining.
        return self.sub_expression(node)
    
    def list(self, start_token):
        self.eat('LBRACKET'); self.skip_newlines(); elements = []
        if self.current_token.type != 'RBRACKET':
            elements.append(self.expression())
            while self.current_token.type == 'COMMA':
                self.eat('COMMA'); self.skip_newlines(); elements.append(self.expression())
        self.skip_newlines(); self.eat('RBRACKET'); return List(start_token, elements)
    
    def dict(self, start_token):
        self.eat('LBRACE'); self.skip_newlines(); pairs = []
        if self.current_token.type != 'RBRACE':
            key = self.expression(); self.eat('COLON'); value = self.expression()
            pairs.append((key, value))
            while self.current_token.type == 'COMMA':
                self.eat('COMMA'); self.skip_newlines(); key = self.expression()
                self.eat('COLON'); value = self.expression(); pairs.append((key, value))
        self.skip_newlines(); self.eat('RBRACE'); return Dict(start_token, pairs)

# --- Built-in Functions and Interop ---

class BuiltInFunctions:
    @staticmethod
    def len(obj): return len(obj)
    @staticmethod
    def range(start, end=None, step=1):
        return list(range(0, int(start), int(step))) if end is None else list(range(int(start), int(end), int(step)))
    @staticmethod
    def input(prompt=""): return input(prompt)
    
    # ADDED: ask function - alias for input with clearer naming
    @staticmethod
    def ask(prompt=""): return input(prompt)
    
    @staticmethod
    def type(obj):
        if obj is None: return "null"
        if isinstance(obj, bool): return "bool"
        if isinstance(obj, (int, float)): return "num"
        if isinstance(obj, str): return "str"
        if isinstance(obj, list): return "list"
        if isinstance(obj, dict): return "dict"
        if isinstance(obj, ChotuFunction): return "function"
        return type(obj).__name__
    @staticmethod
    def sqrt(x): return math.sqrt(float(x))
    @staticmethod
    def abs(x): return abs(float(x))
    @staticmethod
    def round(x, digits=0): return round(float(x), int(digits))
    @staticmethod
    def random(): return random.random()
    @staticmethod
    def randint(a, b): return random.randint(int(a), int(b))
    @staticmethod
    def time(): return time.time()
    @staticmethod
    def sleep(seconds): time.sleep(float(seconds))
    @staticmethod
    def min(*args): return min([float(x) for x in args])
    @staticmethod
    def max(*args): return max([float(x) for x in args])
    @staticmethod
    def sum(lst): 
        try: return sum([float(x) for x in lst])
        except TypeError: raise ChotuRuntimeError("sum() only works on lists containing numbers")
    @staticmethod
    def sorted(lst, reverse=False): return sorted(lst, reverse=bool(reverse))
    @staticmethod
    def reversed(lst): return list(reversed(lst))
    @staticmethod
    def keys(obj): return list(obj.keys())
    @staticmethod
    def values(obj): return list(obj.values())
    @staticmethod
    def items(obj): return list(obj.items())
    @staticmethod
    def read_file(filename):
        with open(filename, 'r') as f: return f.read()
    @staticmethod
    def write_file(filename, content):
        with open(filename, 'w') as f: f.write(str(content))
    @staticmethod
    def append_file(filename, content):
        with open(filename, 'a') as f: f.write(str(content))
    @staticmethod
    def json_parse(string): return json.loads(string)
    @staticmethod
    def json_stringify(obj): return json.dumps(obj)
    @staticmethod
    def flat(lst):
        result = []
        for item in lst:
            if isinstance(item, list): result.extend(BuiltInFunctions.flat(item))
            else: result.append(item)
        return result
    @staticmethod
    def zip(*lists): return list(zip(*lists))
    @staticmethod
    def enumerate(lst, start=0): return list(enumerate(lst, int(start)))

class PythonInterop:
    @staticmethod
    def import_module(module_name, alias=None):
        return importlib.import_module(module_name)
    @staticmethod
    def call_python_function(func, *args, **kwargs):
        return func(*args, **kwargs) if callable(func) else None
    @staticmethod
    def get_attribute(obj, attr): return getattr(obj, attr)
    @staticmethod
    def set_attribute(obj, attr, value): setattr(obj, attr, value)


# --- INTERPRETER ---
class Interpreter:
    def __init__(self):
        self.variables = {}
        self.modules = {}
        self.in_function_call = False
        self.strict_mode = False 
        self.last_executed_node = None # For better error reporting
        
        self.builtins = {
            'len': BuiltInFunctions.len, 
            'range': BuiltInFunctions.range,
            'input': BuiltInFunctions.input,
            'ask': BuiltInFunctions.ask,  # ADDED: ask function
            'type': BuiltInFunctions.type,
            'sqrt': BuiltInFunctions.sqrt, 
            'abs': BuiltInFunctions.abs,
            'round': BuiltInFunctions.round, 
            'random': BuiltInFunctions.random,
            'randint': BuiltInFunctions.randint, 
            'time': BuiltInFunctions.time,
            'sleep': BuiltInFunctions.sleep, 
            'min': BuiltInFunctions.min,
            'max': BuiltInFunctions.max, 
            'sum': BuiltInFunctions.sum,
            'sorted': BuiltInFunctions.sorted, 
            'reversed': BuiltInFunctions.reversed,
            'keys': BuiltInFunctions.keys, 
            'values': BuiltInFunctions.values,
            'items': BuiltInFunctions.items, 
            'read_file': BuiltInFunctions.read_file,
            'write_file': BuiltInFunctions.write_file, 
            'append_file': BuiltInFunctions.append_file,
            'json_parse': BuiltInFunctions.json_parse, 
            'json_stringify': BuiltInFunctions.json_stringify,
            
            # Type conversions
            'num': lambda v: 1 if v is True else 0 if v is False else float(v),
            'str': str,
            'bool': bool,
            'list': lambda v: list(v) if isinstance(v, (str, list, dict, tuple)) else [v],

            # Functional built-ins (need to call Chotu functions)
            'map': lambda f, it: self.builtin_map(f, it),
            'filter': lambda f, it: self.builtin_filter(f, it),
            'reduce': lambda f, it, init=None: self.builtin_reduce(f, it, init),

            'flat': BuiltInFunctions.flat,
            'zip': BuiltInFunctions.zip, 
            'enumerate': BuiltInFunctions.enumerate,
            'py_import': PythonInterop.import_module, 
            'py_call': PythonInterop.call_python_function,
            'py_get': PythonInterop.get_attribute, 
            'py_set': PythonInterop.set_attribute
        }
    
    def builtin_map(self, func, iterable):
        if not callable(func) and not isinstance(func, ChotuFunction):
             raise ChotuRuntimeError("map() expects a function as first argument")
        
        result = []
        if isinstance(func, ChotuFunction):
            for x in iterable:
                result.append(self.call_user_func(func, [x]))
        else:
            for x in iterable:
                result.append(func(x))
        return result

    def builtin_filter(self, func, iterable):
        if not callable(func) and not isinstance(func, ChotuFunction):
             raise ChotuRuntimeError("filter() expects a function as first argument")
        
        result = []
        if isinstance(func, ChotuFunction):
            for x in iterable:
                if self.call_user_func(func, [x]):
                    result.append(x)
        else:
            for x in iterable:
                if func(x):
                    result.append(x)
        return result

    def builtin_reduce(self, func, iterable, initial=None):
        if not callable(func) and not isinstance(func, ChotuFunction):
             raise ChotuRuntimeError("reduce() expects a function as first argument")
        
        it = iter(iterable)
        if initial is None:
            try:
                value = next(it)
            except StopIteration:
                raise ChotuRuntimeError("reduce() of empty sequence with no initial value")
        else:
            value = initial
            
        if isinstance(func, ChotuFunction):
            for element in it:
                value = self.call_user_func(func, [value, element])
        else:
            for element in it:
                value = func(value, element)
        return value

    def interpret(self, node):
        if not isinstance(node, (Number, String, Boolean, Variable)):
            self.last_executed_node = node # Update current node for better error tracing

        try:
            if isinstance(node, Program): return self.visit_program(node)
            elif isinstance(node, PrintStatement): return self.visit_print(node)
            elif isinstance(node, Assignment): return self.visit_assignment(node)
            elif isinstance(node, IfStatement): return self.visit_if(node)
            elif isinstance(node, WhileLoop): return self.visit_while(node)
            elif isinstance(node, ForLoop): return self.visit_for(node)
            elif isinstance(node, FunctionDef): return self.visit_function_def(node)
            elif isinstance(node, FunctionCall): return self.visit_function_call(node)
            elif isinstance(node, ReturnStatement): return self.visit_return(node)
            elif isinstance(node, BreakException): raise node
            elif isinstance(node, ContinueException): raise node
            elif isinstance(node, BinaryOp): return self.visit_binary_op(node)
            elif isinstance(node, Variable): return self.visit_variable(node)
            elif isinstance(node, (Number, String, Boolean)): return node.value
            elif isinstance(node, List): return self.visit_list(node)
            elif isinstance(node, Dict): return self.visit_dict(node)
            elif isinstance(node, Index): return self.visit_index(node)
            elif isinstance(node, MethodCall): return self.visit_method_call(node)
            elif isinstance(node, AttributeAccess): return self.visit_attribute_access(node)
            elif isinstance(node, TypeConversion): return self.visit_type_conversion(node)
            elif isinstance(node, ImportStatement): return self.visit_import(node)
            elif isinstance(node, TryCatch): return self.visit_try_catch(node)
            elif isinstance(node, PipeExpression): return self.visit_pipe(node)
            elif isinstance(node, RangeExpression): return self.visit_range(node)
            elif isinstance(node, NullCoalesce): return self.visit_null_coalesce(node)
            elif isinstance(node, StrictStatement): return self.visit_strict(node)
            elif isinstance(node, Lambda): return self.visit_lambda(node)
            elif node is None: return None
            else: raise ChotuRuntimeError(f"Unknown node type: {type(node)}")
        except (ReturnException, BreakException, ContinueException): raise
        except ChotuRuntimeError: raise
        except Exception as e: 
            # Catch all unhandled exceptions and report with line number if possible
            line = self.last_executed_node.get_line() if self.last_executed_node else None
            raise ChotuRuntimeError(f"Internal error: {str(e)}", line=line)
    
    def visit_program(self, node):
        result = None
        for statement in node.statements: result = self.interpret(statement)
        return result
    
    def visit_print(self, node):
        values = []
        for expr in node.expressions:
            v = self.interpret(expr)
            if v is None: values.append('null')
            elif isinstance(v, ChotuFunction): values.append(repr(v))
            else: values.append(str(v))
        print(" ".join(values))
    
    def visit_assignment(self, node):
        var_name = node.variable.name
        value = self.interpret(node.value)
        
        if self.strict_mode and var_name in self.variables:
            old_val = self.variables[var_name]
            old_type = self.builtins['type'](old_val)
            new_type = self.builtins['type'](value)
            
            if old_type != 'null' and new_type != 'null' and old_type != new_type:
                raise ChotuRuntimeError(f"Strict mode violation: Cannot assign type '{new_type}' to variable '{var_name}' (which is of type '{old_type}').", line=node.get_line())
                
        self.variables[var_name] = value
        return value
    
    def visit_strict(self, node):
        value = self.interpret(node.value)
        if not isinstance(value, bool):
            raise ChotuRuntimeError("strict mode must be set to true or false.", line=node.get_line())
        self.strict_mode = value
        return value

    def visit_return(self, node):
        value = self.interpret(node.value) if node.value is not None else None
        if not self.in_function_call:
            raise ChotuRuntimeError("'ret' used outside of a function.", line=node.get_line())
        raise ReturnException(value)

    def visit_if(self, node):
        condition = self.interpret(node.condition)
        if condition:
            for statement in node.then_branch: self.interpret(statement)
        elif node.else_branch:
            for statement in node.else_branch: self.interpret(statement)
    
    def visit_while(self, node):
        try:
            while self.interpret(node.condition):
                try:
                    for statement in node.body: self.interpret(statement)
                except ContinueException: continue
                except BreakException: break
        except BreakException: pass
    
    def visit_for(self, node):
        iterable = self.interpret(node.iterable); var_name = node.variable.name
        try:
            for item in iterable:
                self.variables[var_name] = item
                try:
                    for statement in node.body: self.interpret(statement)
                except ContinueException: continue
                except BreakException: break
        except BreakException: pass
    
    def visit_function_def(self, node):
        closure = self.variables.copy()
        func_obj = ChotuFunction(node.name, node.parameters, node.body, closure)
        
        # Recursion fix: Add the function to its own closure
        func_obj.closure[node.name] = func_obj

        self.variables[node.name] = func_obj
        return None
    
    def visit_lambda(self, node):
        closure = self.variables.copy()
        # Lambda body is an expression, wrap it in a return statement for execution
        body = [ReturnStatement(node.body)] 
        
        func_obj = ChotuFunction(
            name="<lambda>", 
            parameters=node.parameters, 
            body=body, 
            closure=closure
        )
        return func_obj
    
    def call_user_func(self, func_obj: ChotuFunction, args: list):
        if len(args) != len(func_obj.parameters):
            # Cannot easily get line number here, relying on original exception handler
            raise ChotuRuntimeError(f"Function {func_obj.name} expects {len(func_obj.parameters)} arguments, got {len(args)}")
        
        old_variables = self.variables
        old_in_func = self.in_function_call
        
        self.in_function_call = True
        self.variables = func_obj.closure.copy()
        for param, arg in zip(func_obj.parameters, args):
            self.variables[param] = arg
        
        result = None
        try:
            for statement in func_obj.body: self.interpret(statement)
        except ReturnException as e:
            result = e.value
        except (BreakException, ContinueException):
            raise ChotuRuntimeError("'break' or 'next' used outside of a loop.")
        finally:
            self.variables = old_variables
            self.in_function_call = old_in_func
            
        return result

    def visit_function_call(self, node):
        args = [self.interpret(arg) for arg in node.arguments]
        
        if node.name in self.builtins:
            return self.builtins[node.name](*args)
        
        if node.name in self.modules:
            module = self.modules[node.name]
            if hasattr(module, node.name):
                return getattr(module, node.name)(*args)
        
        if node.name in self.variables:
            func = self.variables[node.name]
            
            if isinstance(func, ChotuFunction):
                return self.call_user_func(func, args)
            
            if callable(func):
                return func(*args)
            
            raise ChotuRuntimeError(f"Variable '{node.name}' is not a function.", line=node.get_line())

        raise ChotuRuntimeError(f"Undefined function: {node.name}", line=node.get_line())
    
    def visit_binary_op(self, node):
        left = self.interpret(node.left); right = self.interpret(node.right)
        
        ops = {
            '+': lambda a,b: a+b, '-': lambda a,b: a-b, '*': lambda a,b: a*b, 
            '/': lambda a,b: a/b, '%': lambda a,b: a%b, '<': lambda a,b: a<b, 
            '>': lambda a,b: a>b, '<=': lambda a,b: a<=b, '>=': lambda a,b: a>=b,
            '==': lambda a,b: a==b, '!=': lambda a,b: a!=b, 'and': lambda a,b: a and b, 
            'or': lambda a,b: a or b
        }
        
        if node.op not in ops:
             raise ChotuRuntimeError(f"Unknown operator: {node.op}", line=node.get_line())
             
        try:
            return ops[node.op](left, right)
        except TypeError:
            raise ChotuRuntimeError(f"Unsupported operation '{node.op}' between {self.builtins['type'](left)} and {self.builtins['type'](right)}", line=node.get_line())
        except ZeroDivisionError:
            raise ChotuRuntimeError("division by zero", line=node.get_line())

    def visit_variable(self, node):
        if node.name in self.variables: 
            return self.variables[node.name]
        
        if node.name in self.builtins:
            return self.builtins[node.name]
            
        raise ChotuRuntimeError(f"Undefined variable: {node.name}", line=node.get_line())
    
    def visit_list(self, node): return [self.interpret(elem) for elem in node.elements]
    
    def visit_dict(self, node):
        return {self.interpret(k): self.interpret(v) for k, v in node.pairs}
    
    def visit_index(self, node):
        obj = self.interpret(node.obj); index = self.interpret(node.index)
        try:
            if isinstance(obj, (list, str)): 
                if not isinstance(index, int): raise ChotuRuntimeError("List/string index must be a number", line=node.get_line())
                return obj[index]
            if isinstance(obj, dict): return obj[index]
        except (IndexError, KeyError):
            raise ChotuRuntimeError(f"Index error: {index} not found.", line=node.get_line())
        raise ChotuRuntimeError("Indexing only supported for lists, dictionaries, and strings", line=node.get_line())
    
    def visit_attribute_access(self, node):
        obj = self.interpret(node.obj)
        try:
            return getattr(obj, node.attribute)
        except AttributeError:
            raise ChotuRuntimeError(f"Attribute '{node.attribute}' not found on {type(obj).__name__}", line=node.get_line())

    def visit_method_call(self, node):
        obj = self.interpret(node.obj); method = node.method
        args = [self.interpret(arg) for arg in node.arguments]
        
        if hasattr(obj, method):
            method_func = getattr(obj, method)
            if callable(method_func):
                return method_func(*args) 
            else:
                raise ChotuRuntimeError(f"Attribute '{method}' is not a callable method.", line=node.get_line())
        
        if isinstance(obj, str): return self._call_string_method(obj, method, args)
        if isinstance(obj, list): return self._call_list_method(obj, method, args)
        if isinstance(obj, dict): return self._call_dict_method(obj, method, args)
        raise ChotuRuntimeError(f"Method '{method}' not found on {type(obj).__name__}", line=node.get_line())

    def _call_string_method(self, obj, method, args):
        methods = {'len': len, 'lower': str.lower, 'upper': str.upper, 'strip': str.strip,
                   'split': str.split, 'replace': str.replace, 'startswith': str.startswith,
                   'endswith': str.endswith, 'contains': lambda s, sub: sub in s}
        if method in methods: 
            if method == 'len': return methods[method](obj)
            return methods[method](obj, *args)
        raise ChotuRuntimeError(f"Unknown string method: {method}")

    def _call_list_method(self, obj, method, args):
        methods = {'len': len, 'append': list.append, 'extend': list.extend, 'pop': list.pop,
                   'insert': list.insert, 'remove': list.remove, 'index': list.index,
                   'count': list.count, 'sort': list.sort, 'reverse': list.reverse,
                   'contains': lambda l, x: x in l}
        if method in methods: 
            if method in ['pop', 'index', 'count', 'len', 'contains']:
                return methods[method](obj, *args)
            else:
                methods[method](obj, *args)
                return None
        raise ChotuRuntimeError(f"Unknown list method: {method}")

    def _call_dict_method(self, obj, method, args):
        methods = {'len': len, 'keys': lambda d: list(d.keys()), 'values': lambda d: list(d.values()),
                   'items': lambda d: list(d.items()), 'get': dict.get, 
                   'set': dict.update, 'delete': dict.pop, 'contains': lambda d, k: k in d}
        
        if method == 'set': 
            if len(args) != 2: raise ChotuRuntimeError("Dict method 'set' requires two arguments: key and value.")
            obj[args[0]] = args[1]; return None
        if method == 'delete':
            if len(args) != 1: raise ChotuRuntimeError("Dict method 'delete' requires one argument: key.")
            try: return methods[method](obj, args[0])
            except KeyError: raise ChotuRuntimeError(f"Dictionary key not found for deletion: {args[0]}")
            
        if method in methods: 
            if method in ['len', 'keys', 'values', 'items', 'get', 'contains']:
                 return methods[method](obj, *args)
            else:
                methods[method](obj, *args)
                return None
        raise ChotuRuntimeError(f"Unknown dict method: {method}")

    def visit_type_conversion(self, node):
        value = self.interpret(node.expression)
        try:
            if node.type_name in self.builtins:
                 return self.builtins[node.type_name](value)
            
            raise ChotuRuntimeError(f"Unknown type conversion: {node.type_name}", line=node.get_line())
        except Exception as e:
            raise ChotuRuntimeError(f"Cannot convert {BuiltInFunctions.type(value)} to {node.type_name}: {str(e)}", line=node.get_line())
    
    def visit_import(self, node):
        module_name = node.module; alias = node.alias or module_name.split('.')[-1]
        try:
            module = importlib.import_module(module_name)
            self.modules[alias] = module; self.variables[alias] = module
            return module
        except ImportError as e: raise ChotuRuntimeError(f"Failed to import {module_name}: {e}", line=node.get_line())
    
    def visit_try_catch(self, node):
        try:
            for statement in node.try_block: self.interpret(statement)
        except Exception as e:
            if isinstance(e, (ReturnException, BreakException, ContinueException)): raise
            if node.error_var: self.variables[node.error_var] = str(e)
            for statement in node.catch_block: self.interpret(statement)
    
    def visit_pipe(self, node):
        value = self.interpret(node.left)
        func = self.interpret(node.right)
        
        if isinstance(func, ChotuFunction):
            return self.call_user_func(func, [value])
        
        if callable(func): 
            try: return func(value)
            except TypeError as e:
                if "missing" in str(e) or "positional arguments" in str(e):
                    raise ChotuRuntimeError(f"Function called in pipe expects one argument, but requires more: {str(e)}", line=node.get_line())
                raise
        raise ChotuRuntimeError(f"Right side of pipe must be callable (a function)", line=node.get_line())
    
    def visit_range(self, node):
        start = self.interpret(node.start); end = self.interpret(node.end)
        step = self.interpret(node.step) if node.step else 1
        
        if not all(isinstance(x, (int, float)) for x in [start, end, step]):
            raise ChotuRuntimeError("Range arguments must be numbers.", line=node.get_line())
            
        return list(range(int(start), int(end), int(step)))
    
    def visit_null_coalesce(self, node):
        left = self.interpret(node.left)
        return left if left is not None else self.interpret(node.right)