Skip to main content

tishlang_bytecode/
compiler.rs

1//! AST to bytecode compiler.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use tishlang_ast::{
7    ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern,
8    ExportDeclaration, Expr, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp,
9    MemberProp, ObjectProp, Program, Span, Statement,
10};
11
12use crate::chunk::{Chunk, Constant};
13use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
14use crate::opcode::{MathUnaryFn, Opcode};
15
16enum SimpleMapResult {
17    Identity,
18    BinOp(BinOp, Constant, bool), // op, constant, param_on_left
19}
20
21/// Provably evaluates to a `String` at runtime — the safety gate for the plain-assign `AppendLocal`
22/// builder fast path (#186). A string literal, a template literal, or an `+` chain containing one
23/// (JS `+` string-coerces the whole expression). Conservative: an unknown/numeric operand returns
24/// `false`, so a numeric accumulator never routes through the builder.
25fn is_string_typed(expr: &Expr) -> bool {
26    match expr {
27        Expr::Literal {
28            value: Literal::String(_),
29            ..
30        } => true,
31        Expr::TemplateLiteral { .. } => true,
32        Expr::Binary {
33            left,
34            op: BinOp::Add,
35            right,
36            ..
37        } => is_string_typed(left) || is_string_typed(right),
38        _ => false,
39    }
40}
41
42fn literal_to_constant(expr: &Expr) -> Option<Constant> {
43    if let Expr::Literal { value, .. } = expr {
44        Some(match value {
45            Literal::Number(n) => Constant::Number(*n),
46            Literal::String(s) => Constant::String(Arc::clone(s)),
47            Literal::Bool(b) => Constant::Bool(*b),
48            Literal::Null => Constant::Null,
49        })
50    } else {
51        None
52    }
53}
54
55#[derive(Debug)]
56pub struct CompileError {
57    pub message: String,
58}
59
60impl std::fmt::Display for CompileError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "{}", self.message)
63    }
64}
65
66impl std::error::Error for CompileError {}
67
68/// Loop boundary for break/continue.
69struct LoopInfo {
70    break_patches: Vec<usize>,
71    /// Operand positions for `continue`: either `JumpBack` (while / do-while / for-of) or `Jump`
72    /// (C-style `for`, where the update clause is emitted after the body).
73    continue_patches: Vec<usize>,
74    /// When true, [`Opcode::Jump`] placeholders in `continue_patches` are patched forward with
75    /// [`Self::patch_jump`]. When false, they are [`Opcode::JumpBack`] patched with
76    /// [`Self::patch_jump_back`].
77    continue_is_forward_jump: bool,
78}
79
80/// Switch boundary: break exits the switch.
81struct SwitchInfo {
82    break_patches: Vec<usize>,
83}
84
85/// Innermost break/continue target for unwinding `EnterBlock` before a jump.
86#[derive(Clone, Copy)]
87enum Breakable {
88    /// `usize` = `block_depth` before the loop body (same as Continue unwind target).
89    Loop { unwind_depth: usize },
90    /// `usize` = `block_depth` before the switch statement.
91    Switch { unwind_depth: usize },
92}
93
94struct Compiler<'a> {
95    chunk: &'a mut Chunk,
96    /// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
97    scope: Vec<HashMap<Arc<str>, bool>>,
98    /// Stack of loop info for break/continue.
99    loop_stack: Vec<LoopInfo>,
100    switch_stack: Vec<SwitchInfo>,
101    /// Parallel to nested loops/switches: innermost target for break/continue block unwind.
102    breakable_stack: Vec<Breakable>,
103    /// Nesting depth of emitted `EnterBlock` (lexical blocks) not yet closed on the compile path.
104    block_depth: usize,
105    /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
106    retain_last_expr: bool,
107    /// When `Some`, this chunk is being compiled in the SIMPLE slot mode: identifier references
108    /// resolve to frame slots (`LoadLocal`) via this param→slot map instead of name-keyed `LoadVar`.
109    /// Set only for self-contained param-only functions (see [`simple_fn_slots`]).
110    slot_ctx: Option<HashMap<Arc<str>, u16>>,
111    /// GENERAL slot mode (`TISH_VM_SLOTS`, capture-aware): a block-scoped stack of name→slot maps,
112    /// innermost last. Allocated DURING compilation so block scoping + shadowing are correct (each
113    /// `let` gets a fresh slot; resolution walks innermost-first; a block pops its frame). Empty unless
114    /// [`general_slots`] is set. Captured names (in [`slot_captured`]) are never allocated here — they
115    /// stay name-based in `local_scope` (which closures capture).
116    slot_scopes: Vec<HashMap<Arc<str>, u16>>,
117    /// Names referenced by a nested closure (over-approx) → must stay name-based even in slot mode.
118    slot_captured: HashSet<Arc<str>>,
119    /// Monotonic slot allocator for [`slot_scopes`] (never reclaimed; final value = frame size).
120    next_slot: u16,
121    /// True while compiling a chunk in general slot mode (see [`slot_scopes`]).
122    general_slots: bool,
123    /// Active `finally` bodies of enclosing `try`s in the CURRENT function (innermost last). A
124    /// `return` that escapes these trys must run each one on the way out (the bytecode VM jumps
125    /// straight to the function return otherwise). Reset per function — nested fns get a fresh
126    /// `Compiler`. The exception-unwind path is handled separately in the `Try` emitter.
127    finally_stack: Vec<Statement>,
128    /// When `Some(name)`, this chunk is the body of `fn name(...)` and `name`'s binding is provably
129    /// stable (no param shadows it, no reassignment/redeclaration in the body — see [`stmt_rebinds`]).
130    /// A direct call `name(args)` then compiles to `SelfCall` (no name lookup / closure dispatch; the
131    /// JIT lowers it to a native recursive call). `None` for anonymous fns, top-level, or anywhere the
132    /// self-binding can't be proven stable.
133    self_fn_name: Option<Arc<str>>,
134    /// #186 — `Math` is provably the global builtin: it is never rebound/shadowed anywhere in the
135    /// program (via the conservative [`stmt_rebinds`] scan). Only then may `Math.<fn>(arg)` lower to
136    /// the [`Opcode::MathUnary`] intrinsic, which the numeric JIT can compile without a shape guard.
137    /// `false` on nested-fn compilers and whenever the scan can't prove stability.
138    math_is_global: bool,
139    /// #187: top-level function names provably stable across the whole program (see
140    /// [`compute_stable_globals`]). A top-level `function N` in this set gets its chunk stamped with
141    /// `global_name = Some(N)`, which lets the numeric JIT register it as a directly-callable callee.
142    /// Threaded unchanged into every nested compiler (shared `Arc`).
143    stable_globals: Arc<std::collections::HashSet<Arc<str>>>,
144}
145
146/// Does `e` reference only the given params (no free/global vars, no nested
147/// functions, no mutation)? Such a function can run on a bare slot frame.
148fn expr_is_param_only(e: &Expr, params: &HashSet<&str>) -> bool {
149    match e {
150        Expr::Literal { .. } => true,
151        Expr::Ident { name, .. } => params.contains(name.as_ref()),
152        Expr::Binary { left, right, .. } => {
153            expr_is_param_only(left, params) && expr_is_param_only(right, params)
154        }
155        Expr::Unary { operand, .. } => expr_is_param_only(operand, params),
156        Expr::Call { callee, args, .. } => {
157            expr_is_param_only(callee, params)
158                && args.iter().all(|a| match a {
159                    CallArg::Expr(x) => expr_is_param_only(x, params),
160                    CallArg::Spread(_) => false,
161                })
162        }
163        Expr::Member { object, prop, .. } => {
164            expr_is_param_only(object, params)
165                && match prop {
166                    MemberProp::Name { .. } => true,
167                    MemberProp::Expr(x) => expr_is_param_only(x, params),
168                }
169        }
170        Expr::Index { object, index, .. } => {
171            expr_is_param_only(object, params) && expr_is_param_only(index, params)
172        }
173        Expr::Conditional {
174            cond,
175            then_branch,
176            else_branch,
177            ..
178        } => {
179            expr_is_param_only(cond, params)
180                && expr_is_param_only(then_branch, params)
181                && expr_is_param_only(else_branch, params)
182        }
183        Expr::NullishCoalesce { left, right, .. } => {
184            expr_is_param_only(left, params) && expr_is_param_only(right, params)
185        }
186        Expr::Array { elements, .. } => elements.iter().all(|el| match el {
187            ArrayElement::Expr(x) => expr_is_param_only(x, params),
188            ArrayElement::Spread(_) => false,
189        }),
190        Expr::Object { props, .. } => props.iter().all(|p| match p {
191            ObjectProp::KeyValue(_, x, _) => expr_is_param_only(x, params),
192            ObjectProp::Spread(_) => false,
193        }),
194        Expr::TemplateLiteral { exprs, .. } => exprs.iter().all(|x| expr_is_param_only(x, params)),
195        Expr::TypeOf { operand, .. } => expr_is_param_only(operand, params),
196        // Mutation, nested fns, async, jsx, native, `new` — not eligible.
197        _ => false,
198    }
199}
200
201/// Statement form of [`expr_is_param_only`]. Only the small set of statements a
202/// pure leaf function body uses is allowed; anything that declares a binding or
203/// loops bails (those keep the name-based path).
204fn stmt_is_param_only(s: &Statement, params: &HashSet<&str>) -> bool {
205    match s {
206        Statement::Block { statements, .. } => {
207            statements.iter().all(|st| stmt_is_param_only(st, params))
208        }
209        Statement::Multi { statements, .. } => {
210            statements.iter().all(|st| stmt_is_param_only(st, params))
211        }
212        Statement::ExprStmt { expr, .. } => expr_is_param_only(expr, params),
213        Statement::Return { value, .. } => {
214            value.as_ref().is_none_or(|e| expr_is_param_only(e, params))
215        }
216        Statement::If {
217            cond,
218            then_branch,
219            else_branch,
220            ..
221        } => {
222            expr_is_param_only(cond, params)
223                && stmt_is_param_only(then_branch, params)
224                && else_branch
225                    .as_ref()
226                    .is_none_or(|b| stmt_is_param_only(b, params))
227        }
228        _ => false,
229    }
230}
231
232/// If a function with these `params` (all simple, no rest) has a body that
233/// references only its params, returns the param→slot map for slot-based
234/// compilation. Slots are the parameter positions (0-based). Returns `None`
235/// when the function must use the name-based path (captures outer scope,
236/// declares locals, mutates, or defines nested functions).
237fn simple_fn_slots(
238    params: &[FunParam],
239    has_rest: bool,
240    body_ok: impl FnOnce(&HashSet<&str>) -> bool,
241) -> Option<HashMap<Arc<str>, u16>> {
242    if has_rest {
243        return None;
244    }
245    let mut map: HashMap<Arc<str>, u16> = HashMap::with_capacity(params.len());
246    for (i, p) in params.iter().enumerate() {
247        match p {
248            FunParam::Simple(tp) => {
249                map.insert(Arc::clone(&tp.name), i as u16);
250            }
251            FunParam::Destructure { .. } => return None,
252        }
253    }
254    let pset: HashSet<&str> = map.keys().map(|k| k.as_ref()).collect();
255    if body_ok(&pset) {
256        Some(map)
257    } else {
258        None
259    }
260}
261
262/// Capture-aware general slot-based locals (params + uncaptured body/top-level `let`s → frame slots).
263/// **Default ON** — validated across the full cross-backend suite + the compute micros (−22..27%) +
264/// the `main.tish` bundle (−22%). Set `TISH_VM_SLOTS=0` to disable (name-based, the old path).
265fn slots_enabled() -> bool {
266    std::env::var("TISH_VM_SLOTS")
267        .map(|v| v != "0")
268        .unwrap_or(true)
269}
270
271/// Is `name` bound by one of `params` (so it would shadow a function's own name)? Conservative:
272/// any destructuring param returns `true` (it could bind `name` via a nested pattern we don't analyze).
273fn params_bind_name(params: &[FunParam], name: &str) -> bool {
274    params.iter().any(|p| match p {
275        FunParam::Simple(tp) => tp.name.as_ref() == name,
276        FunParam::Destructure { .. } => true,
277    })
278}
279
280/// #187: does any parameter DEFAULT expression rebind `name`? A default like `(y = (foo = evil))`
281/// reassigns `foo` when the function is called, so a callee reached through it is NOT stable. The
282/// stability walk must scan these (they are otherwise invisible to the body scan).
283fn params_default_rebinds(params: &[FunParam], name: &str) -> bool {
284    params.iter().any(|p| {
285        let default = match p {
286            FunParam::Simple(tp) => &tp.default,
287            FunParam::Destructure { default, .. } => default,
288        };
289        default.as_ref().is_some_and(|e| expr_rebinds(e, name))
290    })
291}
292
293/// Conservative scan: does `name` get REBOUND (assigned `=`, `+=`, `??=`, `++`/`--`, or re-declared
294/// via `let`/`for-of`) anywhere in `s`? Returns `true` on a rebind OR on any node it can't fully
295/// analyze. Used to decide whether `fn NAME`'s body may emit `SelfCall` for `NAME(...)`: only when
296/// NAME's binding is PROVABLY stable throughout the body, because a wrong `SelfCall` would call the
297/// original chunk after a reassignment — a silent miscompile. Erring toward `true` only costs the
298/// optimization, never correctness.
299fn stmt_rebinds(s: &Statement, name: &str) -> bool {
300    match s {
301        Statement::Block { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
302        Statement::Multi { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
303        Statement::VarDecl { name: n, init, .. } => {
304            n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
305        }
306        Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
307        Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
308        Statement::Throw { value, .. } => expr_rebinds(value, name),
309        Statement::If {
310            cond,
311            then_branch,
312            else_branch,
313            ..
314        } => {
315            expr_rebinds(cond, name)
316                || stmt_rebinds(then_branch, name)
317                || else_branch.as_ref().is_some_and(|s| stmt_rebinds(s, name))
318        }
319        Statement::While { cond, body, .. } => expr_rebinds(cond, name) || stmt_rebinds(body, name),
320        Statement::DoWhile { body, cond, .. } => {
321            stmt_rebinds(body, name) || expr_rebinds(cond, name)
322        }
323        Statement::For {
324            init,
325            cond,
326            update,
327            body,
328            ..
329        } => {
330            init.as_ref().is_some_and(|s| stmt_rebinds(s, name))
331                || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
332                || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
333                || stmt_rebinds(body, name)
334        }
335        Statement::ForOf {
336            name: n,
337            iterable: e,
338            body,
339            ..
340        }
341        | Statement::ForIn {
342            name: n,
343            object: e,
344            body,
345            ..
346        } => n.as_ref() == name || expr_rebinds(e, name) || stmt_rebinds(body, name),
347        Statement::Switch {
348            expr,
349            cases,
350            default_body,
351            ..
352        } => {
353            expr_rebinds(expr, name)
354                || cases.iter().any(|(t, body)| {
355                    t.as_ref().is_some_and(|e| expr_rebinds(e, name))
356                        || body.iter().any(|s| stmt_rebinds(s, name))
357                })
358                || default_body
359                    .as_ref()
360                    .is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
361        }
362        Statement::Try {
363            body,
364            catch_body,
365            finally_body,
366            ..
367        } => {
368            stmt_rebinds(body, name)
369                || catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
370                || finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
371        }
372        Statement::Break { .. } | Statement::Continue { .. } => false,
373        // VarDeclDestructure (could bind `name`), FunDecl (could shadow), and any unknown construct
374        // → conservative: assume it may rebind `name`.
375        _ => true,
376    }
377}
378
379/// Expression half of [`stmt_rebinds`]. `true` if `name` is an assignment/update target, or unknown.
380fn expr_rebinds(e: &Expr, name: &str) -> bool {
381    match e {
382        Expr::Assign { name: n, value, .. }
383        | Expr::CompoundAssign { name: n, value, .. }
384        | Expr::LogicalAssign { name: n, value, .. } => {
385            n.as_ref() == name || expr_rebinds(value, name)
386        }
387        Expr::PostfixInc { name: n, .. }
388        | Expr::PostfixDec { name: n, .. }
389        | Expr::PrefixInc { name: n, .. }
390        | Expr::PrefixDec { name: n, .. } => n.as_ref() == name,
391        Expr::Literal { .. } | Expr::Ident { .. } => false,
392        Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
393            expr_rebinds(left, name) || expr_rebinds(right, name)
394        }
395        Expr::Unary { operand, .. }
396        | Expr::TypeOf { operand, .. }
397        | Expr::Await { operand, .. } => expr_rebinds(operand, name),
398        Expr::Conditional {
399            cond,
400            then_branch,
401            else_branch,
402            ..
403        } => {
404            expr_rebinds(cond, name)
405                || expr_rebinds(then_branch, name)
406                || expr_rebinds(else_branch, name)
407        }
408        Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
409            expr_rebinds(callee, name)
410                || args.iter().any(|a| match a {
411                    CallArg::Expr(e) | CallArg::Spread(e) => expr_rebinds(e, name),
412                })
413        }
414        Expr::Member { object, .. } => expr_rebinds(object, name),
415        Expr::Index { object, index, .. } => {
416            expr_rebinds(object, name) || expr_rebinds(index, name)
417        }
418        Expr::Array { elements, .. } => elements.iter().any(|el| match el {
419            ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
420        }),
421        Expr::Object { props, .. } => props.iter().any(|p| match p {
422            ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_rebinds(e, name),
423        }),
424        Expr::MemberAssign { object, value, .. } => {
425            expr_rebinds(object, name) || expr_rebinds(value, name)
426        }
427        Expr::IndexAssign {
428            object,
429            index,
430            value,
431            ..
432        } => expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name),
433        Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(|e| expr_rebinds(e, name)),
434        // A nested closure could reassign the outer `name` — in its body OR a param default (which
435        // evaluates in the enclosing scope). Recurse into both (over-conservative if it shadows, which
436        // only costs the optimization). #187: the param-default scan closes the `(a = (foo = x)) => …` hole.
437        Expr::ArrowFunction { params, body, .. } => {
438            params_default_rebinds(params, name)
439                || match body {
440                    ArrowBody::Expr(e) => expr_rebinds(e, name),
441                    ArrowBody::Block(s) => stmt_rebinds(s, name),
442                }
443        }
444        // Jsx, NativeModuleLoad, and anything unknown → conservative.
445        _ => true,
446    }
447}
448
449/// #187: whole-program scan — does `name` get REBOUND (assigned/updated, redeclared via `let`/`const`/
450/// `for-of`/`catch`, shadowed by a param, or (re)declared via a `function name`) anywhere in `s`,
451/// INCLUDING inside every function body? Unlike [`stmt_rebinds`] (which returns `true` on any `FunDecl`
452/// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
453/// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
454/// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
455fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
456    match s {
457        Statement::FunDecl {
458            name: n,
459            params,
460            rest_param,
461            body,
462            ..
463        } => {
464            n.as_ref() == name
465                || params_bind_name(params, name)
466                || params_default_rebinds(params, name)
467                || rest_param
468                    .as_ref()
469                    .is_some_and(|rp| rp.name.as_ref() == name)
470                || name_rebinds_in_stmt(body, name)
471        }
472        Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
473            statements.iter().any(|s| name_rebinds_in_stmt(s, name))
474        }
475        Statement::VarDecl { name: n, init, .. } => {
476            n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
477        }
478        Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
479        Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
480        Statement::Throw { value, .. } => expr_rebinds(value, name),
481        Statement::If {
482            cond,
483            then_branch,
484            else_branch,
485            ..
486        } => {
487            expr_rebinds(cond, name)
488                || name_rebinds_in_stmt(then_branch, name)
489                || else_branch
490                    .as_ref()
491                    .is_some_and(|s| name_rebinds_in_stmt(s, name))
492        }
493        Statement::While { cond, body, .. } => {
494            expr_rebinds(cond, name) || name_rebinds_in_stmt(body, name)
495        }
496        Statement::DoWhile { body, cond, .. } => {
497            name_rebinds_in_stmt(body, name) || expr_rebinds(cond, name)
498        }
499        Statement::For {
500            init,
501            cond,
502            update,
503            body,
504            ..
505        } => {
506            init.as_ref().is_some_and(|s| name_rebinds_in_stmt(s, name))
507                || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
508                || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
509                || name_rebinds_in_stmt(body, name)
510        }
511        Statement::ForOf {
512            name: n,
513            iterable: e,
514            body,
515            ..
516        }
517        | Statement::ForIn {
518            name: n,
519            object: e,
520            body,
521            ..
522        } => n.as_ref() == name || expr_rebinds(e, name) || name_rebinds_in_stmt(body, name),
523        Statement::Switch {
524            expr,
525            cases,
526            default_body,
527            ..
528        } => {
529            expr_rebinds(expr, name)
530                || cases.iter().any(|(t, body)| {
531                    t.as_ref().is_some_and(|e| expr_rebinds(e, name))
532                        || body.iter().any(|s| name_rebinds_in_stmt(s, name))
533                })
534                || default_body
535                    .as_ref()
536                    .is_some_and(|b| b.iter().any(|s| name_rebinds_in_stmt(s, name)))
537        }
538        Statement::Try {
539            body,
540            catch_body,
541            finally_body,
542            ..
543        } => {
544            name_rebinds_in_stmt(body, name)
545                || catch_body
546                    .as_ref()
547                    .is_some_and(|s| name_rebinds_in_stmt(s, name))
548                || finally_body
549                    .as_ref()
550                    .is_some_and(|s| name_rebinds_in_stmt(s, name))
551        }
552        Statement::Break { .. } | Statement::Continue { .. } => false,
553        // VarDeclDestructure (could bind `name`) + any unknown construct → conservative.
554        _ => true,
555    }
556}
557
558/// #187: the set of top-level function names that are PROVABLY stable across the whole program — each
559/// declared exactly once as a top-level `function N`, never also bound by a top-level `let`/`var`/
560/// `const`, and never reassigned/redeclared/shadowed anywhere (via [`name_rebinds_in_stmt`], skipping
561/// the single defining declaration but still recursing into its body). A direct native call to such a
562/// callee can never dispatch a stale function, because the binding can never change.
563fn compute_stable_globals(program: &Program) -> std::collections::HashSet<Arc<str>> {
564    let mut fn_count: HashMap<Arc<str>, usize> = HashMap::new();
565    let mut nonfn_toplevel: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
566    for s in &program.statements {
567        match s {
568            Statement::FunDecl { name, .. } => *fn_count.entry(Arc::clone(name)).or_insert(0) += 1,
569            Statement::VarDecl { name, .. } => {
570                nonfn_toplevel.insert(Arc::clone(name));
571            }
572            _ => {}
573        }
574    }
575    let mut stable: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
576    'cand: for (name, &count) in &fn_count {
577        if count != 1 || nonfn_toplevel.contains(name) {
578            continue;
579        }
580        for s in &program.statements {
581            match s {
582                // The single defining `function name`: not a rebind, but its body/params still count.
583                Statement::FunDecl {
584                    name: n,
585                    params,
586                    rest_param,
587                    body,
588                    ..
589                } if n == name => {
590                    if params_bind_name(params, name)
591                        || params_default_rebinds(params, name)
592                        || rest_param
593                            .as_ref()
594                            .is_some_and(|rp| rp.name.as_ref() == name.as_ref())
595                        || name_rebinds_in_stmt(body, name)
596                    {
597                        continue 'cand;
598                    }
599                }
600                other => {
601                    if name_rebinds_in_stmt(other, name) {
602                        continue 'cand;
603                    }
604                }
605            }
606        }
607        stable.insert(Arc::clone(name));
608    }
609    stable
610}
611
612/// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
613/// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
614/// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
615/// captured local must stay name-based in `local_scope` (which closures capture); only uncaptured
616/// locals are slotted. Recurses ALL ordinary control flow (so it finds every closure → the capture
617/// set is complete); returns `false` ONLY on ambient/module constructs it cannot traverse, so the
618/// caller leaves the whole chunk name-based (safe default-bail: a missed closure could otherwise let a
619/// captured local be wrongly slotted). Slot ALLOCATION happens during compilation (scope-aware), not
620/// here — so block scoping + shadowing are handled by the slot-scope stack, not a flat map.
621#[derive(Default)]
622struct SlotScan {
623    captured: HashSet<Arc<str>>,
624}
625
626impl SlotScan {
627    fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
628        match s {
629            Statement::Block { statements, .. } => {
630                statements.iter().all(|s| self.stmt(s, in_closure))
631            }
632            Statement::Multi { statements, .. } => {
633                statements.iter().all(|s| self.stmt(s, in_closure))
634            }
635            Statement::VarDecl { init, .. } => {
636                init.as_ref().is_none_or(|e| self.expr(e, in_closure))
637            }
638            Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
639            Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
640            Statement::If {
641                cond,
642                then_branch,
643                else_branch,
644                ..
645            } => {
646                self.expr(cond, in_closure)
647                    && self.stmt(then_branch, in_closure)
648                    && else_branch
649                        .as_ref()
650                        .is_none_or(|s| self.stmt(s, in_closure))
651            }
652            Statement::While { cond, body, .. } => {
653                self.expr(cond, in_closure) && self.stmt(body, in_closure)
654            }
655            Statement::DoWhile { body, cond, .. } => {
656                self.stmt(body, in_closure) && self.expr(cond, in_closure)
657            }
658            Statement::For {
659                init,
660                cond,
661                update,
662                body,
663                ..
664            } => {
665                init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
666                    && cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
667                    && update.as_ref().is_none_or(|e| self.expr(e, in_closure))
668                    && self.stmt(body, in_closure)
669            }
670            Statement::ForOf { iterable, body, .. } => {
671                self.expr(iterable, in_closure) && self.stmt(body, in_closure)
672            }
673            Statement::Return { value, .. } => {
674                value.as_ref().is_none_or(|e| self.expr(e, in_closure))
675            }
676            Statement::Throw { value, .. } => self.expr(value, in_closure),
677            Statement::Break { .. } | Statement::Continue { .. } => true,
678            Statement::Switch {
679                expr,
680                cases,
681                default_body,
682                ..
683            } => {
684                if !self.expr(expr, in_closure) {
685                    return false;
686                }
687                for (test, body) in cases {
688                    if let Some(t) = test {
689                        if !self.expr(t, in_closure) {
690                            return false;
691                        }
692                    }
693                    if !body.iter().all(|s| self.stmt(s, in_closure)) {
694                        return false;
695                    }
696                }
697                default_body
698                    .as_ref()
699                    .is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
700            }
701            Statement::Try {
702                body,
703                catch_body,
704                finally_body,
705                ..
706            } => {
707                self.stmt(body, in_closure)
708                    && catch_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
709                    && finally_body
710                        .as_ref()
711                        .is_none_or(|s| self.stmt(s, in_closure))
712            }
713            // A nested named function: its param defaults (enclosing-scope) + whole body capture.
714            Statement::FunDecl { params, body, .. } => {
715                self.scan_closure_param_defaults(params) && self.stmt(body, true)
716            }
717            // Ambient/module constructs (Import/Export/TypeAlias/DeclareVar/DeclareFun) → bail.
718            _ => false,
719        }
720    }
721
722    fn expr(&mut self, e: &Expr, in_closure: bool) -> bool {
723        match e {
724            Expr::Literal { .. } => true,
725            Expr::Ident { name, .. } => {
726                if in_closure {
727                    self.captured.insert(Arc::clone(name));
728                }
729                true
730            }
731            Expr::Binary { left, right, .. } => {
732                self.expr(left, in_closure) && self.expr(right, in_closure)
733            }
734            Expr::Unary { operand, .. }
735            | Expr::TypeOf { operand, .. }
736            | Expr::Await { operand, .. } => self.expr(operand, in_closure),
737            Expr::Conditional {
738                cond,
739                then_branch,
740                else_branch,
741                ..
742            } => {
743                self.expr(cond, in_closure)
744                    && self.expr(then_branch, in_closure)
745                    && self.expr(else_branch, in_closure)
746            }
747            Expr::NullishCoalesce { left, right, .. } => {
748                self.expr(left, in_closure) && self.expr(right, in_closure)
749            }
750            Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
751                self.expr(callee, in_closure)
752                    && args.iter().all(|a| match a {
753                        CallArg::Expr(e) | CallArg::Spread(e) => self.expr(e, in_closure),
754                    })
755            }
756            Expr::Member { object, .. } => self.expr(object, in_closure),
757            Expr::Index { object, index, .. } => {
758                self.expr(object, in_closure) && self.expr(index, in_closure)
759            }
760            Expr::Array { elements, .. } => elements.iter().all(|el| match el {
761                ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
762            }),
763            Expr::Object { props, .. } => props.iter().all(|p| match p {
764                ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => self.expr(e, in_closure),
765            }),
766            Expr::Assign { name, value, .. }
767            | Expr::CompoundAssign { name, value, .. }
768            | Expr::LogicalAssign { name, value, .. } => {
769                if in_closure {
770                    self.captured.insert(Arc::clone(name));
771                }
772                self.expr(value, in_closure)
773            }
774            Expr::MemberAssign { object, value, .. } => {
775                self.expr(object, in_closure) && self.expr(value, in_closure)
776            }
777            Expr::IndexAssign {
778                object,
779                index,
780                value,
781                ..
782            } => {
783                self.expr(object, in_closure)
784                    && self.expr(index, in_closure)
785                    && self.expr(value, in_closure)
786            }
787            Expr::PostfixInc { name, .. }
788            | Expr::PostfixDec { name, .. }
789            | Expr::PrefixInc { name, .. }
790            | Expr::PrefixDec { name, .. } => {
791                if in_closure {
792                    self.captured.insert(Arc::clone(name));
793                }
794                true
795            }
796            Expr::TemplateLiteral { exprs, .. } => exprs.iter().all(|e| self.expr(e, in_closure)),
797            Expr::ArrowFunction { params, body, .. } => {
798                if !self.scan_closure_param_defaults(params) {
799                    return false;
800                }
801                match body {
802                    ArrowBody::Expr(e) => self.expr(e, true),
803                    ArrowBody::Block(s) => self.stmt(s, true),
804                }
805            }
806            // Jsx, NativeModuleLoad → bail.
807            _ => false,
808        }
809    }
810
811    /// A nested closure's parameter default expressions evaluate in the ENCLOSING scope → captured.
812    fn scan_closure_param_defaults(&mut self, params: &[FunParam]) -> bool {
813        for p in params {
814            let default = match p {
815                FunParam::Simple(tp) => &tp.default,
816                FunParam::Destructure { default, .. } => default,
817            };
818            if let Some(d) = default {
819                if !self.expr(d, true) {
820                    return false;
821                }
822            }
823        }
824        true
825    }
826}
827
828/// Capture-aware eligibility for general slot-based locals in a FUNCTION. Returns the captured-name set
829/// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
830/// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
831/// (the VM binds params into slots 0..n, but a closure reads captures by name from `local_scope`).
832fn slot_analyze(
833    params: &[FunParam],
834    has_rest: bool,
835    body: &Statement,
836) -> Option<HashSet<Arc<str>>> {
837    if !slots_enabled() || has_rest {
838        return None;
839    }
840    for p in params {
841        if let FunParam::Destructure { .. } = p {
842            return None;
843        }
844    }
845    let mut scan = SlotScan::default();
846    if !scan.stmt(body, false) {
847        return None;
848    }
849    for p in params {
850        if let FunParam::Simple(tp) = p {
851            if scan.captured.contains(&tp.name) {
852                return None;
853            }
854        }
855    }
856    Some(scan.captured)
857}
858
859/// Same, for the TOP-LEVEL program (no params). Caller must additionally ensure non-REPL mode.
860fn slot_analyze_toplevel(statements: &[Statement]) -> Option<HashSet<Arc<str>>> {
861    if !slots_enabled() {
862        return None;
863    }
864    let mut scan = SlotScan::default();
865    for s in statements {
866        if !scan.stmt(s, false) {
867            return None;
868        }
869    }
870    Some(scan.captured)
871}
872
873impl<'a> Compiler<'a> {
874    /// Resolve a name to its frame slot. `None` ⇒ name-based (a captured local, a global, or a
875    /// builtin) — the single source of truth for slot-vs-name. Checks the simple param-only map first
876    /// (a chunk is in exactly one mode), then the general scope stack innermost-first (shadowing).
877    #[inline]
878    fn resolve_slot(&self, name: &str) -> Option<u16> {
879        if let Some(m) = self.slot_ctx.as_ref() {
880            if let Some(s) = m.get(name) {
881                return Some(*s);
882            }
883        }
884        self.slot_scopes
885            .iter()
886            .rev()
887            .find_map(|m| m.get(name).copied())
888    }
889
890    /// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
891    fn emit_var_load(&mut self, name: &Arc<str>) {
892        if let Some(slot) = self.resolve_slot(name) {
893            self.emit_u16(Opcode::LoadLocal, slot);
894        } else {
895            let idx = self.name_idx(name);
896            self.emit_u16(Opcode::LoadVar, idx);
897        }
898    }
899
900    /// Emit a variable WRITE (value already on stack): `StoreLocal` if slotted, else `StoreVar`.
901    fn emit_var_store(&mut self, name: &Arc<str>) {
902        if let Some(slot) = self.resolve_slot(name) {
903            self.emit_u16(Opcode::StoreLocal, slot);
904        } else {
905            let idx = self.name_idx(name);
906            self.emit_u16(Opcode::StoreVar, idx);
907        }
908    }
909
910    fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
911        Self {
912            chunk,
913            scope: vec![HashMap::new()],
914            loop_stack: Vec::new(),
915            switch_stack: Vec::new(),
916            breakable_stack: Vec::new(),
917            block_depth: 0,
918            retain_last_expr,
919            slot_ctx: None,
920            slot_scopes: Vec::new(),
921            slot_captured: HashSet::new(),
922            next_slot: 0,
923            general_slots: false,
924            finally_stack: Vec::new(),
925            self_fn_name: None,
926            math_is_global: false,
927            stable_globals: Arc::new(std::collections::HashSet::new()),
928        }
929    }
930
931    /// Begin a lexical block: push a name-scope frame and (in general slot mode) a slot-scope frame,
932    /// so block-local `let`s shadow correctly and are reclaimed at block end. Pair with [`exit_block_scope`].
933    fn enter_block_scope(&mut self) {
934        self.scope.push(HashMap::default());
935        if self.general_slots {
936            self.slot_scopes.push(HashMap::default());
937        }
938    }
939
940    fn exit_block_scope(&mut self) {
941        let _popped = self.scope.pop();
942        if self.general_slots {
943            self.slot_scopes.pop();
944        }
945    }
946
947    /// Allocate a fresh frame slot for `name` in the innermost slot scope (general mode).
948    fn declare_slot(&mut self, name: &Arc<str>) -> u16 {
949        let slot = self.next_slot;
950        self.next_slot += 1;
951        if let Some(frame) = self.slot_scopes.last_mut() {
952            frame.insert(Arc::clone(name), slot);
953        }
954        slot
955    }
956
957    /// Emit the pending `finally` bodies (innermost first) before a `return` escapes them. While
958    /// emitting, the stack is cleared so a `return` *inside* one of these finallys doesn't recurse.
959    fn emit_pending_finallys(&mut self) -> Result<(), CompileError> {
960        if self.finally_stack.is_empty() {
961            return Ok(());
962        }
963        let saved = std::mem::take(&mut self.finally_stack);
964        for finally in saved.iter().rev() {
965            self.compile_statement(finally)?;
966        }
967        self.finally_stack = saved;
968        Ok(())
969    }
970
971    fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
972        let n = self.block_depth.saturating_sub(target_depth);
973        for _ in 0..n {
974            self.emit(Opcode::ExitBlock);
975        }
976    }
977
978    /// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
979    /// Formal parameters as VM slot names plus optional destructure patterns (one per formal).
980    #[allow(clippy::type_complexity)] // (slot names, optional destructure patterns) — single-use return
981    fn plan_function_params(
982        params: &[FunParam],
983    ) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
984        let mut names = Vec::with_capacity(params.len());
985        let mut slots: Vec<Option<DestructPattern>> = Vec::with_capacity(params.len());
986        let mut syn_counter = 0u32;
987        for p in params {
988            match p {
989                FunParam::Simple(tp) => {
990                    names.push(Arc::clone(&tp.name));
991                    slots.push(None);
992                }
993                FunParam::Destructure {
994                    pattern, default, ..
995                } => {
996                    if default.is_some() {
997                        return Err(CompileError {
998                            message: "Default values on destructuring parameters are not supported in bytecode"
999                                .to_string(),
1000                        });
1001                    }
1002                    names.push(Arc::from(format!("__param_{}", syn_counter)));
1003                    syn_counter += 1;
1004                    slots.push(Some(pattern.clone()));
1005                }
1006            }
1007        }
1008        Ok((names, slots))
1009    }
1010
1011    /// After VM binds positional args to `param_names`, load each destructure slot and bind pattern locals.
1012    fn emit_param_destructure_prologue(
1013        &mut self,
1014        param_names: &[Arc<str>],
1015        slots: &[Option<DestructPattern>],
1016    ) -> Result<(), CompileError> {
1017        debug_assert_eq!(param_names.len(), slots.len());
1018        for (name, slot) in param_names.iter().zip(slots.iter()) {
1019            if let Some(pattern) = slot {
1020                let idx = self.name_idx(name);
1021                self.emit_u16(Opcode::LoadVar, idx);
1022                self.compile_destructure(pattern, false, false)?;
1023            }
1024        }
1025        Ok(())
1026    }
1027
1028    /// Emit the default-parameter prologue: for each simple param `p_i` with a default,
1029    /// `if (arg i was not supplied) p_i = <default>`. Runs at the top of the function body so
1030    /// later defaults can reference earlier (already-bound) params, e.g. `(a, b = a + 1)`.
1031    ///
1032    /// Uses `ArgMissing(i)` (true iff `i >= argc`) + `JumpIfFalse` so the default applies only
1033    /// to *missing* positional args — matching the interpreter, where an explicit `null` keeps
1034    /// the `null` (tish has no `undefined`). The store mirrors variable resolution: a slot-based
1035    /// chunk writes the slot directly (`StoreLocal`); a name-based chunk binds the name
1036    /// (`DeclareVarPlain`, since a missing param is absent from the frame scope).
1037    fn emit_param_defaults_prologue(&mut self, params: &[FunParam]) -> Result<(), CompileError> {
1038        for (i, p) in params.iter().enumerate() {
1039            let FunParam::Simple(tp) = p else { continue };
1040            let Some(default_expr) = &tp.default else {
1041                continue;
1042            };
1043            self.emit_u16(Opcode::ArgMissing, i as u16);
1044            let skip = self.emit_jump(Opcode::JumpIfFalse);
1045            self.compile_expr(default_expr)?;
1046            let slot = self
1047                .slot_ctx
1048                .as_ref()
1049                .and_then(|m| m.get(tp.name.as_ref()))
1050                .copied();
1051            match slot {
1052                Some(slot) => self.emit_u16(Opcode::StoreLocal, slot),
1053                None => {
1054                    let idx = self.name_idx(&tp.name);
1055                    self.emit_u16(Opcode::DeclareVarPlain, idx);
1056                }
1057            }
1058            self.patch_jump(skip, self.chunk.code.len());
1059        }
1060        Ok(())
1061    }
1062
1063    /// Names `let`/`const`-declared DIRECTLY in a loop body block (not nested blocks). Each is a
1064    /// fresh per-iteration binding (ES `let`), so closures created in the body must capture this
1065    /// iteration's value — registered via `LoopVarsBegin`.
1066    fn loop_body_block_lets(body: &Statement) -> Vec<Arc<str>> {
1067        let mut out = Vec::new();
1068        if let Statement::Block { statements, .. } = body {
1069            for s in statements {
1070                if let Statement::VarDecl { name, .. } = s {
1071                    out.push(Arc::clone(name));
1072                }
1073            }
1074        }
1075        out
1076    }
1077
1078    fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
1079        match stmt {
1080            Statement::VarDecl {
1081                name,
1082                init,
1083                mutable: _,
1084                ..
1085            } => {
1086                if let Some(expr) = init {
1087                    self.compile_expr(expr)?;
1088                } else {
1089                    let idx = self.constant_idx(Constant::Null);
1090                    self.emit(Opcode::LoadConst);
1091                    self.chunk.write_u16(idx);
1092                }
1093                if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
1094                    let slot = self.declare_slot(name);
1095                    self.emit_u16(Opcode::StoreLocal, slot);
1096                } else {
1097                    let idx = self.name_idx(name);
1098                    self.emit_u16(Opcode::DeclareVarPlain, idx);
1099                    self.scope
1100                        .last_mut()
1101                        .unwrap()
1102                        .insert(Arc::clone(name), false);
1103                }
1104            }
1105            Statement::VarDeclDestructure { pattern, init, .. } => {
1106                self.compile_expr(init)?;
1107                self.compile_destructure(pattern, false, true)?;
1108            }
1109            _ => self.compile_statement(stmt)?,
1110        }
1111        Ok(())
1112    }
1113
1114    fn name_idx(&mut self, name: &Arc<str>) -> u16 {
1115        self.chunk.add_name(Arc::clone(name))
1116    }
1117
1118    fn constant_idx(&mut self, c: Constant) -> u16 {
1119        self.chunk.add_constant(c)
1120    }
1121
1122    fn emit(&mut self, op: Opcode) {
1123        self.chunk.write_u8(op as u8);
1124    }
1125
1126    /// Record the source line of the code about to be emitted, for runtime error locations
1127    /// (issue #74). Cheap and deduped: only a line *change* adds a table entry.
1128    fn mark_line(&mut self, span: tishlang_ast::Span) {
1129        let offset = self.chunk.code.len();
1130        self.chunk.mark_line(offset, span.start.0 as u32);
1131    }
1132
1133    fn emit_u8(&mut self, op: Opcode, v: u8) {
1134        self.chunk.write_u8(op as u8);
1135        self.chunk.write_u16(v as u16);
1136    }
1137
1138    fn emit_u16(&mut self, op: Opcode, v: u16) {
1139        self.chunk.write_u8(op as u8);
1140        self.chunk.write_u16(v);
1141    }
1142
1143    fn emit_jump(&mut self, op: Opcode) -> usize {
1144        let pos = self.chunk.code.len();
1145        self.chunk.write_u8(op as u8);
1146        self.chunk.write_u16(0); // placeholder
1147        pos + 1
1148    }
1149
1150    /// Emit JumpBack with placeholder distance; patch later with patch_jump_back.
1151    fn emit_jump_back(&mut self) -> usize {
1152        let pos = self.chunk.code.len();
1153        self.chunk.write_u8(Opcode::JumpBack as u8);
1154        self.chunk.write_u16(0);
1155        pos + 1
1156    }
1157
1158    fn patch_jump(&mut self, patch_pos: usize, target: usize) {
1159        let base = patch_pos + 2;
1160        let jump_offset = (target as i32).wrapping_sub(base as i32);
1161        let bytes = (jump_offset as i16).to_be_bytes();
1162        self.chunk.code[patch_pos] = bytes[0];
1163        self.chunk.code[patch_pos + 1] = bytes[1];
1164    }
1165
1166    /// Compile `cond` in CONDITION position (if / while / for / do-while). The operand stack is EMPTY
1167    /// at every emitted `JumpIfFalse` — `&&` / `||` lower to pure control flow rather than the
1168    /// value-producing `Dup` + `BinOp` form, so a hot numeric loop whose only disqualifier was the
1169    /// condition shape stays eligible for the cranelift JIT (#167). Returns the `JumpIfFalse` patch
1170    /// sites the caller must point at its "condition is false" target. The condition value itself is
1171    /// never observed here, so truthiness-only lowering is exact; short-circuit order is preserved.
1172    fn compile_condition_jump_if_false(&mut self, cond: &Expr) -> Result<Vec<usize>, CompileError> {
1173        match cond {
1174            // `a && b` is false if EITHER operand is false — test each in order; both exit to the
1175            // same false-target, so concatenate their patch sites.
1176            Expr::Binary {
1177                left,
1178                op: BinOp::And,
1179                right,
1180                ..
1181            } => {
1182                let mut patches = self.compile_condition_jump_if_false(left)?;
1183                patches.extend(self.compile_condition_jump_if_false(right)?);
1184                Ok(patches)
1185            }
1186            // `a || b`: a truthy left makes the condition hold (skip the right); only the right's
1187            // falsiness exits.
1188            Expr::Binary {
1189                left,
1190                op: BinOp::Or,
1191                right,
1192                ..
1193            } => {
1194                self.compile_expr(left)?;
1195                let take_right = self.emit_jump(Opcode::JumpIfFalse); // left falsy → evaluate right
1196                let done = self.emit_jump(Opcode::Jump); // left truthy → condition holds
1197                self.patch_jump(take_right, self.chunk.code.len());
1198                let patches = self.compile_condition_jump_if_false(right)?;
1199                self.patch_jump(done, self.chunk.code.len());
1200                Ok(patches)
1201            }
1202            // Any other expression: evaluate it; one JumpIfFalse consumes the value.
1203            _ => {
1204                self.compile_expr(cond)?;
1205                Ok(vec![self.emit_jump(Opcode::JumpIfFalse)])
1206            }
1207        }
1208    }
1209
1210    /// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
1211    /// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
1212    fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
1213        let after_insn = patch_pos + 2;
1214        let dist = after_insn.saturating_sub(target);
1215        let bytes = (dist as u16).to_be_bytes();
1216        self.chunk.code[patch_pos] = bytes[0];
1217        self.chunk.code[patch_pos + 1] = bytes[1];
1218    }
1219
1220    /// Detect property-based numeric sort: (a, b) => a.prop - b.prop or (a, b) => b.prop - a.prop.
1221    /// Returns Some((prop_name, asc)) or None.
1222    fn detect_property_sort_comparator(expr: &Expr) -> Option<(Arc<str>, bool)> {
1223        if let Expr::ArrowFunction { params, body, .. } = expr {
1224            if params.len() != 2 {
1225                return None;
1226            }
1227            let (param_a, param_b) = match (&params[0], &params[1]) {
1228                (FunParam::Simple(a), FunParam::Simple(b))
1229                    if a.default.is_none() && b.default.is_none() =>
1230                {
1231                    (a.name.as_ref(), b.name.as_ref())
1232                }
1233                _ => return None,
1234            };
1235            let body_expr = match body {
1236                ArrowBody::Expr(e) => e.as_ref(),
1237                ArrowBody::Block(stmt) => {
1238                    if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
1239                        e
1240                    } else {
1241                        return None;
1242                    }
1243                }
1244            };
1245            if let Expr::Binary {
1246                left,
1247                op: BinOp::Sub,
1248                right,
1249                ..
1250            } = body_expr
1251            {
1252                if let (
1253                    Expr::Member {
1254                        object: lo,
1255                        prop: MemberProp::Name { name: p, .. },
1256                        ..
1257                    },
1258                    Expr::Member {
1259                        object: ro,
1260                        prop: MemberProp::Name { name: pr, .. },
1261                        ..
1262                    },
1263                ) = (left.as_ref(), right.as_ref())
1264                {
1265                    if p != pr {
1266                        return None;
1267                    }
1268                    if let (Expr::Ident { name: ln, .. }, Expr::Ident { name: rn, .. }) =
1269                        (lo.as_ref(), ro.as_ref())
1270                    {
1271                        if ln.as_ref() == param_a && rn.as_ref() == param_b {
1272                            return Some((Arc::clone(p), true));
1273                        }
1274                        if ln.as_ref() == param_b && rn.as_ref() == param_a {
1275                            return Some((Arc::clone(p), false));
1276                        }
1277                    }
1278                }
1279            }
1280        }
1281        None
1282    }
1283
1284    /// Detect numeric sort comparator: (a, b) => a - b (asc) or (a, b) => b - a (desc).
1285    fn detect_numeric_sort_comparator(expr: &Expr) -> Option<bool> {
1286        if let Expr::ArrowFunction { params, body, .. } = expr {
1287            if params.len() != 2 {
1288                return None;
1289            }
1290            let (param_a, param_b) = match (&params[0], &params[1]) {
1291                (FunParam::Simple(a), FunParam::Simple(b))
1292                    if a.default.is_none() && b.default.is_none() =>
1293                {
1294                    (a.name.as_ref(), b.name.as_ref())
1295                }
1296                _ => return None,
1297            };
1298            let body_expr = match body {
1299                ArrowBody::Expr(e) => e.as_ref(),
1300                ArrowBody::Block(stmt) => {
1301                    if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
1302                        e
1303                    } else {
1304                        return None;
1305                    }
1306                }
1307            };
1308            if let Expr::Binary {
1309                left,
1310                op: BinOp::Sub,
1311                right,
1312                ..
1313            } = body_expr
1314            {
1315                if let (
1316                    Expr::Ident {
1317                        name: left_name, ..
1318                    },
1319                    Expr::Ident {
1320                        name: right_name, ..
1321                    },
1322                ) = (left.as_ref(), right.as_ref())
1323                {
1324                    if left_name.as_ref() == param_a && right_name.as_ref() == param_b {
1325                        return Some(true);
1326                    }
1327                    if left_name.as_ref() == param_b && right_name.as_ref() == param_a {
1328                        return Some(false);
1329                    }
1330                }
1331            }
1332        }
1333        None
1334    }
1335
1336    /// Detect simple map callback: x => x (identity) or x => x op const / x => const op x.
1337    /// Returns SimpleMapResult for map optimization.
1338    fn detect_simple_map_callback(expr: &Expr) -> Option<SimpleMapResult> {
1339        let (params, body) = match expr {
1340            Expr::ArrowFunction { params, body, .. } => (params, body),
1341            _ => return None,
1342        };
1343        if params.len() != 1 {
1344            return None;
1345        }
1346        let param_name = match &params[0] {
1347            FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
1348            _ => return None,
1349        };
1350        let expr_ref: &Expr = match body {
1351            ArrowBody::Expr(e) => e.as_ref(),
1352            ArrowBody::Block(stmt) => {
1353                let s = stmt.as_ref();
1354                if let Statement::Return {
1355                    value: Some(ref e), ..
1356                } = s
1357                {
1358                    e
1359                } else if let Statement::ExprStmt { expr: ref e, .. } = s {
1360                    e
1361                } else {
1362                    return None;
1363                }
1364            }
1365        };
1366        // Identity: x => x
1367        if let Expr::Ident { name, .. } = expr_ref {
1368            if name.as_ref() == param_name {
1369                return Some(SimpleMapResult::Identity);
1370            }
1371        }
1372        // Binary: x op const or const op x
1373        if let Expr::Binary {
1374            left, op, right, ..
1375        } = expr_ref
1376        {
1377            let left_is_param =
1378                matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1379            let right_is_param =
1380                matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1381            let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
1382            let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
1383            if left_is_param && right_is_literal {
1384                if let Some(c) = literal_to_constant(right.as_ref()) {
1385                    return Some(SimpleMapResult::BinOp(*op, c, true));
1386                }
1387            }
1388            if left_is_literal && right_is_param {
1389                if let Some(c) = literal_to_constant(left.as_ref()) {
1390                    return Some(SimpleMapResult::BinOp(*op, c, false));
1391                }
1392            }
1393        }
1394        None
1395    }
1396
1397    /// Detect simple filter callback: x => x op const or x => const op x (comparison that returns bool).
1398    fn detect_simple_filter_callback(expr: &Expr) -> Option<(BinOp, Constant, bool)> {
1399        let (params, body) = match expr {
1400            Expr::ArrowFunction { params, body, .. } => (params, body),
1401            _ => return None,
1402        };
1403        if params.len() != 1 {
1404            return None;
1405        }
1406        let param_name = match &params[0] {
1407            FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
1408            _ => return None,
1409        };
1410        let expr_ref: &Expr = match body {
1411            ArrowBody::Expr(e) => e.as_ref(),
1412            ArrowBody::Block(stmt) => {
1413                let s = stmt.as_ref();
1414                if let Statement::Return {
1415                    value: Some(ref e), ..
1416                } = s
1417                {
1418                    e
1419                } else if let Statement::ExprStmt { expr: ref e, .. } = s {
1420                    e
1421                } else {
1422                    return None;
1423                }
1424            }
1425        };
1426        if let Expr::Binary {
1427            left, op, right, ..
1428        } = expr_ref
1429        {
1430            if !matches!(
1431                op,
1432                BinOp::Eq
1433                    | BinOp::Ne
1434                    | BinOp::StrictEq
1435                    | BinOp::StrictNe
1436                    | BinOp::Lt
1437                    | BinOp::Le
1438                    | BinOp::Gt
1439                    | BinOp::Ge
1440                    | BinOp::And
1441                    | BinOp::Or
1442            ) {
1443                return None;
1444            }
1445            let left_is_param =
1446                matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1447            let right_is_param =
1448                matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1449            let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
1450            let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
1451            if left_is_param && right_is_literal {
1452                if let Some(c) = literal_to_constant(right.as_ref()) {
1453                    return Some((*op, c, true));
1454                }
1455            }
1456            if left_is_literal && right_is_param {
1457                if let Some(c) = literal_to_constant(left.as_ref()) {
1458                    return Some((*op, c, false));
1459                }
1460            }
1461        }
1462        None
1463    }
1464
1465    fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
1466        let stmts = &program.statements;
1467        // Top-level general slot-based locals — NON-REPL only (REPL persists top-level `let`s to
1468        // globals across lines, which slots can't do). Set up before compiling; the frame size is the
1469        // monotonic `next_slot` high-water, applied to the chunk after compilation.
1470        if !self.retain_last_expr {
1471            if let Some(cap) = slot_analyze_toplevel(stmts) {
1472                self.general_slots = true;
1473                self.slot_captured = cap;
1474                self.slot_scopes.push(HashMap::default());
1475            }
1476        }
1477        let last_is_expr = self.retain_last_expr
1478            && stmts
1479                .last()
1480                .map(|s| matches!(s, Statement::ExprStmt { .. }))
1481                .unwrap_or(false);
1482
1483        if last_is_expr {
1484            let (rest, last) = stmts.split_at(stmts.len().saturating_sub(1));
1485            for stmt in rest {
1486                self.compile_statement(stmt)?;
1487            }
1488            if let Some(Statement::ExprStmt { expr, .. }) = last.first() {
1489                self.compile_expr(expr)?;
1490            }
1491        } else {
1492            for stmt in stmts {
1493                self.compile_statement(stmt)?;
1494            }
1495            let idx = self.constant_idx(Constant::Null);
1496            self.emit(Opcode::LoadConst);
1497            self.chunk.write_u16(idx);
1498        }
1499        // Apply the top-level slot frame size (only if any local was actually slotted).
1500        if self.general_slots && self.next_slot > 0 {
1501            self.chunk.slot_based = true;
1502            self.chunk.num_slots = self.next_slot;
1503        }
1504        Ok(())
1505    }
1506
1507    fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
1508        self.mark_line(stmt.span());
1509        match stmt {
1510            Statement::Block { statements, .. } => {
1511                self.emit(Opcode::EnterBlock);
1512                self.block_depth += 1;
1513                self.enter_block_scope();
1514                for s in statements {
1515                    self.compile_statement(s)?;
1516                }
1517                self.exit_block_scope();
1518                self.emit(Opcode::ExitBlock);
1519                self.block_depth -= 1;
1520            }
1521            // Comma-declarators: a transparent group — compile each declarator in
1522            // the *current* block scope (no EnterBlock/ExitBlock).
1523            Statement::Multi { statements, .. } => {
1524                for s in statements {
1525                    self.compile_statement(s)?;
1526                }
1527            }
1528            Statement::VarDecl {
1529                name,
1530                init,
1531                mutable: _,
1532                ..
1533            } => {
1534                if let Some(expr) = init {
1535                    self.compile_expr(expr)?;
1536                } else {
1537                    let idx = self.constant_idx(Constant::Null);
1538                    self.emit(Opcode::LoadConst);
1539                    self.chunk.write_u16(idx);
1540                }
1541                if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
1542                    // Uncaptured local → allocate a fresh frame slot + write it directly.
1543                    let slot = self.declare_slot(name);
1544                    self.emit_u16(Opcode::StoreLocal, slot);
1545                } else {
1546                    let idx = self.name_idx(name);
1547                    self.emit_u16(Opcode::DeclareVar, idx);
1548                    self.scope
1549                        .last_mut()
1550                        .unwrap()
1551                        .insert(Arc::clone(name), false);
1552                }
1553            }
1554            Statement::VarDeclDestructure { pattern, init, .. } => {
1555                self.compile_expr(init)?;
1556                self.compile_destructure(pattern, false, false)?;
1557            }
1558            Statement::ExprStmt { expr, .. } => {
1559                // String-builder fast path: statement-position `acc += rhs` on a frame-slot local
1560                // compiles to `<rhs>; AppendLocal slot` (no LoadLocal/Dup/StoreLocal/Pop), letting
1561                // the VM append in amortized O(1) without materializing the discarded result. Only
1562                // for a simple slot-resolved identifier with the `+` compound op; everything else
1563                // (name-based vars, other ops) keeps the generic path.
1564                if let Expr::CompoundAssign {
1565                    name,
1566                    op: CompoundOp::Add,
1567                    value,
1568                    ..
1569                } = expr
1570                {
1571                    if let Some(slot) = self.resolve_slot(name) {
1572                        self.compile_expr(value)?;
1573                        self.emit_u16(Opcode::AppendLocal, slot);
1574                        return Ok(());
1575                    }
1576                }
1577                // Same builder fast path for the plain-assign spelling `s = s + <str>` — but ONLY when
1578                // the appended operand is PROVABLY a string. A numeric accumulator (`i = i + 1`) must
1579                // NOT builder-ize: the VM keeps a single string builder slot, so a second builder-ized
1580                // slot flushes the first every iteration → O(n²) (#186). Restricting to string-typed
1581                // RHS keeps `s = s + "x"` fast (string_concat) while `i = i + 1` stays a plain store.
1582                if let Expr::Assign { name, value, .. } = expr {
1583                    if let Expr::Binary {
1584                        left,
1585                        op: BinOp::Add,
1586                        right,
1587                        ..
1588                    } = value.as_ref()
1589                    {
1590                        if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln == name)
1591                            && is_string_typed(right)
1592                        {
1593                            if let Some(slot) = self.resolve_slot(name) {
1594                                self.compile_expr(right)?;
1595                                self.emit_u16(Opcode::AppendLocal, slot);
1596                                return Ok(());
1597                            }
1598                        }
1599                    }
1600                }
1601                self.compile_expr(expr)?;
1602                self.emit(Opcode::Pop);
1603            }
1604            Statement::If {
1605                cond,
1606                then_branch,
1607                else_branch,
1608                ..
1609            } => {
1610                let jump_else_sites = self.compile_condition_jump_if_false(cond)?;
1611                self.compile_statement(then_branch)?;
1612                let jump_end = self.emit_jump(Opcode::Jump);
1613                let else_target = self.chunk.code.len();
1614                for site in &jump_else_sites {
1615                    self.patch_jump(*site, else_target);
1616                }
1617                if let Some(else_s) = else_branch {
1618                    self.compile_statement(else_s)?;
1619                }
1620                self.patch_jump(jump_end, self.chunk.code.len());
1621            }
1622            Statement::While { cond, body, .. } => {
1623                // Per-iteration `let`: a `let` declared directly in the loop body is a fresh binding
1624                // each iteration, so a closure created in the body captures THIS iteration's value.
1625                // Register those names (same overlay mechanism as for/for-of loop vars).
1626                let body_lets = Self::loop_body_block_lets(body);
1627                for n in &body_lets {
1628                    let idx = self.name_idx(n);
1629                    self.emit_u16(Opcode::LoopVarsBegin, idx);
1630                }
1631                let start = self.chunk.code.len();
1632                self.loop_stack.push(LoopInfo {
1633                    break_patches: Vec::new(),
1634                    continue_patches: Vec::new(),
1635                    continue_is_forward_jump: false,
1636                });
1637                self.breakable_stack.push(Breakable::Loop {
1638                    unwind_depth: self.block_depth,
1639                });
1640                // Condition-position lowering: empty stack at each JumpIfFalse keeps the loop
1641                // JIT-eligible (#167). Each returned site exits to `end`.
1642                let jump_out_sites = self.compile_condition_jump_if_false(cond)?;
1643                self.compile_statement(body)?;
1644                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
1645                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1646                let end = self.chunk.code.len();
1647                for site in &jump_out_sites {
1648                    self.patch_jump(*site, end);
1649                }
1650                let info = self.loop_stack.pop().unwrap();
1651                self.breakable_stack.pop();
1652                for p in info.continue_patches {
1653                    self.patch_jump_back(p, start);
1654                }
1655                for p in info.break_patches {
1656                    self.patch_jump(p, end);
1657                }
1658                for _ in &body_lets {
1659                    self.emit(Opcode::LoopVarsEnd);
1660                }
1661            }
1662            Statement::For {
1663                init,
1664                cond,
1665                update,
1666                body,
1667                ..
1668            } => {
1669                self.enter_block_scope();
1670                if let Some(i) = init {
1671                    self.compile_for_init_statement(i.as_ref())?;
1672                }
1673                // ES per-iteration `let`: register the loop var so a closure created in the body
1674                // captures THIS iteration's value (not the final one). One push per loop entry; the
1675                // per-iteration snapshot only happens when a closure is actually created, so
1676                // closure-free loops are unaffected.
1677                let loop_var: Option<Arc<str>> = match init.as_deref() {
1678                    Some(Statement::VarDecl { name, .. }) => Some(Arc::clone(name)),
1679                    _ => None,
1680                };
1681                if let Some(ref n) = loop_var {
1682                    let idx = self.name_idx(n);
1683                    self.emit_u16(Opcode::LoopVarsBegin, idx);
1684                }
1685                let cond_start = self.chunk.code.len();
1686                // Condition-position lowering keeps the loop JIT-eligible (#167). The absent-condition
1687                // `for (;;)` keeps its `true` constant + single exit jump.
1688                let jump_out_sites = if let Some(c) = cond {
1689                    self.compile_condition_jump_if_false(c)?
1690                } else {
1691                    let idx = self.constant_idx(Constant::Bool(true));
1692                    self.emit(Opcode::LoadConst);
1693                    self.chunk.write_u16(idx);
1694                    vec![self.emit_jump(Opcode::JumpIfFalse)]
1695                };
1696                self.loop_stack.push(LoopInfo {
1697                    break_patches: Vec::new(),
1698                    continue_patches: Vec::new(),
1699                    continue_is_forward_jump: true,
1700                });
1701                self.breakable_stack.push(Breakable::Loop {
1702                    unwind_depth: self.block_depth,
1703                });
1704                self.compile_statement(body)?;
1705                let update_start = self.chunk.code.len();
1706                if let Some(u) = update {
1707                    self.compile_expr(u)?;
1708                    self.emit(Opcode::Pop);
1709                }
1710                let info = self.loop_stack.pop().unwrap();
1711                for p in info.continue_patches {
1712                    self.patch_jump(p, update_start);
1713                }
1714                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
1715                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1716                let end = self.chunk.code.len();
1717                for site in &jump_out_sites {
1718                    self.patch_jump(*site, end);
1719                }
1720                for p in info.break_patches {
1721                    self.patch_jump(p, end);
1722                }
1723                // After the loop fully exits (normal or break, both land at `end`): close the
1724                // per-iteration region.
1725                if loop_var.is_some() {
1726                    self.emit(Opcode::LoopVarsEnd);
1727                }
1728                self.breakable_stack.pop();
1729                self.exit_block_scope();
1730            }
1731            Statement::ForOf {
1732                name,
1733                iterable,
1734                body,
1735                ..
1736            } => {
1737                self.compile_expr(iterable)?;
1738                // Normalize a JS iterator object (Map/Set `.values()` etc.) to an array so the
1739                // index-based loop below can iterate it; arrays/strings pass through untouched.
1740                self.emit(Opcode::IterNormalize);
1741                self.enter_block_scope();
1742                let arr_name = Arc::from("__forof_arr__");
1743                let i_name = Arc::from("__forof_i__");
1744                let len_name = Arc::from("__forof_len__");
1745                let arr_idx = self.name_idx(&arr_name);
1746                let i_idx = self.name_idx(&i_name);
1747                let len_idx = self.name_idx(&len_name);
1748                let name_idx = self.name_idx(name);
1749                self.emit_u16(Opcode::DeclareVar, arr_idx);
1750                self.scope
1751                    .last_mut()
1752                    .unwrap()
1753                    .insert(arr_name.clone(), false);
1754                self.emit_u16(Opcode::LoadVar, arr_idx);
1755                let len_name_idx = self.name_idx(&Arc::from("length"));
1756                self.emit_u16(Opcode::GetMember, len_name_idx);
1757                self.emit_u16(Opcode::DeclareVar, len_idx);
1758                self.scope
1759                    .last_mut()
1760                    .unwrap()
1761                    .insert(len_name.clone(), false);
1762                let zero_idx = self.constant_idx(Constant::Number(0.0));
1763                self.emit(Opcode::LoadConst);
1764                self.chunk.write_u16(zero_idx);
1765                self.emit_u16(Opcode::DeclareVar, i_idx);
1766                self.scope.last_mut().unwrap().insert(i_name.clone(), false);
1767                // ES per-iteration `let` for `for (let v of …)`: register the loop var so a closure
1768                // in the body captures this iteration's element (emitted once, before loop_start).
1769                self.emit_u16(Opcode::LoopVarsBegin, name_idx);
1770                // Pre-tested loop, like the C-style `for` above: test `i < len` at the TOP, before
1771                // reading `arr[i]`. A bottom-tested loop ran the body once on an empty array (reading
1772                // `arr[0]` → null) and spun forever on `continue` (which skipped the increment).
1773                let cond_start = self.chunk.code.len();
1774                self.emit_u16(Opcode::LoadVar, i_idx);
1775                self.emit_u16(Opcode::LoadVar, len_idx);
1776                self.emit_u8(Opcode::BinOp, 10);
1777                let jump_out = self.emit_jump(Opcode::JumpIfFalse);
1778                self.loop_stack.push(LoopInfo {
1779                    break_patches: Vec::new(),
1780                    continue_patches: Vec::new(),
1781                    continue_is_forward_jump: true,
1782                });
1783                self.breakable_stack.push(Breakable::Loop {
1784                    unwind_depth: self.block_depth,
1785                });
1786                self.emit_u16(Opcode::LoadVar, arr_idx);
1787                self.emit_u16(Opcode::LoadVar, i_idx);
1788                self.emit(Opcode::GetIndex);
1789                self.emit_u16(Opcode::DeclareVar, name_idx);
1790                self.scope
1791                    .last_mut()
1792                    .unwrap()
1793                    .insert(Arc::clone(name), false);
1794                self.compile_statement(body)?;
1795                // `continue` lands here: increment `i`, then fall through to the JumpBack → re-test.
1796                let update_start = self.chunk.code.len();
1797                self.emit_u16(Opcode::LoadVar, i_idx);
1798                let one_idx = self.constant_idx(Constant::Number(1.0));
1799                self.emit(Opcode::LoadConst);
1800                self.chunk.write_u16(one_idx);
1801                self.emit_u8(Opcode::BinOp, 0);
1802                self.emit_u16(Opcode::StoreVar, i_idx);
1803                let info = self.loop_stack.pop().unwrap();
1804                self.breakable_stack.pop();
1805                for p in info.continue_patches {
1806                    self.patch_jump(p, update_start);
1807                }
1808                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
1809                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1810                let end = self.chunk.code.len();
1811                self.patch_jump(jump_out, end);
1812                for p in info.break_patches {
1813                    self.patch_jump(p, end);
1814                }
1815                self.emit(Opcode::LoopVarsEnd);
1816                self.exit_block_scope();
1817            }
1818            Statement::ForIn {
1819                name,
1820                name_span,
1821                object,
1822                body,
1823                span,
1824            } => {
1825                // Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413): Object.keys
1826                // yields exactly the own enumerable keys the interpreter enumerates — insertion order
1827                // for objects, index strings for arrays, and `[]` for a non-object (so `for (k in
1828                // null)` no-ops). Reuses the whole for-of iteration path.
1829                let dummy = Span {
1830                    start: (0, 0),
1831                    end: (0, 0),
1832                };
1833                let keys_call = Expr::Call {
1834                    callee: Box::new(Expr::Member {
1835                        object: Box::new(Expr::Ident {
1836                            name: Arc::from("Object"),
1837                            span: dummy,
1838                        }),
1839                        prop: MemberProp::Name {
1840                            name: Arc::from("keys"),
1841                            span: dummy,
1842                        },
1843                        optional: false,
1844                        span: dummy,
1845                    }),
1846                    args: vec![CallArg::Expr(object.clone())],
1847                    span: dummy,
1848                };
1849                let lowered = Statement::ForOf {
1850                    name: Arc::clone(name),
1851                    name_span: *name_span,
1852                    iterable: keys_call,
1853                    body: body.clone(),
1854                    span: *span,
1855                };
1856                self.compile_statement(&lowered)?;
1857            }
1858            Statement::Return { value, .. } => {
1859                // Evaluate the return value first (JS order), then run any enclosing `finally`
1860                // blocks (they're stack-neutral, so the value stays on top), then return.
1861                if let Some(v) = value {
1862                    self.compile_expr(v)?;
1863                } else {
1864                    let idx = self.constant_idx(Constant::Null);
1865                    self.emit(Opcode::LoadConst);
1866                    self.chunk.write_u16(idx);
1867                }
1868                self.emit_pending_finallys()?;
1869                self.emit(Opcode::Return);
1870            }
1871            Statement::Break { .. } => {
1872                let unwind_depth = match self.breakable_stack.last() {
1873                    Some(Breakable::Loop { unwind_depth })
1874                    | Some(Breakable::Switch { unwind_depth }) => *unwind_depth,
1875                    None => {
1876                        return Err(CompileError {
1877                            message: "break not inside a loop or switch".to_string(),
1878                        });
1879                    }
1880                };
1881                self.emit_exit_blocks_until_depth(unwind_depth);
1882                let pos = self.emit_jump(Opcode::Jump);
1883                match self.breakable_stack.last() {
1884                    Some(Breakable::Loop { .. }) => {
1885                        self.loop_stack.last_mut().unwrap().break_patches.push(pos);
1886                    }
1887                    Some(Breakable::Switch { .. }) => {
1888                        self.switch_stack
1889                            .last_mut()
1890                            .unwrap()
1891                            .break_patches
1892                            .push(pos);
1893                    }
1894                    None => {}
1895                }
1896            }
1897            Statement::Continue { .. } => {
1898                let unwind_depth = self
1899                    .breakable_stack
1900                    .iter()
1901                    .rev()
1902                    .find_map(|b| match b {
1903                        Breakable::Loop { unwind_depth } => Some(*unwind_depth),
1904                        Breakable::Switch { .. } => None,
1905                    })
1906                    .ok_or_else(|| CompileError {
1907                        message: "continue not inside a loop".to_string(),
1908                    })?;
1909                self.emit_exit_blocks_until_depth(unwind_depth);
1910                let forward = self
1911                    .loop_stack
1912                    .last()
1913                    .expect("continue not inside a loop")
1914                    .continue_is_forward_jump;
1915                let pos = if forward {
1916                    self.emit_jump(Opcode::Jump)
1917                } else {
1918                    self.emit_jump_back()
1919                };
1920                self.loop_stack
1921                    .last_mut()
1922                    .expect("continue not inside a loop")
1923                    .continue_patches
1924                    .push(pos);
1925            }
1926            Statement::FunDecl {
1927                name,
1928                params,
1929                body,
1930                rest_param,
1931                async_: _,
1932                ..
1933            } => {
1934                let formal_len = params.len();
1935                let (mut param_names, slots) = Self::plan_function_params(params)?;
1936                let simple_slots = simple_fn_slots(params, rest_param.is_some(), |pset| {
1937                    stmt_is_param_only(body, pset)
1938                });
1939                // Capture-aware general slot-based locals when the simple param-only fast path doesn't
1940                // apply. Gated by `TISH_VM_SLOTS` (off ⇒ None ⇒ byte-identical). Frame size is known only
1941                // AFTER compilation (slots are allocated as the body declares locals) → set on the chunk below.
1942                let captured = if simple_slots.is_none() {
1943                    slot_analyze(params, rest_param.is_some(), body)
1944                } else {
1945                    None
1946                };
1947                let mut inner = Chunk::new();
1948                inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
1949                if let Some(rp) = rest_param {
1950                    param_names.push(Arc::clone(&rp.name));
1951                    inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
1952                }
1953                for p in &param_names {
1954                    inner.add_name(Arc::clone(p));
1955                }
1956                inner.param_count = param_names.len() as u16;
1957                if simple_slots.is_some() {
1958                    inner.slot_based = true;
1959                    inner.num_slots = param_names.len() as u16;
1960                }
1961                // #187: stamp the chunk with its global name so the JIT can register it as a directly-
1962                // callable callee — but ONLY when it is a provably-stable top-level function. `self`
1963                // being the ROOT compiler (top-level) is indicated by `self.self_fn_name.is_none()` and
1964                // an empty `slot_scopes`; simplest sound check: the name is in `stable_globals` (which
1965                // by construction contains only the unique top-level `function name`).
1966                if self.stable_globals.contains(name) {
1967                    inner.global_name = Some(Arc::clone(name));
1968                }
1969                let mut inner_comp = Compiler::new(&mut inner, false);
1970                inner_comp.stable_globals = Arc::clone(&self.stable_globals);
1971                // Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
1972                // param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
1973                // compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
1974                // recursive call. Conservative `stmt_rebinds` errs toward NOT enabling (safe).
1975                if !params_bind_name(params, name.as_ref()) && !stmt_rebinds(body, name.as_ref()) {
1976                    inner_comp.self_fn_name = Some(Arc::clone(name));
1977                }
1978                let mut general_frame_slots: Option<u16> = None;
1979                if let Some(map) = simple_slots {
1980                    inner_comp.slot_ctx = Some(map);
1981                    inner_comp.emit_param_defaults_prologue(params)?;
1982                    inner_comp.compile_statement(body)?;
1983                } else if let Some(cap) = captured {
1984                    // Params (all uncaptured — gated) → slots 0..n (matching the VM's param binding);
1985                    // uncaptured body `let`s get fresh slots via the scope-aware allocator; captured
1986                    // locals stay name-based in `local_scope` (which closures capture).
1987                    inner_comp.general_slots = true;
1988                    inner_comp.slot_captured = cap;
1989                    inner_comp.slot_scopes.push(HashMap::new());
1990                    for p in &param_names {
1991                        inner_comp.declare_slot(p);
1992                    }
1993                    inner_comp.emit_param_defaults_prologue(params)?;
1994                    inner_comp.compile_statement(body)?;
1995                    general_frame_slots = Some(inner_comp.next_slot);
1996                } else {
1997                    inner_comp.scope = vec![param_names
1998                        .iter()
1999                        .map(|n| (Arc::clone(n), false))
2000                        .collect::<HashMap<_, _>>()];
2001                    inner_comp
2002                        .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2003                    inner_comp.emit_param_defaults_prologue(params)?;
2004                    inner_comp.compile_statement(body)?;
2005                }
2006                inner_comp.emit(Opcode::LoadConst);
2007                let idx = inner_comp.constant_idx(Constant::Null);
2008                inner_comp.chunk.write_u16(idx);
2009                inner_comp.emit(Opcode::Return);
2010                if let Some(n) = general_frame_slots {
2011                    inner_comp.chunk.slot_based = true;
2012                    inner_comp.chunk.num_slots = n;
2013                }
2014                let nested_idx = self.chunk.add_nested(inner);
2015                self.emit(Opcode::LoadConst);
2016                let idx = self.constant_idx(Constant::Closure(nested_idx));
2017                self.chunk.write_u16(idx);
2018                let idx = self.name_idx(name);
2019                self.emit_u16(Opcode::DeclareVar, idx);
2020                self.scope
2021                    .last_mut()
2022                    .unwrap()
2023                    .insert(Arc::clone(name), false);
2024            }
2025            Statement::DoWhile { body, cond, .. } => {
2026                let body_lets = Self::loop_body_block_lets(body);
2027                for n in &body_lets {
2028                    let idx = self.name_idx(n);
2029                    self.emit_u16(Opcode::LoopVarsBegin, idx);
2030                }
2031                let start = self.chunk.code.len();
2032                self.loop_stack.push(LoopInfo {
2033                    break_patches: Vec::new(),
2034                    continue_patches: Vec::new(),
2035                    // `continue` jumps to the condition test, which is emitted AFTER the body — a
2036                    // FORWARD jump. (A backward JumpBack would patch to dist = 0 via saturating_sub
2037                    // of a forward target, becoming a no-op; execution then fell through and re-ran
2038                    // the body's already-unwound ExitBlock on an empty block stack — the
2039                    // "ExitBlock without matching EnterBlock" crash.)
2040                    continue_is_forward_jump: true,
2041                });
2042                self.breakable_stack.push(Breakable::Loop {
2043                    unwind_depth: self.block_depth,
2044                });
2045                self.compile_statement(body)?;
2046                let cond_start = self.chunk.code.len();
2047                // Condition-position lowering: a false condition exits to `end`, a true one falls
2048                // through to the JumpBack — JIT-eligible, no value left on the stack (#167).
2049                let exit_sites = self.compile_condition_jump_if_false(cond)?;
2050                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
2051                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
2052                let end = self.chunk.code.len();
2053                for site in &exit_sites {
2054                    self.patch_jump(*site, end);
2055                }
2056                let info = self.loop_stack.pop().unwrap();
2057                self.breakable_stack.pop();
2058                for p in info.continue_patches {
2059                    // Forward jump to the condition (see continue_is_forward_jump above).
2060                    self.patch_jump(p, cond_start);
2061                }
2062                for p in info.break_patches {
2063                    self.patch_jump(p, end);
2064                }
2065                for _ in &body_lets {
2066                    self.emit(Opcode::LoopVarsEnd);
2067                }
2068            }
2069            Statement::Switch {
2070                expr,
2071                cases,
2072                default_body,
2073                ..
2074            } => {
2075                let switch_unwind_depth = self.block_depth;
2076                self.switch_stack.push(SwitchInfo {
2077                    break_patches: Vec::new(),
2078                });
2079                self.breakable_stack.push(Breakable::Switch {
2080                    unwind_depth: switch_unwind_depth,
2081                });
2082                self.compile_expr(expr)?;
2083                self.emit(Opcode::Dup);
2084                let mut end_patches = Vec::new();
2085                for (case_expr, case_body) in cases {
2086                    self.emit(Opcode::Dup);
2087                    if let Some(ce) = case_expr {
2088                        self.compile_expr(ce)?;
2089                        self.emit_u8(Opcode::BinOp, 8);
2090                        let jump_next = self.emit_jump(Opcode::JumpIfFalse);
2091                        // JumpIfFalse already pops the match result when taking this case
2092                        self.compile_statement(&Statement::Block {
2093                            statements: case_body.clone(),
2094                            span: Span {
2095                                start: (0, 0),
2096                                end: (0, 0),
2097                            },
2098                        })?;
2099                        let jump_end = self.emit_jump(Opcode::Jump);
2100                        end_patches.push(jump_end);
2101                        self.patch_jump(jump_next, self.chunk.code.len());
2102                    } else {
2103                        self.emit(Opcode::Pop);
2104                        self.compile_statement(&Statement::Block {
2105                            statements: case_body.clone(),
2106                            span: Span {
2107                                start: (0, 0),
2108                                end: (0, 0),
2109                            },
2110                        })?;
2111                    }
2112                }
2113                if let Some(body) = default_body {
2114                    self.emit(Opcode::Pop);
2115                    self.compile_statement(&Statement::Block {
2116                        statements: body.clone(),
2117                        span: Span {
2118                            start: (0, 0),
2119                            end: (0, 0),
2120                        },
2121                    })?;
2122                } else {
2123                    self.emit(Opcode::Pop);
2124                }
2125                for p in end_patches {
2126                    self.patch_jump(p, self.chunk.code.len());
2127                }
2128                let sw = self.switch_stack.pop().unwrap();
2129                self.breakable_stack.pop();
2130                for p in sw.break_patches {
2131                    self.patch_jump(p, self.chunk.code.len());
2132                }
2133            }
2134            Statement::Throw { value, .. } => {
2135                self.compile_expr(value)?;
2136                self.emit(Opcode::Throw);
2137            }
2138            Statement::Try {
2139                body,
2140                catch_param,
2141                catch_body,
2142                finally_body,
2143                ..
2144            } => {
2145                let catch_offset_pos = self.chunk.code.len();
2146                self.emit(Opcode::EnterTry);
2147                self.chunk.write_u16(0);
2148                // A `return` inside the body/catch must run this finally on the way out.
2149                if let Some(f) = finally_body {
2150                    self.finally_stack.push((**f).clone());
2151                }
2152                self.compile_statement(body)?;
2153                self.emit(Opcode::ExitTry);
2154                let jump_over_catch = self.emit_jump(Opcode::Jump);
2155                let catch_start = self.chunk.code.len();
2156                if let Some(catch_stmt) = catch_body {
2157                    if let Some(param) = catch_param {
2158                        self.emit(Opcode::EnterBlock);
2159                        self.block_depth += 1;
2160                        self.enter_block_scope();
2161                        let param_idx = self.name_idx(param);
2162                        self.emit_u16(Opcode::DeclareVar, param_idx);
2163                        self.scope
2164                            .last_mut()
2165                            .unwrap()
2166                            .insert(Arc::clone(param), false);
2167                        self.compile_statement(catch_stmt)?;
2168                        self.exit_block_scope();
2169                        self.emit(Opcode::ExitBlock);
2170                        self.block_depth -= 1;
2171                    } else {
2172                        self.emit(Opcode::Pop);
2173                        self.compile_statement(catch_stmt)?;
2174                    }
2175                } else {
2176                    // No catch: run `finally` on the exception path, then re-raise (propagate).
2177                    if let Some(f) = finally_body {
2178                        self.compile_statement(f)?;
2179                    }
2180                    self.emit(Opcode::Throw);
2181                }
2182                let after_catch = self.chunk.code.len();
2183                self.patch_jump(jump_over_catch, after_catch);
2184                // The finally is no longer pending for enclosing returns once we emit its inline
2185                // (normal-path) copy below.
2186                if finally_body.is_some() {
2187                    self.finally_stack.pop();
2188                }
2189                if let Some(finally) = finally_body {
2190                    self.compile_statement(finally)?;
2191                }
2192                let catch_offset =
2193                    catch_start.wrapping_sub(catch_offset_pos).wrapping_sub(3) as u16;
2194                self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
2195                self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
2196            }
2197            Statement::Import { .. } => {
2198                return Err(CompileError {
2199                    message: "Import not supported in bytecode".to_string(),
2200                });
2201            }
2202            Statement::Export { declaration, .. } => match declaration.as_ref() {
2203                ExportDeclaration::Named(inner_stmt) => {
2204                    self.compile_statement(inner_stmt.as_ref())?;
2205                }
2206                ExportDeclaration::Default(_) => {
2207                    return Err(CompileError {
2208                        message: "export default is not supported in bytecode".to_string(),
2209                    });
2210                }
2211                ExportDeclaration::ReExport { .. } => {}
2212            },
2213            Statement::TypeAlias { .. }
2214            | Statement::DeclareVar { .. }
2215            | Statement::DeclareFun { .. } => {}
2216        }
2217        Ok(())
2218    }
2219
2220    fn compile_destructure(
2221        &mut self,
2222        pattern: &DestructPattern,
2223        mutable: bool,
2224        for_header_binding: bool,
2225    ) -> Result<(), CompileError> {
2226        let decl_op = if for_header_binding {
2227            Opcode::DeclareVarPlain
2228        } else {
2229            Opcode::DeclareVar
2230        };
2231        match pattern {
2232            DestructPattern::Array(elements) => {
2233                for (i, elem) in elements.iter().enumerate() {
2234                    match elem {
2235                        Some(DestructElement::Ident(name, _)) => {
2236                            self.emit(Opcode::Dup);
2237                            let idx = self.constant_idx(Constant::Number(i as f64));
2238                            self.emit(Opcode::LoadConst);
2239                            self.chunk.write_u16(idx);
2240                            self.emit(Opcode::GetIndex);
2241                            let idx = self.name_idx(name);
2242                            self.emit_u16(decl_op, idx);
2243                            self.scope
2244                                .last_mut()
2245                                .unwrap()
2246                                .insert(Arc::clone(name), false);
2247                        }
2248                        // Array hole `[a, , c]`: position is skipped, no binding emitted.
2249                        None => {}
2250                        // Nested pattern `[[a, b], c]` or `[{x}, y]`: push source[i] and recurse.
2251                        // compile_destructure is stack-balanced (consumes exactly the value it
2252                        // destructures), so the source array beneath stays intact.
2253                        Some(DestructElement::Pattern(sub)) => {
2254                            self.emit(Opcode::Dup);
2255                            let idx = self.constant_idx(Constant::Number(i as f64));
2256                            self.emit(Opcode::LoadConst);
2257                            self.chunk.write_u16(idx);
2258                            self.emit(Opcode::GetIndex);
2259                            self.compile_destructure(sub, mutable, for_header_binding)?;
2260                        }
2261                        // Rest `[a, ...rest]`: rest = source.slice(i). Use GetMember (not GetIndex)
2262                        // so the array's `slice` method resolves via get_member; GetIndex rejects
2263                        // string keys on arrays.
2264                        Some(DestructElement::Rest(name, _)) => {
2265                            self.emit(Opcode::Dup);
2266                            let slice_idx = self.name_idx(&Arc::from("slice"));
2267                            self.emit_u16(Opcode::GetMember, slice_idx);
2268                            let idx = self.constant_idx(Constant::Number(i as f64));
2269                            self.emit(Opcode::LoadConst);
2270                            self.chunk.write_u16(idx);
2271                            self.emit_u16(Opcode::Call, 1);
2272                            let nidx = self.name_idx(name);
2273                            self.emit_u16(decl_op, nidx);
2274                            self.scope
2275                                .last_mut()
2276                                .unwrap()
2277                                .insert(Arc::clone(name), false);
2278                        }
2279                    }
2280                }
2281                self.emit(Opcode::Pop);
2282            }
2283            DestructPattern::Object(props) => {
2284                for prop in props {
2285                    self.emit(Opcode::Dup);
2286                    let key_idx = self.constant_idx(Constant::String(Arc::clone(&prop.key)));
2287                    self.emit(Opcode::LoadConst);
2288                    self.chunk.write_u16(key_idx);
2289                    self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
2290                    match &prop.value {
2291                        DestructElement::Ident(name, _) => {
2292                            let idx = self.name_idx(name);
2293                            self.emit_u16(decl_op, idx);
2294                            if mutable {
2295                                self.scope
2296                                    .last_mut()
2297                                    .unwrap()
2298                                    .insert(Arc::clone(name), false);
2299                            }
2300                        }
2301                        // Nested value `{ outer: { inner } }` or `{ arr: [a, b] }`: obj[key] is
2302                        // already on the stack (GetIndex above); recurse to destructure it.
2303                        DestructElement::Pattern(sub) => {
2304                            self.compile_destructure(sub, mutable, for_header_binding)?;
2305                        }
2306                        // `{ ...rest }` needs the set of *remaining* keys; not yet supported.
2307                        DestructElement::Rest(_, _) => {
2308                            return Err(CompileError {
2309                                message: "Object rest destructuring not yet supported".to_string(),
2310                            });
2311                        }
2312                    }
2313                }
2314                self.emit(Opcode::Pop);
2315            }
2316        }
2317        Ok(())
2318    }
2319
2320    fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
2321        self.mark_line(expr.span());
2322        match expr {
2323            Expr::Literal { value, .. } => {
2324                let c = match value {
2325                    Literal::Number(n) => Constant::Number(*n),
2326                    Literal::String(s) => Constant::String(Arc::clone(s)),
2327                    Literal::Bool(b) => Constant::Bool(*b),
2328                    Literal::Null => Constant::Null,
2329                };
2330                let idx = self.constant_idx(c);
2331                self.emit(Opcode::LoadConst);
2332                self.chunk.write_u16(idx);
2333            }
2334            Expr::Ident { name, .. } => {
2335                // `resolve_slot` checks BOTH the simple param-only map and the general scope stack.
2336                self.emit_var_load(name);
2337            }
2338            Expr::Binary {
2339                left, op, right, ..
2340            } => {
2341                match op {
2342                    BinOp::And => {
2343                        // Short-circuit + value-returning (JS): `a && b` is `a` when `a` is falsy,
2344                        // else `b` — NOT a coerced boolean (#240). Mirror the `||` lowering below:
2345                        // keep the left operand on a falsy short-circuit, discard it and evaluate the
2346                        // right otherwise. (The old `BinOp(And)` here coerced a truthy-left result to
2347                        // a Bool, so `five() && 7` yielded `true` instead of `7`.)
2348                        self.compile_expr(left)?;
2349                        self.emit(Opcode::Dup);
2350                        let jump_end = self.emit_jump(Opcode::JumpIfFalse); // a falsy → keep a
2351                        self.emit(Opcode::Pop); // a truthy → discard a …
2352                        self.compile_expr(right)?; // … and yield b
2353                        self.patch_jump(jump_end, self.chunk.code.len());
2354                    }
2355                    BinOp::Or => {
2356                        // Short-circuit: a || b => if a then a else b
2357                        self.compile_expr(left)?;
2358                        self.emit(Opcode::Dup);
2359                        let jump_eval_right = self.emit_jump(Opcode::JumpIfFalse);
2360                        let jump_end = self.emit_jump(Opcode::Jump);
2361                        self.patch_jump(jump_eval_right, self.chunk.code.len());
2362                        self.emit(Opcode::Pop); // discard falsy left
2363                        self.compile_expr(right)?;
2364                        self.patch_jump(jump_end, self.chunk.code.len());
2365                    }
2366                    _ => {
2367                        self.compile_expr(left)?;
2368                        self.compile_expr(right)?;
2369                        self.emit_u8(Opcode::BinOp, binop_to_u8(*op));
2370                    }
2371                }
2372            }
2373            Expr::Unary { op, operand, .. } => {
2374                self.compile_expr(operand)?;
2375                self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
2376            }
2377            Expr::Call { callee, args, .. } => {
2378                // #186: `Math.<unaryfn>(arg)` → `<arg>; MathUnary(id)` when `Math` is provably the
2379                // global builtin (unshadowed program-wide) and there is exactly one argument. Lets the
2380                // numeric JIT lower the intrinsic (math_trig; a Math.* prereq for other kernels).
2381                if self.math_is_global && args.len() == 1 {
2382                    if let Expr::Member {
2383                        object,
2384                        prop: MemberProp::Name { name: fname, .. },
2385                        optional: false,
2386                        ..
2387                    } = callee.as_ref()
2388                    {
2389                        if let (Expr::Ident { name: obj, .. }, CallArg::Expr(arg)) =
2390                            (object.as_ref(), &args[0])
2391                        {
2392                            if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
2393                                if let Some(mfn) = MathUnaryFn::from_name(fname.as_ref()) {
2394                                    self.compile_expr(arg)?;
2395                                    self.emit_u16(Opcode::MathUnary, mfn as u16);
2396                                    return Ok(());
2397                                }
2398                            }
2399                        }
2400                    }
2401                }
2402                // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
2403                if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
2404                    && args.len() == 1
2405                    && matches!(args[0], CallArg::Expr(_))
2406                {
2407                    if let (
2408                        Expr::Member {
2409                            object,
2410                            prop: MemberProp::Name { name: key, .. },
2411                            optional: false,
2412                            ..
2413                        },
2414                        CallArg::Expr(cmp_expr),
2415                    ) = (callee.as_ref(), &args[0])
2416                    {
2417                        if key.as_ref() == "sort" {
2418                            if let Some(ascending) = Self::detect_numeric_sort_comparator(cmp_expr)
2419                            {
2420                                self.compile_expr(object)?;
2421                                self.emit_u8(
2422                                    Opcode::ArraySortNumeric,
2423                                    if ascending { 0 } else { 1 },
2424                                );
2425                                return Ok(());
2426                            }
2427                            if let Some((prop, ascending)) =
2428                                Self::detect_property_sort_comparator(cmp_expr)
2429                            {
2430                                self.compile_expr(object)?;
2431                                let prop_idx = self.constant_idx(Constant::String(prop));
2432                                self.emit(Opcode::ArraySortByProperty);
2433                                self.chunk.write_u16(prop_idx);
2434                                self.chunk.write_u16(if ascending { 0 } else { 1 });
2435                                return Ok(());
2436                            }
2437                        }
2438                        if key.as_ref() == "map" {
2439                            if let Some(simple) = Self::detect_simple_map_callback(cmp_expr) {
2440                                self.compile_expr(object)?;
2441                                match simple {
2442                                    SimpleMapResult::Identity => {
2443                                        self.emit(Opcode::ArrayMapIdentity);
2444                                    }
2445                                    SimpleMapResult::BinOp(op, c, param_left) => {
2446                                        let const_idx = self.constant_idx(c);
2447                                        self.emit(Opcode::ArrayMapBinOp);
2448                                        self.chunk.write_u8(binop_to_u8(op));
2449                                        self.chunk.write_u16(const_idx);
2450                                        self.chunk.write_u8(if param_left { 0 } else { 1 });
2451                                    }
2452                                }
2453                                return Ok(());
2454                            }
2455                        }
2456                        if key.as_ref() == "filter" {
2457                            if let Some((op, const_val, param_left)) =
2458                                Self::detect_simple_filter_callback(cmp_expr)
2459                            {
2460                                self.compile_expr(object)?;
2461                                let const_idx = self.constant_idx(const_val);
2462                                self.emit(Opcode::ArrayFilterBinOp);
2463                                self.chunk.write_u8(binop_to_u8(op));
2464                                self.chunk.write_u16(const_idx);
2465                                self.chunk.write_u8(if param_left { 0 } else { 1 });
2466                                return Ok(());
2467                            }
2468                        }
2469                    }
2470                }
2471                let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
2472                if has_spread {
2473                    // Build args array [a, ...b, c], then callee, then CallSpread
2474                    self.emit_u16(Opcode::NewArray, 0);
2475                    for arg in args {
2476                        match arg {
2477                            CallArg::Expr(e) => {
2478                                self.compile_expr(e)?;
2479                                self.emit_u16(Opcode::NewArray, 1);
2480                                self.emit(Opcode::ConcatArray);
2481                            }
2482                            CallArg::Spread(expr) => {
2483                                self.compile_expr(expr)?;
2484                                self.emit(Opcode::ConcatArray);
2485                            }
2486                        }
2487                    }
2488                    self.compile_expr(callee)?;
2489                    self.emit(Opcode::CallSpread);
2490                } else {
2491                    // Self-recursion fast path: `name(args)` where `name` is this function's own
2492                    // provably-stable binding → `SelfCall` (no callee LoadVar, no closure dispatch;
2493                    // the JIT lowers it to a native recursive call). `self_fn_name` is only `Some`
2494                    // when the compiler proved `name` isn't shadowed or rebound (see FunDecl).
2495                    let is_self_call = matches!(
2496                        callee.as_ref(),
2497                        Expr::Ident { name, .. } if self.self_fn_name.as_deref() == Some(name.as_ref())
2498                    );
2499                    if is_self_call {
2500                        for arg in args {
2501                            if let CallArg::Expr(e) = arg {
2502                                self.compile_expr(e)?;
2503                            }
2504                        }
2505                        self.emit_u16(Opcode::SelfCall, args.len() as u16);
2506                    } else {
2507                        self.compile_expr(callee)?;
2508                        for arg in args {
2509                            if let CallArg::Expr(e) = arg {
2510                                self.compile_expr(e)?;
2511                            }
2512                        }
2513                        self.emit_u16(Opcode::Call, args.len() as u16);
2514                    }
2515                }
2516            }
2517            Expr::Member {
2518                object,
2519                prop,
2520                optional,
2521                ..
2522            } => {
2523                self.compile_expr(object)?;
2524                if *optional {
2525                    self.emit(Opcode::Dup);
2526                    let null_idx = self.constant_idx(Constant::Null);
2527                    self.emit(Opcode::LoadConst);
2528                    self.chunk.write_u16(null_idx);
2529                    self.emit_u8(Opcode::BinOp, 8);
2530                    let jump_to_null = self.emit_jump(Opcode::JumpIfFalse);
2531                    let jump_to_get_instr = self.chunk.code.len();
2532                    let jump_to_get = self.emit_jump(Opcode::Jump);
2533                    self.patch_jump(jump_to_null, jump_to_get_instr);
2534                    self.emit(Opcode::Pop);
2535                    self.emit(Opcode::LoadConst);
2536                    self.chunk.write_u16(null_idx);
2537                    let jump_end = self.emit_jump(Opcode::Jump);
2538                    self.patch_jump(jump_to_get, self.chunk.code.len());
2539                    match prop {
2540                        MemberProp::Name { name: key, .. } => {
2541                            let idx = self.name_idx(key);
2542                            self.emit_u16(Opcode::GetMemberOptional, idx);
2543                        }
2544                        MemberProp::Expr(e) => {
2545                            self.compile_expr(e)?;
2546                            self.emit(Opcode::GetIndex);
2547                        }
2548                    }
2549                    self.patch_jump(jump_end, self.chunk.code.len());
2550                } else {
2551                    match prop {
2552                        MemberProp::Name { name: key, .. } => {
2553                            let idx = self.name_idx(key);
2554                            self.emit_u16(Opcode::GetMember, idx);
2555                        }
2556                        MemberProp::Expr(e) => {
2557                            self.compile_expr(e)?;
2558                            self.emit(Opcode::GetIndex);
2559                        }
2560                    }
2561                }
2562            }
2563            Expr::Index { object, index, .. } => {
2564                self.compile_expr(object)?;
2565                self.compile_expr(index)?;
2566                self.emit(Opcode::GetIndex);
2567            }
2568            Expr::Conditional {
2569                cond,
2570                then_branch,
2571                else_branch,
2572                ..
2573            } => {
2574                self.compile_expr(cond)?;
2575                let jump_else = self.emit_jump(Opcode::JumpIfFalse);
2576                // JumpIfFalse pops condition when taking then; when taking else it also pops
2577                self.compile_expr(then_branch)?;
2578                let jump_end = self.emit_jump(Opcode::Jump);
2579                self.patch_jump(jump_else, self.chunk.code.len());
2580                // no Pop: condition was already popped by JumpIfFalse
2581                self.compile_expr(else_branch)?;
2582                self.patch_jump(jump_end, self.chunk.code.len());
2583            }
2584            Expr::NullishCoalesce { left, right, .. } => {
2585                self.compile_expr(left)?;
2586                self.emit(Opcode::Dup);
2587                let idx = self.constant_idx(Constant::Null);
2588                self.emit(Opcode::LoadConst);
2589                self.chunk.write_u16(idx);
2590                self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictNe));
2591                let jump_to_right = self.emit_jump(Opcode::JumpIfFalse);
2592                let jump_end = self.emit_jump(Opcode::Jump);
2593                self.patch_jump(jump_to_right, self.chunk.code.len());
2594                self.emit(Opcode::Pop);
2595                self.compile_expr(right)?;
2596                self.patch_jump(jump_end, self.chunk.code.len());
2597            }
2598            Expr::Array { elements, .. } => {
2599                let has_spread = elements
2600                    .iter()
2601                    .any(|e| matches!(e, ArrayElement::Spread(_)));
2602                if has_spread {
2603                    // Build array incrementally: start with [], concat each element
2604                    self.emit_u16(Opcode::NewArray, 0);
2605                    for elem in elements {
2606                        match elem {
2607                            ArrayElement::Expr(e) => {
2608                                self.compile_expr(e)?;
2609                                self.emit_u16(Opcode::NewArray, 1);
2610                                self.emit(Opcode::ConcatArray);
2611                            }
2612                            ArrayElement::Spread(expr) => {
2613                                self.compile_expr(expr)?;
2614                                self.emit(Opcode::ConcatArray);
2615                            }
2616                        }
2617                    }
2618                } else {
2619                    for elem in elements {
2620                        if let ArrayElement::Expr(e) = elem {
2621                            self.compile_expr(e)?;
2622                        }
2623                    }
2624                    self.emit_u16(Opcode::NewArray, elements.len() as u16);
2625                }
2626            }
2627            Expr::Object { props, .. } => {
2628                let has_spread = props.iter().any(|p| matches!(p, ObjectProp::Spread(_)));
2629                if has_spread {
2630                    self.emit_u16(Opcode::NewObject, 0); // start with {}
2631                    for prop in props {
2632                        match prop {
2633                            ObjectProp::KeyValue(k, v, _) => {
2634                                let idx = self.constant_idx(Constant::String(Arc::clone(k)));
2635                                self.emit(Opcode::LoadConst);
2636                                self.chunk.write_u16(idx);
2637                                self.compile_expr(v)?;
2638                                self.emit_u16(Opcode::NewObject, 1);
2639                                self.emit(Opcode::MergeObject);
2640                            }
2641                            ObjectProp::Spread(expr) => {
2642                                self.compile_expr(expr)?;
2643                                self.emit(Opcode::MergeObject);
2644                            }
2645                        }
2646                    }
2647                } else {
2648                    for prop in props {
2649                        if let ObjectProp::KeyValue(k, v, _) = prop {
2650                            let idx = self.constant_idx(Constant::String(Arc::clone(k)));
2651                            self.emit(Opcode::LoadConst);
2652                            self.chunk.write_u16(idx);
2653                            self.compile_expr(v)?;
2654                        }
2655                    }
2656                    self.emit_u16(Opcode::NewObject, props.len() as u16);
2657                }
2658            }
2659            Expr::Assign { name, value, .. } => {
2660                self.compile_expr(value)?;
2661                self.emit_var_store(name);
2662                self.emit_var_load(name); // assign yields value
2663            }
2664            Expr::TypeOf { operand, .. } => {
2665                let typeof_idx = self.name_idx(&Arc::from("typeof"));
2666                self.emit_u16(Opcode::LoadGlobal, typeof_idx);
2667                self.compile_expr(operand)?;
2668                self.emit_u16(Opcode::Call, 1);
2669            }
2670            Expr::ArrowFunction { params, body, .. } => {
2671                let formal_len = params.len();
2672                let (param_names, slots) = Self::plan_function_params(params)?;
2673                let simple_slots = simple_fn_slots(params, false, |pset| match body {
2674                    ArrowBody::Expr(e) => expr_is_param_only(e, pset),
2675                    ArrowBody::Block(s) => stmt_is_param_only(s, pset),
2676                });
2677                let mut inner = Chunk::new();
2678                inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
2679                for p in &param_names {
2680                    inner.add_name(Arc::clone(p));
2681                }
2682                inner.param_count = param_names.len() as u16;
2683                if simple_slots.is_some() {
2684                    inner.slot_based = true;
2685                    inner.num_slots = param_names.len() as u16;
2686                }
2687                let mut inner_comp = Compiler::new(&mut inner, false);
2688                inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
2689                if let Some(map) = simple_slots {
2690                    inner_comp.slot_ctx = Some(map);
2691                } else {
2692                    inner_comp.scope = vec![param_names
2693                        .iter()
2694                        .map(|n| (Arc::clone(n), false))
2695                        .collect::<HashMap<_, _>>()];
2696                    inner_comp
2697                        .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2698                }
2699                inner_comp.emit_param_defaults_prologue(params)?;
2700                match body {
2701                    ArrowBody::Expr(e) => {
2702                        inner_comp.compile_expr(e)?;
2703                        inner_comp.emit(Opcode::Return);
2704                    }
2705                    ArrowBody::Block(s) => {
2706                        inner_comp.compile_statement(s)?;
2707                        let idx = inner_comp.constant_idx(Constant::Null);
2708                        inner_comp.emit(Opcode::LoadConst);
2709                        inner_comp.chunk.write_u16(idx);
2710                        inner_comp.emit(Opcode::Return);
2711                    }
2712                }
2713                let nested_idx = self.chunk.add_nested(inner);
2714                let idx = self.constant_idx(Constant::Closure(nested_idx));
2715                self.emit(Opcode::LoadConst);
2716                self.chunk.write_u16(idx);
2717            }
2718            Expr::TemplateLiteral { quasis, exprs, .. } => {
2719                if exprs.is_empty() {
2720                    let s = quasis[0].to_string();
2721                    let idx = self.constant_idx(Constant::String(Arc::from(s)));
2722                    self.emit(Opcode::LoadConst);
2723                    self.chunk.write_u16(idx);
2724                } else {
2725                    // Interleave quasis and exprs: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + quasi[n]
2726                    let first = quasis[0].to_string();
2727                    let idx = self.constant_idx(Constant::String(Arc::from(first)));
2728                    self.emit(Opcode::LoadConst);
2729                    self.chunk.write_u16(idx);
2730                    for (i, expr) in exprs.iter().enumerate() {
2731                        self.compile_expr(expr)?;
2732                        self.emit_u8(Opcode::BinOp, 0); // Add (string concat)
2733                        let quasi_s = quasis[i + 1].to_string();
2734                        let qidx = self.constant_idx(Constant::String(Arc::from(quasi_s)));
2735                        self.emit(Opcode::LoadConst);
2736                        self.chunk.write_u16(qidx);
2737                        self.emit_u8(Opcode::BinOp, 0); // Add
2738                    }
2739                }
2740            }
2741            Expr::PostfixInc { name, .. } => {
2742                let one = self.constant_idx(Constant::Number(1.0));
2743                self.emit_var_load(name);
2744                self.emit(Opcode::Dup);
2745                self.emit(Opcode::LoadConst);
2746                self.chunk.write_u16(one);
2747                self.emit_u8(Opcode::BinOp, 0);
2748                self.emit_var_store(name);
2749            }
2750            Expr::PostfixDec { name, .. } => {
2751                let one = self.constant_idx(Constant::Number(1.0));
2752                self.emit_var_load(name);
2753                self.emit(Opcode::Dup);
2754                self.emit(Opcode::LoadConst);
2755                self.chunk.write_u16(one);
2756                self.emit_u8(Opcode::BinOp, 1);
2757                self.emit_var_store(name);
2758            }
2759            Expr::PrefixInc { name, .. } => {
2760                let one = self.constant_idx(Constant::Number(1.0));
2761                self.emit_var_load(name);
2762                self.emit(Opcode::LoadConst);
2763                self.chunk.write_u16(one);
2764                self.emit_u8(Opcode::BinOp, 0);
2765                self.emit(Opcode::Dup);
2766                self.emit_var_store(name);
2767            }
2768            Expr::PrefixDec { name, .. } => {
2769                let one = self.constant_idx(Constant::Number(1.0));
2770                self.emit_var_load(name);
2771                self.emit(Opcode::LoadConst);
2772                self.chunk.write_u16(one);
2773                self.emit_u8(Opcode::BinOp, 1);
2774                self.emit(Opcode::Dup);
2775                self.emit_var_store(name);
2776            }
2777            Expr::CompoundAssign {
2778                name, op, value, ..
2779            } => {
2780                self.emit_var_load(name);
2781                self.compile_expr(value)?;
2782                self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
2783                self.emit(Opcode::Dup);
2784                self.emit_var_store(name);
2785            }
2786            Expr::MemberAssign {
2787                object,
2788                prop,
2789                value,
2790                ..
2791            } => {
2792                self.compile_expr(object)?;
2793                self.compile_expr(value)?;
2794                let idx = self.name_idx(prop);
2795                self.emit_u16(Opcode::SetMember, idx); // SetMember pops obj, val and pushes val back
2796            }
2797            Expr::IndexAssign {
2798                object,
2799                index,
2800                value,
2801                ..
2802            } => {
2803                self.compile_expr(object)?;
2804                self.compile_expr(index)?;
2805                self.compile_expr(value)?;
2806                self.emit(Opcode::Dup); // leave copy for assignment expression result
2807                self.emit(Opcode::SetIndex);
2808            }
2809            Expr::NativeModuleLoad {
2810                spec, export_name, ..
2811            } => {
2812                let spec_idx = self.constant_idx(Constant::String(Arc::clone(spec)));
2813                let export_idx = self.constant_idx(Constant::String(Arc::clone(export_name)));
2814                self.emit(Opcode::LoadNativeExport);
2815                self.chunk.write_u16(spec_idx);
2816                self.chunk.write_u16(export_idx);
2817            }
2818            Expr::JsxElement {
2819                tag,
2820                props,
2821                children,
2822                ..
2823            } => {
2824                self.compile_jsx_element(tag, props, children)?;
2825            }
2826            Expr::JsxFragment { children, .. } => {
2827                self.compile_jsx_fragment(children)?;
2828            }
2829            Expr::Await { operand, .. } => {
2830                // await expr => evaluate operand, then VM Opcode::AwaitPromise (throw on reject).
2831                self.compile_expr(operand)?;
2832                self.emit(Opcode::AwaitPromise);
2833            }
2834            Expr::Delete { target, .. } => {
2835                // `delete obj.prop` / `delete obj[key]` → push [obj, key], then DeleteIndex
2836                // pops both, removes the property, and pushes `true`. Deleting anything that
2837                // isn't a property reference is a no-op that still yields `true` (JS).
2838                match target.as_ref() {
2839                    Expr::Member {
2840                        object,
2841                        prop: MemberProp::Name { name, .. },
2842                        ..
2843                    } => {
2844                        self.compile_expr(object)?;
2845                        let idx = self.constant_idx(Constant::String(Arc::clone(name)));
2846                        self.emit(Opcode::LoadConst);
2847                        self.chunk.write_u16(idx);
2848                        self.emit(Opcode::DeleteIndex);
2849                    }
2850                    Expr::Member {
2851                        object,
2852                        prop: MemberProp::Expr(key),
2853                        ..
2854                    } => {
2855                        self.compile_expr(object)?;
2856                        self.compile_expr(key)?;
2857                        self.emit(Opcode::DeleteIndex);
2858                    }
2859                    Expr::Index { object, index, .. } => {
2860                        self.compile_expr(object)?;
2861                        self.compile_expr(index)?;
2862                        self.emit(Opcode::DeleteIndex);
2863                    }
2864                    _ => {
2865                        let idx = self.constant_idx(Constant::Bool(true));
2866                        self.emit(Opcode::LoadConst);
2867                        self.chunk.write_u16(idx);
2868                    }
2869                }
2870            }
2871            Expr::LogicalAssign {
2872                name, op, value, ..
2873            } => {
2874                match op {
2875                    LogicalAssignOp::OrOr => {
2876                        // ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
2877                        self.emit_var_load(name);
2878                        self.emit(Opcode::Dup);
2879                        let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
2880                        let j_end = self.emit_jump(Opcode::Jump);
2881                        self.patch_jump(j_rhs, self.chunk.code.len());
2882                        self.emit(Opcode::Pop);
2883                        self.compile_expr(value)?;
2884                        self.emit_var_store(name);
2885                        self.emit_var_load(name);
2886                        let end = self.chunk.code.len();
2887                        self.patch_jump(j_end, end);
2888                    }
2889                    LogicalAssignOp::AndAnd => {
2890                        // &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
2891                        self.emit_var_load(name);
2892                        self.emit(Opcode::Dup);
2893                        let j_short = self.emit_jump(Opcode::JumpIfFalse);
2894                        self.emit(Opcode::Pop);
2895                        self.compile_expr(value)?;
2896                        self.emit_var_store(name);
2897                        self.emit_var_load(name);
2898                        let j_end = self.emit_jump(Opcode::Jump);
2899                        let end = self.chunk.code.len();
2900                        self.patch_jump(j_short, end);
2901                        self.patch_jump(j_end, end);
2902                    }
2903                    LogicalAssignOp::Nullish => {
2904                        // ??= : assign only when current === null (matches interpreter)
2905                        let null_c = self.constant_idx(Constant::Null);
2906                        self.emit_var_load(name);
2907                        self.emit(Opcode::Dup);
2908                        self.emit(Opcode::LoadConst);
2909                        self.chunk.write_u16(null_c);
2910                        self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictEq));
2911                        let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
2912                        self.emit(Opcode::Pop);
2913                        self.compile_expr(value)?;
2914                        self.emit_var_store(name);
2915                        self.emit_var_load(name);
2916                        let j_end = self.emit_jump(Opcode::Jump);
2917                        let end = self.chunk.code.len();
2918                        self.patch_jump(j_not_null, end);
2919                        self.patch_jump(j_end, end);
2920                    }
2921                }
2922            }
2923            Expr::New { callee, args, .. } => {
2924                let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
2925                if has_spread {
2926                    self.emit_u16(Opcode::NewArray, 0);
2927                    for arg in args {
2928                        match arg {
2929                            CallArg::Expr(e) => {
2930                                self.compile_expr(e)?;
2931                                self.emit_u16(Opcode::NewArray, 1);
2932                                self.emit(Opcode::ConcatArray);
2933                            }
2934                            CallArg::Spread(expr) => {
2935                                self.compile_expr(expr)?;
2936                                self.emit(Opcode::ConcatArray);
2937                            }
2938                        }
2939                    }
2940                    self.compile_expr(callee)?;
2941                    self.emit(Opcode::ConstructSpread);
2942                } else {
2943                    self.compile_expr(callee)?;
2944                    for arg in args {
2945                        if let CallArg::Expr(e) = arg {
2946                            self.compile_expr(e)?;
2947                        }
2948                    }
2949                    self.emit_u16(Opcode::Construct, args.len() as u16);
2950                }
2951            }
2952        }
2953        Ok(())
2954    }
2955
2956    fn compile_jsx_element(
2957        &mut self,
2958        tag: &Arc<str>,
2959        props: &[JsxProp],
2960        children: &[JsxChild],
2961    ) -> Result<(), CompileError> {
2962        let h_idx = self.name_idx(&Arc::from("h"));
2963        self.emit_u16(Opcode::LoadGlobal, h_idx);
2964        let tag_str = tag.as_ref();
2965        let is_component = tag_str
2966            .chars()
2967            .next()
2968            .map(|c| c.is_uppercase())
2969            .unwrap_or(false);
2970        if is_component {
2971            let tag_idx = self.name_idx(tag);
2972            self.emit_u16(Opcode::LoadGlobal, tag_idx);
2973        } else {
2974            let tag_const = self.constant_idx(Constant::String(Arc::from(tag_str)));
2975            self.emit(Opcode::LoadConst);
2976            self.chunk.write_u16(tag_const);
2977        }
2978        self.compile_jsx_props(props)?;
2979        self.compile_jsx_children(children)?;
2980        self.emit_u16(Opcode::Call, 3);
2981        Ok(())
2982    }
2983
2984    fn compile_jsx_fragment(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
2985        let h_idx = self.name_idx(&Arc::from("h"));
2986        self.emit_u16(Opcode::LoadGlobal, h_idx);
2987        let fragment_idx = self.name_idx(&Arc::from("Fragment"));
2988        self.emit_u16(Opcode::LoadGlobal, fragment_idx);
2989        let null_idx = self.constant_idx(Constant::Null);
2990        self.emit(Opcode::LoadConst);
2991        self.chunk.write_u16(null_idx);
2992        self.compile_jsx_children(children)?;
2993        self.emit_u16(Opcode::Call, 3);
2994        Ok(())
2995    }
2996
2997    fn compile_jsx_props(&mut self, props: &[JsxProp]) -> Result<(), CompileError> {
2998        if props.is_empty() {
2999            let null_idx = self.constant_idx(Constant::Null);
3000            self.emit(Opcode::LoadConst);
3001            self.chunk.write_u16(null_idx);
3002            return Ok(());
3003        }
3004        let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
3005        if has_spread {
3006            self.emit_u16(Opcode::NewObject, 0);
3007            for prop in props {
3008                match prop {
3009                    JsxProp::Attr { name, value } => {
3010                        let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
3011                        self.emit(Opcode::LoadConst);
3012                        self.chunk.write_u16(key_idx);
3013                        match value {
3014                            JsxAttrValue::String(s) => {
3015                                let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
3016                                self.emit(Opcode::LoadConst);
3017                                self.chunk.write_u16(val_idx);
3018                            }
3019                            JsxAttrValue::Expr(e) => self.compile_expr(e)?,
3020                            JsxAttrValue::ImplicitTrue => {
3021                                let true_idx = self.constant_idx(Constant::Bool(true));
3022                                self.emit(Opcode::LoadConst);
3023                                self.chunk.write_u16(true_idx);
3024                            }
3025                        }
3026                        self.emit_u16(Opcode::NewObject, 1);
3027                        self.emit(Opcode::MergeObject);
3028                    }
3029                    JsxProp::Spread(expr) => {
3030                        self.compile_expr(expr)?;
3031                        self.emit(Opcode::MergeObject);
3032                    }
3033                }
3034            }
3035        } else {
3036            for prop in props {
3037                if let JsxProp::Attr { name, value } = prop {
3038                    let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
3039                    self.emit(Opcode::LoadConst);
3040                    self.chunk.write_u16(key_idx);
3041                    match value {
3042                        JsxAttrValue::String(s) => {
3043                            let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
3044                            self.emit(Opcode::LoadConst);
3045                            self.chunk.write_u16(val_idx);
3046                        }
3047                        JsxAttrValue::Expr(e) => self.compile_expr(e)?,
3048                        JsxAttrValue::ImplicitTrue => {
3049                            let true_idx = self.constant_idx(Constant::Bool(true));
3050                            self.emit(Opcode::LoadConst);
3051                            self.chunk.write_u16(true_idx);
3052                        }
3053                    }
3054                }
3055            }
3056            self.emit_u16(Opcode::NewObject, props.len() as u16);
3057        }
3058        Ok(())
3059    }
3060
3061    fn compile_jsx_children(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
3062        for child in children {
3063            match child {
3064                JsxChild::Text(s) => {
3065                    let idx = self.constant_idx(Constant::String(Arc::clone(s)));
3066                    self.emit(Opcode::LoadConst);
3067                    self.chunk.write_u16(idx);
3068                }
3069                JsxChild::Expr(e) => self.compile_expr(e)?,
3070            }
3071        }
3072        self.emit_u16(Opcode::NewArray, children.len() as u16);
3073        Ok(())
3074    }
3075}
3076
3077/// Compile a Tish program to bytecode (with peephole optimizations).
3078pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
3079    compile_internal(program, true, false, None)
3080}
3081
3082/// Compile, tagging the chunk with a source file path so runtime errors can report
3083/// `file:line` (issue #74). The line table is built during compilation and survives the
3084/// in-place peephole pass; it is not serialized.
3085pub fn compile_with_source(
3086    program: &Program,
3087    source: Option<std::sync::Arc<str>>,
3088) -> Result<Chunk, CompileError> {
3089    compile_internal(program, true, false, source)
3090}
3091
3092/// Compile without peephole optimizations (for --no-optimize).
3093pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
3094    compile_internal(program, false, false, None)
3095}
3096
3097/// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
3098pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
3099    compile_internal(program, true, true, None)
3100}
3101
3102/// Compile for REPL without peephole optimizations.
3103pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
3104    compile_internal(program, false, true, None)
3105}
3106
3107fn compile_internal(
3108    program: &Program,
3109    peephole: bool,
3110    retain_last_expr: bool,
3111    source: Option<std::sync::Arc<str>>,
3112) -> Result<Chunk, CompileError> {
3113    let mut chunk = Chunk::new();
3114    chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
3115    let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
3116    // #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
3117    // `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
3118    // `false` here only forgoes the optimization, never risks a miscompile.
3119    compiler.math_is_global = !program.statements.iter().any(|s| stmt_rebinds(s, "Math"));
3120    // #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
3121    // shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
3122    compiler.stable_globals = Arc::new(compute_stable_globals(program));
3123    compiler.compile_program(program)?;
3124    if peephole {
3125        crate::peephole::optimize(&mut chunk);
3126    }
3127    Ok(chunk)
3128}
3129
3130#[cfg(test)]
3131mod stable_globals_tests {
3132    use super::compute_stable_globals;
3133
3134    fn stable(src: &str) -> std::collections::HashSet<String> {
3135        let prog = tishlang_parser::parse(src).expect("parse");
3136        compute_stable_globals(&prog)
3137            .into_iter()
3138            .map(|n| n.to_string())
3139            .collect()
3140    }
3141
3142    /// #187 gate: the exact spectral_norm shape — five top-level functions, none reassigned — must ALL
3143    /// be provably stable (this is what lets `multiplyAv` directly call `evalA`). The naive
3144    /// `stmt_rebinds`-based scan would return the empty set here (it treats every `FunDecl` as a
3145    /// rebind), so this is the regression tripwire for the purpose-built analysis.
3146    #[test]
3147    fn all_unreassigned_top_level_fns_are_stable() {
3148        let s = stable(
3149            "function evalA(i, j) { return 1.0 / (i + j) }\n\
3150             function multiplyAv(n, v, av) { let i = 0; while (i < n) { av[i] = evalA(i, i) * v[i]; i = i + 1 } }\n\
3151             function spectralNorm(n) { multiplyAv(n, n, n); return n }\n",
3152        );
3153        assert!(
3154            s.contains("evalA"),
3155            "evalA (called by multiplyAv) must be stable"
3156        );
3157        assert!(s.contains("multiplyAv"));
3158        assert!(s.contains("spectralNorm"));
3159    }
3160
3161    /// A reassigned function is NOT stable (a direct call could hit a stale binding) — but a sibling
3162    /// that is never reassigned still is.
3163    #[test]
3164    fn reassigned_function_is_excluded() {
3165        let s = stable(
3166            "function f(x) { return x + 1 }\n\
3167             function g(x) { return f(x) * 2 }\n\
3168             f = (x) => x + 100\n",
3169        );
3170        assert!(!s.contains("f"), "f is reassigned → must NOT be stable");
3171        assert!(s.contains("g"), "g is never reassigned → stable");
3172    }
3173
3174    /// Reassignment/redeclaration/shadowing INSIDE another function body must disqualify the name —
3175    /// the whole-program walk has to recurse into every body (the whole point vs `stmt_rebinds`).
3176    #[test]
3177    fn cross_body_rebind_and_shadow_and_redecl_excluded() {
3178        // reassigned inside another function's body
3179        assert!(!stable(
3180            "function h(x) { return x }\n\
3181             function k() { h = 5; return h }\n"
3182        )
3183        .contains("h"));
3184        // shadowed by a param in another function
3185        assert!(!stable(
3186            "function p(x) { return x }\n\
3187             function q(p) { return p }\n"
3188        )
3189        .contains("p"));
3190        // declared twice at top level
3191        assert!(!stable("function d(x) { return x }\nfunction d(y) { return y }\n").contains("d"));
3192        // also bound by a top-level let
3193        assert!(!stable("function e(x) { return x }\nlet e = 3\n").contains("e"));
3194    }
3195
3196    /// A global reassigned inside a PARAMETER DEFAULT (of a function or arrow) must disqualify it — the
3197    /// default runs in the enclosing scope when the function is called, so a direct call would be stale.
3198    #[test]
3199    fn param_default_rebind_excluded() {
3200        // `foo` reassigned in another function's param default
3201        assert!(!stable(
3202            "function evil(x) { return x + 1000 }\n\
3203             function foo(x) { return x + 1 }\n\
3204             function resetFoo(y = (foo = evil)) { return y }\n"
3205        )
3206        .contains("foo"));
3207        // `bar` reassigned in an arrow's param default
3208        assert!(!stable(
3209            "function bar(x) { return x }\n\
3210             let f = (z = (bar = 5)) => z\n"
3211        )
3212        .contains("bar"));
3213        // a param default that does NOT touch the name leaves it stable
3214        assert!(
3215            stable("function ok(x) { return x }\nfunction u(a = 1) { return a }\n").contains("ok")
3216        );
3217    }
3218}