"""
TENGWAR Interpreter

Tree-walking evaluator for the TENGWAR AST.
Supports closures, recursion, pattern matching, mutable state,
and parallel execution.
"""
import sys
import hashlib
import concurrent.futures
import json
from typing import Any, Dict, List, Optional
from .ast_nodes import *
from .errors import RuntimeError_, ProofError

# Allow deep recursion for recursive Tengwar programs
sys.setrecursionlimit(50000)


class TengwarValue:
    """Base class for all TENGWAR runtime values"""
    pass


class TengwarInt(TengwarValue):
    def __init__(self, value: int):
        self.value = value
    def __repr__(self): return str(self.value)
    def __eq__(self, other): return isinstance(other, TengwarInt) and self.value == other.value
    def __hash__(self): return hash(self.value)


class TengwarFloat(TengwarValue):
    def __init__(self, value: float):
        self.value = value
    def __repr__(self): return str(self.value)
    def __eq__(self, other): return isinstance(other, TengwarFloat) and self.value == other.value
    def __hash__(self): return hash(self.value)


class TengwarStr(TengwarValue):
    def __init__(self, value: str):
        self.value = value
    def __repr__(self): return f'"{self.value}"'
    def __eq__(self, other): return isinstance(other, TengwarStr) and self.value == other.value
    def __hash__(self): return hash(self.value)


class TengwarBool(TengwarValue):
    def __init__(self, value: bool):
        self.value = value
    def __repr__(self): return 'true' if self.value else 'false'
    def __eq__(self, other): return isinstance(other, TengwarBool) and self.value == other.value
    def __hash__(self): return hash(self.value)


class TengwarUnit(TengwarValue):
    def __repr__(self): return '∅'
    def __eq__(self, other): return isinstance(other, TengwarUnit)
    def __hash__(self): return hash(None)


class TengwarTuple(TengwarValue):
    def __init__(self, elements: list):
        self.elements = elements
    def __repr__(self): return '⟨' + ' '.join(repr(e) for e in self.elements) + '⟩'
    def __eq__(self, other): return isinstance(other, TengwarTuple) and self.elements == other.elements


class TengwarVector(TengwarValue):
    def __init__(self, elements: list):
        self.elements = list(elements)
    def __repr__(self): return '⟦' + ' '.join(repr(e) for e in self.elements) + '⟧'
    def __eq__(self, other): return isinstance(other, TengwarVector) and self.elements == other.elements


class TengwarClosure(TengwarValue):
    def __init__(self, params: list, body, env: 'Environment', name: str = ""):
        self.params = params
        self.body = body
        self.env = env
        self.name = name
    def __repr__(self):
        params = ' '.join(p for p in self.params)
        return f'(λ {params} ...)'


class TengwarBuiltin(TengwarValue):
    def __init__(self, name: str, func):
        self.name = name
        self.func = func
    def __repr__(self): return f'<builtin:{self.name}>'


class TengwarMutableCell(TengwarValue):
    def __init__(self, value: TengwarValue):
        self.value = value
    def __repr__(self): return f'(μ {repr(self.value)})'


class TengwarModule(TengwarValue):
    def __init__(self, name: str, bindings: Dict[str, TengwarValue], addr: str = ""):
        self.name = name
        self.bindings = bindings
        self.addr = addr
    def __repr__(self): return f'(□ {self.name or self.addr})'


class TengwarDict(TengwarValue):
    """Hash map / dictionary"""
    def __init__(self, data: Dict = None):
        self.data = data or {}
    def __repr__(self):
        pairs = ' '.join(f'{repr(k)} {repr(v)}' for k, v in self.data.items())
        return f'%{{{pairs}}}'
    def __eq__(self, other):
        return isinstance(other, TengwarDict) and self.data == other.data


class TengwarPyObject(TengwarValue):
    """Wrapper for Python objects for interop"""
    def __init__(self, obj, name: str = ""):
        self.obj = obj
        self.name = name or type(obj).__name__
    def __repr__(self): return f'<py:{self.name}>'
    def __eq__(self, other):
        return isinstance(other, TengwarPyObject) and self.obj == other.obj


class TailCall:
    """Sentinel for tail call optimization trampolining"""
    __slots__ = ('func', 'args')
    def __init__(self, func, args):
        self.func = func
        self.args = args


class Environment:
    """Lexically scoped environment with content-addressable store"""

    def __init__(self, parent: Optional['Environment'] = None):
        self.bindings: Dict[str, TengwarValue] = {}
        self.parent = parent
        self.content_store: Dict[str, TengwarValue] = {}  # @addr → value

    def get(self, name: str) -> TengwarValue:
        if name in self.bindings:
            return self.bindings[name]
        if self.parent:
            return self.parent.get(name)
        raise RuntimeError_(f"Unbound identifier: {name}")

    def set(self, name: str, value: TengwarValue):
        self.bindings[name] = value

    def store(self, addr: str, value: TengwarValue):
        """Store a value at a content address"""
        self.content_store[addr] = value
        # Also propagate to root
        env = self
        while env.parent:
            env = env.parent
        if env is not self:
            env.content_store[addr] = value

    def lookup_addr(self, addr: str) -> TengwarValue:
        """Look up a content-addressed value"""
        if addr in self.content_store:
            return self.content_store[addr]
        if self.parent:
            return self.parent.lookup_addr(addr)
        raise RuntimeError_(f"Unknown address: {addr}")

    def child(self) -> 'Environment':
        return Environment(parent=self)


def content_hash(value: str) -> str:
    """Generate a short content hash"""
    return hashlib.sha256(value.encode()).hexdigest()[:8]


class Interpreter:
    def __init__(self, sandbox=False):
        self.global_env = Environment()
        self.trace = False
        # Sandbox controls
        self.sandbox_allow_io = not sandbox
        self.sandbox_allow_net = not sandbox
        self.sandbox_allow_py = not sandbox
        self.max_steps = 1000000 if sandbox else 0  # 0 = unlimited
        self.step_count = 0
        self._setup_stdlib()

    def _setup_stdlib(self):
        """Install standard library functions"""
        env = self.global_env

        # I/O
        env.set('print', TengwarBuiltin('print', self._builtin_print))
        env.set('println', TengwarBuiltin('println', self._builtin_println))
        env.set('str', TengwarBuiltin('str', self._builtin_str))
        env.set('repr', TengwarBuiltin('repr', self._builtin_repr))
        env.set('input', TengwarBuiltin('input', self._builtin_input))

        # Type checks
        env.set('int?', TengwarBuiltin('int?', lambda args: TengwarBool(isinstance(args[0], TengwarInt))))
        env.set('float?', TengwarBuiltin('float?', lambda args: TengwarBool(isinstance(args[0], TengwarFloat))))
        env.set('str?', TengwarBuiltin('str?', lambda args: TengwarBool(isinstance(args[0], TengwarStr))))
        env.set('bool?', TengwarBuiltin('bool?', lambda args: TengwarBool(isinstance(args[0], TengwarBool))))
        env.set('unit?', TengwarBuiltin('unit?', lambda args: TengwarBool(isinstance(args[0], TengwarUnit))))
        env.set('vec?', TengwarBuiltin('vec?', lambda args: TengwarBool(isinstance(args[0], TengwarVector))))
        env.set('tuple?', TengwarBuiltin('tuple?', lambda args: TengwarBool(isinstance(args[0], TengwarTuple))))
        env.set('fn?', TengwarBuiltin('fn?', lambda args: TengwarBool(isinstance(args[0], (TengwarClosure, TengwarBuiltin)))))

        # Type conversions
        env.set('to-int', TengwarBuiltin('to-int', self._builtin_to_int))
        env.set('to-float', TengwarBuiltin('to-float', self._builtin_to_float))
        env.set('to-str', TengwarBuiltin('to-str', self._builtin_to_str))

        # Math
        env.set('abs', TengwarBuiltin('abs', lambda args: self._num_op(args[0], abs)))
        env.set('min', TengwarBuiltin('min', lambda args: self._num_cmp(args, min)))
        env.set('max', TengwarBuiltin('max', lambda args: self._num_cmp(args, max)))
        env.set('pow', TengwarBuiltin('pow', self._builtin_pow))
        env.set('sqrt', TengwarBuiltin('sqrt', self._builtin_sqrt))
        env.set('mod', TengwarBuiltin('mod', self._builtin_mod))

        # String ops
        env.set('len', TengwarBuiltin('len', self._builtin_len))
        env.set('concat', TengwarBuiltin('concat', self._builtin_concat))
        env.set('substr', TengwarBuiltin('substr', self._builtin_substr))
        env.set('split', TengwarBuiltin('split', self._builtin_split))
        env.set('join', TengwarBuiltin('join', self._builtin_join))
        env.set('upper', TengwarBuiltin('upper', self._builtin_upper))
        env.set('lower', TengwarBuiltin('lower', self._builtin_lower))
        env.set('trim', TengwarBuiltin('trim', self._builtin_trim))
        env.set('contains', TengwarBuiltin('contains', self._builtin_contains))
        env.set('replace', TengwarBuiltin('replace', self._builtin_replace))

        # Vector/sequence ops
        env.set('vec', TengwarBuiltin('vec', lambda args: TengwarVector(list(args))))
        env.set('push', TengwarBuiltin('push', self._builtin_push))
        env.set('pop', TengwarBuiltin('pop', self._builtin_pop))
        env.set('get', TengwarBuiltin('get', self._builtin_get))
        env.set('set-at', TengwarBuiltin('set-at', self._builtin_set_at))
        env.set('slice', TengwarBuiltin('slice', self._builtin_slice))
        env.set('map', TengwarBuiltin('map', self._builtin_map))
        env.set('filter', TengwarBuiltin('filter', self._builtin_filter))
        env.set('reduce', TengwarBuiltin('reduce', self._builtin_reduce))
        env.set('sum', TengwarBuiltin('sum', self._builtin_sum))
        env.set('product', TengwarBuiltin('product', self._builtin_product))
        env.set('count-if', TengwarBuiltin('count-if', self._builtin_count_if))
        env.set('average', TengwarBuiltin('average', self._builtin_average))
        env.set('range', TengwarBuiltin('range', self._builtin_range))
        env.set('reverse', TengwarBuiltin('reverse', self._builtin_reverse))
        env.set('sort', TengwarBuiltin('sort', self._builtin_sort))
        env.set('flatten', TengwarBuiltin('flatten', self._builtin_flatten))
        env.set('zip', TengwarBuiltin('zip', self._builtin_zip))
        env.set('enumerate', TengwarBuiltin('enumerate', self._builtin_enumerate))
        env.set('head', TengwarBuiltin('head', self._builtin_head))
        env.set('tail', TengwarBuiltin('tail', self._builtin_tail))
        env.set('empty?', TengwarBuiltin('empty?', self._builtin_empty))

        # Tuple ops
        env.set('tuple', TengwarBuiltin('tuple', lambda args: TengwarTuple(list(args))))
        env.set('fst', TengwarBuiltin('fst', lambda args: self._expect_tuple(args[0]).elements[0]))
        env.set('snd', TengwarBuiltin('snd', lambda args: self._expect_tuple(args[0]).elements[1]))
        env.set('nth', TengwarBuiltin('nth', self._builtin_nth))

        # Mutable cell ops
        env.set('deref', TengwarBuiltin('deref', self._builtin_deref))
        env.set('set!', TengwarBuiltin('set!', self._builtin_set_mut))

        # Hash / content addressing
        env.set('hash', TengwarBuiltin('hash', self._builtin_hash))

        # Functional
        env.set('compose', TengwarBuiltin('compose', self._builtin_compose))
        env.set('apply', TengwarBuiltin('apply', self._builtin_apply))
        env.set('identity', TengwarBuiltin('identity', lambda args: args[0]))
        env.set('sum', TengwarBuiltin('sum', self._builtin_sum))
        env.set('product', TengwarBuiltin('product', self._builtin_product))
        env.set('count', TengwarBuiltin('count', self._builtin_count))
        env.set('any', TengwarBuiltin('any', self._builtin_any))
        env.set('all', TengwarBuiltin('all', self._builtin_all))

        # Combo builtins — token-efficient patterns
        env.set('sum-by', TengwarBuiltin('sum-by', self._builtin_sum_by))
        env.set('min-by', TengwarBuiltin('min-by', self._builtin_min_by))
        env.set('max-by', TengwarBuiltin('max-by', self._builtin_max_by))
        env.set('filter-map', TengwarBuiltin('filter-map', self._builtin_filter_map))
        env.set('flat', TengwarBuiltin('flat', self._builtin_flat))
        env.set('zip', TengwarBuiltin('zip', self._builtin_zip))
        env.set('enumerate', TengwarBuiltin('enumerate', self._builtin_enumerate))
        env.set('range-map', TengwarBuiltin('range-map', self._builtin_range_map))

        # Error handling
        env.set('err', TengwarBuiltin('err', self._builtin_err))
        env.set('try', TengwarBuiltin('try', self._builtin_try))

        # Operators as first-class functions
        env.set('+', TengwarBuiltin('+', lambda args: self._eval_binop('+', args[0], args[1])))
        env.set('-', TengwarBuiltin('-', lambda args: self._eval_binop('-', args[0], args[1]) if len(args) == 2 else self._eval_unop('-', args[0])))
        env.set('*', TengwarBuiltin('*', lambda args: self._eval_binop('*', args[0], args[1])))
        env.set('/', TengwarBuiltin('/', lambda args: self._eval_binop('/', args[0], args[1])))
        env.set('%', TengwarBuiltin('%', lambda args: self._eval_binop('%', args[0], args[1])))
        env.set('=', TengwarBuiltin('=', lambda args: self._eval_binop('=', args[0], args[1])))
        env.set('!=', TengwarBuiltin('!=', lambda args: self._eval_binop('!=', args[0], args[1])))
        env.set('<', TengwarBuiltin('<', lambda args: self._eval_binop('<', args[0], args[1])))
        env.set('>', TengwarBuiltin('>', lambda args: self._eval_binop('>', args[0], args[1])))
        env.set('<=', TengwarBuiltin('<=', lambda args: self._eval_binop('<=', args[0], args[1])))
        env.set('>=', TengwarBuiltin('>=', lambda args: self._eval_binop('>=', args[0], args[1])))
        env.set('&', TengwarBuiltin('&', lambda args: self._eval_binop('&', args[0], args[1])))
        env.set('|', TengwarBuiltin('|', lambda args: self._eval_binop('|', args[0], args[1])))
        env.set('!', TengwarBuiltin('!', lambda args: self._eval_unop('!', args[0])))

        # === DICT / HASHMAP ===
        env.set('dict', TengwarBuiltin('dict', self._builtin_dict))
        env.set('dict-get', TengwarBuiltin('dict-get', self._builtin_dict_get))
        env.set('dict-set', TengwarBuiltin('dict-set', self._builtin_dict_set))
        env.set('dict-del', TengwarBuiltin('dict-del', self._builtin_dict_del))
        env.set('dict-keys', TengwarBuiltin('dict-keys', self._builtin_dict_keys))
        env.set('dict-vals', TengwarBuiltin('dict-vals', self._builtin_dict_vals))
        env.set('dict-pairs', TengwarBuiltin('dict-pairs', self._builtin_dict_pairs))
        env.set('dict-has?', TengwarBuiltin('dict-has?', self._builtin_dict_has))
        env.set('dict-merge', TengwarBuiltin('dict-merge', self._builtin_dict_merge))
        env.set('dict-size', TengwarBuiltin('dict-size', self._builtin_dict_size))

        # === EXTENDED FUNCTIONAL ===
        env.set('flat-map', TengwarBuiltin('flat-map', self._builtin_flat_map))
        env.set('find', TengwarBuiltin('find', self._builtin_find))
        env.set('index-of', TengwarBuiltin('index-of', self._builtin_index_of))
        env.set('take', TengwarBuiltin('take', self._builtin_take))
        env.set('drop', TengwarBuiltin('drop', self._builtin_drop))
        env.set('take-while', TengwarBuiltin('take-while', self._builtin_take_while))
        env.set('drop-while', TengwarBuiltin('drop-while', self._builtin_drop_while))
        env.set('zip-with', TengwarBuiltin('zip-with', self._builtin_zip_with))
        env.set('group-by', TengwarBuiltin('group-by', self._builtin_group_by))
        env.set('unique', TengwarBuiltin('unique', self._builtin_unique))
        env.set('frequencies', TengwarBuiltin('frequencies', self._builtin_frequencies))
        env.set('partition', TengwarBuiltin('partition', self._builtin_partition))
        env.set('scan', TengwarBuiltin('scan', self._builtin_scan))
        env.set('chunks', TengwarBuiltin('chunks', self._builtin_chunks))
        env.set('interleave', TengwarBuiltin('interleave', self._builtin_interleave))
        env.set('repeat', TengwarBuiltin('repeat', self._builtin_repeat))
        env.set('iterate', TengwarBuiltin('iterate', self._builtin_iterate))
        env.set('juxt', TengwarBuiltin('juxt', self._builtin_juxt))
        env.set('min-by', TengwarBuiltin('min-by', self._builtin_min_by))
        env.set('max-by', TengwarBuiltin('max-by', self._builtin_max_by))
        env.set('sort-by', TengwarBuiltin('sort-by', self._builtin_sort_by))
        env.set('for-each', TengwarBuiltin('for-each', self._builtin_for_each))

        # === STRING EXTENDED ===
        env.set('fmt', TengwarBuiltin('fmt', self._builtin_fmt))
        env.set('starts-with?', TengwarBuiltin('starts-with?', self._builtin_starts_with))
        env.set('ends-with?', TengwarBuiltin('ends-with?', self._builtin_ends_with))
        env.set('chars', TengwarBuiltin('chars', self._builtin_chars))
        env.set('char-at', TengwarBuiltin('char-at', self._builtin_char_at))
        env.set('pad-left', TengwarBuiltin('pad-left', self._builtin_pad_left))
        env.set('pad-right', TengwarBuiltin('pad-right', self._builtin_pad_right))

        # === MATH EXTENDED ===
        env.set('floor', TengwarBuiltin('floor', lambda args: TengwarInt(int(self._num_val(args[0]) // 1))))
        env.set('ceil', TengwarBuiltin('ceil', lambda args: TengwarInt(int(-(-self._num_val(args[0]) // 1)))))
        env.set('round', TengwarBuiltin('round', lambda args: TengwarInt(round(self._num_val(args[0])))))
        env.set('abs', TengwarBuiltin('abs', lambda args: type(args[0])(abs(self._num_val(args[0])))))
        env.set('min', TengwarBuiltin('min', self._builtin_min))
        env.set('max', TengwarBuiltin('max', self._builtin_max))
        env.set('clamp', TengwarBuiltin('clamp', self._builtin_clamp))
        env.set('pi', TengwarFloat(3.141592653589793))
        env.set('e', TengwarFloat(2.718281828459045))
        env.set('inf', TengwarFloat(float('inf')))
        env.set('nan', TengwarFloat(float('nan')))

        # === TYPE CHECKING ===
        env.set('int?', TengwarBuiltin('int?', lambda args: TengwarBool(isinstance(args[0], TengwarInt))))
        env.set('float?', TengwarBuiltin('float?', lambda args: TengwarBool(isinstance(args[0], TengwarFloat))))
        env.set('num?', TengwarBuiltin('num?', lambda args: TengwarBool(isinstance(args[0], (TengwarInt, TengwarFloat)))))
        env.set('str?', TengwarBuiltin('str?', lambda args: TengwarBool(isinstance(args[0], TengwarStr))))
        env.set('bool?', TengwarBuiltin('bool?', lambda args: TengwarBool(isinstance(args[0], TengwarBool))))
        env.set('vec?', TengwarBuiltin('vec?', lambda args: TengwarBool(isinstance(args[0], TengwarVector))))
        env.set('tuple?', TengwarBuiltin('tuple?', lambda args: TengwarBool(isinstance(args[0], TengwarTuple))))
        env.set('dict?', TengwarBuiltin('dict?', lambda args: TengwarBool(isinstance(args[0], TengwarDict))))
        env.set('fn?', TengwarBuiltin('fn?', lambda args: TengwarBool(isinstance(args[0], (TengwarClosure, TengwarBuiltin)))))
        env.set('nil?', TengwarBuiltin('nil?', lambda args: TengwarBool(isinstance(args[0], TengwarUnit))))
        env.set('py?', TengwarBuiltin('py?', lambda args: TengwarBool(isinstance(args[0], TengwarPyObject))))
        env.set('type', TengwarBuiltin('type', self._builtin_type))

        # === NUMERIC PREDICATES ===
        env.set('zero?', TengwarBuiltin('zero?', lambda args: TengwarBool(self._num_val(args[0]) == 0)))
        env.set('pos?', TengwarBuiltin('pos?', lambda args: TengwarBool(self._num_val(args[0]) > 0)))
        env.set('neg?', TengwarBuiltin('neg?', lambda args: TengwarBool(self._num_val(args[0]) < 0)))
        env.set('even?', TengwarBuiltin('even?', lambda args: TengwarBool(self._num_val(args[0]) % 2 == 0)))
        env.set('odd?', TengwarBuiltin('odd?', lambda args: TengwarBool(self._num_val(args[0]) % 2 != 0)))
        env.set('divides?', TengwarBuiltin('divides?', lambda args: TengwarBool(self._num_val(args[0]) % self._num_val(args[1]) == 0)))
        env.set('between?', TengwarBuiltin('between?', lambda args: TengwarBool(self._num_val(args[1]) <= self._num_val(args[0]) <= self._num_val(args[2]))))
        env.set('empty?', TengwarBuiltin('empty?', lambda args: TengwarBool(
            (isinstance(args[0], TengwarVector) and len(args[0].elements) == 0) or
            (isinstance(args[0], TengwarStr) and args[0].value == "") or
            (isinstance(args[0], TengwarDict) and len(args[0].entries) == 0)
        )))

        # === COMPACT MATH (token-efficient) ===
        env.set('inc', TengwarBuiltin('inc', lambda args: TengwarInt(self._num_val(args[0]) + 1) if isinstance(args[0], TengwarInt) else TengwarFloat(self._num_val(args[0]) + 1)))
        env.set('dec', TengwarBuiltin('dec', lambda args: TengwarInt(self._num_val(args[0]) - 1) if isinstance(args[0], TengwarInt) else TengwarFloat(self._num_val(args[0]) - 1)))
        env.set('sqr', TengwarBuiltin('sqr', lambda args: TengwarInt(self._num_val(args[0]) ** 2) if isinstance(args[0], TengwarInt) else TengwarFloat(self._num_val(args[0]) ** 2)))
        env.set('cube', TengwarBuiltin('cube', lambda args: TengwarInt(self._num_val(args[0]) ** 3) if isinstance(args[0], TengwarInt) else TengwarFloat(self._num_val(args[0]) ** 3)))
        env.set('pow', TengwarBuiltin('pow', lambda args: TengwarInt(int(self._num_val(args[0]) ** self._num_val(args[1]))) if isinstance(args[0], TengwarInt) and isinstance(args[1], TengwarInt) else TengwarFloat(self._num_val(args[0]) ** self._num_val(args[1]))))
        env.set('sqrt', TengwarBuiltin('sqrt', lambda args: TengwarFloat(self._num_val(args[0]) ** 0.5)))
        env.set('double', TengwarBuiltin('double', lambda args: TengwarInt(self._num_val(args[0]) * 2) if isinstance(args[0], TengwarInt) else TengwarFloat(self._num_val(args[0]) * 2)))
        env.set('half', TengwarBuiltin('half', lambda args: TengwarFloat(self._num_val(args[0]) / 2)))
        env.set('neg', TengwarBuiltin('neg', lambda args: TengwarInt(-self._num_val(args[0])) if isinstance(args[0], TengwarInt) else TengwarFloat(-self._num_val(args[0]))))
        env.set('id', TengwarBuiltin('id', lambda args: args[0]))

        # Memoization
        def _memo(args):
            fn = args[0]
            cache = {}
            def memoized(call_args):
                # Create hashable key from args
                key = tuple(self._display(a) for a in call_args)
                if key not in cache:
                    cache[key] = self._call_function(fn, call_args)
                return cache[key]
            return TengwarBuiltin(f'memo({getattr(fn, "name", "fn")})', memoized)
        env.set('memo', TengwarBuiltin('memo', _memo))

        # === JSON ===
        env.set('json-parse', TengwarBuiltin('json-parse', self._builtin_json_parse))
        env.set('json-encode', TengwarBuiltin('json-encode', self._builtin_json_encode))

        # === FILE I/O ===
        env.set('read-file', TengwarBuiltin('read-file', self._builtin_read_file))
        env.set('write-file', TengwarBuiltin('write-file', self._builtin_write_file))
        env.set('append-file', TengwarBuiltin('append-file', self._builtin_append_file))
        env.set('file-exists?', TengwarBuiltin('file-exists?', self._builtin_file_exists))

        # === HTTP ===
        env.set('http-get', TengwarBuiltin('http-get', self._builtin_http_get))
        env.set('http-post', TengwarBuiltin('http-post', self._builtin_http_post))

        # === PYTHON INTEROP ===
        env.set('py-call', TengwarBuiltin('py-call', self._builtin_py_call))
        env.set('py-attr', TengwarBuiltin('py-attr', self._builtin_py_attr))
        env.set('py-eval', TengwarBuiltin('py-eval', self._builtin_py_eval))
        env.set('to-py', TengwarBuiltin('to-py', self._builtin_to_py))
        env.set('to-tw', TengwarBuiltin('to-tw', self._builtin_to_tw))

        # === SYSTEM ===
        env.set('time-ms', TengwarBuiltin('time-ms', self._builtin_time_ms))
        env.set('sleep', TengwarBuiltin('sleep', self._builtin_sleep))
        env.set('env-get', TengwarBuiltin('env-get', self._builtin_env_get))
        env.set('rand', TengwarBuiltin('rand', self._builtin_rand))
        env.set('rand-int', TengwarBuiltin('rand-int', self._builtin_rand_int))
        env.set('uuid', TengwarBuiltin('uuid', self._builtin_uuid))
        env.set('exit', TengwarBuiltin('exit', lambda args: exit(int(self._num_val(args[0])) if args else 0)))

        # === CONVERSION ===
        env.set('vec->tuple', TengwarBuiltin('vec->tuple', lambda args: TengwarTuple(args[0].elements) if isinstance(args[0], TengwarVector) else args[0]))
        env.set('tuple->vec', TengwarBuiltin('tuple->vec', lambda args: TengwarVector(args[0].elements) if isinstance(args[0], TengwarTuple) else args[0]))
        env.set('dict-to-vec', TengwarBuiltin('dict-to-vec', self._builtin_dict_to_vec))
        env.set('vec-to-dict', TengwarBuiltin('vec-to-dict', self._builtin_vec_to_dict))

    # === BUILTIN IMPLEMENTATIONS ===

    def _builtin_print(self, args):
        parts = [self._display(a) for a in args]
        print(' '.join(parts), end='')
        return TengwarUnit()

    def _builtin_println(self, args):
        parts = [self._display(a) for a in args]
        print(' '.join(parts))
        return TengwarUnit()

    def _builtin_str(self, args):
        return TengwarStr(self._display(args[0]))

    def _builtin_repr(self, args):
        return TengwarStr(repr(args[0]))

    def _builtin_input(self, args):
        prompt = self._display(args[0]) if args else ""
        return TengwarStr(input(prompt))

    def _builtin_to_int(self, args):
        v = args[0]
        if isinstance(v, TengwarInt): return v
        if isinstance(v, TengwarFloat): return TengwarInt(int(v.value))
        if isinstance(v, TengwarStr): return TengwarInt(int(v.value))
        if isinstance(v, TengwarBool): return TengwarInt(1 if v.value else 0)
        raise RuntimeError_(f"Cannot convert {type(v).__name__} to int")

    def _builtin_to_float(self, args):
        v = args[0]
        if isinstance(v, TengwarFloat): return v
        if isinstance(v, TengwarInt): return TengwarFloat(float(v.value))
        if isinstance(v, TengwarStr): return TengwarFloat(float(v.value))
        raise RuntimeError_(f"Cannot convert {type(v).__name__} to float")

    def _builtin_to_str(self, args):
        return TengwarStr(self._display(args[0]))

    def _num_op(self, val, op):
        if isinstance(val, TengwarInt): return TengwarInt(op(val.value))
        if isinstance(val, TengwarFloat): return TengwarFloat(op(val.value))
        raise RuntimeError_(f"Expected number, got {type(val).__name__}")

    def _num_cmp(self, args, op):
        nums = [a.value for a in args]
        result = op(nums)
        if isinstance(args[0], TengwarFloat) or isinstance(args[1] if len(args) > 1 else args[0], TengwarFloat):
            return TengwarFloat(result)
        return TengwarInt(result)

    def _builtin_pow(self, args):
        base, exp = args[0].value, args[1].value
        if isinstance(args[0], TengwarFloat) or isinstance(args[1], TengwarFloat):
            return TengwarFloat(base ** exp)
        return TengwarInt(base ** exp)

    def _builtin_sqrt(self, args):
        import math
        return TengwarFloat(math.sqrt(args[0].value))

    def _builtin_mod(self, args):
        return TengwarInt(args[0].value % args[1].value)

    def _builtin_len(self, args):
        v = args[0]
        if isinstance(v, TengwarStr): return TengwarInt(len(v.value))
        if isinstance(v, TengwarVector): return TengwarInt(len(v.elements))
        if isinstance(v, TengwarTuple): return TengwarInt(len(v.elements))
        raise RuntimeError_(f"Cannot get length of {type(v).__name__}")

    def _builtin_concat(self, args):
        if isinstance(args[0], TengwarStr):
            return TengwarStr(''.join(a.value for a in args))
        if isinstance(args[0], TengwarVector):
            result = []
            for a in args:
                result.extend(a.elements)
            return TengwarVector(result)
        raise RuntimeError_(f"Cannot concat {type(args[0]).__name__}")

    def _builtin_substr(self, args):
        s, start = args[0].value, args[1].value
        end = args[2].value if len(args) > 2 else len(s)
        return TengwarStr(s[start:end])

    def _builtin_split(self, args):
        s, sep = args[0].value, args[1].value
        return TengwarVector([TengwarStr(p) for p in s.split(sep)])

    def _builtin_join(self, args):
        sep = args[0].value
        vec = args[1]
        return TengwarStr(sep.join(self._display(e) for e in vec.elements))

    def _builtin_upper(self, args):
        return TengwarStr(args[0].value.upper())

    def _builtin_lower(self, args):
        return TengwarStr(args[0].value.lower())

    def _builtin_trim(self, args):
        return TengwarStr(args[0].value.strip())

    def _builtin_contains(self, args):
        if isinstance(args[0], TengwarStr):
            return TengwarBool(args[1].value in args[0].value)
        if isinstance(args[0], TengwarVector):
            return TengwarBool(args[1] in args[0].elements)
        raise RuntimeError_(f"Cannot check containment in {type(args[0]).__name__}")

    def _builtin_replace(self, args):
        return TengwarStr(args[0].value.replace(args[1].value, args[2].value))

    def _builtin_push(self, args):
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("push requires a vector")
        return TengwarVector(vec.elements + [args[1]])

    def _builtin_pop(self, args):
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("pop requires a vector")
        if not vec.elements:
            raise RuntimeError_("Cannot pop from empty vector")
        return TengwarTuple([TengwarVector(vec.elements[:-1]), vec.elements[-1]])

    def _builtin_get(self, args):
        col, idx = args[0], args[1].value
        if isinstance(col, TengwarVector):
            if idx < 0 or idx >= len(col.elements):
                raise RuntimeError_(f"Index {idx} out of bounds (len={len(col.elements)})")
            return col.elements[idx]
        if isinstance(col, TengwarTuple):
            return col.elements[idx]
        if isinstance(col, TengwarStr):
            return TengwarStr(col.value[idx])
        raise RuntimeError_(f"Cannot index into {type(col).__name__}")

    def _builtin_set_at(self, args):
        vec, idx, val = args[0], args[1].value, args[2]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("set-at requires a vector")
        new_elements = list(vec.elements)
        new_elements[idx] = val
        return TengwarVector(new_elements)

    def _builtin_slice(self, args):
        vec, start = args[0], args[1].value
        end = args[2].value if len(args) > 2 else len(vec.elements)
        return TengwarVector(vec.elements[start:end])

    def _builtin_map(self, args):
        fn, vec = args[0], args[1]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("map requires a vector")
        return TengwarVector([self._call_function(fn, [e]) for e in vec.elements])

    def _builtin_filter(self, args):
        fn, vec = args[0], args[1]
        result = []
        for e in vec.elements:
            if self._is_truthy(self._call_function(fn, [e])):
                result.append(e)
        return TengwarVector(result)

    def _builtin_reduce(self, args):
        fn, init, vec = args[0], args[1], args[2]
        acc = init
        for e in vec.elements:
            acc = self._call_function(fn, [acc, e])
        return acc

    def _builtin_sum(self, args):
        """(sum vec) — sum all numeric elements"""
        vec = args[0]
        total = 0
        for e in vec.elements:
            total += self._num_val(e)
        return TengwarInt(total) if isinstance(total, int) else TengwarFloat(total)

    def _builtin_product(self, args):
        """(product vec) — multiply all numeric elements"""
        vec = args[0]
        total = 1
        for e in vec.elements:
            total *= self._num_val(e)
        return TengwarInt(total) if isinstance(total, int) else TengwarFloat(total)

    def _builtin_count_if(self, args):
        """(count-if pred vec) — count elements matching predicate"""
        pred, vec = args[0], args[1]
        count = 0
        for e in vec.elements:
            if self._is_truthy(self._call_function(pred, [e])):
                count += 1
        return TengwarInt(count)

    def _builtin_average(self, args):
        """(average vec) — arithmetic mean"""
        vec = args[0]
        if not vec.elements:
            raise RuntimeError_("Cannot average empty vector")
        total = sum(self._num_val(e) for e in vec.elements)
        return TengwarFloat(total / len(vec.elements))

    def _builtin_range(self, args):
        start = 0
        step = 1
        if len(args) == 1:
            end = args[0].value
        elif len(args) == 2:
            start, end = args[0].value, args[1].value
        else:
            start, end, step = args[0].value, args[1].value, args[2].value
        return TengwarVector([TengwarInt(i) for i in range(start, end, step)])

    def _builtin_reverse(self, args):
        v = args[0]
        if isinstance(v, TengwarVector): return TengwarVector(list(reversed(v.elements)))
        if isinstance(v, TengwarStr): return TengwarStr(v.value[::-1])
        raise RuntimeError_(f"Cannot reverse {type(v).__name__}")

    def _builtin_sort(self, args):
        vec = args[0]
        key_fn = args[1] if len(args) > 1 else None
        if key_fn:
            return TengwarVector(sorted(vec.elements, key=lambda e: self._call_function(key_fn, [e]).value))
        return TengwarVector(sorted(vec.elements, key=lambda e: e.value))

    def _builtin_flatten(self, args):
        result = []
        for e in args[0].elements:
            if isinstance(e, TengwarVector):
                result.extend(e.elements)
            else:
                result.append(e)
        return TengwarVector(result)

    def _builtin_zip(self, args):
        vecs = [a.elements for a in args]
        return TengwarVector([
            TengwarTuple(list(elems)) for elems in zip(*vecs)
        ])

    def _builtin_enumerate(self, args):
        vec = args[0]
        return TengwarVector([
            TengwarTuple([TengwarInt(i), e]) for i, e in enumerate(vec.elements)
        ])

    def _builtin_head(self, args):
        vec = args[0]
        if not vec.elements:
            raise RuntimeError_("head of empty vector")
        return vec.elements[0]

    def _builtin_tail(self, args):
        vec = args[0]
        if not vec.elements:
            raise RuntimeError_("tail of empty vector")
        return TengwarVector(vec.elements[1:])

    def _builtin_empty(self, args):
        v = args[0]
        if isinstance(v, TengwarVector): return TengwarBool(len(v.elements) == 0)
        if isinstance(v, TengwarStr): return TengwarBool(len(v.value) == 0)
        return TengwarBool(False)

    def _builtin_nth(self, args):
        col, idx = args[0], args[1].value
        if isinstance(col, TengwarTuple): return col.elements[idx]
        if isinstance(col, TengwarVector): return col.elements[idx]
        raise RuntimeError_(f"Cannot nth into {type(col).__name__}")

    def _builtin_deref(self, args):
        cell = args[0]
        if not isinstance(cell, TengwarMutableCell):
            raise RuntimeError_("deref requires a mutable cell")
        return cell.value

    def _builtin_set_mut(self, args):
        cell, value = args[0], args[1]
        if not isinstance(cell, TengwarMutableCell):
            raise RuntimeError_("set! requires a mutable cell")
        cell.value = value
        return value

    def _builtin_hash(self, args):
        return TengwarStr(content_hash(self._display(args[0])))

    def _builtin_compose(self, args):
        f, g = args[0], args[1]
        def composed(call_args):
            return self._call_function(f, [self._call_function(g, call_args)])
        return TengwarBuiltin('composed', composed)

    def _builtin_sum(self, args):
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("sum requires a vector")
        total = 0
        use_float = False
        for e in vec.elements:
            if isinstance(e, TengwarFloat):
                use_float = True
            total += e.value
        return TengwarFloat(total) if use_float else TengwarInt(total)

    def _builtin_product(self, args):
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("product requires a vector")
        total = 1
        use_float = False
        for e in vec.elements:
            if isinstance(e, TengwarFloat):
                use_float = True
            total *= e.value
        return TengwarFloat(total) if use_float else TengwarInt(total)

    def _builtin_count(self, args):
        fn, vec = args[0], args[1]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("count requires a vector")
        return TengwarInt(sum(1 for e in vec.elements if self._is_truthy(self._call_function(fn, [e]))))

    def _builtin_any(self, args):
        fn, vec = args[0], args[1]
        return TengwarBool(any(self._is_truthy(self._call_function(fn, [e])) for e in vec.elements))

    def _builtin_all(self, args):
        fn, vec = args[0], args[1]
        return TengwarBool(all(self._is_truthy(self._call_function(fn, [e])) for e in vec.elements))

    # === COMBO BUILTINS (token-efficient compound operations) ===

    def _builtin_sum_by(self, args):
        """(sum-by f vec) = (sum (map f vec)) in one call"""
        fn, vec = args[0], args[1]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("sum-by requires a vector")
        total = 0
        use_float = False
        for e in vec.elements:
            r = self._call_function(fn, [e])
            if isinstance(r, TengwarFloat):
                use_float = True
            total += self._num_val(r)
        return TengwarFloat(total) if use_float else TengwarInt(total)

    def _builtin_min_by(self, args):
        """(min-by f vec) = minimum of (map f vec)"""
        fn, vec = args[0], args[1]
        if not isinstance(vec, TengwarVector) or not vec.elements:
            raise RuntimeError_("min-by requires a non-empty vector")
        return min((self._call_function(fn, [e]) for e in vec.elements),
                   key=lambda x: self._num_val(x))

    def _builtin_max_by(self, args):
        """(max-by f vec) = maximum of (map f vec)"""
        fn, vec = args[0], args[1]
        if not isinstance(vec, TengwarVector) or not vec.elements:
            raise RuntimeError_("max-by requires a non-empty vector")
        return max((self._call_function(fn, [e]) for e in vec.elements),
                   key=lambda x: self._num_val(x))

    def _builtin_filter_map(self, args):
        """(filter-map pred f vec) = (map f (filter pred vec)) in one pass"""
        pred, fn, vec = args[0], args[1], args[2]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("filter-map requires a vector")
        results = []
        for e in vec.elements:
            if self._is_truthy(self._call_function(pred, [e])):
                results.append(self._call_function(fn, [e]))
        return TengwarVector(results)

    def _builtin_flat(self, args):
        """(flat vec) = flatten one level"""
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("flat requires a vector")
        result = []
        for e in vec.elements:
            if isinstance(e, TengwarVector):
                result.extend(e.elements)
            else:
                result.append(e)
        return TengwarVector(result)

    def _builtin_zip(self, args):
        """(zip v1 v2) = vector of tuples"""
        v1, v2 = args[0], args[1]
        if not isinstance(v1, TengwarVector) or not isinstance(v2, TengwarVector):
            raise RuntimeError_("zip requires two vectors")
        return TengwarVector([
            TengwarTuple([a, b])
            for a, b in zip(v1.elements, v2.elements)
        ])

    def _builtin_enumerate(self, args):
        """(enumerate vec) = vector of (index, element) tuples"""
        vec = args[0]
        if not isinstance(vec, TengwarVector):
            raise RuntimeError_("enumerate requires a vector")
        return TengwarVector([
            TengwarTuple([TengwarInt(i), e])
            for i, e in enumerate(vec.elements)
        ])

    def _builtin_range_map(self, args):
        """(range-map f a b) = (map f (range a b)) in one call"""
        fn, a, b = args[0], self._num_val(args[1]), self._num_val(args[2])
        return TengwarVector([
            self._call_function(fn, [TengwarInt(i)])
            for i in range(int(a), int(b))
        ])

    def _builtin_apply(self, args):
        fn, arg_vec = args[0], args[1]
        if isinstance(arg_vec, TengwarVector):
            return self._call_function(fn, arg_vec.elements)
        if isinstance(arg_vec, TengwarTuple):
            return self._call_function(fn, arg_vec.elements)
        return self._call_function(fn, [arg_vec])

    def _builtin_err(self, args):
        raise RuntimeError_(self._display(args[0]) if args else "error")

    def _builtin_try(self, args):
        fn, handler = args[0], args[1]
        try:
            result = self._call_function(fn, [])
            return TengwarTuple([TengwarBool(True), result])
        except (RuntimeError_, ProofError) as e:
            return TengwarTuple([TengwarBool(False), TengwarStr(str(e))])

    # === DICT BUILTINS ===

    def _builtin_dict(self, args):
        """(dict k1 v1 k2 v2 ...) or (dict vec-of-pairs)"""
        if len(args) == 1 and isinstance(args[0], TengwarVector):
            d = {}
            for pair in args[0].elements:
                if isinstance(pair, (TengwarTuple, TengwarVector)):
                    d[pair.elements[0]] = pair.elements[1]
            return TengwarDict(d)
        d = {}
        for i in range(0, len(args) - 1, 2):
            d[args[i]] = args[i + 1]
        return TengwarDict(d)

    def _builtin_dict_get(self, args):
        d, k = args[0], args[1]
        default = args[2] if len(args) > 2 else TengwarUnit()
        return d.data.get(k, default)

    def _builtin_dict_set(self, args):
        d, k, v = args[0], args[1], args[2]
        new_data = dict(d.data)
        new_data[k] = v
        return TengwarDict(new_data)

    def _builtin_dict_del(self, args):
        d, k = args[0], args[1]
        new_data = dict(d.data)
        new_data.pop(k, None)
        return TengwarDict(new_data)

    def _builtin_dict_keys(self, args):
        return TengwarVector(list(args[0].data.keys()))

    def _builtin_dict_vals(self, args):
        return TengwarVector(list(args[0].data.values()))

    def _builtin_dict_pairs(self, args):
        return TengwarVector([TengwarTuple([k, v]) for k, v in args[0].data.items()])

    def _builtin_dict_has(self, args):
        return TengwarBool(args[1] in args[0].data)

    def _builtin_dict_merge(self, args):
        result = dict(args[0].data)
        for a in args[1:]:
            result.update(a.data)
        return TengwarDict(result)

    def _builtin_dict_size(self, args):
        return TengwarInt(len(args[0].data))

    # === EXTENDED FUNCTIONAL ===

    def _builtin_flat_map(self, args):
        fn, vec = args[0], args[1]
        result = []
        for e in vec.elements:
            mapped = self._call_function(fn, [e])
            if isinstance(mapped, TengwarVector):
                result.extend(mapped.elements)
            else:
                result.append(mapped)
        return TengwarVector(result)

    def _builtin_find(self, args):
        fn, vec = args[0], args[1]
        for e in vec.elements:
            if self._is_truthy(self._call_function(fn, [e])):
                return e
        return TengwarUnit()

    def _builtin_index_of(self, args):
        fn, vec = args[0], args[1]
        for i, e in enumerate(vec.elements):
            if self._is_truthy(self._call_function(fn, [e])):
                return TengwarInt(i)
        return TengwarInt(-1)

    def _builtin_take(self, args):
        n, vec = int(self._num_val(args[0])), args[1]
        return TengwarVector(vec.elements[:n])

    def _builtin_drop(self, args):
        n, vec = int(self._num_val(args[0])), args[1]
        return TengwarVector(vec.elements[n:])

    def _builtin_take_while(self, args):
        fn, vec = args[0], args[1]
        result = []
        for e in vec.elements:
            if self._is_truthy(self._call_function(fn, [e])):
                result.append(e)
            else:
                break
        return TengwarVector(result)

    def _builtin_drop_while(self, args):
        fn, vec = args[0], args[1]
        dropping = True
        result = []
        for e in vec.elements:
            if dropping and self._is_truthy(self._call_function(fn, [e])):
                continue
            dropping = False
            result.append(e)
        return TengwarVector(result)

    def _builtin_zip_with(self, args):
        fn, v1, v2 = args[0], args[1], args[2]
        result = [self._call_function(fn, [a, b]) for a, b in zip(v1.elements, v2.elements)]
        return TengwarVector(result)

    def _builtin_group_by(self, args):
        fn, vec = args[0], args[1]
        groups = {}
        order = []
        for e in vec.elements:
            key = self._call_function(fn, [e])
            key_r = repr(key)
            if key_r not in groups:
                groups[key_r] = (key, [])
                order.append(key_r)
            groups[key_r][1].append(e)
        return TengwarDict({groups[k][0]: TengwarVector(groups[k][1]) for k in order})

    def _builtin_unique(self, args):
        seen = []
        result = []
        for e in args[0].elements:
            if e not in seen:
                seen.append(e)
                result.append(e)
        return TengwarVector(result)

    def _builtin_frequencies(self, args):
        counts = {}
        for e in args[0].elements:
            r = repr(e)
            if r not in counts:
                counts[r] = (e, 0)
            counts[r] = (e, counts[r][1] + 1)
        return TengwarDict({e: TengwarInt(c) for _, (e, c) in counts.items()})

    def _builtin_partition(self, args):
        fn, vec = args[0], args[1]
        yes, no = [], []
        for e in vec.elements:
            if self._is_truthy(self._call_function(fn, [e])):
                yes.append(e)
            else:
                no.append(e)
        return TengwarTuple([TengwarVector(yes), TengwarVector(no)])

    def _builtin_scan(self, args):
        fn, init, vec = args[0], args[1], args[2]
        acc = init
        result = [acc]
        for e in vec.elements:
            acc = self._call_function(fn, [acc, e])
            result.append(acc)
        return TengwarVector(result)

    def _builtin_chunks(self, args):
        n, vec = int(self._num_val(args[0])), args[1]
        els = vec.elements
        return TengwarVector([TengwarVector(els[i:i+n]) for i in range(0, len(els), n)])

    def _builtin_interleave(self, args):
        vecs = [a.elements for a in args]
        result = []
        for items in zip(*vecs):
            result.extend(items)
        return TengwarVector(result)

    def _builtin_repeat(self, args):
        n, val = int(self._num_val(args[0])), args[1]
        return TengwarVector([val] * n)

    def _builtin_iterate(self, args):
        fn, init, n = args[0], args[1], int(self._num_val(args[2]))
        result = [init]
        val = init
        for _ in range(n - 1):
            val = self._call_function(fn, [val])
            result.append(val)
        return TengwarVector(result)

    def _builtin_juxt(self, args):
        """(juxt f1 f2 ... val) — apply multiple fns to same value"""
        fns, val = args[:-1], args[-1]
        return TengwarVector([self._call_function(fn, [val]) for fn in fns])

    def _builtin_min_by(self, args):
        fn, vec = args[0], args[1]
        return min(vec.elements, key=lambda e: self._num_val(self._call_function(fn, [e])))

    def _builtin_max_by(self, args):
        fn, vec = args[0], args[1]
        return max(vec.elements, key=lambda e: self._num_val(self._call_function(fn, [e])))

    def _builtin_sort_by(self, args):
        fn, vec = args[0], args[1]
        return TengwarVector(sorted(vec.elements, key=lambda e: self._num_val(self._call_function(fn, [e]))))

    def _builtin_for_each(self, args):
        fn, vec = args[0], args[1]
        for e in vec.elements:
            self._call_function(fn, [e])
        return TengwarUnit()

    # === STRING EXTENDED ===

    def _builtin_fmt(self, args):
        """(fmt template arg1 arg2...) — {} placeholders"""
        template = args[0].value
        vals = [self._display(a) for a in args[1:]]
        try:
            return TengwarStr(template.format(*vals))
        except (IndexError, KeyError):
            return TengwarStr(template)

    def _builtin_starts_with(self, args):
        return TengwarBool(args[0].value.startswith(args[1].value))

    def _builtin_ends_with(self, args):
        return TengwarBool(args[0].value.endswith(args[1].value))

    def _builtin_chars(self, args):
        return TengwarVector([TengwarStr(c) for c in args[0].value])

    def _builtin_char_at(self, args):
        return TengwarStr(args[0].value[int(self._num_val(args[1]))])

    def _builtin_pad_left(self, args):
        s, n = args[0].value, int(self._num_val(args[1]))
        ch = args[2].value if len(args) > 2 else ' '
        return TengwarStr(s.rjust(n, ch[0]))

    def _builtin_pad_right(self, args):
        s, n = args[0].value, int(self._num_val(args[1]))
        ch = args[2].value if len(args) > 2 else ' '
        return TengwarStr(s.ljust(n, ch[0]))

    # === MATH EXTENDED ===

    def _builtin_min(self, args):
        if len(args) == 1 and isinstance(args[0], TengwarVector):
            return min(args[0].elements, key=lambda e: self._num_val(e))
        return min(args, key=lambda e: self._num_val(e))

    def _builtin_max(self, args):
        if len(args) == 1 and isinstance(args[0], TengwarVector):
            return max(args[0].elements, key=lambda e: self._num_val(e))
        return max(args, key=lambda e: self._num_val(e))

    def _builtin_clamp(self, args):
        val, lo, hi = self._num_val(args[0]), self._num_val(args[1]), self._num_val(args[2])
        result = max(lo, min(val, hi))
        return TengwarFloat(result) if isinstance(args[0], TengwarFloat) else TengwarInt(int(result))

    # === TYPE ===

    def _builtin_type(self, args):
        v = args[0]
        type_names = {
            TengwarInt: "int", TengwarFloat: "float", TengwarStr: "str",
            TengwarBool: "bool", TengwarUnit: "nil", TengwarVector: "vec",
            TengwarTuple: "tuple", TengwarDict: "dict", TengwarClosure: "fn",
            TengwarBuiltin: "builtin", TengwarModule: "module",
            TengwarMutableCell: "mut", TengwarPyObject: "py-object"
        }
        return TengwarStr(type_names.get(type(v), "unknown"))

    def _builtin_memo(self, args):
        """Memoize a function: (memo fn) → memoized fn"""
        func = args[0]
        cache = {}
        def memoized(call_args):
            key = tuple(repr(a) for a in call_args)
            if key not in cache:
                cache[key] = self._call_function(func, call_args)
            return cache[key]
        return TengwarBuiltin(f'memo<{getattr(func, "name", "fn")}>', memoized)

    # === JSON ===

    def _builtin_json_parse(self, args):
        return self._py_to_tw(json.loads(args[0].value))

    def _builtin_json_encode(self, args):
        return TengwarStr(json.dumps(self._tw_to_py(args[0])))

    # === FILE I/O ===

    def _builtin_read_file(self, args):
        if not self.sandbox_allow_io:
            raise RuntimeError_("File I/O disabled in sandbox mode")
        try:
            with open(args[0].value, 'r') as f:
                return TengwarStr(f.read())
        except Exception as e:
            raise RuntimeError_(f"read-file: {e}")

    def _builtin_write_file(self, args):
        if not self.sandbox_allow_io:
            raise RuntimeError_("File I/O disabled in sandbox mode")
        try:
            with open(args[0].value, 'w') as f:
                f.write(self._display(args[1]))
            return TengwarBool(True)
        except Exception as e:
            raise RuntimeError_(f"write-file: {e}")

    def _builtin_append_file(self, args):
        if not self.sandbox_allow_io:
            raise RuntimeError_("File I/O disabled in sandbox mode")
        try:
            with open(args[0].value, 'a') as f:
                f.write(self._display(args[1]))
            return TengwarBool(True)
        except Exception as e:
            raise RuntimeError_(f"append-file: {e}")

    def _builtin_file_exists(self, args):
        import os
        return TengwarBool(os.path.exists(args[0].value))

    # === HTTP ===

    def _builtin_http_get(self, args):
        if not self.sandbox_allow_net:
            raise RuntimeError_("Network access disabled in sandbox mode")
        try:
            import urllib.request
            url = args[0].value
            headers = {}
            if len(args) > 1 and isinstance(args[1], TengwarDict):
                headers = {self._display(k): self._display(v) for k, v in args[1].data.items()}
            req = urllib.request.Request(url, headers=headers)
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode('utf-8')
                return TengwarDict({
                    TengwarStr("status"): TengwarInt(resp.status),
                    TengwarStr("body"): TengwarStr(body),
                    TengwarStr("headers"): TengwarDict({
                        TengwarStr(k): TengwarStr(v) for k, v in resp.headers.items()
                    })
                })
        except Exception as e:
            raise RuntimeError_(f"http-get: {e}")

    def _builtin_http_post(self, args):
        if not self.sandbox_allow_net:
            raise RuntimeError_("Network access disabled in sandbox mode")
        try:
            import urllib.request
            url = args[0].value
            body = self._display(args[1]) if len(args) > 1 else ""
            headers = {"Content-Type": "application/json"}
            if len(args) > 2 and isinstance(args[2], TengwarDict):
                headers.update({self._display(k): self._display(v) for k, v in args[2].data.items()})
            data = body.encode('utf-8')
            req = urllib.request.Request(url, data=data, headers=headers, method='POST')
            with urllib.request.urlopen(req, timeout=30) as resp:
                resp_body = resp.read().decode('utf-8')
                return TengwarDict({
                    TengwarStr("status"): TengwarInt(resp.status),
                    TengwarStr("body"): TengwarStr(resp_body),
                })
        except Exception as e:
            raise RuntimeError_(f"http-post: {e}")

    # === PYTHON INTEROP ===

    def _builtin_py_call(self, args):
        """(py-call obj method_name arg1 arg2...) or (py-call fn arg1 arg2...)"""
        if not self.sandbox_allow_py:
            raise RuntimeError_("Python interop disabled in sandbox mode")
        obj = args[0]
        if isinstance(obj, TengwarPyObject) and len(args) >= 2 and isinstance(args[1], TengwarStr):
            method = getattr(obj.obj, args[1].value)
            py_args = [self._tw_to_py(a) for a in args[2:]]
            result = method(*py_args)
            return self._py_to_tw(result)
        elif isinstance(obj, TengwarPyObject) and callable(obj.obj):
            py_args = [self._tw_to_py(a) for a in args[1:]]
            result = obj.obj(*py_args)
            return self._py_to_tw(result)
        raise RuntimeError_(f"py-call: expected py-object, got {type(obj).__name__}")

    def _builtin_py_attr(self, args):
        """(py-attr obj attr_name)"""
        obj, attr = args[0], args[1].value
        if isinstance(obj, TengwarPyObject):
            val = getattr(obj.obj, attr)
            return self._py_to_tw(val)
        raise RuntimeError_(f"py-attr: expected py-object, got {type(obj).__name__}")

    def _builtin_py_eval(self, args):
        """(py-eval \"python_expression\")"""
        if not self.sandbox_allow_py:
            raise RuntimeError_("Python eval disabled in sandbox mode")
        result = eval(args[0].value)
        return self._py_to_tw(result)

    def _builtin_to_py(self, args):
        """(->py tengwar_value) — convert to Python object"""
        return TengwarPyObject(self._tw_to_py(args[0]))

    def _builtin_to_tw(self, args):
        """(->tw py_object) — convert to Tengwar value"""
        if isinstance(args[0], TengwarPyObject):
            return self._py_to_tw(args[0].obj)
        return args[0]

    # === SYSTEM ===

    def _builtin_time_ms(self, args):
        import time
        return TengwarInt(int(time.time() * 1000))

    def _builtin_sleep(self, args):
        import time
        time.sleep(self._num_val(args[0]) / 1000)
        return TengwarUnit()

    def _builtin_env_get(self, args):
        import os
        val = os.environ.get(args[0].value, "")
        return TengwarStr(val) if val else TengwarUnit()

    def _builtin_rand(self, args):
        import random
        return TengwarFloat(random.random())

    def _builtin_rand_int(self, args):
        import random
        lo = int(self._num_val(args[0]))
        hi = int(self._num_val(args[1]))
        return TengwarInt(random.randint(lo, hi))

    def _builtin_uuid(self, args):
        import uuid
        return TengwarStr(str(uuid.uuid4()))

    # === CONVERSION HELPERS ===

    def _builtin_dict_to_vec(self, args):
        d = args[0]
        return TengwarVector([TengwarTuple([k, v]) for k, v in d.data.items()])

    def _builtin_vec_to_dict(self, args):
        vec = args[0]
        d = {}
        for pair in vec.elements:
            if isinstance(pair, (TengwarTuple, TengwarVector)) and len(pair.elements) >= 2:
                d[pair.elements[0]] = pair.elements[1]
        return TengwarDict(d)

    # === PY <-> TW CONVERSION ===

    def _tw_to_py(self, val):
        """Convert Tengwar value to Python value"""
        if isinstance(val, TengwarInt): return val.value
        if isinstance(val, TengwarFloat): return val.value
        if isinstance(val, TengwarStr): return val.value
        if isinstance(val, TengwarBool): return val.value
        if isinstance(val, TengwarUnit): return None
        if isinstance(val, TengwarVector): return [self._tw_to_py(e) for e in val.elements]
        if isinstance(val, TengwarTuple): return tuple(self._tw_to_py(e) for e in val.elements)
        if isinstance(val, TengwarDict): return {self._tw_to_py(k): self._tw_to_py(v) for k, v in val.data.items()}
        if isinstance(val, TengwarPyObject): return val.obj
        return val

    def _py_to_tw(self, val):
        """Convert Python value to Tengwar value"""
        if val is None: return TengwarUnit()
        if isinstance(val, bool): return TengwarBool(val)
        if isinstance(val, int): return TengwarInt(val)
        if isinstance(val, float): return TengwarFloat(val)
        if isinstance(val, str): return TengwarStr(val)
        if isinstance(val, list): return TengwarVector([self._py_to_tw(e) for e in val])
        if isinstance(val, tuple): return TengwarTuple([self._py_to_tw(e) for e in val])
        if isinstance(val, dict): return TengwarDict({self._py_to_tw(k): self._py_to_tw(v) for k, v in val.items()})
        if isinstance(val, set): return TengwarVector([self._py_to_tw(e) for e in val])
        # Wrap anything else as py-object
        return TengwarPyObject(val)

    def _num_val(self, v):
        """Extract numeric value from Tengwar value"""
        if isinstance(v, TengwarInt): return v.value
        if isinstance(v, TengwarFloat): return v.value
        raise RuntimeError_(f"Expected number, got {type(v).__name__}")

    # === HELPERS ===

    def _expect_tuple(self, v):
        if not isinstance(v, TengwarTuple):
            raise RuntimeError_(f"Expected tuple, got {type(v).__name__}")
        return v

    def _display(self, v: TengwarValue) -> str:
        if isinstance(v, TengwarStr): return v.value
        return repr(v)

    def _is_truthy(self, v: TengwarValue) -> bool:
        if isinstance(v, TengwarBool): return v.value
        if isinstance(v, TengwarUnit): return False
        if isinstance(v, TengwarInt): return v.value != 0
        if isinstance(v, TengwarStr): return len(v.value) > 0
        if isinstance(v, TengwarVector): return len(v.elements) > 0
        if isinstance(v, TengwarDict): return len(v.data) > 0
        if isinstance(v, TengwarPyObject): return bool(v.obj)
        return True

    def _call_function(self, fn: TengwarValue, args: list) -> TengwarValue:
        # Trampoline loop for TCO
        while True:
            if isinstance(fn, TengwarBuiltin):
                return fn.func(args)
            if isinstance(fn, TengwarPyObject) and callable(fn.obj):
                py_args = [self._tw_to_py(a) for a in args]
                return self._py_to_tw(fn.obj(*py_args))
            # Handle CompiledClosure from VM
            try:
                from .vm import CompiledClosure, VM
                if isinstance(fn, CompiledClosure):
                    child_env = Environment(parent=fn.env)
                    for param, arg in zip(fn.code_obj.params, args):
                        child_env.set(param, arg)
                    vm = VM(self)
                    return vm.execute(fn.code_obj, child_env)
            except ImportError:
                pass
            if isinstance(fn, TengwarClosure):
                child_env = fn.env.child()
                for param, arg in zip(fn.params, args):
                    child_env.set(param, arg)
                # Support partial application
                if len(args) < len(fn.params):
                    remaining = fn.params[len(args):]
                    return TengwarClosure(remaining, fn.body, child_env, fn.name)
                # TCO: if body is a tail call, trampoline instead of recursing
                result = self.eval(fn.body, child_env)
                if isinstance(result, TailCall):
                    fn = result.func
                    args = result.args
                    continue
                return result
            raise RuntimeError_(f"Cannot call {type(fn).__name__}")

    def _get_identifier_name(self, node: ASTNode) -> str:
        if isinstance(node, Symbol): return node.name
        if isinstance(node, HashId): return node.hash
        if isinstance(node, AddrRef): return node.addr
        raise RuntimeError_(f"Expected identifier, got {type(node).__name__}")

    def _match_pattern(self, pattern: ASTNode, value: TengwarValue, env: Environment) -> bool:
        """Try to match a pattern against a value, binding variables in env"""
        # Wildcard
        if isinstance(pattern, Symbol) and pattern.name == '_':
            return True

        # Literal matching
        if isinstance(pattern, IntLit):
            return isinstance(value, TengwarInt) and value.value == pattern.value
        if isinstance(pattern, FloatLit):
            return isinstance(value, TengwarFloat) and value.value == pattern.value
        if isinstance(pattern, StrLit):
            return isinstance(value, TengwarStr) and value.value == pattern.value
        if isinstance(pattern, BoolLit):
            return isinstance(value, TengwarBool) and value.value == pattern.value
        if isinstance(pattern, UnitLit):
            return isinstance(value, TengwarUnit)

        # Variable binding
        if isinstance(pattern, Symbol):
            env.set(pattern.name, value)
            return True
        if isinstance(pattern, HashId):
            env.set(pattern.hash, value)
            return True

        # Tuple pattern
        if isinstance(pattern, Tuple) and isinstance(value, TengwarTuple):
            if len(pattern.elements) != len(value.elements):
                return False
            for p, v in zip(pattern.elements, value.elements):
                if not self._match_pattern(p, v, env):
                    return False
            return True

        # Vector pattern
        if isinstance(pattern, Vector) and isinstance(value, TengwarVector):
            if len(pattern.elements) != len(value.elements):
                return False
            for p, v in zip(pattern.elements, value.elements):
                if not self._match_pattern(p, v, env):
                    return False
            return True

        return False

    # === EVAL ===

    def eval(self, node: ASTNode, env: Optional[Environment] = None) -> TengwarValue:
        if env is None:
            env = self.global_env

        if self.trace:
            print(f"  EVAL: {type(node).__name__}")

        # Sandbox step limit
        if self.max_steps > 0:
            self.step_count += 1
            if self.step_count > self.max_steps:
                raise RuntimeError_("Execution step limit exceeded (sandbox)")

        # Literals
        if isinstance(node, IntLit):
            return TengwarInt(node.value)
        if isinstance(node, FloatLit):
            return TengwarFloat(node.value)
        if isinstance(node, StrLit):
            return TengwarStr(node.value)
        if isinstance(node, BoolLit):
            return TengwarBool(node.value)
        if isinstance(node, UnitLit):
            return TengwarUnit()

        # Identifiers
        if isinstance(node, Symbol):
            name = node.name
            # Dot indexing: s.1 → (nth s 1), s.0.name → chained access
            if '.' in name and not name.startswith('.') and not name.endswith('.'):
                parts = name.split('.')
                val = env.get(parts[0])
                for part in parts[1:]:
                    if part.isdigit():
                        idx = int(part)
                        if isinstance(val, TengwarTuple):
                            val = val.elements[idx]
                        elif isinstance(val, TengwarVector):
                            val = val.elements[idx]
                        elif isinstance(val, TengwarStr):
                            val = TengwarStr(val.value[idx])
                        else:
                            raise RuntimeError_(f"Cannot index into {type(val).__name__}")
                    elif isinstance(val, TengwarModule) and part in val.bindings:
                        val = val.bindings[part]
                    elif isinstance(val, TengwarDict):
                        key = TengwarStr(part)
                        if key in val.data:
                            val = val.data[key]
                        else:
                            raise RuntimeError_(f"Dict has no key '{part}'")
                    elif isinstance(val, TengwarPyObject):
                        attr = getattr(val.obj, part, None)
                        if attr is not None:
                            val = self._py_to_tw(attr) if not callable(attr) else TengwarPyObject(attr, f"{val.name}.{part}")
                        else:
                            raise RuntimeError_(f"Python object has no attribute '{part}'")
                    else:
                        raise RuntimeError_(f"Cannot access .{part} on {type(val).__name__}")
                return val
            return env.get(name)
        if isinstance(node, HashId):
            return env.get(node.hash)
        if isinstance(node, AddrRef):
            return env.lookup_addr(node.addr)

        # Lambda
        if isinstance(node, Lambda):
            param_names = [self._get_identifier_name(p) for p in node.params]
            return TengwarClosure(param_names, node.body, env)

        # Bind: expr → target
        if isinstance(node, Bind):
            value = self.eval(node.expr, env)
            if isinstance(node.target, AddrRef):
                # Content-address store
                addr = node.target.addr
                env.store(addr, value)
                # Also bind the addr as a name for convenience
                env.set(addr, value)
            elif isinstance(node.target, HashId):
                env.set(node.target.hash, value)
            elif isinstance(node.target, Symbol):
                env.set(node.target.name, value)
            elif isinstance(node.target, Tuple):
                # Destructuring bind
                if isinstance(value, TengwarTuple):
                    for pat, val in zip(node.target.elements, value.elements):
                        name = self._get_identifier_name(pat)
                        env.set(name, val)
                else:
                    raise RuntimeError_("Cannot destructure non-tuple")
            else:
                raise RuntimeError_(f"Invalid bind target: {type(node.target).__name__}")
            return value

        # Define
        if isinstance(node, Define):
            name = self._get_identifier_name(node.name)
            value = self.eval(node.value, env)
            # If defining a function, make it self-recursive
            if isinstance(value, TengwarClosure):
                value.name = name
                value.env.set(name, value)
            env.set(name, value)
            # Also compute content address
            addr = '@' + content_hash(name)
            env.store(addr, value)
            return value

        # Conditional
        if isinstance(node, Cond):
            cond_val = self.eval(node.condition, env)
            if self._is_truthy(cond_val):
                return self.eval(node.then_branch, env)
            else:
                return self.eval(node.else_branch, env)

        # Match
        if isinstance(node, Match):
            value = self.eval(node.expr, env)
            for pattern, body in node.cases:
                match_env = env.child()
                if self._match_pattern(pattern, value, match_env):
                    return self.eval(body, match_env)
            raise RuntimeError_(f"No matching pattern for {repr(value)}")

        # Sequence
        if isinstance(node, Seq):
            result = TengwarUnit()
            for expr in node.exprs:
                result = self.eval(expr, env)
            return result

        # Parallel
        if isinstance(node, Parallel):
            with concurrent.futures.ThreadPoolExecutor() as executor:
                futures = [executor.submit(self.eval, expr, env) for expr in node.exprs]
                results = [f.result() for f in futures]
            return TengwarTuple(results)

        # Module
        if isinstance(node, Module):
            mod_env = env.child()
            for expr in node.body:
                self.eval(expr, mod_env)
            module = TengwarModule(node.name, dict(mod_env.bindings))
            if node.name:
                env.set(node.name, module)
            return module

        # Recurse
        if isinstance(node, Recurse):
            name = self._get_identifier_name(node.name)
            # Create a closure that references itself
            child_env = env.child()
            body_val = self.eval(node.body, child_env)
            if isinstance(body_val, TengwarClosure):
                body_val.name = name
                # Make closure recursive by binding name in its own env
                body_val.env.set(name, body_val)
                child_env.set(name, body_val)
            env.set(name, body_val)
            return body_val

        # Proof
        if isinstance(node, Proof):
            result = self.eval(node.assertion, env)
            if not self._is_truthy(result):
                raise ProofError(f"Proof obligation failed: {repr(result)}")
            return result

        # Mutate
        if isinstance(node, Mutate):
            name = self._get_identifier_name(node.name)
            value = self.eval(node.value, env)
            cell = TengwarMutableCell(value)
            env.set(name, cell)
            return cell

        # Let — local bindings
        if isinstance(node, Let):
            child_env = env.child()
            for name_node, val_node in node.bindings:
                name = self._get_identifier_name(name_node)
                val = self.eval(val_node, child_env)
                child_env.set(name, val)
            return self.eval(node.body, child_env)

        # Pipe — thread value through functions (Clojure-style ->>)
        # (pipe val f1 (f2 arg) {short-lambda})
        #   - Symbol/lambda: called with val as sole arg
        #   - Apply node: val is appended as last argument
        if isinstance(node, Pipe):
            val = self.eval(node.value, env)
            for fn_node in node.funcs:
                if isinstance(fn_node, Apply):
                    # Thread: (reduce + 0) with piped val → (reduce + 0 val)
                    fn = self.eval(fn_node.func, env)
                    args = [self.eval(a, env) for a in fn_node.args]
                    args.append(val)
                    val = self._call_function(fn, args)
                else:
                    fn = self.eval(fn_node, env)
                    val = self._call_function(fn, [val])
            return val

        # Throw
        if isinstance(node, Throw):
            val = self.eval(node.expr, env)
            raise RuntimeError_(self._display(val))

        # Catch — (catch expr handler)
        if isinstance(node, Catch):
            try:
                return self.eval(node.expr, env)
            except (RuntimeError_, ProofError) as e:
                handler = self.eval(node.handler, env)
                return self._call_function(handler, [TengwarStr(str(e))])

        # Python import
        if isinstance(node, PyImportNode):
            if not self.sandbox_allow_py:
                raise RuntimeError_("Python imports disabled in sandbox mode")
            mod_name = self.eval(node.module_name, env)
            if isinstance(mod_name, TengwarStr):
                import importlib
                try:
                    py_mod = importlib.import_module(mod_name.value)
                    tw_mod = TengwarPyObject(py_mod, mod_name.value)
                    alias = node.alias or mod_name.value.split('.')[-1]
                    env.set(alias, tw_mod)
                    return tw_mod
                except ImportError as e:
                    raise RuntimeError_(f"py-import: {e}")
            raise RuntimeError_("py-import requires string module name")

        # Tuple
        if isinstance(node, Tuple):
            elements = [self.eval(e, env) for e in node.elements]
            return TengwarTuple(elements)

        # Vector
        if isinstance(node, Vector):
            elements = [self.eval(e, env) for e in node.elements]
            return TengwarVector(elements)

        # Application
        if isinstance(node, Apply):
            func = self.eval(node.func, env)
            # Handle dot access on modules
            if isinstance(func, TengwarModule):
                # (module.field args...)
                raise RuntimeError_("Direct module call not supported; use dot access")

            args = [self.eval(a, env) for a in node.args]
            return self._call_function(func, args)

        # Binary ops
        if isinstance(node, BinOp):
            left = self.eval(node.left, env)
            right = self.eval(node.right, env)
            return self._eval_binop(node.op, left, right)

        # Unary ops
        if isinstance(node, UnOp):
            operand = self.eval(node.operand, env)
            return self._eval_unop(node.op, operand)

        # Program
        if isinstance(node, Program):
            result = TengwarUnit()
            for expr in node.body:
                result = self.eval(expr, env)
            return result

        raise RuntimeError_(f"Unknown node type: {type(node).__name__}")

    def _eval_binop(self, op: str, left: TengwarValue, right: TengwarValue) -> TengwarValue:
        # String concatenation with +
        if op == '+' and isinstance(left, TengwarStr):
            return TengwarStr(left.value + self._display(right))

        # Numeric operations
        if isinstance(left, (TengwarInt, TengwarFloat)) and isinstance(right, (TengwarInt, TengwarFloat)):
            lv, rv = left.value, right.value
            use_float = isinstance(left, TengwarFloat) or isinstance(right, TengwarFloat)
            wrap = TengwarFloat if use_float else TengwarInt

            if op == '+': return wrap(lv + rv)
            if op == '-': return wrap(lv - rv)
            if op == '*': return wrap(lv * rv)
            if op == '/':
                if rv == 0: raise RuntimeError_("Division by zero")
                return TengwarFloat(lv / rv) if use_float else TengwarInt(lv // rv)
            if op == '%': return wrap(lv % rv)
            if op == '=': return TengwarBool(lv == rv)
            if op == '!=': return TengwarBool(lv != rv)
            if op == '<': return TengwarBool(lv < rv)
            if op == '>': return TengwarBool(lv > rv)
            if op == '<=': return TengwarBool(lv <= rv)
            if op == '>=': return TengwarBool(lv >= rv)

        # Boolean operations
        if isinstance(left, TengwarBool) and isinstance(right, TengwarBool):
            if op == '&': return TengwarBool(left.value and right.value)
            if op == '|': return TengwarBool(left.value or right.value)

        # Equality on any type
        if op == '=': return TengwarBool(left == right)
        if op == '!=': return TengwarBool(left != right)

        raise RuntimeError_(f"Invalid operation: {repr(left)} {op} {repr(right)}")

    def _eval_unop(self, op: str, operand: TengwarValue) -> TengwarValue:
        if op == '!' and isinstance(operand, TengwarBool):
            return TengwarBool(not operand.value)
        if op == '-' and isinstance(operand, TengwarInt):
            return TengwarInt(-operand.value)
        if op == '-' and isinstance(operand, TengwarFloat):
            return TengwarFloat(-operand.value)
        raise RuntimeError_(f"Invalid unary operation: {op} {repr(operand)}")

    def run(self, program: Program) -> TengwarValue:
        """Run a parsed program"""
        return self.eval(program, self.global_env)

    def run_source(self, source: str) -> TengwarValue:
        """Lex, parse, and run source code"""
        from .lexer import tokenize
        from .parser import parse
        tokens = tokenize(source)
        ast = parse(tokens)
        return self.run(ast)
