"""
Nexa Language — Interpreter v25
New: var statement, native assert, trait/impl system, use imports, /= assign, no debug prints
"""
from __future__ import annotations
import asyncio, os
from typing import Any, Dict, List, Optional

from .ast_nodes import *
from .builtins import BUILTINS, NexaFunction, NexaModel, NexaBuiltinError, SysModule


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

class YieldException(Exception):
    """Internal: signals a yield inside a generator function."""
    def __init__(self, value): self.value = value

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

class NexaTypedError(Exception):
    def __init__(self, type_name, value):
        self.type_name = type_name; self.value = value
        super().__init__(f"{type_name}: {value}")

class NexaRuntimeError(Exception):
    pass

class NexaBreak(Exception): pass
class NexaContinue(Exception): pass

class NexaTestFailure(Exception):
    def __init__(self, msg): self.msg = msg


# ── Generator support ─────────────────────────────────────────────────────────
class NexaGenerator:
    """Wraps a NexaFunction that contains `yield` statements."""
    def __init__(self, fn: "NexaFunction", args: list, kwargs: dict, interp):
        self._fn = fn
        self._args = args
        self._kwargs = kwargs
        self._interp = interp
        self._generator = None

    def _build(self):
        """Build the underlying Python generator lazily."""
        fn = self._fn; interp = self._interp
        args = self._args; kwargs = self._kwargs

        def _gen():
            fn_env = Environment(fn.closure)
            _bind_args(fn, fn_env, args, kwargs)
            # Walk the body and yield through
            yield from interp._exec_generator(fn.body, fn_env)

        self._generator = _gen()

    def __iter__(self):
        if self._generator is None: self._build()
        return self._generator

    def __next__(self):
        if self._generator is None: self._build()
        return next(self._generator)

    def to_list(self):
        return list(self)

    def __repr__(self): return f"<generator {self._fn.name}>"


# ── Struct support ────────────────────────────────────────────────────────────
class NexaStruct:
    """A struct type — creates lightweight typed instances."""
    def __init__(self, name: str, fields: list):
        self.name = name
        self.fields = fields   # [(field_name, type_hint), ...]

    def call(self, args, kwargs, interp, env):
        instance = NexaStructInstance(self.name, self.fields)
        # Positional args: assign in field order
        for i, (fname, _) in enumerate(self.fields):
            if i < len(args): instance._attrs[fname] = args[i]
        # Keyword args
        for k, v in kwargs.items():
            if k in {n for n, _ in self.fields}:
                instance._attrs[k] = v
        return instance

    def __repr__(self): return f"<struct {self.name}>"


class NexaStructInstance:
    def __init__(self, name: str, fields: list):
        self._name = name
        self._fields = fields
        self._attrs = {fname: None for fname, _ in fields}

    def get_attr(self, name, interp=None):
        if name in self._attrs: return self._attrs[name]
        raise NexaRuntimeError(f"Struct '{self._name}' has no field '{name}'")

    def set_attr(self, name, value):
        if name in self._attrs: self._attrs[name] = value
        else: raise NexaRuntimeError(f"Struct '{self._name}' has no field '{name}'")

    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in self._attrs.items())
        return f"{self._name}({fields})"


class NexaClass:
    def __init__(self, name, methods, base=None):
        self.name = name; self.methods = methods; self.base = base

    def call(self, args, kwargs, interp, env):
        instance = NexaInstance(self)
        init = self._find_method("__init__") or self._find_method("init")
        if init:
            bound = BoundMethod(instance, init, interp)
            bound(*args, **kwargs)
        return instance

    def _find_method(self, name):
        if name in self.methods: return self.methods[name]
        if self.base: return self.base._find_method(name)
        return None

    def get_attr(self, name, interp):
        method = self._find_method(name)
        if method:
            # Check if this method was decorated with @staticmethod
            # In native python, static methods are just functions when accessed from class
            is_static = getattr(method, "_is_staticmethod", False)
            if is_static: return method
            raise NexaRuntimeError(f"Cannot access instance method '{name}' from class '{self.name}' without an instance")
        raise NexaRuntimeError(f"Class '{self.name}' has no attribute '{name}'")

    def __repr__(self): return f"<class '{self.name}'>"


class NexaInstance:
    def __init__(self, klass):
        self._class = klass; self._attrs = {}

    def get_attr(self, name, interp):
        if name in self._attrs: return self._attrs[name]
        method = self._class._find_method(name)
        if method:
            if getattr(method, "_is_staticmethod", False):
                return method # Do not bind instance
            return BoundMethod(self, method, interp)
        raise NexaRuntimeError(f"'{self._class.name}' has no attribute '{name}'")

    def set_attr(self, name, value): self._attrs[name] = value

    def __repr__(self):
        attrs = {k: v for k, v in self._attrs.items() if not k.startswith("_")}
        return f"<{self._class.name} {attrs}>"

class NexaSuper:
    def __init__(self, instance):
        self.instance = instance

    def get_attr(self, name, interp):
        if not self.instance._class.base:
            raise NexaRuntimeError(f"'{self.instance._class.name}' has no base class")
        method = self.instance._class.base._find_method(name)
        if method: return BoundMethod(self.instance, method, interp)
        raise NexaRuntimeError(f"Base '{self.instance._class.base.name}' has no attribute '{name}'")

class NexaEnum:
    def __init__(self, name, members):
        self.name = name
        self.members = {m: m for m in members}

    def get_attr(self, name, interp):
        if name in self.members: return self.members[name]
        raise NexaRuntimeError(f"Enum '{self.name}' has no member '{name}'")

    def __repr__(self): return f"<enum '{self.name}'>"


class BoundMethod:
    def __init__(self, instance, fn, interp):
        self.instance = instance; self.fn = fn; self.interp = interp

    def __call__(self, *args, **kwargs):
        fn_env = Environment(self.fn.closure)
        _bind_args(self.fn, fn_env, [self.instance] + list(args), kwargs)
        try: return self.interp._exec_block(self.fn.body, fn_env)
        except ReturnException as ret: return ret.value


class NexaCoroutine:
    def __init__(self, fn, args, kwargs, interp):
        self.fn = fn; self.args = args; self.kwargs = kwargs; self.interp = interp

    def __await__(self): return self._run().__await__()

    async def _run(self):
        fn_env = Environment(self.fn.closure)
        fn_env.set("self", None)
        _bind_args(self.fn, fn_env, self.args, self.kwargs)
        try:
            result = self.interp._exec_block(self.fn.body, fn_env)
            if asyncio.iscoroutine(result): return await result
            return result
        except ReturnException as ret:
            val = ret.value
            if asyncio.iscoroutine(val): return await val
            return val


def _check_type(val: Any, type_hint: str, context_msg: str):
    if val is None: return  # null bypasses type checks slightly like optional
    
    type_map = {
        "int": int, "float": float, "str": str, "bool": bool,
        "list": list, "dict": dict, "set": set
    }
    
    if type_hint in type_map:
        if not isinstance(val, type_map[type_hint]):
            raise NexaRuntimeError(f"Type Error: {context_msg} expected '{type_hint}', got '{type(val).__name__}'")
    elif type_hint == "model":
        if type(val).__name__ != "NexaModel":
            raise NexaRuntimeError(f"Type Error: {context_msg} expected 'model', got '{type(val).__name__}'")
    else:
        # Check struct or class matching
        val_type_name = getattr(val, "_name", getattr(getattr(val, "_class", None), "name", None))
        if val_type_name and val_type_name != type_hint:
             raise NexaRuntimeError(f"Type Error: {context_msg} expected '{type_hint}', got '{val_type_name}'")


def _bind_args(fn: NexaFunction, env: "Environment", args: list, kwargs: dict):
    """Bind positional args, kwargs, and defaults to fn params."""
    params = fn.params
    defaults = getattr(fn, "defaults", {})
    param_types = getattr(fn, "param_types", {})

    # Positional
    for i, param in enumerate(params):
        if param.startswith("..."):
            rest_args = args[i:]
            env.set(param[3:], rest_args)
            break
        
        val = None
        if i < len(args):
            val = args[i]
        elif param in kwargs:
            val = kwargs[param]
        elif param in defaults:
            val = defaults[param]  # already evaluated
        
        # Type enforcement
        if param in param_types and val is not None:
            _check_type(val, param_types[param], f"Argument '{param}'")
            
        env.set(param, val)
        
    # Extra kwargs
    for k, v in kwargs.items():
        if k not in env._vars:
            env.set(k, v)


class Environment:
    def __init__(self, parent=None):
        self._vars: Dict[str, Any] = {}; self.parent = parent
        self._consts: set = set()   # v27: names that are const-locked

    def get(self, name):
        if name in self._vars: return self._vars[name]
        if self.parent: return self.parent.get(name)
        raise NexaRuntimeError(f"Undefined: '{name}'")

    def set(self, name, value):  self._vars[name] = value

    def set_const(self, name, value):
        """v27: Store a const — immutable even at interpreter level."""
        self._vars[name] = value
        self._consts.add(name)

    def is_const(self, name) -> bool:
        if name in self._consts: return True
        return self.parent.is_const(name) if self.parent else False

    def assign(self, name, value):
        if name in self._vars:
            if name in self._consts:
                raise NexaRuntimeError(
                    f"Assignment to const '{name}' is not allowed. "
                    f"Use 'var' instead of 'const' if you need a mutable binding."
                )
            self._vars[name] = value; return
        if self.parent: self.parent.assign(name, value); return
        self._vars[name] = value


# ── Test runner state ─────────────────────────────────────────────────────────
class TestRunner:
    def __init__(self):
        self.passed = 0; self.failed = 0; self.errors = []

    def report(self):
        total = self.passed + self.failed
        color_ok = "\033[92m"; color_fail = "\033[91m"; reset = "\033[0m"
        print(f"\n{'='*50}")
        print(f"  Test Results: {self.passed}/{total} passed")
        for err in self.errors:
            print(f"  {color_fail}✗ {err}{reset}")
        if self.failed == 0:
            print(f"  {color_ok}✅ All tests passed!{reset}")
        print(f"{'='*50}")
        return self.failed == 0


# ── Interpreter ───────────────────────────────────────────────────────────────
class Interpreter:
    def __init__(self, debug=False, source_dir="."):
        self.debug = debug
        self.source_dir = source_dir
        self.global_env = Environment()
        self.test_runner = TestRunner()
        self.call_stack = []
        for name, val in BUILTINS.items():
            self.global_env.set(name, val)
        self.global_env.set("async_map", self._builtin_async_map)
        self.global_env.set("gather",    self._builtin_gather)

    def run(self, program: Program) -> Any:
        return self._exec_block(program.statements, self.global_env)

    def _exec_block(self, stmts, env) -> Any:
        result = None
        for stmt in stmts:
            result = self._exec(stmt, env)
        return result

    def _exec_generator(self, stmts, env):
        for stmt in stmts:
            try:
                self._exec(stmt, env)
            except YieldException as y:
                yield y.value
            except ReturnException:
                break
            except Exception:
                raise

    def _exec(self, node, env) -> Any:
        try:
            method = f"_exec_{type(node).__name__}"
            handler = getattr(self, method, None)
            if handler: return handler(node, env)
            return self._eval(node, env)
        except Exception as e:
            if not getattr(e, '_nexa_handled', False):
                line = getattr(node, 'line', -1)
                self.call_stack.append(f"at line {line}" if line > 0 else "at <unknown line>")
                e._nexa_handled = True
            raise

    # ── Statements ────────────────────────────────────────────────────────
    def _exec_FunctionDef(self, node: FunctionDef, env: Environment):
        fn = NexaFunction(node.name, node.params, node.body, env)
        fn.is_async = node.is_async
        fn.param_types = node.param_types
        fn.return_type = node.return_type
        # Pre-evaluate defaults in current scope
        fn.defaults = {k: self._eval(v, env) for k, v in node.defaults.items()}
        
        result_fn = fn
        for dec in reversed(node.decorators):
            dec_fn = self._eval(dec.expr, env)
            result_fn = self._call(dec_fn, [result_fn], {}, env)
        
        env.set(node.name, result_fn)

    def _exec_ClassDef(self, node: ClassDef, env: Environment):
        methods = {}
        for stmt in node.body:
            if isinstance(stmt, FunctionDef):
                fn = NexaFunction(stmt.name, stmt.params, stmt.body, env)
                fn.is_async = stmt.is_async
                fn.defaults = {k: self._eval(v, env) for k, v in stmt.defaults.items()}
                
                result_fn = fn
                for dec in reversed(stmt.decorators):
                    dec_fn = self._eval(dec.expr, env)
                    result_fn = self._call(dec_fn, [result_fn], {}, env)
                
                methods[stmt.name] = result_fn
                
        base = env.get(node.base) if node.base else None
        
        # v24: Copy default methods from implemented interfaces
        if hasattr(node, "implements") and node.implements:
            for iface_name in node.implements:
                iface = env.get(iface_name)
                if isinstance(iface, dict) and iface.get("__interface__"):
                    for mname, mdef in iface.get("defaults", {}).items():
                        if mname not in methods:
                            methods[mname] = NexaFunction(mdef.name, mdef.params, mdef.body, env)
        # v25: apply trait impls registered for this class
        trait_registry = getattr(env, '_traits', {})
        for tname, timpl in trait_registry.items():
            if timpl.get('class_name') == node.name:
                for m in timpl.get('methods', []):
                    if m.name not in methods:
                        fn = NexaFunction(m.name, m.params, m.body, env)
                        fn.param_types = m.param_types
                        methods[m.name] = fn

        klass = NexaClass(node.name, methods, base)

        result_klass = klass
        for dec in reversed(node.decorators):
            dec_fn = self._eval(dec.expr, env)
            result_klass = self._call(dec_fn, [result_klass], {}, env)

        env.set(node.name, result_klass)

    def _exec_EnumDef(self, node: EnumDef, env: Environment):
        env.set(node.name, NexaEnum(node.name, node.members))

    # ── v27 Language Core Handlers ────────────────────────────────────────────

    def _exec_ConstStatement(self, node, env):
        """const MAX: Int = 100  — immutable, cross-file constant."""
        val = self._eval(node.value, env)
        hint = getattr(node, "type_hint", None)
        self._runtime_null_check(val, hint, node.name)
        if hint:
            _check_type(val, hint, f"Const '{node.name}'")
        # Store in global env (const is always top-level)
        self.global_env.set_const(node.name, val)

    def _exec_TypeAlias(self, node, env):
        """type UserId = Int  — register type alias in type registry."""
        self.global_env.set(f"__type_{node.name}__", node.target)
        # Also expose as a callable validator
        target = node.target
        def _type_alias_ctor(v):
            return v  # passthrough — alias doesn't change runtime type
        _type_alias_ctor.__name__ = node.name
        env.set(node.name, _type_alias_ctor)

    def _exec_NewtypeStatement(self, node, env):
        """newtype Celsius = Float  — branded, non-interchangeable wrapper."""
        brand_name = node.name
        base       = node.base_type

        class _Newtype:
            _brand  = brand_name
            _base   = base
            __slots__ = ("_val",)
            def __init__(s, v): s._val = v
            def __repr__(s):    return f"{brand_name}({s._val!r})"
            def __add__(s, o):  return _Newtype(s._val + (o._val if isinstance(o, _Newtype) else o))
            def __sub__(s, o):  return _Newtype(s._val - (o._val if isinstance(o, _Newtype) else o))
            def __mul__(s, o):  return _Newtype(s._val * (o._val if isinstance(o, _Newtype) else o))
            def __truediv__(s, o): return _Newtype(s._val / (o._val if isinstance(o, _Newtype) else o))
            def __eq__(s, o):   return isinstance(o, _Newtype) and s._brand == o._brand and s._val == o._val
            def __lt__(s, o):   return s._val < (o._val if isinstance(o, _Newtype) else o)
            def __le__(s, o):   return s._val <= (o._val if isinstance(o, _Newtype) else o)
            def __float__(s):   return float(s._val)
            def __int__(s):     return int(s._val)

        _Newtype.__name__     = brand_name
        _Newtype.__qualname__ = brand_name
        env.set(brand_name, _Newtype)

    def _eval_PipeErrExpr(self, node, env):
        """|? error pipe — runs handler only if value is a Result Err."""
        val = self._eval(node.value, env)
        # Check if val is an Err (dict with __tag__ == "Err")
        is_err = (isinstance(val, dict) and val.get("__tag__") == "Err") or \
                 (isinstance(val, Exception))
        if is_err:
            handler = self._eval(node.handler, env)
            if callable(handler):
                err_val = val.get("__val__", [val])[0] if isinstance(val, dict) else val
                return self._call(handler, [err_val], {}, env)
            return handler
        return val   # pass through on Ok

    def _eval_TemplateString(self, node, env):
        """template \"\"\"...\"\"\" — multi-line interpolated string."""
        parts = []
        for kind, content in node.parts:
            if kind == "lit":
                parts.append(str(content))
            else:
                # Parse and evaluate embedded expression
                from .lexer import Lexer
                from .parser import Parser
                try:
                    tokens = Lexer(content.strip()).tokenize()
                    expr   = Parser(tokens)._expr()
                    val    = self._eval(expr, env)
                    parts.append(str(val) if val is not None else "")
                except Exception:
                    parts.append(f"{{{content}}}")
        return "".join(parts)

    def _exec_GenericRuntimeCheck(self, node, env):
        """Enforce List<Int> element types at runtime."""
        target = self._eval(node.target, env)
        if isinstance(target, list):
            type_map = {"Int": int, "Float": float, "String": str, "Bool": bool}
            expected = type_map.get(node.item_type)
            if expected:
                for i, item in enumerate(target):
                    if not isinstance(item, expected):
                        raise NexaRuntimeError(
                            f"Generic type violation: element [{i}] expected {node.item_type}, "
                            f"got {type(item).__name__} ({item!r})"
                        )
        return target

    def _eval_ExpectExpr(self, node, env):
        """expect(x).to_equal(5) — rich test matcher."""
        from .matchers import expect as _expect
        subject = self._eval(node.subject, env)
        ex = _expect(subject)
        method = getattr(ex, node.method, None)
        if method is None:
            raise NexaRuntimeError(f"expect(): unknown matcher '{node.method}'")
        args = [self._eval(a, env) for a in node.args]
        return method(*args)

    # ── v26 Runtime Null Safety ───────────────────────────────────────────────
    @staticmethod
    def _is_nullable_hint(hint) -> bool:
        """Return True if hint allows null (e.g. String?, Int | None, UnionType with None)."""
        from .ast_nodes import UnionType as _UT
        if hint is None:
            return True  # unannotated — no enforcement
        if isinstance(hint, _UT):
            return "None" in hint.types
        if isinstance(hint, str):
            return hint in ("None", "Any", "Object")
        return True  # Unknown hint type — don't enforce

    def _runtime_null_check(self, val, hint, name: str):
        """Raise NexaRuntimeError if val is None and hint is non-nullable."""
        if val is None and not self._is_nullable_hint(hint):
            raise NexaRuntimeError(
                f"Null Safety Violation: cannot assign null to '{name}' "
                f"(type '{hint}' is not nullable). Use '{hint}?' to allow null."
            )

    def _exec_ModelDef(self, node, env):
        layers_val = self._eval(node.layers, env) if hasattr(node, "layers") and node.layers else []
        config_val = {k: self._eval(v, env) for k, v in node.config.items()}
        
        model = NexaModel(node.name, layers_val, config_val)
        if "optimizer" in config_val or "loss" in config_val:
            model.compile(config_val.get("optimizer", "adam"), config_val.get("loss", "mse"))
        env.set(node.name, model)

    def _exec_TrainStatement(self, node, env):
        model = env.get(node.model_name)
        if type(model).__name__ != "NexaModel":
            raise NexaRuntimeError(f"'{node.model_name}' is not a Model")
        data = self._eval(node.data, env)
        config = self._eval(node.config, env)
        
        X = data.get("X", []) if isinstance(data, dict) else []
        y = data.get("y", []) if isinstance(data, dict) else []
        epochs = config.get("epochs", 10) if isinstance(config, dict) else 10
        
        if len(X) > 0 and isinstance(X[0], str):
            from .builtins import NLPModule
            nlp = NLPModule()
            X = [nlp.embed(text) for text in X]
            model._is_text_model = True
            
        model.fit(X, y, epochs=epochs)

    def _eval_PredictExpr(self, node, env):
        model = env.get(node.model_name)
        if type(model).__name__ != "NexaModel":
             raise NexaRuntimeError(f"'{node.model_name}' is not a Model")
        input_data = self._eval(node.input_data, env)
        
        if not isinstance(input_data, list):
            input_list = [input_data]
            was_single = True
        else:
            input_list = input_data
            was_single = False
            
        if getattr(model, "_is_text_model", False):
            from .builtins import NLPModule
            nlp = NLPModule()
            input_list = [nlp.embed(text) if isinstance(text, str) else text for text in input_list]
            
        res = model.predict(input_list)
        return res[0] if was_single and len(res) == 1 else res

    def _exec_LetStatement(self, node: LetStatement, env):
        val = self._eval(node.value, env)
        hint = getattr(node, "type_hint", None)
        self._runtime_null_check(val, hint, node.name)   # v26: runtime null safety
        if hint:
            _check_type(val, hint, f"Variable '{node.name}'")
        env.set(node.name, val)

    def _exec_DestructuredAssignment(self, node: DestructuredAssignment, env: Environment):
        val = self._eval(node.value, env)
        if node.is_dict:
            if not isinstance(val, dict):
                raise NexaRuntimeError(f"Cannot destructure non-dict {type(val).__name__}")
            for target in node.targets:
                env.set(target, val.get(target))
        else:
            try: seq = list(val)
            except TypeError: raise NexaRuntimeError(f"Cannot destructure non-iterable {type(val).__name__}")
            if len(seq) < len(node.targets):
                raise NexaRuntimeError(f"Not enough values to unpack (expected {len(node.targets)}, got {len(seq)})")
            for t, v in zip(node.targets, seq):
                env.set(t, v)

    def _exec_ReturnStatement(self, node: ReturnStatement, env): raise ReturnException(self._eval(node.value, env))
    def _exec_YieldStatement(self, node: YieldStatement, env): raise YieldException(self._eval(node.value, env))

    def _exec_BreakStatement(self, node: BreakStatement, env): raise NexaBreak()
    def _exec_ContinueStatement(self, node: ContinueStatement, env): raise NexaContinue()

    # ── v26 Async Generators ─────────────────────────────────────────────────
    def _exec_AsyncGeneratorDef(self, node, env):
        """async def gen() { await …; yield item } — creates an async generator."""
        interp = self

        def _async_gen_body(*args):
            """Python generator that simulates the async generator body."""
            fn_env = Environment(env)
            # Bind params
            params = node.params
            for i, p in enumerate(params):
                pname = p.name if hasattr(p, "name") else str(p)
                fn_env.set(pname, args[i] if i < len(args) else None)
            # Execute body and collect yields — use YieldException protocol
            import types as _types
            for stmt in node.body:
                try:
                    interp._exec(stmt, fn_env)
                except YieldException as ye:
                    yield ye.value
                except ReturnException:
                    return

        class AsyncGenerator:
            def __init__(ag_self, *args):
                ag_self._gen = _async_gen_body(*args)
                ag_self._name = node.name
            def __iter__(ag_self): return ag_self._gen
            def __next__(ag_self): return next(ag_self._gen)
            def __repr__(ag_self): return f"<async_generator '{node.name}'>"

        class AsyncGenCallable:
            def call(ac_self, args, kwargs, interp_=None, env_=None):
                return AsyncGenerator(*args)
            def __call__(ac_self, *args):
                return AsyncGenerator(*args)
            def __repr__(ac_self): return f"<async_gen_fn '{node.name}'>"

        env.set(node.name, AsyncGenCallable())

    def _exec_AsyncForStatement(self, node, env):
        """async for item in generator() { } — iterates an async generator."""
        gen = self._eval(node.iterable, env)
        for item in gen:
            child = Environment(env)
            child.set(node.var, item)
            try:
                self._exec_block(node.body, child)
            except NexaBreak: break
            except NexaContinue: continue

    def _eval_IfExpr(self, node: IfExpr, env):
        if self._truthy(self._eval(node.condition, env)):
            return self._exec_block(node.then_block, Environment(env))
        elif node.else_block:
            return self._exec_block(node.else_block, Environment(env))
        return None

    def _exec_ForStatement(self, node: ForStatement, env):
        result = None
        for item in self._eval(node.iterable, env):
            child = Environment(env); child.set(node.var, item)
            try:
                result = self._exec_block(node.body, child)
            except NexaBreak: break
            except NexaContinue: continue
        return result

    def _exec_WhileStatement(self, node: WhileStatement, env):
        result, i = None, 0
        while self._truthy(self._eval(node.condition, env)):
            try:
                result = self._exec_block(node.body, Environment(env))
            except NexaBreak: break
            except NexaContinue: continue
            i += 1
            if i >= 100_000: raise NexaRuntimeError("Infinite loop")
        return result
    def _eval_TryExpr(self, node: TryExpr, env: Environment):
        try:
            return self._exec_block(node.try_block, Environment(env))
        except ReturnException:
            raise
        except Exception as e:
            if node.catch_block:
                catch_env = Environment(env)
                err_val = e.value if isinstance(e, NexaError) else str(e)
                if node.catch_var: catch_env.set(node.catch_var, err_val)
                return self._exec_block(node.catch_block, catch_env)
            return None
        finally:
            if node.finally_block:
                self._exec_block(node.finally_block, Environment(env))

    def _exec_ThrowStatement(self, node: ThrowStatement, env):
        if node.type_name:
            raise NexaTypedError(node.type_name, self._eval(node.value, env))
        raise NexaError(self._eval(node.value, env))

    def _exec_InterfaceDef(self, node, env):
        defaults = getattr(node, "defaults", {})
        env.set(node.name, {"__interface__": True, "methods": node.method_names, "defaults": defaults})

    # ── v23 New Handlers ─────────────────────────────────────────────────────
    def _exec_ConstStatement(self, node, env):
        """const PI = 3.14 — immutable binding, raises on re-assign."""
        val = self._eval(node.value, env)
        env.set(node.name, val)
        # Mark as const so Assignment handler can guard it
        if not hasattr(env, '_consts'): env._consts = set()
        env._consts.add(node.name)

    def _exec_EnumDef(self, node, env):
        """Register an enum type with variant constructor functions."""
        enum_ns = {"__enum__": node.name, "__variants__": {}}
        for variant in node.variants:
            vname = variant.name
            vfields = variant.fields
            if vfields:
                # Variant with data: Option.Some(42) → {"__tag__":"Some","__val__":[42]}
                def make_variant_ctor(vn, vf):
                    def ctor(*args):
                        if len(args) != len(vf):
                            raise NexaRuntimeError(
                                f"Enum {node.name}.{vn} expects {len(vf)} arg(s), got {len(args)}")
                        return {"__tag__": vn, "__enum__": node.name,
                                "__val__": list(args), "__fields__": vf}
                    ctor.__name__ = vn
                    return ctor
                enum_ns[vname] = make_variant_ctor(vname, vfields)
                enum_ns["__variants__"][vname] = vfields
            else:
                # Unit variant: Option.None → {"__tag__":"None","__val__":[]}
                unit = {"__tag__": vname, "__enum__": node.name, "__val__": [], "__fields__": []}
                enum_ns[vname] = unit
                enum_ns["__variants__"][vname] = []
        env.set(node.name, enum_ns)

    def _exec_BreakIfStatement(self, node, env):
        """break if expr — raises NexaBreak only when condition is truthy."""
        if self._eval(node.condition, env):
            raise NexaBreak()

    def _eval_VariantCall(self, node, env):
        """Option.Some(42) — calls the variant constructor."""
        enum_ns = env.get(node.enum_name)
        if not isinstance(enum_ns, dict) or "__enum__" not in enum_ns:
            raise NexaRuntimeError(f"'{node.enum_name}' is not an enum")
        ctor = enum_ns.get(node.variant_name)
        if ctor is None:
            raise NexaRuntimeError(f"Unknown variant '{node.variant_name}' in enum '{node.enum_name}'")
        args = [self._eval(a, env) for a in node.args]
        return ctor(*args) if callable(ctor) else ctor

    def _match_enum_variant(self, pattern, subject, env) -> bool:
        """Try to match an EnumVariantPattern against a tagged-union value."""
        if not isinstance(subject, dict) or subject.get("__tag__") != pattern.variant_name:
            return False
        if pattern.enum_name:
            actual_enum = subject.get("__enum__") or subject.get("__sealed__")
            if pattern.enum_name != actual_enum:
                return False
        vals = subject.get("__val__", [])
        for i, bind in enumerate(pattern.binds):
            env.set(bind, vals[i] if i < len(vals) else None)
        return True

    def _exec_LoopStatement(self, node, env):
        """Infinite loop — loop { ... } — exits via break."""
        i = 0
        while True:
            try:
                self._exec_block(node.body, Environment(env))
            except NexaBreak: break
            except NexaContinue: continue
            i += 1
            if i >= 1_000_000: raise NexaRuntimeError("Infinite loop exceeded 1M iterations")

    def _exec_AbstractMethodDef(self, node, env):
        """Register abstract stub that raises if called without override."""
        name = node.name
        def abstract_stub(*args, **kwargs):
            raise NexaTypedError("AbstractError", f"Abstract method '{name}' not implemented")
        abstract_stub.__abstract__ = True
        env.set(name, abstract_stub)

    def _exec_ExportStatement(self, node, env):
        """Register export list in current env scope."""
        env.set("__exports__", node.names)

    def _eval_NonNullAssertion(self, node, env):
        """expr! — raises NullError if value is None."""
        val = self._eval(node.expr, env)
        if val is None:
            raise NexaTypedError("NullError", "Non-null assertion failed: value is null")
        return val

    def _match_destructure(self, pattern, subject, env) -> bool:
        """Try to match DestructurePattern against subject; bind vars in env. Returns bool."""
        if pattern.kind == 'dict':
            if not isinstance(subject, dict): return False
            for field in pattern.fields:
                if field not in subject: return False
                env.set(field, subject[field])
            return True
        elif pattern.kind == 'list':
            if not isinstance(subject, (list, tuple)): return False
            regs = [f for f in pattern.fields if not str(f).startswith('...')]
            rest = next((str(f)[3:] for f in pattern.fields if str(f).startswith('...')), None)
            if rest is None and len(subject) != len(regs): return False
            if rest is not None and len(subject) < len(regs): return False
            for i, field in enumerate(regs): env.set(field, subject[i])
            if rest: env.set(rest, list(subject[len(regs):]))
            return True
        elif pattern.kind == 'class':
            if not isinstance(subject, NexaInstance): return False
            if pattern.class_name and getattr(subject, '__class_name__', '') != pattern.class_name:
                return False
            for field in pattern.fields:
                env.set(field, subject.attrs.get(field))
            return True
        return False

    def _eval_RangeExpr(self, node, env):
        start = int(self._eval(node.start, env))
        end   = int(self._eval(node.end, env))
        return list(range(start, end + (1 if node.inclusive else 0)))

    def _exec_ModelDef(self, node: ModelDef, env):
        layers = self._eval(node.layers, env) if node.layers else []
        config = {k: self._eval(v, env) for k, v in node.config.items()}
        env.set(node.name, NexaModel(node.name, layers, config))

    def _exec_ImportStatement(self, node: ImportStatement, env):
        if node.is_file:
            self._import_file(node.module, node.alias, env); return
        known = {"automl","nlp","vision","math","stats","fs","json",
                 "http","time","random","agent","df",
                 "db", "re", "serve", "tensor", "env", "sys",
                 "datetime", "crypto", "base64", "task", "concurrency",
                 "result", "option", "string", "array"}   # v25
        if node.module in known:
            mod = BUILTINS.get(node.module)
            if mod is None:
                raise NexaRuntimeError(f"Module '{node.module}' not available in builtins")
            env.set(node.alias or node.module, mod); return
        raise NexaRuntimeError(f"Unknown module: '{node.module}'")

    def _exec_FromImportStatement(self, node: FromImportStatement, env):
        module_name = node.module
        known = {"automl","nlp","vision","math","stats","fs","json",
                 "http","time","random","agent","df",
                 "db", "re", "serve", "tensor", "env", "sys",
                 "datetime", "crypto", "base64", "task", "concurrency"}
        if module_name in known:
            mod = BUILTINS[module_name]
        else:
            mod = self._import_file_namespace(module_name)
            
        for name in node.names:
            if isinstance(mod, dict):
                if name not in mod: raise NexaRuntimeError(f"Cannot import '{name}' from '{module_name}'")
                env.set(name, mod[name])
            else:
                try: env.set(name, getattr(mod, name))
                except AttributeError: raise NexaRuntimeError(f"Cannot import '{name}' from '{module_name}'")

    def _import_file_namespace(self, path):
        # v25: check BUILTINS first (handles import "result", import "option", etc.)
        from .builtins import BUILTINS as _B
        if path in _B:
            mod = _B[path]
            # Return as flat dict of bound attributes
            return {k: getattr(mod, k) for k in dir(mod)
                    if not k.startswith("_") and callable(getattr(mod, k))}

        from .lexer import Lexer
        from .parser import Parser
        full = os.path.join(self.source_dir, path)
        is_path_nexa = full.endswith(".nexa")
        if not is_path_nexa: full += ".nexa"

        if not os.path.exists(full):
            # Try package manager resolution
            from .pkg_manager import PackageManager
            lock = PackageManager()._read_lock()
            if path in lock.get("installed", {}):
                full = lock["installed"][path]["entry"]
            else:
                pkg_path = os.path.expanduser(f"~/.nexa/packages/{path}/{path}.nexa")
                if os.path.exists(pkg_path): full = pkg_path
                else: raise NexaRuntimeError(f"Module not found: {path}")

        src = open(full, encoding="utf-8").read()
        tokens = Lexer(src).tokenize()
        ast = Parser(tokens).parse()
        mi = Interpreter(debug=self.debug, source_dir=os.path.dirname(full))
        mi.run(ast)
        ns = {k: v for k, v in mi.global_env._vars.items() if k not in BUILTINS}
        return ns

    def _import_file(self, path, alias, env):
        ns = self._import_file_namespace(path)
        name = alias or os.path.splitext(os.path.basename(path))[0]
        env.set(name, ns)
        print(f"  ✅ Imported '{name}' ({len(ns)} exports)")

    # ── Match expression ───────────────────────────────────────────────────
    def _eval_MatchExpr(self, node: MatchExpr, env):
        value = self._eval(node.subject, env)
        for case in node.cases:
            if hasattr(case, 'pattern'):
                pattern, body = case.pattern, case.body
            else:
                pattern, body = case[0], case[1]
            case_env = Environment(env)
            ptype = type(pattern).__name__
            # v23: EnumVariantPattern
            if ptype == 'EnumVariantPattern':
                if self._match_enum_variant(pattern, value, case_env):
                    return self._exec_block(body, case_env)
                continue
            # v24: TypedMatchCase
            if ptype == 'TypedMatchCase':
                if self._exec_TypedMatchCase(pattern, value, case_env):
                    return self._exec_block(body, case_env)
                continue
            # v22: DestructurePattern
            if ptype == 'DestructurePattern':
                if self._match_destructure(pattern, value, case_env):
                    return self._exec_block(body, case_env)
                continue
            # Regular value equality
            try:
                pattern_val = self._eval(pattern, env)
            except Exception:
                continue
            if value == pattern_val:
                return self._exec_block(body, case_env)
        if node.default_body:
            return self._exec_block(node.default_body, Environment(env))
        return None

    def _eval_BlockExpr(self, node: BlockExpr, env):
        return self._exec_block(node.stmts, Environment(env))

    # ── Struct ────────────────────────────────────────────────────────────
    def _exec_StructDef(self, node: StructDef, env):
        """Register a struct type in the environment."""
        struct_type = NexaStruct(node.name, node.fields)
        env.set(node.name, struct_type)

    # ── Generators ────────────────────────────────────────────────────────
    def _exec_YieldStatement(self, node: YieldStatement, env):
        """Raise YieldException — caught by _exec_generator."""
        raise YieldException(self._eval(node.value, env))

    def _exec_generator(self, stmts, env):
        """Execute a list of statements, yielding on YieldStatement."""
        for stmt in stmts:
            if isinstance(stmt, YieldStatement):
                yield self._eval(stmt.value, env)
            elif isinstance(stmt, ForStatement):
                for item in self._eval(stmt.iterable, env):
                    child = Environment(env); child.set(stmt.var, item)
                    yield from self._exec_generator(stmt.body, child)
            elif isinstance(stmt, WhileStatement):
                count = 0
                while self._truthy(self._eval(stmt.condition, env)):
                    yield from self._exec_generator(stmt.body, Environment(env))
                    count += 1
                    if count > 100_000: raise NexaRuntimeError("Generator infinite loop")
            elif isinstance(stmt, IfExpr):
                if self._truthy(self._eval(stmt.condition, env)):
                    yield from self._exec_generator(stmt.then_block, Environment(env))
                elif stmt.else_block:
                    yield from self._exec_generator(stmt.else_block, Environment(env))
            else:
                self._exec(stmt, env)


    # ── Test / Assert ─────────────────────────────────────────────────────
    def _exec_TestBlock(self, node: TestBlock, env):
        print(f"\n  🧪 Running: \"{node.name}\"")
        try:
            self._exec_block(node.body, Environment(env))
            print(f"  \033[92m✅ PASS: {node.name}\033[0m")
            self.test_runner.passed += 1
        except NexaTestFailure as f:
            print(f"  \033[91m✗ FAIL: {node.name} — {f.msg}\033[0m")
            self.test_runner.failed += 1
            self.test_runner.errors.append(f"{node.name}: {f.msg}")

    def _exec_AssertStatement(self, node: AssertStatement, env):
        args = [self._eval(a, env) for a in node.args]
        kind = node.kind
        ok = True; msg = ""
        if kind == "eq":
            ok = self._eval_binary(BinaryOp("==", NumberLiteral(args[0]) if isinstance(args[0], (int, float)) else StringLiteral(args[0]), NumberLiteral(args[1]) if isinstance(args[1], (int, float)) else StringLiteral(args[1])), env) if False else args[0] == args[1]
            msg = f"Expected {args[0]!r} == {args[1]!r}"
        elif kind == "ne":
            ok = args[0] != args[1]
            msg = f"Expected {args[0]!r} != {args[1]!r}"
        elif kind == "true":
            ok = self._truthy(args[0])
            msg = f"Expected truthy, got {args[0]!r}"
        elif kind == "false":
            ok = not self._truthy(args[0])
            msg = f"Expected falsy, got {args[0]!r}"
        elif kind == "contains":
            ok = args[1] in args[0]
            msg = f"Expected {args[1]!r} in {args[0]!r}"
        elif kind == "raises":
            try:
                self._call(args[0], args[1:], {})
                ok = False; msg = "Expected an exception but none was raised"
            except (NexaError, NexaRuntimeError, Exception):
                ok = True
        if not ok:
            raise NexaTestFailure(msg)

    def _exec_ExprStatement(self, node: ExprStatement, env):
        return self._eval(node.expr, env)

    # ── Expressions ───────────────────────────────────────────────────────
    def _eval(self, node, env) -> Any:
        try:
            return self._eval_inner(node, env)
        except Exception as e:
            if not getattr(e, '_nexa_handled', False):
                line = getattr(node, 'line', -1)
                self.call_stack.append(f"at line {line}" if line > 0 else "at <unknown line>")
                e._nexa_handled = True
            raise
            
    def _eval_inner(self, node, env) -> Any:
        if isinstance(node, NumberLiteral): return node.value
        if isinstance(node, StringLiteral): return node.value
        if isinstance(node, BoolLiteral):   return node.value
        if isinstance(node, NullLiteral):   return None
        if isinstance(node, SelfRef):       return env.get("self")
        if isinstance(node, SuperRef):      return NexaSuper(env.get("self"))
        # v21: Range literals  0..5 / 0..=5
        if isinstance(node, RangeExpr):     return self._eval_RangeExpr(node, env)
        if isinstance(node, FString):       return self._eval_fstring(node, env)
        if isinstance(node, ListLiteral):
            res = []
            for e in node.elements:
                if isinstance(e, SpreadExpr): res.extend(list(self._eval(e.expr, env)))
                else: res.append(self._eval(e, env))
            return res
        if isinstance(node, SetLiteral):
            res = set()
            for e in node.elements:
                if isinstance(e, SpreadExpr): res.update(set(self._eval(e.expr, env)))
                else: res.add(self._eval(e, env))
            return res
        if isinstance(node, ListComp):      return self._eval_listcomp(node, env)
        if isinstance(node, DictLiteral):
            res = {}
            for k, v in node.pairs:
                if isinstance(k, SpreadExpr): res.update(dict(self._eval(k.expr, env)))
                else: res[self._eval(k, env)] = self._eval(v, env)
            return res
        # v24: StructUpdate { ...base, key: val }
        if isinstance(node, StructUpdate):
            base = dict(self._eval(node.base, env))
            base.update({k: self._eval(v, env) for k, v in node.updates.items()})
            return base
        if isinstance(node, LambdaExpr):
            return NexaFunction("<lambda>", node.params, node.body, env)
        if isinstance(node, Identifier):    return env.get(node.name)
        if isinstance(node, BinaryOp):      return self._eval_binary(node, env)
        if isinstance(node, UnaryOp):       return self._eval_unary(node, env)
        if isinstance(node, TernaryOp):
            return self._eval(node.true_expr, env) if self._truthy(self._eval(node.condition, env)) else self._eval(node.false_expr, env)
        if isinstance(node, NullishCoalescing):
            left = self._eval(node.left, env)
            return left if left is not None else self._eval(node.right, env)
        if isinstance(node, OptionalAccess):
            obj = self._eval(node.obj, env)
            if obj is None: return None
            try: return self._get_attr(obj, node.member)
            except NexaRuntimeError: return None
        if isinstance(node, PipelineExpr):  return self._eval_pipeline(node, env)
        if isinstance(node, Assignment):
            val = self._eval(node.value, env); env.assign(node.name, val); return val
        if isinstance(node, MemberAssignment):
            obj = self._eval(node.obj, env); val = self._eval(node.value, env)
            if isinstance(obj, NexaInstance): obj.set_attr(node.member, val)
            elif isinstance(obj, dict): obj[node.member] = val
            else:
                if isinstance(obj, int):
                    print(f"DEBUG: MemberAssignment obj is int! node.member={node.member}, val={val}")
                setattr(obj, node.member, val)
            return val
        if isinstance(node, IndexAssignment):
            obj = self._eval(node.obj, env)
            idx = self._eval(node.index, env) if type(node.index).__name__ != "SliceExpr" else self._eval_sliceexpr(node.index, env)
            val = self._eval(node.value, env); obj[idx] = val; return val
        if isinstance(node, CompoundAssignment): return self._eval_compound(node, env)
        if isinstance(node, MemberAccess):
            obj_val = self._eval(node.obj, env)
            if isinstance(obj_val, NexaSuper): return obj_val.get_attr(node.member, self)
            elif isinstance(obj_val, NexaInstance): return obj_val.get_attr(node.member, self)
            elif isinstance(obj_val, NexaEnum): return obj_val.get_attr(node.member, self)
            # v24: SealedProxy member access (sealed class variants)
            elif hasattr(obj_val, 'get_attr'): return obj_val.get_attr(node.member, self)
            return self._get_attr(obj_val, node.member)
        if isinstance(node, IndexAccess):
            obj = self._eval(node.obj, env)
            idx = self._eval(node.index, env) if type(node.index).__name__ != "SliceExpr" else self._eval_sliceexpr(node.index, env)
            return obj[idx]
        if isinstance(node, FunctionCall):  return self._eval_call(node, env)
        if isinstance(node, AwaitExpr):     return self._eval_await(node, env)
        if isinstance(node, NewExpr):       return self._eval_new(node, env)
        if isinstance(node, MatchExpr):     return self._eval_MatchExpr(node, env)
        if isinstance(node, IfExpr):        return self._eval_IfExpr(node, env)
        if isinstance(node, TryExpr):       return self._eval_TryExpr(node, env)
        if isinstance(node, BlockExpr):     return self._eval_BlockExpr(node, env)
        # v24 expression nodes
        if isinstance(node, QuestionExpr): return self._eval_QuestionExpr(node, env)
        if isinstance(node, MacroCall):    return self._eval_MacroCall(node, env)
        return node

    def _eval_listcomp(self, node: ListComp, env: Environment) -> list:
        """[expr for var in iterable if condition]"""
        result = []
        for item in self._eval(node.iterable, env):
            child = Environment(env)
            child.set(node.var, item)
            if node.condition is None or self._truthy(self._eval(node.condition, child)):
                result.append(self._eval(node.expr, child))
        return result

    def _eval_fstring(self, node: FString, env: Environment) -> str:
        result = ""
        for kind, val in node.parts:
            result += val if kind == "lit" else str(self._eval(val, env))
        return result

    def _eval_pipeline(self, node: PipelineExpr, env: Environment) -> Any:
        value = self._eval(node.stages[0], env)
        for stage in node.stages[1:]:
            fn = self._eval(stage, env); value = self._call(fn, [value], {})
        return value

    def _eval_compound(self, node: CompoundAssignment, env: Environment) -> Any:
        left = env.get(node.name); right = self._eval(node.value, env)
        ops = {"+": lambda a,b: a+b, "-": lambda a,b: a-b, "*": lambda a,b: a*b,
               "/": lambda a,b: a/b,   # v25: /= divide-assign
               "&": 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}
        if node.op not in ops: raise NexaRuntimeError(f"Unsupported compound op: {node.op}=")
        result = ops[node.op](left, right)
        env.assign(node.name, result); return result

    def _eval_new(self, node: NewExpr, env: Environment) -> Any:
        klass = env.get(node.class_name)
        args = [self._eval(a, env) for a in node.args]
        kwargs = {k: self._eval(v, env) for k, v in node.kwargs.items()}
        return klass.call(args, kwargs, self, env)

    def _eval_await(self, node: AwaitExpr, env: Environment) -> Any:
        val = self._eval(node.expr, env)
        if isinstance(val, NexaCoroutine):
            try: return asyncio.run(val._run())
            except RuntimeError:
                import concurrent.futures
                with concurrent.futures.ThreadPoolExecutor() as pool:
                    return pool.submit(asyncio.run, val._run()).result()
        if asyncio.iscoroutine(val):
            try: return asyncio.run(val)
            except RuntimeError:
                import concurrent.futures
                with concurrent.futures.ThreadPoolExecutor() as pool:
                    return pool.submit(asyncio.run, val).result()
        return val

    def _eval_sliceexpr(self, node: Any, env: Environment) -> slice:
        start = self._eval(node.start, env) if node.start else None
        stop = self._eval(node.stop, env) if node.stop else None
        step = self._eval(node.step, env) if node.step else None
        return slice(start, stop, step)

    def _eval_binary(self, node: BinaryOp, env: Environment) -> Any:
        op = node.op
        if op == "and":
            l = self._eval(node.left, env)
            return l if not self._truthy(l) else self._eval(node.right, env)
        if op == "or":
            l = self._eval(node.left, env)
            return l if self._truthy(l) else self._eval(node.right, env)
        left, right = self._eval(node.left, env), self._eval(node.right, env)
        
        # Override for Magic Methods
        magic_map = {
            "+": "__add__", "-": "__sub__", "*": "__mul__", "/": "__div__",
            "%": "__mod__", "**": "__pow__", "==": "__eq__", "!=": "__neq__",
            "<": "__lt__", ">": "__gt__", "<=": "__le__", ">=": "__ge__"
        }
        if isinstance(left, NexaInstance) and op in magic_map:
            try:
                method = left.get_attr(magic_map[op], self)
                if isinstance(method, BoundMethod): return method(right)
            except NexaRuntimeError:
                pass # Fallback to default
        
        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,">=": 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
        }
        fn = ops.get(op)
        if fn is None: raise NexaRuntimeError(f"Unknown op: {op}")
        return fn(left, right)

    def _eval_unary(self, node: UnaryOp, env: Environment) -> Any:
        val = self._eval(node.operand, env)
        if node.op == "-": return -val
        if node.op == "~": return ~val
        if node.op == "not": return not self._truthy(val)
        raise NexaRuntimeError(f"Unknown unary: {node.op}")

    def _eval_call(self, node: FunctionCall, env: Environment) -> Any:
        kwargs = {k: self._eval(v, env) for k, v in node.kwargs.items()}
        args   = [self._eval(a, env) for a in node.args]
        if isinstance(node.callee, MemberAccess):
            obj = self._eval(node.callee.obj, env)
            method = self._get_attr(obj, node.callee.member)
            return self._call(method, args, kwargs, env, obj)
        callee = self._eval(node.callee, env)
        return self._call(callee, args, kwargs, env)

    def _call(self, callee, args, kwargs, env, bound_obj=None) -> Any:
        if isinstance(callee, NexaClass):
            # Evaluate elements in arguments in case they are spread
            eval_args = []
            for a in args:
                if isinstance(a, SpreadExpr): eval_args.extend(list(self._eval(a.expr, env)))
                else: eval_args.append(self._eval(a, env))
            return callee.call(eval_args, kwargs, self, self.global_env)
        if isinstance(callee, NexaStruct):
            eval_args = []
            for a in args:
                if isinstance(a, SpreadExpr): eval_args.extend(list(self._eval(a.expr, env)))
                else: eval_args.append(self._eval(a, env))
            return callee.call(eval_args, kwargs, self, self.global_env)
        if isinstance(callee, BoundMethod):
            return callee(*args, **kwargs)
        if callable(callee) and not isinstance(callee, NexaFunction):
            # Wrap any NexaFunction args as Python callables so built-ins can call them
            wrapped_args = [self._py_callable(a) if isinstance(a, NexaFunction) else a
                            for a in args]
            wrapped_kwargs = {k: self._py_callable(v) if isinstance(v, NexaFunction) else v
                              for k, v in kwargs.items()}
            try: return callee(*wrapped_args, **wrapped_kwargs)
            except Exception as e: raise NexaRuntimeError(f"Built-in error: {e}")
        if isinstance(callee, NexaFunction):
            if getattr(callee, "is_async", False):
                # v26: async def with yield = async generator (iterable), not coroutine
                if self._is_generator(callee.body):
                    return NexaGenerator(callee, args, kwargs, self)
                return NexaCoroutine(callee, args, kwargs, self)
            # Detect generator function (any yield in body)
            if self._is_generator(callee.body):
                return NexaGenerator(callee, args, kwargs, self)
            fn_env = Environment(callee.closure)
            
            # _bind_args logic expanded for Rest Params here
            bound_args = []
            for a in args:
                if getattr(a, "__class__", None).__name__ == "SpreadExpr":
                    bound_args.extend(list(self._eval(a.expr, fn_env) if hasattr(a, 'expr') else a))
                else: bound_args.append(a)
                
            self._bind_args_with_rest(callee, fn_env, bound_args, kwargs)
            
            # Format traceback stack
            func_name = callee.name if hasattr(callee, 'name') else "<anonymous>"
            self.call_stack.append(f"in call to {func_name}()")
            
            try: return self._exec_block(callee.body, fn_env)
            except ReturnException as ret: return ret.value
            except Exception as e:
                # Top level trace print
                if len(self.call_stack) > 0 and not getattr(e, '_nexa_traced', False):
                    trace = "\n".join("  " + s for s in reversed(self.call_stack))
                    print(f"\n\033[91mNexa Runtime Traceback (most recent call last):\n{trace}\033[0m")
                    e._nexa_traced = True
                raise
            finally:
                if self.call_stack: self.call_stack.pop()
                
        raise NexaRuntimeError(f"'{callee}' is not callable")

    def _is_generator(self, stmts) -> bool:
        """Return True if any statement in the body is a YieldStatement."""
        for stmt in stmts:
            if isinstance(stmt, YieldStatement): return True
            if hasattr(stmt, 'body') and isinstance(stmt.body, list):
                if self._is_generator(stmt.body): return True
            if isinstance(stmt, IfExpr):
                if self._is_generator(stmt.then_block) or self._is_generator(stmt.else_block):
                    return True
        return False

    def _bind_args_with_rest(self, fn: NexaFunction, env: Environment, args: list, kwargs: dict):
        # Support rest param def f(x, ...y)
        params = fn.params
        param_types = getattr(fn, "param_types", {})
        has_rest = len(params) > 0 and params[-1].startswith("...")
        
        pos_args = list(args)
        for i, p in enumerate(params):
            val = None
            if p.startswith("..."):
                rest_name = p[3:]
                val = pos_args[i:]
                env.set(rest_name, val)
                break
            
            if i < len(pos_args):
                val = pos_args[i]
            elif p in kwargs:
                val = kwargs[p]
            elif p in fn.defaults:
                val = fn.defaults[p]
            else:
                raise NexaRuntimeError(f"Missing argument '{p}' for {fn.name}()")
            
            if p in param_types and val is not None:
                _check_type(val, param_types[p], f"Argument '{p}'")
                
            env.set(p, val)

    def _py_callable(self, nexa_fn: NexaFunction):
        """Return a Python callable that executes a NexaFunction."""
        interp = self
        def wrapper(*args):
            return interp._call(nexa_fn, list(args), {}, interp.global_env)
        wrapper._nexa_fn = nexa_fn
        return wrapper



    def _get_attr(self, obj, name) -> Any:
        if isinstance(obj, NexaSuper):
            base_class = obj.instance._class.base
            if not base_class:
                raise NexaRuntimeError("super() used in class without base")
            method = base_class._find_method(name)
            if method: return BoundMethod(obj.instance, method, self)
            raise NexaRuntimeError(f"'{base_class.name}' has no attribute '{name}'")
        if isinstance(obj, NexaClass):          return obj.get_attr(name, self)
        if isinstance(obj, NexaInstance):       return obj.get_attr(name, self)
        if isinstance(obj, NexaStructInstance): return obj.get_attr(name, self)
        if isinstance(obj, NexaGenerator):
            if name == "to_list": return obj.to_list
        if isinstance(obj, NexaModel):
            attr = getattr(obj, name, None)
            if attr is not None: return attr
        if isinstance(obj, NexaEnum):
            return obj.get_attr(name, self)
        if isinstance(obj, list):
            if name == "push": return obj.append
            if name == "pop": return obj.pop
            if name == "sort": return obj.sort
            if name == "join": return lambda sep="": sep.join(str(x) for x in obj)
            if name == "map":
                return lambda fn: [self._call(fn, [x], {}, self.global_env) if isinstance(fn, NexaFunction) else fn(x) for x in obj]
            if name == "filter":
                return lambda fn: [x for x in obj if self._truthy(self._call(fn, [x], {}, self.global_env) if isinstance(fn, NexaFunction) else fn(x))]
            if name == "reduce":
                def _reduce(fn, initial=None):
                    it = iter(obj)
                    try: val = initial if initial is not None else next(it)
                    except StopIteration: raise NexaRuntimeError("reduce() of empty list with no initial value")
                    for x in it:
                        val = self._call(fn, [val, x], {}, self.global_env) if isinstance(fn, NexaFunction) else fn(val, x)
                    return val
                return _reduce

        if isinstance(obj, str):
            if name == "split": return obj.split
            if name == "upper": return obj.upper
            if name == "lower": return obj.lower
            if name == "replace": return obj.replace
            if name == "strip": return obj.strip
            if name == "startswith": return obj.startswith
            if name == "endswith": return obj.endswith

        if isinstance(obj, dict):
            if name == "keys": return lambda: list(obj.keys())
            if name == "values": return lambda: list(obj.values())
            if name == "get": return obj.get

        attr = getattr(obj, name, None)
        if attr is not None: return attr
        if isinstance(obj, dict) and name in obj: return obj[name]
        if type(obj).__name__ == "SealedProxy": return obj.get_attr(name, self)

        raise NexaRuntimeError(f"Cannot get attribute '{name}' on {type(obj).__name__}")

    def _builtin_async_map(self, fn, items):
        async def _run():
            coros = [NexaCoroutine(fn, [x], {}, self)._run()
                     if isinstance(fn, NexaFunction) else asyncio.to_thread(fn, x)
                     for x in items]
            return await asyncio.gather(*coros)
        try: return asyncio.run(_run())
        except RuntimeError: return [self._call(fn, [x], {}) for x in items]

    def _builtin_gather(self, *coros):
        async def _run():
            tasks = [c._run() if isinstance(c, NexaCoroutine) else c for c in coros]
            return list(await asyncio.gather(*tasks))
        return asyncio.run(_run())

    @staticmethod
    def _truthy(val) -> bool:
        if val is None or val is False: return False
        if isinstance(val, (int, float)): return val != 0
        if isinstance(val, (str, list, dict)): return len(val) > 0
        return True

    # ── v24 New Handlers ─────────────────────────────────────────────────────

    def _exec_IfLetStatement(self, node: IfLetStatement, env):
        """if let Pattern = expr { then } else { else }"""
        val = self._eval(node.expr, env)
        bound_env = Environment(env)
        if self._v24_match_pattern(node.pattern, val, bound_env):
            self._exec_block(node.then_body, bound_env)
        elif node.else_body:
            self._exec_block(node.else_body, Environment(env))

    def _exec_WhileLetStatement(self, node: WhileLetStatement, env):
        """while let Pattern = expr { body }"""
        while True:
            val = self._eval(node.expr, env)
            bound_env = Environment(env)
            if not self._v24_match_pattern(node.pattern, val, bound_env):
                break
            try:
                self._exec_block(node.body, bound_env)
            except NexaBreak:
                break
            except NexaContinue:
                continue

    def _v24_match_pattern(self, pattern, val, env) -> bool:
        """Try to match a pattern against val; bind vars into env. Returns True on match."""
        if isinstance(pattern, EnumVariantPattern):
            if not isinstance(val, dict): return False
            if val.get("__tag__") != pattern.variant_name: return False
            if val.get("__enum__") != pattern.enum_name: return False
            vals = val.get("__val__", [])
            for i, name in enumerate(pattern.binds):
                env.set(name, vals[i] if i < len(vals) else None)
            return True
        if isinstance(pattern, Identifier):
            env.set(pattern.name, val)
            return True
        # Literal pattern
        lit = self._eval(pattern, env)
        return val == lit

    def _exec_SealedClassDef(self, node: SealedClassDef, env):
        """Register a sealed class hierarchy — each variant becomes a NexaClass."""
        sealed_ns = {"__sealed__": node.name, "__variants__": {}}
        for v in node.variants:
            vname = v.name
            fields = [f[0] for f in v.fields]   # field names only
            def make_variant_class(vn, vf, sname):
                cls = type(f"NexaSealed_{vn}", (), {})()
                cls._sealed_name = sname
                cls._variant_name = vn
                cls._fields = vf
                class VarFactory:
                    def __call__(inner_self, *args, **kwargs):
                        inst = {"__sealed__": sname, "__tag__": vn, "__val__": list(args)}
                        for i, fn in enumerate(vf):
                            inst[fn] = args[i] if i < len(args) else None
                        return inst
                return VarFactory()
            sealed_ns["__variants__"][vname] = make_variant_class(vname, fields, node.name)
        # Register both the namespace and a dot-access proxy
        class SealedProxy:
            def __init__(self, ns): self._ns = ns
            def get_attr(self, name, *_):
                if name in self._ns["__variants__"]: return self._ns["__variants__"][name]
                raise NexaRuntimeError(f"Sealed class '{self._ns['__sealed__']}' has no variant '{name}'")
        env.set(node.name, SealedProxy(sealed_ns))

    def _exec_MacroDef(self, node: MacroDef, env):
        """Store macro definition as a callable template."""
        if not hasattr(env, '_macros'): env._macros = {}
        env._macros[node.name] = node   # store the AST node

    def _eval_MacroCall(self, node: MacroCall, env):
        """Expand a macro call by substituting args into body and executing."""
        # Find macro definition in env chain
        macro_def = None
        cur = env
        while cur is not None:
            if hasattr(cur, '_macros') and node.name in cur._macros:
                macro_def = cur._macros[node.name]
                break
            cur = getattr(cur, 'parent', None)
        if macro_def is None:
            raise NexaRuntimeError(f"Undefined macro '{node.name}!'")
        # Bind params to evaluated args in a new env
        macro_env = Environment(env)
        evaled_args = [self._eval(a, env) for a in node.args]
        for p, v in zip(macro_def.params, evaled_args):
            macro_env.set(p, v)
        # Execute macro body
        try:
            return self._exec_block(macro_def.body, macro_env)
        except ReturnException as r:
            return r.value

    def _eval_QuestionExpr(self, node: QuestionExpr, env):
        """expr? — if val is None or an Err variant, do an early return."""
        val = self._eval(node.expr, env)
        # None propagation
        if val is None:
            raise ReturnException(None)
        # Err variant propagation (Result.Err)
        if isinstance(val, dict) and val.get("__tag__") == "Err":
            raise ReturnException(val)
        return val

    def _exec_TypeAlias(self, node: TypeAlias, env):
        """type UserId = Int — register as a simple name alias."""
        # Type aliases are metadata; store them for type checker / doc generator
        if not hasattr(env, '_type_aliases'): env._type_aliases = {}
        env._type_aliases[node.name] = node.target
        # Also set as identity function (can be used as constructor hint)
        env.set(node.name, lambda x: x)

    def _exec_BenchBlock(self, node, env):
        """bench blocks are handled by the benchmarker, not the interpreter."""
        pass  # silently skip at runtime (benchmarker.py handles these)

    def _exec_TypedMatchCase(self, node, value, env) -> bool:
        """Evaluates type match and guard. Returns True if matched."""
        # 1. Type check
        tn = node.type_name
        if tn == "Int" and not isinstance(value, int): return False
        if tn == "Float" and not isinstance(value, float): return False
        if tn == "String" and not isinstance(value, str): return False
        if tn == "Bool" and not isinstance(value, bool): return False
        if tn == "List" and not isinstance(value, list): return False
        if tn == "Dict" and not isinstance(value, dict): return False
        if tn not in ["Int", "Float", "String", "Bool", "List", "Dict"]:
            if getattr(value, "__class_name__", type(value).__name__) != tn:
                return False
        
        # 2. Bind name
        if node.bind_name:
            env.set(node.bind_name, value)
            
        # 3. Evaluate guard
        if node.guard:
            if not self._truthy(self._eval(node.guard, env)):
                return False
                
        return True

    # ── v25: New Statement Handlers ───────────────────────────────────────────

    def _exec_VarStatement(self, node, env):
        """var x: Type = expr  — explicit mutable binding (same semantics as let for now)."""
        from .ast_nodes import VarStatement as _VS
        val = self._eval(node.value, env)
        if node.type_hint and isinstance(node.type_hint, str):
            _check_type(val, node.type_hint, f"Variable '{node.name}'")
        env.set(node.name, val)

    def _exec_NativeAssertStatement(self, node, env):
        """assert expr  or  assert expr, 'message'  — clean assertion syntax."""
        val = self._truthy(self._eval(node.condition, env))
        if not val:
            if node.message is not None:
                msg = self._eval(node.message, env)
                raise NexaTestFailure(str(msg))
            # Build a nice message from the AST
            import ast as _ast
            cond_repr = repr(node.condition)
            raise NexaTestFailure(f"Assertion failed: {cond_repr}")

    def _exec_TraitDef(self, node, env):
        """Register a trait in the environment."""
        if not hasattr(env, '_traits'):
            env._traits = {}
        # Store trait as a dict with method signatures (like interfaces but richer)
        method_map = {}
        for m in node.methods:
            fn = NexaFunction(m.name, m.params, m.body, env)
            fn.param_types = m.param_types
            method_map[m.name] = fn
        env.set(node.name, {
            "__trait__": True,
            "name": node.name,
            "methods": list(method_map.keys()),
            "defaults": method_map,
        })

    def _exec_TraitImpl(self, node, env):
        """Register impl Trait for Class — injects methods into class at runtime."""
        if not hasattr(env, '_traits'):
            env._traits = {}
        # Store the impl so ClassDef can pick it up, or inject immediately
        trait_entry = env.get(node.trait_name) if self._safe_lookup(node.trait_name, env) else {}
        try:
            klass = env.get(node.class_name)
        except NexaRuntimeError:
            klass = None

        if isinstance(klass, NexaClass):
            for m in node.methods:
                if m.name not in klass.methods:
                    fn = NexaFunction(m.name, m.params, m.body, env)
                    fn.param_types = m.param_types
                    klass.methods[m.name] = fn
        # Also store in registry for forward-declarations
        env._traits[f"{node.trait_name}_{node.class_name}"] = {
            "trait_name": node.trait_name,
            "class_name": node.class_name,
            "methods": node.methods,
        }

    def _safe_lookup(self, name, env):
        """Look up a name without raising if not found."""
        try:
            env.get(name)
            return True
        except NexaRuntimeError:
            return False

    def _exec_UseStatement(self, node, env):
        """use std::math::sin  or  use std::math::{sin, cos}"""
        known_modules = {
            "std": {
                "math": "math",
                "fs":   "fs",
                "http": "http",
                "io":   "fs",
                "json": "json",
                "time": "time",
                "random": "random",
                "string": "string",
                "array":  "array",
            },
            "math": "math",
            "fs":   "fs",
            "http": "http",
        }
        path = node.path
        if not path:
            return

        # Resolve module
        mod_name = None
        if len(path) == 1:
            mod_name = path[0]
        elif len(path) >= 2 and path[0] in known_modules:
            mapping = known_modules[path[0]]
            if isinstance(mapping, dict) and path[1] in mapping:
                mod_name = mapping[path[1]]
            elif isinstance(mapping, str):
                mod_name = mapping

        known_builtins = {
            "automl","nlp","vision","math","stats","fs","json",
            "http","time","random","agent","df","db","re","serve",
            "tensor","env","sys","datetime","crypto","base64","task","concurrency",
            "string","array"
        }
        if mod_name and mod_name in known_builtins:
            from .builtins import BUILTINS
            mod = BUILTINS.get(mod_name)
            if mod is None:
                raise NexaRuntimeError(f"Module '{mod_name}' not found in builtins")
            if node.names:
                # use std::math::{sin, cos}
                for name in node.names:
                    val = getattr(mod, name, None)
                    if val is None and isinstance(mod, dict):
                        val = mod.get(name)
                    if val is None:
                        raise NexaRuntimeError(f"'{name}' not found in module '{mod_name}'")
                    env.set(name, val)
            elif len(path) >= 3:
                # use std::math::sin  — import last component
                fn_name = path[-1]
                val = getattr(mod, fn_name, None)
                if val is None:
                    raise NexaRuntimeError(f"'{fn_name}' not found in module '{mod_name}'")
                env.set(node.alias or fn_name, val)
            else:
                # use std::math  — import whole module
                env.set(node.alias or (mod_name or path[-1]), mod)
        else:
            # Fallback: treat as file import
            self._exec_ImportStatement(
                type('ImportStatement', (), {
                    'module': "/".join(path),
                    'alias': node.alias,
                    'is_file': True,
                })(),
                env
            )

    def _exec_SlashEqAssignment(self, node, env):
        """x /= 2  — divide-assign compound statement."""
        current = env.get(node.name)
        divisor = self._eval(node.value, env)
        env.assign(node.name, current / divisor)

