@tishlang/tish-format 1.0.12 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,2604 @@
1
+ //! AST to bytecode compiler.
2
+
3
+ use std::collections::{HashMap, HashSet};
4
+ use std::sync::Arc;
5
+
6
+ use tishlang_ast::{
7
+ ArrayElement, ArrowBody, BinOp, CallArg, DestructElement, DestructPattern, ExportDeclaration,
8
+ Expr, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp,
9
+ ObjectProp, Program, Span, Statement,
10
+ };
11
+
12
+ use crate::chunk::{Chunk, Constant};
13
+ use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
14
+ use crate::opcode::Opcode;
15
+
16
+ enum SimpleMapResult {
17
+ Identity,
18
+ BinOp(BinOp, Constant, bool), // op, constant, param_on_left
19
+ }
20
+
21
+ fn literal_to_constant(expr: &Expr) -> Option<Constant> {
22
+ if let Expr::Literal { value, .. } = expr {
23
+ Some(match value {
24
+ Literal::Number(n) => Constant::Number(*n),
25
+ Literal::String(s) => Constant::String(Arc::clone(s)),
26
+ Literal::Bool(b) => Constant::Bool(*b),
27
+ Literal::Null => Constant::Null,
28
+ })
29
+ } else {
30
+ None
31
+ }
32
+ }
33
+
34
+ #[derive(Debug)]
35
+ pub struct CompileError {
36
+ pub message: String,
37
+ }
38
+
39
+ impl std::fmt::Display for CompileError {
40
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41
+ write!(f, "{}", self.message)
42
+ }
43
+ }
44
+
45
+ impl std::error::Error for CompileError {}
46
+
47
+ /// Loop boundary for break/continue.
48
+ struct LoopInfo {
49
+ break_patches: Vec<usize>,
50
+ /// Operand positions for `continue`: either `JumpBack` (while / do-while / for-of) or `Jump`
51
+ /// (C-style `for`, where the update clause is emitted after the body).
52
+ continue_patches: Vec<usize>,
53
+ /// When true, [`Opcode::Jump`] placeholders in `continue_patches` are patched forward with
54
+ /// [`Self::patch_jump`]. When false, they are [`Opcode::JumpBack`] patched with
55
+ /// [`Self::patch_jump_back`].
56
+ continue_is_forward_jump: bool,
57
+ }
58
+
59
+ /// Switch boundary: break exits the switch.
60
+ struct SwitchInfo {
61
+ break_patches: Vec<usize>,
62
+ }
63
+
64
+ /// Innermost break/continue target for unwinding `EnterBlock` before a jump.
65
+ #[derive(Clone, Copy)]
66
+ enum Breakable {
67
+ /// `usize` = `block_depth` before the loop body (same as Continue unwind target).
68
+ Loop { unwind_depth: usize },
69
+ /// `usize` = `block_depth` before the switch statement.
70
+ Switch { unwind_depth: usize },
71
+ }
72
+
73
+ struct Compiler<'a> {
74
+ chunk: &'a mut Chunk,
75
+ /// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
76
+ scope: Vec<HashMap<Arc<str>, bool>>,
77
+ /// Stack of loop info for break/continue.
78
+ loop_stack: Vec<LoopInfo>,
79
+ switch_stack: Vec<SwitchInfo>,
80
+ /// Parallel to nested loops/switches: innermost target for break/continue block unwind.
81
+ breakable_stack: Vec<Breakable>,
82
+ /// Nesting depth of emitted `EnterBlock` (lexical blocks) not yet closed on the compile path.
83
+ block_depth: usize,
84
+ /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
85
+ retain_last_expr: bool,
86
+ /// When `Some`, this chunk is being compiled in the SIMPLE slot mode: identifier references
87
+ /// resolve to frame slots (`LoadLocal`) via this param→slot map instead of name-keyed `LoadVar`.
88
+ /// Set only for self-contained param-only functions (see [`simple_fn_slots`]).
89
+ slot_ctx: Option<HashMap<Arc<str>, u16>>,
90
+ /// GENERAL slot mode (`TISH_VM_SLOTS`, capture-aware): a block-scoped stack of name→slot maps,
91
+ /// innermost last. Allocated DURING compilation so block scoping + shadowing are correct (each
92
+ /// `let` gets a fresh slot; resolution walks innermost-first; a block pops its frame). Empty unless
93
+ /// [`general_slots`] is set. Captured names (in [`slot_captured`]) are never allocated here — they
94
+ /// stay name-based in `local_scope` (which closures capture).
95
+ slot_scopes: Vec<HashMap<Arc<str>, u16>>,
96
+ /// Names referenced by a nested closure (over-approx) → must stay name-based even in slot mode.
97
+ slot_captured: HashSet<Arc<str>>,
98
+ /// Monotonic slot allocator for [`slot_scopes`] (never reclaimed; final value = frame size).
99
+ next_slot: u16,
100
+ /// True while compiling a chunk in general slot mode (see [`slot_scopes`]).
101
+ general_slots: bool,
102
+ /// Active `finally` bodies of enclosing `try`s in the CURRENT function (innermost last). A
103
+ /// `return` that escapes these trys must run each one on the way out (the bytecode VM jumps
104
+ /// straight to the function return otherwise). Reset per function — nested fns get a fresh
105
+ /// `Compiler`. The exception-unwind path is handled separately in the `Try` emitter.
106
+ finally_stack: Vec<Statement>,
107
+ /// When `Some(name)`, this chunk is the body of `fn name(...)` and `name`'s binding is provably
108
+ /// stable (no param shadows it, no reassignment/redeclaration in the body — see [`stmt_rebinds`]).
109
+ /// A direct call `name(args)` then compiles to `SelfCall` (no name lookup / closure dispatch; the
110
+ /// JIT lowers it to a native recursive call). `None` for anonymous fns, top-level, or anywhere the
111
+ /// self-binding can't be proven stable.
112
+ self_fn_name: Option<Arc<str>>,
113
+ }
114
+
115
+ /// Does `e` reference only the given params (no free/global vars, no nested
116
+ /// functions, no mutation)? Such a function can run on a bare slot frame.
117
+ fn expr_is_param_only(e: &Expr, params: &HashSet<&str>) -> bool {
118
+ match e {
119
+ Expr::Literal { .. } => true,
120
+ Expr::Ident { name, .. } => params.contains(name.as_ref()),
121
+ Expr::Binary { left, right, .. } => {
122
+ expr_is_param_only(left, params) && expr_is_param_only(right, params)
123
+ }
124
+ Expr::Unary { operand, .. } => expr_is_param_only(operand, params),
125
+ Expr::Call { callee, args, .. } => {
126
+ expr_is_param_only(callee, params)
127
+ && args.iter().all(|a| match a {
128
+ CallArg::Expr(x) => expr_is_param_only(x, params),
129
+ CallArg::Spread(_) => false,
130
+ })
131
+ }
132
+ Expr::Member { object, prop, .. } => {
133
+ expr_is_param_only(object, params)
134
+ && match prop {
135
+ MemberProp::Name { .. } => true,
136
+ MemberProp::Expr(x) => expr_is_param_only(x, params),
137
+ }
138
+ }
139
+ Expr::Index { object, index, .. } => {
140
+ expr_is_param_only(object, params) && expr_is_param_only(index, params)
141
+ }
142
+ Expr::Conditional {
143
+ cond,
144
+ then_branch,
145
+ else_branch,
146
+ ..
147
+ } => {
148
+ expr_is_param_only(cond, params)
149
+ && expr_is_param_only(then_branch, params)
150
+ && expr_is_param_only(else_branch, params)
151
+ }
152
+ Expr::NullishCoalesce { left, right, .. } => {
153
+ expr_is_param_only(left, params) && expr_is_param_only(right, params)
154
+ }
155
+ Expr::Array { elements, .. } => elements.iter().all(|el| match el {
156
+ ArrayElement::Expr(x) => expr_is_param_only(x, params),
157
+ ArrayElement::Spread(_) => false,
158
+ }),
159
+ Expr::Object { props, .. } => props.iter().all(|p| match p {
160
+ ObjectProp::KeyValue(_, x) => expr_is_param_only(x, params),
161
+ ObjectProp::Spread(_) => false,
162
+ }),
163
+ Expr::TemplateLiteral { exprs, .. } => {
164
+ exprs.iter().all(|x| expr_is_param_only(x, params))
165
+ }
166
+ Expr::TypeOf { operand, .. } => expr_is_param_only(operand, params),
167
+ // Mutation, nested fns, async, jsx, native, `new` — not eligible.
168
+ _ => false,
169
+ }
170
+ }
171
+
172
+ /// Statement form of [`expr_is_param_only`]. Only the small set of statements a
173
+ /// pure leaf function body uses is allowed; anything that declares a binding or
174
+ /// loops bails (those keep the name-based path).
175
+ fn stmt_is_param_only(s: &Statement, params: &HashSet<&str>) -> bool {
176
+ match s {
177
+ Statement::Block { statements, .. } => {
178
+ statements.iter().all(|st| stmt_is_param_only(st, params))
179
+ }
180
+ Statement::Multi { statements, .. } => {
181
+ statements.iter().all(|st| stmt_is_param_only(st, params))
182
+ }
183
+ Statement::ExprStmt { expr, .. } => expr_is_param_only(expr, params),
184
+ Statement::Return { value, .. } => {
185
+ value.as_ref().is_none_or(|e| expr_is_param_only(e, params))
186
+ }
187
+ Statement::If {
188
+ cond,
189
+ then_branch,
190
+ else_branch,
191
+ ..
192
+ } => {
193
+ expr_is_param_only(cond, params)
194
+ && stmt_is_param_only(then_branch, params)
195
+ && else_branch
196
+ .as_ref()
197
+ .is_none_or(|b| stmt_is_param_only(b, params))
198
+ }
199
+ _ => false,
200
+ }
201
+ }
202
+
203
+ /// If a function with these `params` (all simple, no rest) has a body that
204
+ /// references only its params, returns the param→slot map for slot-based
205
+ /// compilation. Slots are the parameter positions (0-based). Returns `None`
206
+ /// when the function must use the name-based path (captures outer scope,
207
+ /// declares locals, mutates, or defines nested functions).
208
+ fn simple_fn_slots(
209
+ params: &[FunParam],
210
+ has_rest: bool,
211
+ body_ok: impl FnOnce(&HashSet<&str>) -> bool,
212
+ ) -> Option<HashMap<Arc<str>, u16>> {
213
+ if has_rest {
214
+ return None;
215
+ }
216
+ let mut map: HashMap<Arc<str>, u16> = HashMap::with_capacity(params.len());
217
+ for (i, p) in params.iter().enumerate() {
218
+ match p {
219
+ FunParam::Simple(tp) => {
220
+ map.insert(Arc::clone(&tp.name), i as u16);
221
+ }
222
+ FunParam::Destructure { .. } => return None,
223
+ }
224
+ }
225
+ let pset: HashSet<&str> = map.keys().map(|k| k.as_ref()).collect();
226
+ if body_ok(&pset) {
227
+ Some(map)
228
+ } else {
229
+ None
230
+ }
231
+ }
232
+
233
+ /// Capture-aware general slot-based locals (params + uncaptured body/top-level `let`s → frame slots).
234
+ /// **Default ON** — validated across the full cross-backend suite + the compute micros (−22..27%) +
235
+ /// the `main.tish` bundle (−22%). Set `TISH_VM_SLOTS=0` to disable (name-based, the old path).
236
+ fn slots_enabled() -> bool {
237
+ std::env::var("TISH_VM_SLOTS").map(|v| v != "0").unwrap_or(true)
238
+ }
239
+
240
+ /// Is `name` bound by one of `params` (so it would shadow a function's own name)? Conservative:
241
+ /// any destructuring param returns `true` (it could bind `name` via a nested pattern we don't analyze).
242
+ fn params_bind_name(params: &[FunParam], name: &str) -> bool {
243
+ params.iter().any(|p| match p {
244
+ FunParam::Simple(tp) => tp.name.as_ref() == name,
245
+ FunParam::Destructure { .. } => true,
246
+ })
247
+ }
248
+
249
+ /// Conservative scan: does `name` get REBOUND (assigned `=`, `+=`, `??=`, `++`/`--`, or re-declared
250
+ /// via `let`/`for-of`) anywhere in `s`? Returns `true` on a rebind OR on any node it can't fully
251
+ /// analyze. Used to decide whether `fn NAME`'s body may emit `SelfCall` for `NAME(...)`: only when
252
+ /// NAME's binding is PROVABLY stable throughout the body, because a wrong `SelfCall` would call the
253
+ /// original chunk after a reassignment — a silent miscompile. Erring toward `true` only costs the
254
+ /// optimization, never correctness.
255
+ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
256
+ match s {
257
+ Statement::Block { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
258
+ Statement::Multi { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
259
+ Statement::VarDecl { name: n, init, .. } => {
260
+ n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
261
+ }
262
+ Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
263
+ Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
264
+ Statement::Throw { value, .. } => expr_rebinds(value, name),
265
+ Statement::If { cond, then_branch, else_branch, .. } => {
266
+ expr_rebinds(cond, name)
267
+ || stmt_rebinds(then_branch, name)
268
+ || else_branch.as_ref().is_some_and(|s| stmt_rebinds(s, name))
269
+ }
270
+ Statement::While { cond, body, .. } => expr_rebinds(cond, name) || stmt_rebinds(body, name),
271
+ Statement::DoWhile { body, cond, .. } => stmt_rebinds(body, name) || expr_rebinds(cond, name),
272
+ Statement::For { init, cond, update, body, .. } => {
273
+ init.as_ref().is_some_and(|s| stmt_rebinds(s, name))
274
+ || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
275
+ || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
276
+ || stmt_rebinds(body, name)
277
+ }
278
+ Statement::ForOf { name: n, iterable, body, .. } => {
279
+ n.as_ref() == name || expr_rebinds(iterable, name) || stmt_rebinds(body, name)
280
+ }
281
+ Statement::Switch { expr, cases, default_body, .. } => {
282
+ expr_rebinds(expr, name)
283
+ || cases.iter().any(|(t, body)| {
284
+ t.as_ref().is_some_and(|e| expr_rebinds(e, name))
285
+ || body.iter().any(|s| stmt_rebinds(s, name))
286
+ })
287
+ || default_body
288
+ .as_ref()
289
+ .is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
290
+ }
291
+ Statement::Try { body, catch_body, finally_body, .. } => {
292
+ stmt_rebinds(body, name)
293
+ || catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
294
+ || finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
295
+ }
296
+ Statement::Break { .. } | Statement::Continue { .. } => false,
297
+ // VarDeclDestructure (could bind `name`), FunDecl (could shadow), and any unknown construct
298
+ // → conservative: assume it may rebind `name`.
299
+ _ => true,
300
+ }
301
+ }
302
+
303
+ /// Expression half of [`stmt_rebinds`]. `true` if `name` is an assignment/update target, or unknown.
304
+ fn expr_rebinds(e: &Expr, name: &str) -> bool {
305
+ match e {
306
+ Expr::Assign { name: n, value, .. }
307
+ | Expr::CompoundAssign { name: n, value, .. }
308
+ | Expr::LogicalAssign { name: n, value, .. } => n.as_ref() == name || expr_rebinds(value, name),
309
+ Expr::PostfixInc { name: n, .. }
310
+ | Expr::PostfixDec { name: n, .. }
311
+ | Expr::PrefixInc { name: n, .. }
312
+ | Expr::PrefixDec { name: n, .. } => n.as_ref() == name,
313
+ Expr::Literal { .. } | Expr::Ident { .. } => false,
314
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
315
+ expr_rebinds(left, name) || expr_rebinds(right, name)
316
+ }
317
+ Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
318
+ expr_rebinds(operand, name)
319
+ }
320
+ Expr::Conditional { cond, then_branch, else_branch, .. } => {
321
+ expr_rebinds(cond, name) || expr_rebinds(then_branch, name) || expr_rebinds(else_branch, name)
322
+ }
323
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
324
+ expr_rebinds(callee, name)
325
+ || args.iter().any(|a| match a {
326
+ CallArg::Expr(e) | CallArg::Spread(e) => expr_rebinds(e, name),
327
+ })
328
+ }
329
+ Expr::Member { object, .. } => expr_rebinds(object, name),
330
+ Expr::Index { object, index, .. } => expr_rebinds(object, name) || expr_rebinds(index, name),
331
+ Expr::Array { elements, .. } => elements.iter().any(|el| match el {
332
+ ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
333
+ }),
334
+ Expr::Object { props, .. } => props.iter().any(|p| match p {
335
+ ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_rebinds(e, name),
336
+ }),
337
+ Expr::MemberAssign { object, value, .. } => expr_rebinds(object, name) || expr_rebinds(value, name),
338
+ Expr::IndexAssign { object, index, value, .. } => {
339
+ expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name)
340
+ }
341
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(|e| expr_rebinds(e, name)),
342
+ // A nested closure could reassign the outer `name`; recurse (over-conservative if it shadows,
343
+ // which only costs the optimization).
344
+ Expr::ArrowFunction { body, .. } => match body {
345
+ ArrowBody::Expr(e) => expr_rebinds(e, name),
346
+ ArrowBody::Block(s) => stmt_rebinds(s, name),
347
+ },
348
+ // Jsx, NativeModuleLoad, and anything unknown → conservative.
349
+ _ => true,
350
+ }
351
+ }
352
+
353
+ /// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
354
+ /// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
355
+ /// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
356
+ /// captured local must stay name-based in `local_scope` (which closures capture); only uncaptured
357
+ /// locals are slotted. Recurses ALL ordinary control flow (so it finds every closure → the capture
358
+ /// set is complete); returns `false` ONLY on ambient/module constructs it cannot traverse, so the
359
+ /// caller leaves the whole chunk name-based (safe default-bail: a missed closure could otherwise let a
360
+ /// captured local be wrongly slotted). Slot ALLOCATION happens during compilation (scope-aware), not
361
+ /// here — so block scoping + shadowing are handled by the slot-scope stack, not a flat map.
362
+ #[derive(Default)]
363
+ struct SlotScan {
364
+ captured: HashSet<Arc<str>>,
365
+ }
366
+
367
+ impl SlotScan {
368
+ fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
369
+ match s {
370
+ Statement::Block { statements, .. } => statements.iter().all(|s| self.stmt(s, in_closure)),
371
+ Statement::Multi { statements, .. } => statements.iter().all(|s| self.stmt(s, in_closure)),
372
+ Statement::VarDecl { init, .. } => init.as_ref().is_none_or(|e| self.expr(e, in_closure)),
373
+ Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
374
+ Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
375
+ Statement::If { cond, then_branch, else_branch, .. } => {
376
+ self.expr(cond, in_closure)
377
+ && self.stmt(then_branch, in_closure)
378
+ && else_branch.as_ref().is_none_or(|s| self.stmt(s, in_closure))
379
+ }
380
+ Statement::While { cond, body, .. } => self.expr(cond, in_closure) && self.stmt(body, in_closure),
381
+ Statement::DoWhile { body, cond, .. } => self.stmt(body, in_closure) && self.expr(cond, in_closure),
382
+ Statement::For { init, cond, update, body, .. } => {
383
+ init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
384
+ && cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
385
+ && update.as_ref().is_none_or(|e| self.expr(e, in_closure))
386
+ && self.stmt(body, in_closure)
387
+ }
388
+ Statement::ForOf { iterable, body, .. } => {
389
+ self.expr(iterable, in_closure) && self.stmt(body, in_closure)
390
+ }
391
+ Statement::Return { value, .. } => value.as_ref().is_none_or(|e| self.expr(e, in_closure)),
392
+ Statement::Throw { value, .. } => self.expr(value, in_closure),
393
+ Statement::Break { .. } | Statement::Continue { .. } => true,
394
+ Statement::Switch { expr, cases, default_body, .. } => {
395
+ if !self.expr(expr, in_closure) {
396
+ return false;
397
+ }
398
+ for (test, body) in cases {
399
+ if let Some(t) = test {
400
+ if !self.expr(t, in_closure) {
401
+ return false;
402
+ }
403
+ }
404
+ if !body.iter().all(|s| self.stmt(s, in_closure)) {
405
+ return false;
406
+ }
407
+ }
408
+ default_body
409
+ .as_ref()
410
+ .is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
411
+ }
412
+ Statement::Try { body, catch_body, finally_body, .. } => {
413
+ self.stmt(body, in_closure)
414
+ && catch_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
415
+ && finally_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
416
+ }
417
+ // A nested named function: its param defaults (enclosing-scope) + whole body capture.
418
+ Statement::FunDecl { params, body, .. } => {
419
+ self.scan_closure_param_defaults(params) && self.stmt(body, true)
420
+ }
421
+ // Ambient/module constructs (Import/Export/TypeAlias/DeclareVar/DeclareFun) → bail.
422
+ _ => false,
423
+ }
424
+ }
425
+
426
+ fn expr(&mut self, e: &Expr, in_closure: bool) -> bool {
427
+ match e {
428
+ Expr::Literal { .. } => true,
429
+ Expr::Ident { name, .. } => {
430
+ if in_closure {
431
+ self.captured.insert(Arc::clone(name));
432
+ }
433
+ true
434
+ }
435
+ Expr::Binary { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
436
+ Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
437
+ self.expr(operand, in_closure)
438
+ }
439
+ Expr::Conditional { cond, then_branch, else_branch, .. } => {
440
+ self.expr(cond, in_closure) && self.expr(then_branch, in_closure) && self.expr(else_branch, in_closure)
441
+ }
442
+ Expr::NullishCoalesce { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
443
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
444
+ self.expr(callee, in_closure)
445
+ && args.iter().all(|a| match a {
446
+ CallArg::Expr(e) | CallArg::Spread(e) => self.expr(e, in_closure),
447
+ })
448
+ }
449
+ Expr::Member { object, .. } => self.expr(object, in_closure),
450
+ Expr::Index { object, index, .. } => self.expr(object, in_closure) && self.expr(index, in_closure),
451
+ Expr::Array { elements, .. } => elements.iter().all(|el| match el {
452
+ ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
453
+ }),
454
+ Expr::Object { props, .. } => props.iter().all(|p| match p {
455
+ ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => self.expr(e, in_closure),
456
+ }),
457
+ Expr::Assign { name, value, .. }
458
+ | Expr::CompoundAssign { name, value, .. }
459
+ | Expr::LogicalAssign { name, value, .. } => {
460
+ if in_closure {
461
+ self.captured.insert(Arc::clone(name));
462
+ }
463
+ self.expr(value, in_closure)
464
+ }
465
+ Expr::MemberAssign { object, value, .. } => self.expr(object, in_closure) && self.expr(value, in_closure),
466
+ Expr::IndexAssign { object, index, value, .. } => {
467
+ self.expr(object, in_closure) && self.expr(index, in_closure) && self.expr(value, in_closure)
468
+ }
469
+ Expr::PostfixInc { name, .. }
470
+ | Expr::PostfixDec { name, .. }
471
+ | Expr::PrefixInc { name, .. }
472
+ | Expr::PrefixDec { name, .. } => {
473
+ if in_closure {
474
+ self.captured.insert(Arc::clone(name));
475
+ }
476
+ true
477
+ }
478
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().all(|e| self.expr(e, in_closure)),
479
+ Expr::ArrowFunction { params, body, .. } => {
480
+ if !self.scan_closure_param_defaults(params) {
481
+ return false;
482
+ }
483
+ match body {
484
+ ArrowBody::Expr(e) => self.expr(e, true),
485
+ ArrowBody::Block(s) => self.stmt(s, true),
486
+ }
487
+ }
488
+ // Jsx, NativeModuleLoad → bail.
489
+ _ => false,
490
+ }
491
+ }
492
+
493
+ /// A nested closure's parameter default expressions evaluate in the ENCLOSING scope → captured.
494
+ fn scan_closure_param_defaults(&mut self, params: &[FunParam]) -> bool {
495
+ for p in params {
496
+ let default = match p {
497
+ FunParam::Simple(tp) => &tp.default,
498
+ FunParam::Destructure { default, .. } => default,
499
+ };
500
+ if let Some(d) = default {
501
+ if !self.expr(d, true) {
502
+ return false;
503
+ }
504
+ }
505
+ }
506
+ true
507
+ }
508
+ }
509
+
510
+ /// Capture-aware eligibility for general slot-based locals in a FUNCTION. Returns the captured-name set
511
+ /// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
512
+ /// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
513
+ /// (the VM binds params into slots 0..n, but a closure reads captures by name from `local_scope`).
514
+ fn slot_analyze(params: &[FunParam], has_rest: bool, body: &Statement) -> Option<HashSet<Arc<str>>> {
515
+ if !slots_enabled() || has_rest {
516
+ return None;
517
+ }
518
+ for p in params {
519
+ if let FunParam::Destructure { .. } = p {
520
+ return None;
521
+ }
522
+ }
523
+ let mut scan = SlotScan::default();
524
+ if !scan.stmt(body, false) {
525
+ return None;
526
+ }
527
+ for p in params {
528
+ if let FunParam::Simple(tp) = p {
529
+ if scan.captured.contains(&tp.name) {
530
+ return None;
531
+ }
532
+ }
533
+ }
534
+ Some(scan.captured)
535
+ }
536
+
537
+ /// Same, for the TOP-LEVEL program (no params). Caller must additionally ensure non-REPL mode.
538
+ fn slot_analyze_toplevel(statements: &[Statement]) -> Option<HashSet<Arc<str>>> {
539
+ if !slots_enabled() {
540
+ return None;
541
+ }
542
+ let mut scan = SlotScan::default();
543
+ for s in statements {
544
+ if !scan.stmt(s, false) {
545
+ return None;
546
+ }
547
+ }
548
+ Some(scan.captured)
549
+ }
550
+
551
+ impl<'a> Compiler<'a> {
552
+ /// Resolve a name to its frame slot. `None` ⇒ name-based (a captured local, a global, or a
553
+ /// builtin) — the single source of truth for slot-vs-name. Checks the simple param-only map first
554
+ /// (a chunk is in exactly one mode), then the general scope stack innermost-first (shadowing).
555
+ #[inline]
556
+ fn resolve_slot(&self, name: &str) -> Option<u16> {
557
+ if let Some(m) = self.slot_ctx.as_ref() {
558
+ if let Some(s) = m.get(name) {
559
+ return Some(*s);
560
+ }
561
+ }
562
+ self.slot_scopes.iter().rev().find_map(|m| m.get(name).copied())
563
+ }
564
+
565
+ /// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
566
+ fn emit_var_load(&mut self, name: &Arc<str>) {
567
+ if let Some(slot) = self.resolve_slot(name) {
568
+ self.emit_u16(Opcode::LoadLocal, slot);
569
+ } else {
570
+ let idx = self.name_idx(name);
571
+ self.emit_u16(Opcode::LoadVar, idx);
572
+ }
573
+ }
574
+
575
+ /// Emit a variable WRITE (value already on stack): `StoreLocal` if slotted, else `StoreVar`.
576
+ fn emit_var_store(&mut self, name: &Arc<str>) {
577
+ if let Some(slot) = self.resolve_slot(name) {
578
+ self.emit_u16(Opcode::StoreLocal, slot);
579
+ } else {
580
+ let idx = self.name_idx(name);
581
+ self.emit_u16(Opcode::StoreVar, idx);
582
+ }
583
+ }
584
+
585
+ fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
586
+ Self {
587
+ chunk,
588
+ scope: vec![HashMap::new()],
589
+ loop_stack: Vec::new(),
590
+ switch_stack: Vec::new(),
591
+ breakable_stack: Vec::new(),
592
+ block_depth: 0,
593
+ retain_last_expr,
594
+ slot_ctx: None,
595
+ slot_scopes: Vec::new(),
596
+ slot_captured: HashSet::new(),
597
+ next_slot: 0,
598
+ general_slots: false,
599
+ finally_stack: Vec::new(),
600
+ self_fn_name: None,
601
+ }
602
+ }
603
+
604
+ /// Begin a lexical block: push a name-scope frame and (in general slot mode) a slot-scope frame,
605
+ /// so block-local `let`s shadow correctly and are reclaimed at block end. Pair with [`exit_block_scope`].
606
+ fn enter_block_scope(&mut self) {
607
+ self.scope.push(HashMap::default());
608
+ if self.general_slots {
609
+ self.slot_scopes.push(HashMap::default());
610
+ }
611
+ }
612
+
613
+ fn exit_block_scope(&mut self) {
614
+ let _popped = self.scope.pop();
615
+ if self.general_slots {
616
+ self.slot_scopes.pop();
617
+ }
618
+ }
619
+
620
+ /// Allocate a fresh frame slot for `name` in the innermost slot scope (general mode).
621
+ fn declare_slot(&mut self, name: &Arc<str>) -> u16 {
622
+ let slot = self.next_slot;
623
+ self.next_slot += 1;
624
+ if let Some(frame) = self.slot_scopes.last_mut() {
625
+ frame.insert(Arc::clone(name), slot);
626
+ }
627
+ slot
628
+ }
629
+
630
+ /// Emit the pending `finally` bodies (innermost first) before a `return` escapes them. While
631
+ /// emitting, the stack is cleared so a `return` *inside* one of these finallys doesn't recurse.
632
+ fn emit_pending_finallys(&mut self) -> Result<(), CompileError> {
633
+ if self.finally_stack.is_empty() {
634
+ return Ok(());
635
+ }
636
+ let saved = std::mem::take(&mut self.finally_stack);
637
+ for finally in saved.iter().rev() {
638
+ self.compile_statement(finally)?;
639
+ }
640
+ self.finally_stack = saved;
641
+ Ok(())
642
+ }
643
+
644
+ fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
645
+ let n = self.block_depth.saturating_sub(target_depth);
646
+ for _ in 0..n {
647
+ self.emit(Opcode::ExitBlock);
648
+ }
649
+ }
650
+
651
+ /// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
652
+ /// Formal parameters as VM slot names plus optional destructure patterns (one per formal).
653
+ #[allow(clippy::type_complexity)] // (slot names, optional destructure patterns) — single-use return
654
+ fn plan_function_params(
655
+ params: &[FunParam],
656
+ ) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
657
+ let mut names = Vec::with_capacity(params.len());
658
+ let mut slots: Vec<Option<DestructPattern>> = Vec::with_capacity(params.len());
659
+ let mut syn_counter = 0u32;
660
+ for p in params {
661
+ match p {
662
+ FunParam::Simple(tp) => {
663
+ names.push(Arc::clone(&tp.name));
664
+ slots.push(None);
665
+ }
666
+ FunParam::Destructure {
667
+ pattern, default, ..
668
+ } => {
669
+ if default.is_some() {
670
+ return Err(CompileError {
671
+ message: "Default values on destructuring parameters are not supported in bytecode"
672
+ .to_string(),
673
+ });
674
+ }
675
+ names.push(Arc::from(format!("__param_{}", syn_counter)));
676
+ syn_counter += 1;
677
+ slots.push(Some(pattern.clone()));
678
+ }
679
+ }
680
+ }
681
+ Ok((names, slots))
682
+ }
683
+
684
+ /// After VM binds positional args to `param_names`, load each destructure slot and bind pattern locals.
685
+ fn emit_param_destructure_prologue(
686
+ &mut self,
687
+ param_names: &[Arc<str>],
688
+ slots: &[Option<DestructPattern>],
689
+ ) -> Result<(), CompileError> {
690
+ debug_assert_eq!(param_names.len(), slots.len());
691
+ for (name, slot) in param_names.iter().zip(slots.iter()) {
692
+ if let Some(pattern) = slot {
693
+ let idx = self.name_idx(name);
694
+ self.emit_u16(Opcode::LoadVar, idx);
695
+ self.compile_destructure(pattern, false, false)?;
696
+ }
697
+ }
698
+ Ok(())
699
+ }
700
+
701
+ /// Emit the default-parameter prologue: for each simple param `p_i` with a default,
702
+ /// `if (arg i was not supplied) p_i = <default>`. Runs at the top of the function body so
703
+ /// later defaults can reference earlier (already-bound) params, e.g. `(a, b = a + 1)`.
704
+ ///
705
+ /// Uses `ArgMissing(i)` (true iff `i >= argc`) + `JumpIfFalse` so the default applies only
706
+ /// to *missing* positional args — matching the interpreter, where an explicit `null` keeps
707
+ /// the `null` (tish has no `undefined`). The store mirrors variable resolution: a slot-based
708
+ /// chunk writes the slot directly (`StoreLocal`); a name-based chunk binds the name
709
+ /// (`DeclareVarPlain`, since a missing param is absent from the frame scope).
710
+ fn emit_param_defaults_prologue(&mut self, params: &[FunParam]) -> Result<(), CompileError> {
711
+ for (i, p) in params.iter().enumerate() {
712
+ let FunParam::Simple(tp) = p else { continue };
713
+ let Some(default_expr) = &tp.default else {
714
+ continue;
715
+ };
716
+ self.emit_u16(Opcode::ArgMissing, i as u16);
717
+ let skip = self.emit_jump(Opcode::JumpIfFalse);
718
+ self.compile_expr(default_expr)?;
719
+ let slot = self
720
+ .slot_ctx
721
+ .as_ref()
722
+ .and_then(|m| m.get(tp.name.as_ref()))
723
+ .copied();
724
+ match slot {
725
+ Some(slot) => self.emit_u16(Opcode::StoreLocal, slot),
726
+ None => {
727
+ let idx = self.name_idx(&tp.name);
728
+ self.emit_u16(Opcode::DeclareVarPlain, idx);
729
+ }
730
+ }
731
+ self.patch_jump(skip, self.chunk.code.len());
732
+ }
733
+ Ok(())
734
+ }
735
+
736
+ /// Names `let`/`const`-declared DIRECTLY in a loop body block (not nested blocks). Each is a
737
+ /// fresh per-iteration binding (ES `let`), so closures created in the body must capture this
738
+ /// iteration's value — registered via `LoopVarsBegin`.
739
+ fn loop_body_block_lets(body: &Statement) -> Vec<Arc<str>> {
740
+ let mut out = Vec::new();
741
+ if let Statement::Block { statements, .. } = body {
742
+ for s in statements {
743
+ if let Statement::VarDecl { name, .. } = s {
744
+ out.push(Arc::clone(name));
745
+ }
746
+ }
747
+ }
748
+ out
749
+ }
750
+
751
+ fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
752
+ match stmt {
753
+ Statement::VarDecl {
754
+ name,
755
+ init,
756
+ mutable: _,
757
+ ..
758
+ } => {
759
+ if let Some(expr) = init {
760
+ self.compile_expr(expr)?;
761
+ } else {
762
+ let idx = self.constant_idx(Constant::Null);
763
+ self.emit(Opcode::LoadConst);
764
+ self.chunk.write_u16(idx);
765
+ }
766
+ if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
767
+ let slot = self.declare_slot(name);
768
+ self.emit_u16(Opcode::StoreLocal, slot);
769
+ } else {
770
+ let idx = self.name_idx(name);
771
+ self.emit_u16(Opcode::DeclareVarPlain, idx);
772
+ self.scope
773
+ .last_mut()
774
+ .unwrap()
775
+ .insert(Arc::clone(name), false);
776
+ }
777
+ }
778
+ Statement::VarDeclDestructure { pattern, init, .. } => {
779
+ self.compile_expr(init)?;
780
+ self.compile_destructure(pattern, false, true)?;
781
+ }
782
+ _ => self.compile_statement(stmt)?,
783
+ }
784
+ Ok(())
785
+ }
786
+
787
+ fn name_idx(&mut self, name: &Arc<str>) -> u16 {
788
+ self.chunk.add_name(Arc::clone(name))
789
+ }
790
+
791
+ fn constant_idx(&mut self, c: Constant) -> u16 {
792
+ self.chunk.add_constant(c)
793
+ }
794
+
795
+ fn emit(&mut self, op: Opcode) {
796
+ self.chunk.write_u8(op as u8);
797
+ }
798
+
799
+ /// Record the source line of the code about to be emitted, for runtime error locations
800
+ /// (issue #74). Cheap and deduped: only a line *change* adds a table entry.
801
+ fn mark_line(&mut self, span: tishlang_ast::Span) {
802
+ let offset = self.chunk.code.len();
803
+ self.chunk.mark_line(offset, span.start.0 as u32);
804
+ }
805
+
806
+ fn emit_u8(&mut self, op: Opcode, v: u8) {
807
+ self.chunk.write_u8(op as u8);
808
+ self.chunk.write_u16(v as u16);
809
+ }
810
+
811
+ fn emit_u16(&mut self, op: Opcode, v: u16) {
812
+ self.chunk.write_u8(op as u8);
813
+ self.chunk.write_u16(v);
814
+ }
815
+
816
+ fn emit_jump(&mut self, op: Opcode) -> usize {
817
+ let pos = self.chunk.code.len();
818
+ self.chunk.write_u8(op as u8);
819
+ self.chunk.write_u16(0); // placeholder
820
+ pos + 1
821
+ }
822
+
823
+ /// Emit JumpBack with placeholder distance; patch later with patch_jump_back.
824
+ fn emit_jump_back(&mut self) -> usize {
825
+ let pos = self.chunk.code.len();
826
+ self.chunk.write_u8(Opcode::JumpBack as u8);
827
+ self.chunk.write_u16(0);
828
+ pos + 1
829
+ }
830
+
831
+ fn patch_jump(&mut self, patch_pos: usize, target: usize) {
832
+ let base = patch_pos + 2;
833
+ let jump_offset = (target as i32).wrapping_sub(base as i32);
834
+ let bytes = (jump_offset as i16).to_be_bytes();
835
+ self.chunk.code[patch_pos] = bytes[0];
836
+ self.chunk.code[patch_pos + 1] = bytes[1];
837
+ }
838
+
839
+ /// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
840
+ /// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
841
+ fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
842
+ let after_insn = patch_pos + 2;
843
+ let dist = after_insn.saturating_sub(target);
844
+ let bytes = (dist as u16).to_be_bytes();
845
+ self.chunk.code[patch_pos] = bytes[0];
846
+ self.chunk.code[patch_pos + 1] = bytes[1];
847
+ }
848
+
849
+ /// Detect property-based numeric sort: (a, b) => a.prop - b.prop or (a, b) => b.prop - a.prop.
850
+ /// Returns Some((prop_name, asc)) or None.
851
+ fn detect_property_sort_comparator(expr: &Expr) -> Option<(Arc<str>, bool)> {
852
+ if let Expr::ArrowFunction { params, body, .. } = expr {
853
+ if params.len() != 2 {
854
+ return None;
855
+ }
856
+ let (param_a, param_b) = match (&params[0], &params[1]) {
857
+ (FunParam::Simple(a), FunParam::Simple(b))
858
+ if a.default.is_none() && b.default.is_none() =>
859
+ {
860
+ (a.name.as_ref(), b.name.as_ref())
861
+ }
862
+ _ => return None,
863
+ };
864
+ let body_expr = match body {
865
+ ArrowBody::Expr(e) => e.as_ref(),
866
+ ArrowBody::Block(stmt) => {
867
+ if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
868
+ e
869
+ } else {
870
+ return None;
871
+ }
872
+ }
873
+ };
874
+ if let Expr::Binary {
875
+ left,
876
+ op: BinOp::Sub,
877
+ right,
878
+ ..
879
+ } = body_expr
880
+ {
881
+ if let (
882
+ Expr::Member {
883
+ object: lo,
884
+ prop: MemberProp::Name { name: p, .. },
885
+ ..
886
+ },
887
+ Expr::Member {
888
+ object: ro,
889
+ prop: MemberProp::Name { name: pr, .. },
890
+ ..
891
+ },
892
+ ) = (left.as_ref(), right.as_ref())
893
+ {
894
+ if p != pr {
895
+ return None;
896
+ }
897
+ if let (Expr::Ident { name: ln, .. }, Expr::Ident { name: rn, .. }) =
898
+ (lo.as_ref(), ro.as_ref())
899
+ {
900
+ if ln.as_ref() == param_a && rn.as_ref() == param_b {
901
+ return Some((Arc::clone(p), true));
902
+ }
903
+ if ln.as_ref() == param_b && rn.as_ref() == param_a {
904
+ return Some((Arc::clone(p), false));
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }
910
+ None
911
+ }
912
+
913
+ /// Detect numeric sort comparator: (a, b) => a - b (asc) or (a, b) => b - a (desc).
914
+ fn detect_numeric_sort_comparator(expr: &Expr) -> Option<bool> {
915
+ if let Expr::ArrowFunction { params, body, .. } = expr {
916
+ if params.len() != 2 {
917
+ return None;
918
+ }
919
+ let (param_a, param_b) = match (&params[0], &params[1]) {
920
+ (FunParam::Simple(a), FunParam::Simple(b))
921
+ if a.default.is_none() && b.default.is_none() =>
922
+ {
923
+ (a.name.as_ref(), b.name.as_ref())
924
+ }
925
+ _ => return None,
926
+ };
927
+ let body_expr = match body {
928
+ ArrowBody::Expr(e) => e.as_ref(),
929
+ ArrowBody::Block(stmt) => {
930
+ if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
931
+ e
932
+ } else {
933
+ return None;
934
+ }
935
+ }
936
+ };
937
+ if let Expr::Binary {
938
+ left,
939
+ op: BinOp::Sub,
940
+ right,
941
+ ..
942
+ } = body_expr
943
+ {
944
+ if let (
945
+ Expr::Ident {
946
+ name: left_name, ..
947
+ },
948
+ Expr::Ident {
949
+ name: right_name, ..
950
+ },
951
+ ) = (left.as_ref(), right.as_ref())
952
+ {
953
+ if left_name.as_ref() == param_a && right_name.as_ref() == param_b {
954
+ return Some(true);
955
+ }
956
+ if left_name.as_ref() == param_b && right_name.as_ref() == param_a {
957
+ return Some(false);
958
+ }
959
+ }
960
+ }
961
+ }
962
+ None
963
+ }
964
+
965
+ /// Detect simple map callback: x => x (identity) or x => x op const / x => const op x.
966
+ /// Returns SimpleMapResult for map optimization.
967
+ fn detect_simple_map_callback(expr: &Expr) -> Option<SimpleMapResult> {
968
+ let (params, body) = match expr {
969
+ Expr::ArrowFunction { params, body, .. } => (params, body),
970
+ _ => return None,
971
+ };
972
+ if params.len() != 1 {
973
+ return None;
974
+ }
975
+ let param_name = match &params[0] {
976
+ FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
977
+ _ => return None,
978
+ };
979
+ let expr_ref: &Expr = match body {
980
+ ArrowBody::Expr(e) => e.as_ref(),
981
+ ArrowBody::Block(stmt) => {
982
+ let s = stmt.as_ref();
983
+ if let Statement::Return {
984
+ value: Some(ref e), ..
985
+ } = s
986
+ {
987
+ e
988
+ } else if let Statement::ExprStmt { expr: ref e, .. } = s {
989
+ e
990
+ } else {
991
+ return None;
992
+ }
993
+ }
994
+ };
995
+ // Identity: x => x
996
+ if let Expr::Ident { name, .. } = expr_ref {
997
+ if name.as_ref() == param_name {
998
+ return Some(SimpleMapResult::Identity);
999
+ }
1000
+ }
1001
+ // Binary: x op const or const op x
1002
+ if let Expr::Binary {
1003
+ left, op, right, ..
1004
+ } = expr_ref
1005
+ {
1006
+ let left_is_param =
1007
+ matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1008
+ let right_is_param =
1009
+ matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1010
+ let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
1011
+ let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
1012
+ if left_is_param && right_is_literal {
1013
+ if let Some(c) = literal_to_constant(right.as_ref()) {
1014
+ return Some(SimpleMapResult::BinOp(*op, c, true));
1015
+ }
1016
+ }
1017
+ if left_is_literal && right_is_param {
1018
+ if let Some(c) = literal_to_constant(left.as_ref()) {
1019
+ return Some(SimpleMapResult::BinOp(*op, c, false));
1020
+ }
1021
+ }
1022
+ }
1023
+ None
1024
+ }
1025
+
1026
+ /// Detect simple filter callback: x => x op const or x => const op x (comparison that returns bool).
1027
+ fn detect_simple_filter_callback(expr: &Expr) -> Option<(BinOp, Constant, bool)> {
1028
+ let (params, body) = match expr {
1029
+ Expr::ArrowFunction { params, body, .. } => (params, body),
1030
+ _ => return None,
1031
+ };
1032
+ if params.len() != 1 {
1033
+ return None;
1034
+ }
1035
+ let param_name = match &params[0] {
1036
+ FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
1037
+ _ => return None,
1038
+ };
1039
+ let expr_ref: &Expr = match body {
1040
+ ArrowBody::Expr(e) => e.as_ref(),
1041
+ ArrowBody::Block(stmt) => {
1042
+ let s = stmt.as_ref();
1043
+ if let Statement::Return {
1044
+ value: Some(ref e), ..
1045
+ } = s
1046
+ {
1047
+ e
1048
+ } else if let Statement::ExprStmt { expr: ref e, .. } = s {
1049
+ e
1050
+ } else {
1051
+ return None;
1052
+ }
1053
+ }
1054
+ };
1055
+ if let Expr::Binary {
1056
+ left, op, right, ..
1057
+ } = expr_ref
1058
+ {
1059
+ if !matches!(
1060
+ op,
1061
+ BinOp::Eq
1062
+ | BinOp::Ne
1063
+ | BinOp::StrictEq
1064
+ | BinOp::StrictNe
1065
+ | BinOp::Lt
1066
+ | BinOp::Le
1067
+ | BinOp::Gt
1068
+ | BinOp::Ge
1069
+ | BinOp::And
1070
+ | BinOp::Or
1071
+ ) {
1072
+ return None;
1073
+ }
1074
+ let left_is_param =
1075
+ matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1076
+ let right_is_param =
1077
+ matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
1078
+ let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
1079
+ let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
1080
+ if left_is_param && right_is_literal {
1081
+ if let Some(c) = literal_to_constant(right.as_ref()) {
1082
+ return Some((*op, c, true));
1083
+ }
1084
+ }
1085
+ if left_is_literal && right_is_param {
1086
+ if let Some(c) = literal_to_constant(left.as_ref()) {
1087
+ return Some((*op, c, false));
1088
+ }
1089
+ }
1090
+ }
1091
+ None
1092
+ }
1093
+
1094
+ fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
1095
+ let stmts = &program.statements;
1096
+ // Top-level general slot-based locals — NON-REPL only (REPL persists top-level `let`s to
1097
+ // globals across lines, which slots can't do). Set up before compiling; the frame size is the
1098
+ // monotonic `next_slot` high-water, applied to the chunk after compilation.
1099
+ if !self.retain_last_expr {
1100
+ if let Some(cap) = slot_analyze_toplevel(stmts) {
1101
+ self.general_slots = true;
1102
+ self.slot_captured = cap;
1103
+ self.slot_scopes.push(HashMap::default());
1104
+ }
1105
+ }
1106
+ let last_is_expr = self.retain_last_expr
1107
+ && stmts
1108
+ .last()
1109
+ .map(|s| matches!(s, Statement::ExprStmt { .. }))
1110
+ .unwrap_or(false);
1111
+
1112
+ if last_is_expr {
1113
+ let (rest, last) = stmts.split_at(stmts.len().saturating_sub(1));
1114
+ for stmt in rest {
1115
+ self.compile_statement(stmt)?;
1116
+ }
1117
+ if let Some(Statement::ExprStmt { expr, .. }) = last.first() {
1118
+ self.compile_expr(expr)?;
1119
+ }
1120
+ } else {
1121
+ for stmt in stmts {
1122
+ self.compile_statement(stmt)?;
1123
+ }
1124
+ let idx = self.constant_idx(Constant::Null);
1125
+ self.emit(Opcode::LoadConst);
1126
+ self.chunk.write_u16(idx);
1127
+ }
1128
+ // Apply the top-level slot frame size (only if any local was actually slotted).
1129
+ if self.general_slots && self.next_slot > 0 {
1130
+ self.chunk.slot_based = true;
1131
+ self.chunk.num_slots = self.next_slot;
1132
+ }
1133
+ Ok(())
1134
+ }
1135
+
1136
+ fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
1137
+ self.mark_line(stmt.span());
1138
+ match stmt {
1139
+ Statement::Block { statements, .. } => {
1140
+ self.emit(Opcode::EnterBlock);
1141
+ self.block_depth += 1;
1142
+ self.enter_block_scope();
1143
+ for s in statements {
1144
+ self.compile_statement(s)?;
1145
+ }
1146
+ self.exit_block_scope();
1147
+ self.emit(Opcode::ExitBlock);
1148
+ self.block_depth -= 1;
1149
+ }
1150
+ // Comma-declarators: a transparent group — compile each declarator in
1151
+ // the *current* block scope (no EnterBlock/ExitBlock).
1152
+ Statement::Multi { statements, .. } => {
1153
+ for s in statements {
1154
+ self.compile_statement(s)?;
1155
+ }
1156
+ }
1157
+ Statement::VarDecl {
1158
+ name,
1159
+ init,
1160
+ mutable: _,
1161
+ ..
1162
+ } => {
1163
+ if let Some(expr) = init {
1164
+ self.compile_expr(expr)?;
1165
+ } else {
1166
+ let idx = self.constant_idx(Constant::Null);
1167
+ self.emit(Opcode::LoadConst);
1168
+ self.chunk.write_u16(idx);
1169
+ }
1170
+ if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
1171
+ // Uncaptured local → allocate a fresh frame slot + write it directly.
1172
+ let slot = self.declare_slot(name);
1173
+ self.emit_u16(Opcode::StoreLocal, slot);
1174
+ } else {
1175
+ let idx = self.name_idx(name);
1176
+ self.emit_u16(Opcode::DeclareVar, idx);
1177
+ self.scope
1178
+ .last_mut()
1179
+ .unwrap()
1180
+ .insert(Arc::clone(name), false);
1181
+ }
1182
+ }
1183
+ Statement::VarDeclDestructure { pattern, init, .. } => {
1184
+ self.compile_expr(init)?;
1185
+ self.compile_destructure(pattern, false, false)?;
1186
+ }
1187
+ Statement::ExprStmt { expr, .. } => {
1188
+ self.compile_expr(expr)?;
1189
+ self.emit(Opcode::Pop);
1190
+ }
1191
+ Statement::If {
1192
+ cond,
1193
+ then_branch,
1194
+ else_branch,
1195
+ ..
1196
+ } => {
1197
+ self.compile_expr(cond)?;
1198
+ let jump_else = self.emit_jump(Opcode::JumpIfFalse);
1199
+ self.compile_statement(then_branch)?;
1200
+ let jump_end = self.emit_jump(Opcode::Jump);
1201
+ self.patch_jump(jump_else, self.chunk.code.len());
1202
+ if let Some(else_s) = else_branch {
1203
+ self.compile_statement(else_s)?;
1204
+ }
1205
+ self.patch_jump(jump_end, self.chunk.code.len());
1206
+ }
1207
+ Statement::While { cond, body, .. } => {
1208
+ // Per-iteration `let`: a `let` declared directly in the loop body is a fresh binding
1209
+ // each iteration, so a closure created in the body captures THIS iteration's value.
1210
+ // Register those names (same overlay mechanism as for/for-of loop vars).
1211
+ let body_lets = Self::loop_body_block_lets(body);
1212
+ for n in &body_lets {
1213
+ let idx = self.name_idx(n);
1214
+ self.emit_u16(Opcode::LoopVarsBegin, idx);
1215
+ }
1216
+ let start = self.chunk.code.len();
1217
+ self.loop_stack.push(LoopInfo {
1218
+ break_patches: Vec::new(),
1219
+ continue_patches: Vec::new(),
1220
+ continue_is_forward_jump: false,
1221
+ });
1222
+ self.breakable_stack.push(Breakable::Loop {
1223
+ unwind_depth: self.block_depth,
1224
+ });
1225
+ self.compile_expr(cond)?;
1226
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
1227
+ // JumpIfFalse already pops condition when taking body
1228
+ self.compile_statement(body)?;
1229
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
1230
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1231
+ let end = self.chunk.code.len();
1232
+ self.patch_jump(jump_out, end);
1233
+ let info = self.loop_stack.pop().unwrap();
1234
+ self.breakable_stack.pop();
1235
+ for p in info.continue_patches {
1236
+ self.patch_jump_back(p, start);
1237
+ }
1238
+ for p in info.break_patches {
1239
+ self.patch_jump(p, end);
1240
+ }
1241
+ for _ in &body_lets {
1242
+ self.emit(Opcode::LoopVarsEnd);
1243
+ }
1244
+ }
1245
+ Statement::For {
1246
+ init,
1247
+ cond,
1248
+ update,
1249
+ body,
1250
+ ..
1251
+ } => {
1252
+ self.enter_block_scope();
1253
+ if let Some(i) = init {
1254
+ self.compile_for_init_statement(i.as_ref())?;
1255
+ }
1256
+ // ES per-iteration `let`: register the loop var so a closure created in the body
1257
+ // captures THIS iteration's value (not the final one). One push per loop entry; the
1258
+ // per-iteration snapshot only happens when a closure is actually created, so
1259
+ // closure-free loops are unaffected.
1260
+ let loop_var: Option<Arc<str>> = match init.as_deref() {
1261
+ Some(Statement::VarDecl { name, .. }) => Some(Arc::clone(name)),
1262
+ _ => None,
1263
+ };
1264
+ if let Some(ref n) = loop_var {
1265
+ let idx = self.name_idx(n);
1266
+ self.emit_u16(Opcode::LoopVarsBegin, idx);
1267
+ }
1268
+ let cond_start = self.chunk.code.len();
1269
+ if let Some(c) = cond {
1270
+ self.compile_expr(c)?;
1271
+ } else {
1272
+ let idx = self.constant_idx(Constant::Bool(true));
1273
+ self.emit(Opcode::LoadConst);
1274
+ self.chunk.write_u16(idx);
1275
+ }
1276
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
1277
+ self.loop_stack.push(LoopInfo {
1278
+ break_patches: Vec::new(),
1279
+ continue_patches: Vec::new(),
1280
+ continue_is_forward_jump: true,
1281
+ });
1282
+ self.breakable_stack.push(Breakable::Loop {
1283
+ unwind_depth: self.block_depth,
1284
+ });
1285
+ self.compile_statement(body)?;
1286
+ let update_start = self.chunk.code.len();
1287
+ if let Some(u) = update {
1288
+ self.compile_expr(u)?;
1289
+ self.emit(Opcode::Pop);
1290
+ }
1291
+ let info = self.loop_stack.pop().unwrap();
1292
+ for p in info.continue_patches {
1293
+ self.patch_jump(p, update_start);
1294
+ }
1295
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
1296
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1297
+ let end = self.chunk.code.len();
1298
+ self.patch_jump(jump_out, end);
1299
+ for p in info.break_patches {
1300
+ self.patch_jump(p, end);
1301
+ }
1302
+ // After the loop fully exits (normal or break, both land at `end`): close the
1303
+ // per-iteration region.
1304
+ if loop_var.is_some() {
1305
+ self.emit(Opcode::LoopVarsEnd);
1306
+ }
1307
+ self.breakable_stack.pop();
1308
+ self.exit_block_scope();
1309
+ }
1310
+ Statement::ForOf {
1311
+ name,
1312
+ iterable,
1313
+ body,
1314
+ ..
1315
+ } => {
1316
+ self.compile_expr(iterable)?;
1317
+ // Normalize a JS iterator object (Map/Set `.values()` etc.) to an array so the
1318
+ // index-based loop below can iterate it; arrays/strings pass through untouched.
1319
+ self.emit(Opcode::IterNormalize);
1320
+ self.enter_block_scope();
1321
+ let arr_name = Arc::from("__forof_arr__");
1322
+ let i_name = Arc::from("__forof_i__");
1323
+ let len_name = Arc::from("__forof_len__");
1324
+ let arr_idx = self.name_idx(&arr_name);
1325
+ let i_idx = self.name_idx(&i_name);
1326
+ let len_idx = self.name_idx(&len_name);
1327
+ let name_idx = self.name_idx(name);
1328
+ self.emit_u16(Opcode::DeclareVar, arr_idx);
1329
+ self.scope
1330
+ .last_mut()
1331
+ .unwrap()
1332
+ .insert(arr_name.clone(), false);
1333
+ self.emit_u16(Opcode::LoadVar, arr_idx);
1334
+ let len_name_idx = self.name_idx(&Arc::from("length"));
1335
+ self.emit_u16(Opcode::GetMember, len_name_idx);
1336
+ self.emit_u16(Opcode::DeclareVar, len_idx);
1337
+ self.scope
1338
+ .last_mut()
1339
+ .unwrap()
1340
+ .insert(len_name.clone(), false);
1341
+ let zero_idx = self.constant_idx(Constant::Number(0.0));
1342
+ self.emit(Opcode::LoadConst);
1343
+ self.chunk.write_u16(zero_idx);
1344
+ self.emit_u16(Opcode::DeclareVar, i_idx);
1345
+ self.scope.last_mut().unwrap().insert(i_name.clone(), false);
1346
+ // ES per-iteration `let` for `for (let v of …)`: register the loop var so a closure
1347
+ // in the body captures this iteration's element (emitted once, before loop_start).
1348
+ self.emit_u16(Opcode::LoopVarsBegin, name_idx);
1349
+ // Pre-tested loop, like the C-style `for` above: test `i < len` at the TOP, before
1350
+ // reading `arr[i]`. A bottom-tested loop ran the body once on an empty array (reading
1351
+ // `arr[0]` → null) and spun forever on `continue` (which skipped the increment).
1352
+ let cond_start = self.chunk.code.len();
1353
+ self.emit_u16(Opcode::LoadVar, i_idx);
1354
+ self.emit_u16(Opcode::LoadVar, len_idx);
1355
+ self.emit_u8(Opcode::BinOp, 10);
1356
+ let jump_out = self.emit_jump(Opcode::JumpIfFalse);
1357
+ self.loop_stack.push(LoopInfo {
1358
+ break_patches: Vec::new(),
1359
+ continue_patches: Vec::new(),
1360
+ continue_is_forward_jump: true,
1361
+ });
1362
+ self.breakable_stack.push(Breakable::Loop {
1363
+ unwind_depth: self.block_depth,
1364
+ });
1365
+ self.emit_u16(Opcode::LoadVar, arr_idx);
1366
+ self.emit_u16(Opcode::LoadVar, i_idx);
1367
+ self.emit(Opcode::GetIndex);
1368
+ self.emit_u16(Opcode::DeclareVar, name_idx);
1369
+ self.scope
1370
+ .last_mut()
1371
+ .unwrap()
1372
+ .insert(Arc::clone(name), false);
1373
+ self.compile_statement(body)?;
1374
+ // `continue` lands here: increment `i`, then fall through to the JumpBack → re-test.
1375
+ let update_start = self.chunk.code.len();
1376
+ self.emit_u16(Opcode::LoadVar, i_idx);
1377
+ let one_idx = self.constant_idx(Constant::Number(1.0));
1378
+ self.emit(Opcode::LoadConst);
1379
+ self.chunk.write_u16(one_idx);
1380
+ self.emit_u8(Opcode::BinOp, 0);
1381
+ self.emit_u16(Opcode::StoreVar, i_idx);
1382
+ let info = self.loop_stack.pop().unwrap();
1383
+ self.breakable_stack.pop();
1384
+ for p in info.continue_patches {
1385
+ self.patch_jump(p, update_start);
1386
+ }
1387
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
1388
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1389
+ let end = self.chunk.code.len();
1390
+ self.patch_jump(jump_out, end);
1391
+ for p in info.break_patches {
1392
+ self.patch_jump(p, end);
1393
+ }
1394
+ self.emit(Opcode::LoopVarsEnd);
1395
+ self.exit_block_scope();
1396
+ }
1397
+ Statement::Return { value, .. } => {
1398
+ // Evaluate the return value first (JS order), then run any enclosing `finally`
1399
+ // blocks (they're stack-neutral, so the value stays on top), then return.
1400
+ if let Some(v) = value {
1401
+ self.compile_expr(v)?;
1402
+ } else {
1403
+ let idx = self.constant_idx(Constant::Null);
1404
+ self.emit(Opcode::LoadConst);
1405
+ self.chunk.write_u16(idx);
1406
+ }
1407
+ self.emit_pending_finallys()?;
1408
+ self.emit(Opcode::Return);
1409
+ }
1410
+ Statement::Break { .. } => {
1411
+ let unwind_depth = match self.breakable_stack.last() {
1412
+ Some(Breakable::Loop { unwind_depth })
1413
+ | Some(Breakable::Switch { unwind_depth }) => *unwind_depth,
1414
+ None => {
1415
+ return Err(CompileError {
1416
+ message: "break not inside a loop or switch".to_string(),
1417
+ });
1418
+ }
1419
+ };
1420
+ self.emit_exit_blocks_until_depth(unwind_depth);
1421
+ let pos = self.emit_jump(Opcode::Jump);
1422
+ match self.breakable_stack.last() {
1423
+ Some(Breakable::Loop { .. }) => {
1424
+ self.loop_stack.last_mut().unwrap().break_patches.push(pos);
1425
+ }
1426
+ Some(Breakable::Switch { .. }) => {
1427
+ self.switch_stack
1428
+ .last_mut()
1429
+ .unwrap()
1430
+ .break_patches
1431
+ .push(pos);
1432
+ }
1433
+ None => {}
1434
+ }
1435
+ }
1436
+ Statement::Continue { .. } => {
1437
+ let unwind_depth = self
1438
+ .breakable_stack
1439
+ .iter()
1440
+ .rev()
1441
+ .find_map(|b| match b {
1442
+ Breakable::Loop { unwind_depth } => Some(*unwind_depth),
1443
+ Breakable::Switch { .. } => None,
1444
+ })
1445
+ .ok_or_else(|| CompileError {
1446
+ message: "continue not inside a loop".to_string(),
1447
+ })?;
1448
+ self.emit_exit_blocks_until_depth(unwind_depth);
1449
+ let forward = self
1450
+ .loop_stack
1451
+ .last()
1452
+ .expect("continue not inside a loop")
1453
+ .continue_is_forward_jump;
1454
+ let pos = if forward {
1455
+ self.emit_jump(Opcode::Jump)
1456
+ } else {
1457
+ self.emit_jump_back()
1458
+ };
1459
+ self.loop_stack
1460
+ .last_mut()
1461
+ .expect("continue not inside a loop")
1462
+ .continue_patches
1463
+ .push(pos);
1464
+ }
1465
+ Statement::FunDecl {
1466
+ name,
1467
+ params,
1468
+ body,
1469
+ rest_param,
1470
+ async_: _,
1471
+ ..
1472
+ } => {
1473
+ let formal_len = params.len();
1474
+ let (mut param_names, slots) = Self::plan_function_params(params)?;
1475
+ let simple_slots = simple_fn_slots(params, rest_param.is_some(), |pset| {
1476
+ stmt_is_param_only(body, pset)
1477
+ });
1478
+ // Capture-aware general slot-based locals when the simple param-only fast path doesn't
1479
+ // apply. Gated by `TISH_VM_SLOTS` (off ⇒ None ⇒ byte-identical). Frame size is known only
1480
+ // AFTER compilation (slots are allocated as the body declares locals) → set on the chunk below.
1481
+ let captured = if simple_slots.is_none() {
1482
+ slot_analyze(params, rest_param.is_some(), body)
1483
+ } else {
1484
+ None
1485
+ };
1486
+ let mut inner = Chunk::new();
1487
+ inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
1488
+ if let Some(rp) = rest_param {
1489
+ param_names.push(Arc::clone(&rp.name));
1490
+ inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
1491
+ }
1492
+ for p in &param_names {
1493
+ inner.add_name(Arc::clone(p));
1494
+ }
1495
+ inner.param_count = param_names.len() as u16;
1496
+ if simple_slots.is_some() {
1497
+ inner.slot_based = true;
1498
+ inner.num_slots = param_names.len() as u16;
1499
+ }
1500
+ let mut inner_comp = Compiler::new(&mut inner, false);
1501
+ // Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
1502
+ // param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
1503
+ // compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
1504
+ // recursive call. Conservative `stmt_rebinds` errs toward NOT enabling (safe).
1505
+ if !params_bind_name(params, name.as_ref()) && !stmt_rebinds(body, name.as_ref()) {
1506
+ inner_comp.self_fn_name = Some(Arc::clone(name));
1507
+ }
1508
+ let mut general_frame_slots: Option<u16> = None;
1509
+ if let Some(map) = simple_slots {
1510
+ inner_comp.slot_ctx = Some(map);
1511
+ inner_comp.emit_param_defaults_prologue(params)?;
1512
+ inner_comp.compile_statement(body)?;
1513
+ } else if let Some(cap) = captured {
1514
+ // Params (all uncaptured — gated) → slots 0..n (matching the VM's param binding);
1515
+ // uncaptured body `let`s get fresh slots via the scope-aware allocator; captured
1516
+ // locals stay name-based in `local_scope` (which closures capture).
1517
+ inner_comp.general_slots = true;
1518
+ inner_comp.slot_captured = cap;
1519
+ inner_comp.slot_scopes.push(HashMap::new());
1520
+ for p in &param_names {
1521
+ inner_comp.declare_slot(p);
1522
+ }
1523
+ inner_comp.emit_param_defaults_prologue(params)?;
1524
+ inner_comp.compile_statement(body)?;
1525
+ general_frame_slots = Some(inner_comp.next_slot);
1526
+ } else {
1527
+ inner_comp.scope = vec![param_names
1528
+ .iter()
1529
+ .map(|n| (Arc::clone(n), false))
1530
+ .collect::<HashMap<_, _>>()];
1531
+ inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
1532
+ inner_comp.emit_param_defaults_prologue(params)?;
1533
+ inner_comp.compile_statement(body)?;
1534
+ }
1535
+ inner_comp.emit(Opcode::LoadConst);
1536
+ let idx = inner_comp.constant_idx(Constant::Null);
1537
+ inner_comp.chunk.write_u16(idx);
1538
+ inner_comp.emit(Opcode::Return);
1539
+ if let Some(n) = general_frame_slots {
1540
+ inner_comp.chunk.slot_based = true;
1541
+ inner_comp.chunk.num_slots = n;
1542
+ }
1543
+ let nested_idx = self.chunk.add_nested(inner);
1544
+ self.emit(Opcode::LoadConst);
1545
+ let idx = self.constant_idx(Constant::Closure(nested_idx));
1546
+ self.chunk.write_u16(idx);
1547
+ let idx = self.name_idx(name);
1548
+ self.emit_u16(Opcode::DeclareVar, idx);
1549
+ self.scope
1550
+ .last_mut()
1551
+ .unwrap()
1552
+ .insert(Arc::clone(name), false);
1553
+ }
1554
+ Statement::DoWhile { body, cond, .. } => {
1555
+ let body_lets = Self::loop_body_block_lets(body);
1556
+ for n in &body_lets {
1557
+ let idx = self.name_idx(n);
1558
+ self.emit_u16(Opcode::LoopVarsBegin, idx);
1559
+ }
1560
+ let start = self.chunk.code.len();
1561
+ self.loop_stack.push(LoopInfo {
1562
+ break_patches: Vec::new(),
1563
+ continue_patches: Vec::new(),
1564
+ continue_is_forward_jump: false,
1565
+ });
1566
+ self.breakable_stack.push(Breakable::Loop {
1567
+ unwind_depth: self.block_depth,
1568
+ });
1569
+ self.compile_statement(body)?;
1570
+ let cond_start = self.chunk.code.len();
1571
+ self.compile_expr(cond)?;
1572
+ let jump_back = self.emit_jump(Opcode::JumpIfFalse);
1573
+ let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
1574
+ self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
1575
+ let end = self.chunk.code.len();
1576
+ self.patch_jump(jump_back, end);
1577
+ let info = self.loop_stack.pop().unwrap();
1578
+ self.breakable_stack.pop();
1579
+ for p in info.continue_patches {
1580
+ self.patch_jump_back(p, cond_start);
1581
+ }
1582
+ for p in info.break_patches {
1583
+ self.patch_jump(p, end);
1584
+ }
1585
+ for _ in &body_lets {
1586
+ self.emit(Opcode::LoopVarsEnd);
1587
+ }
1588
+ }
1589
+ Statement::Switch {
1590
+ expr,
1591
+ cases,
1592
+ default_body,
1593
+ ..
1594
+ } => {
1595
+ let switch_unwind_depth = self.block_depth;
1596
+ self.switch_stack.push(SwitchInfo {
1597
+ break_patches: Vec::new(),
1598
+ });
1599
+ self.breakable_stack.push(Breakable::Switch {
1600
+ unwind_depth: switch_unwind_depth,
1601
+ });
1602
+ self.compile_expr(expr)?;
1603
+ self.emit(Opcode::Dup);
1604
+ let mut end_patches = Vec::new();
1605
+ for (case_expr, case_body) in cases {
1606
+ self.emit(Opcode::Dup);
1607
+ if let Some(ce) = case_expr {
1608
+ self.compile_expr(ce)?;
1609
+ self.emit_u8(Opcode::BinOp, 8);
1610
+ let jump_next = self.emit_jump(Opcode::JumpIfFalse);
1611
+ // JumpIfFalse already pops the match result when taking this case
1612
+ self.compile_statement(&Statement::Block {
1613
+ statements: case_body.clone(),
1614
+ span: Span {
1615
+ start: (0, 0),
1616
+ end: (0, 0),
1617
+ },
1618
+ })?;
1619
+ let jump_end = self.emit_jump(Opcode::Jump);
1620
+ end_patches.push(jump_end);
1621
+ self.patch_jump(jump_next, self.chunk.code.len());
1622
+ } else {
1623
+ self.emit(Opcode::Pop);
1624
+ self.compile_statement(&Statement::Block {
1625
+ statements: case_body.clone(),
1626
+ span: Span {
1627
+ start: (0, 0),
1628
+ end: (0, 0),
1629
+ },
1630
+ })?;
1631
+ }
1632
+ }
1633
+ if let Some(body) = default_body {
1634
+ self.emit(Opcode::Pop);
1635
+ self.compile_statement(&Statement::Block {
1636
+ statements: body.clone(),
1637
+ span: Span {
1638
+ start: (0, 0),
1639
+ end: (0, 0),
1640
+ },
1641
+ })?;
1642
+ } else {
1643
+ self.emit(Opcode::Pop);
1644
+ }
1645
+ for p in end_patches {
1646
+ self.patch_jump(p, self.chunk.code.len());
1647
+ }
1648
+ let sw = self.switch_stack.pop().unwrap();
1649
+ self.breakable_stack.pop();
1650
+ for p in sw.break_patches {
1651
+ self.patch_jump(p, self.chunk.code.len());
1652
+ }
1653
+ }
1654
+ Statement::Throw { value, .. } => {
1655
+ self.compile_expr(value)?;
1656
+ self.emit(Opcode::Throw);
1657
+ }
1658
+ Statement::Try {
1659
+ body,
1660
+ catch_param,
1661
+ catch_body,
1662
+ finally_body,
1663
+ ..
1664
+ } => {
1665
+ let catch_offset_pos = self.chunk.code.len();
1666
+ self.emit(Opcode::EnterTry);
1667
+ self.chunk.write_u16(0);
1668
+ // A `return` inside the body/catch must run this finally on the way out.
1669
+ if let Some(f) = finally_body {
1670
+ self.finally_stack.push((**f).clone());
1671
+ }
1672
+ self.compile_statement(body)?;
1673
+ self.emit(Opcode::ExitTry);
1674
+ let jump_over_catch = self.emit_jump(Opcode::Jump);
1675
+ let catch_start = self.chunk.code.len();
1676
+ if let Some(catch_stmt) = catch_body {
1677
+ if let Some(param) = catch_param {
1678
+ self.emit(Opcode::EnterBlock);
1679
+ self.block_depth += 1;
1680
+ self.enter_block_scope();
1681
+ let param_idx = self.name_idx(param);
1682
+ self.emit_u16(Opcode::DeclareVar, param_idx);
1683
+ self.scope
1684
+ .last_mut()
1685
+ .unwrap()
1686
+ .insert(Arc::clone(param), false);
1687
+ self.compile_statement(catch_stmt)?;
1688
+ self.exit_block_scope();
1689
+ self.emit(Opcode::ExitBlock);
1690
+ self.block_depth -= 1;
1691
+ } else {
1692
+ self.emit(Opcode::Pop);
1693
+ self.compile_statement(catch_stmt)?;
1694
+ }
1695
+ } else {
1696
+ // No catch: run `finally` on the exception path, then re-raise (propagate).
1697
+ if let Some(f) = finally_body {
1698
+ self.compile_statement(f)?;
1699
+ }
1700
+ self.emit(Opcode::Throw);
1701
+ }
1702
+ let after_catch = self.chunk.code.len();
1703
+ self.patch_jump(jump_over_catch, after_catch);
1704
+ // The finally is no longer pending for enclosing returns once we emit its inline
1705
+ // (normal-path) copy below.
1706
+ if finally_body.is_some() {
1707
+ self.finally_stack.pop();
1708
+ }
1709
+ if let Some(finally) = finally_body {
1710
+ self.compile_statement(finally)?;
1711
+ }
1712
+ let catch_offset =
1713
+ catch_start.wrapping_sub(catch_offset_pos).wrapping_sub(3) as u16;
1714
+ self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
1715
+ self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
1716
+ }
1717
+ Statement::Import { .. } => {
1718
+ return Err(CompileError {
1719
+ message: "Import not supported in bytecode".to_string(),
1720
+ });
1721
+ }
1722
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1723
+ ExportDeclaration::Named(inner_stmt) => {
1724
+ self.compile_statement(inner_stmt.as_ref())?;
1725
+ }
1726
+ ExportDeclaration::Default(_) => {
1727
+ return Err(CompileError {
1728
+ message: "export default is not supported in bytecode".to_string(),
1729
+ });
1730
+ }
1731
+ },
1732
+ Statement::TypeAlias { .. }
1733
+ | Statement::DeclareVar { .. }
1734
+ | Statement::DeclareFun { .. } => {}
1735
+ }
1736
+ Ok(())
1737
+ }
1738
+
1739
+ fn compile_destructure(
1740
+ &mut self,
1741
+ pattern: &DestructPattern,
1742
+ mutable: bool,
1743
+ for_header_binding: bool,
1744
+ ) -> Result<(), CompileError> {
1745
+ let decl_op = if for_header_binding {
1746
+ Opcode::DeclareVarPlain
1747
+ } else {
1748
+ Opcode::DeclareVar
1749
+ };
1750
+ match pattern {
1751
+ DestructPattern::Array(elements) => {
1752
+ for (i, elem) in elements.iter().enumerate() {
1753
+ match elem {
1754
+ Some(DestructElement::Ident(name, _)) => {
1755
+ self.emit(Opcode::Dup);
1756
+ let idx = self.constant_idx(Constant::Number(i as f64));
1757
+ self.emit(Opcode::LoadConst);
1758
+ self.chunk.write_u16(idx);
1759
+ self.emit(Opcode::GetIndex);
1760
+ let idx = self.name_idx(name);
1761
+ self.emit_u16(decl_op, idx);
1762
+ self.scope
1763
+ .last_mut()
1764
+ .unwrap()
1765
+ .insert(Arc::clone(name), false);
1766
+ }
1767
+ // Array hole `[a, , c]`: position is skipped, no binding emitted.
1768
+ None => {}
1769
+ // Nested pattern `[[a, b], c]` or `[{x}, y]`: push source[i] and recurse.
1770
+ // compile_destructure is stack-balanced (consumes exactly the value it
1771
+ // destructures), so the source array beneath stays intact.
1772
+ Some(DestructElement::Pattern(sub)) => {
1773
+ self.emit(Opcode::Dup);
1774
+ let idx = self.constant_idx(Constant::Number(i as f64));
1775
+ self.emit(Opcode::LoadConst);
1776
+ self.chunk.write_u16(idx);
1777
+ self.emit(Opcode::GetIndex);
1778
+ self.compile_destructure(sub, mutable, for_header_binding)?;
1779
+ }
1780
+ // Rest `[a, ...rest]`: rest = source.slice(i). Use GetMember (not GetIndex)
1781
+ // so the array's `slice` method resolves via get_member; GetIndex rejects
1782
+ // string keys on arrays.
1783
+ Some(DestructElement::Rest(name, _)) => {
1784
+ self.emit(Opcode::Dup);
1785
+ let slice_idx = self.name_idx(&Arc::from("slice"));
1786
+ self.emit_u16(Opcode::GetMember, slice_idx);
1787
+ let idx = self.constant_idx(Constant::Number(i as f64));
1788
+ self.emit(Opcode::LoadConst);
1789
+ self.chunk.write_u16(idx);
1790
+ self.emit_u16(Opcode::Call, 1);
1791
+ let nidx = self.name_idx(name);
1792
+ self.emit_u16(decl_op, nidx);
1793
+ self.scope
1794
+ .last_mut()
1795
+ .unwrap()
1796
+ .insert(Arc::clone(name), false);
1797
+ }
1798
+ }
1799
+ }
1800
+ self.emit(Opcode::Pop);
1801
+ }
1802
+ DestructPattern::Object(props) => {
1803
+ for prop in props {
1804
+ self.emit(Opcode::Dup);
1805
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(&prop.key)));
1806
+ self.emit(Opcode::LoadConst);
1807
+ self.chunk.write_u16(key_idx);
1808
+ self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
1809
+ match &prop.value {
1810
+ DestructElement::Ident(name, _) => {
1811
+ let idx = self.name_idx(name);
1812
+ self.emit_u16(decl_op, idx);
1813
+ if mutable {
1814
+ self.scope
1815
+ .last_mut()
1816
+ .unwrap()
1817
+ .insert(Arc::clone(name), false);
1818
+ }
1819
+ }
1820
+ // Nested value `{ outer: { inner } }` or `{ arr: [a, b] }`: obj[key] is
1821
+ // already on the stack (GetIndex above); recurse to destructure it.
1822
+ DestructElement::Pattern(sub) => {
1823
+ self.compile_destructure(sub, mutable, for_header_binding)?;
1824
+ }
1825
+ // `{ ...rest }` needs the set of *remaining* keys; not yet supported.
1826
+ DestructElement::Rest(_, _) => {
1827
+ return Err(CompileError {
1828
+ message: "Object rest destructuring not yet supported".to_string(),
1829
+ });
1830
+ }
1831
+ }
1832
+ }
1833
+ self.emit(Opcode::Pop);
1834
+ }
1835
+ }
1836
+ Ok(())
1837
+ }
1838
+
1839
+ fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
1840
+ self.mark_line(expr.span());
1841
+ match expr {
1842
+ Expr::Literal { value, .. } => {
1843
+ let c = match value {
1844
+ Literal::Number(n) => Constant::Number(*n),
1845
+ Literal::String(s) => Constant::String(Arc::clone(s)),
1846
+ Literal::Bool(b) => Constant::Bool(*b),
1847
+ Literal::Null => Constant::Null,
1848
+ };
1849
+ let idx = self.constant_idx(c);
1850
+ self.emit(Opcode::LoadConst);
1851
+ self.chunk.write_u16(idx);
1852
+ }
1853
+ Expr::Ident { name, .. } => {
1854
+ // `resolve_slot` checks BOTH the simple param-only map and the general scope stack.
1855
+ self.emit_var_load(name);
1856
+ }
1857
+ Expr::Binary {
1858
+ left, op, right, ..
1859
+ } => {
1860
+ match op {
1861
+ BinOp::And => {
1862
+ // Short-circuit: a && b => if !a then a else b
1863
+ self.compile_expr(left)?;
1864
+ self.emit(Opcode::Dup);
1865
+ let jump_shortcut = self.emit_jump(Opcode::JumpIfFalse);
1866
+ self.compile_expr(right)?; // left still on stack from Dup
1867
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::And));
1868
+ let jump_end = self.emit_jump(Opcode::Jump);
1869
+ self.patch_jump(jump_shortcut, self.chunk.code.len());
1870
+ self.patch_jump(jump_end, self.chunk.code.len());
1871
+ }
1872
+ BinOp::Or => {
1873
+ // Short-circuit: a || b => if a then a else b
1874
+ self.compile_expr(left)?;
1875
+ self.emit(Opcode::Dup);
1876
+ let jump_eval_right = self.emit_jump(Opcode::JumpIfFalse);
1877
+ let jump_end = self.emit_jump(Opcode::Jump);
1878
+ self.patch_jump(jump_eval_right, self.chunk.code.len());
1879
+ self.emit(Opcode::Pop); // discard falsy left
1880
+ self.compile_expr(right)?;
1881
+ self.patch_jump(jump_end, self.chunk.code.len());
1882
+ }
1883
+ _ => {
1884
+ self.compile_expr(left)?;
1885
+ self.compile_expr(right)?;
1886
+ self.emit_u8(Opcode::BinOp, binop_to_u8(*op));
1887
+ }
1888
+ }
1889
+ }
1890
+ Expr::Unary { op, operand, .. } => {
1891
+ self.compile_expr(operand)?;
1892
+ self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
1893
+ }
1894
+ Expr::Call { callee, args, .. } => {
1895
+ // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
1896
+ if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
1897
+ && args.len() == 1
1898
+ && matches!(args[0], CallArg::Expr(_))
1899
+ {
1900
+ if let (
1901
+ Expr::Member {
1902
+ object,
1903
+ prop: MemberProp::Name { name: key, .. },
1904
+ optional: false,
1905
+ ..
1906
+ },
1907
+ CallArg::Expr(cmp_expr),
1908
+ ) = (callee.as_ref(), &args[0])
1909
+ {
1910
+ if key.as_ref() == "sort" {
1911
+ if let Some(ascending) = Self::detect_numeric_sort_comparator(cmp_expr)
1912
+ {
1913
+ self.compile_expr(object)?;
1914
+ self.emit_u8(
1915
+ Opcode::ArraySortNumeric,
1916
+ if ascending { 0 } else { 1 },
1917
+ );
1918
+ return Ok(());
1919
+ }
1920
+ if let Some((prop, ascending)) =
1921
+ Self::detect_property_sort_comparator(cmp_expr)
1922
+ {
1923
+ self.compile_expr(object)?;
1924
+ let prop_idx = self.constant_idx(Constant::String(prop));
1925
+ self.emit(Opcode::ArraySortByProperty);
1926
+ self.chunk.write_u16(prop_idx);
1927
+ self.chunk.write_u16(if ascending { 0 } else { 1 });
1928
+ return Ok(());
1929
+ }
1930
+ }
1931
+ if key.as_ref() == "map" {
1932
+ if let Some(simple) = Self::detect_simple_map_callback(cmp_expr) {
1933
+ self.compile_expr(object)?;
1934
+ match simple {
1935
+ SimpleMapResult::Identity => {
1936
+ self.emit(Opcode::ArrayMapIdentity);
1937
+ }
1938
+ SimpleMapResult::BinOp(op, c, param_left) => {
1939
+ let const_idx = self.constant_idx(c);
1940
+ self.emit(Opcode::ArrayMapBinOp);
1941
+ self.chunk.write_u8(binop_to_u8(op));
1942
+ self.chunk.write_u16(const_idx);
1943
+ self.chunk.write_u8(if param_left { 0 } else { 1 });
1944
+ }
1945
+ }
1946
+ return Ok(());
1947
+ }
1948
+ }
1949
+ if key.as_ref() == "filter" {
1950
+ if let Some((op, const_val, param_left)) =
1951
+ Self::detect_simple_filter_callback(cmp_expr)
1952
+ {
1953
+ self.compile_expr(object)?;
1954
+ let const_idx = self.constant_idx(const_val);
1955
+ self.emit(Opcode::ArrayFilterBinOp);
1956
+ self.chunk.write_u8(binop_to_u8(op));
1957
+ self.chunk.write_u16(const_idx);
1958
+ self.chunk.write_u8(if param_left { 0 } else { 1 });
1959
+ return Ok(());
1960
+ }
1961
+ }
1962
+ }
1963
+ }
1964
+ let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
1965
+ if has_spread {
1966
+ // Build args array [a, ...b, c], then callee, then CallSpread
1967
+ self.emit_u16(Opcode::NewArray, 0);
1968
+ for arg in args {
1969
+ match arg {
1970
+ CallArg::Expr(e) => {
1971
+ self.compile_expr(e)?;
1972
+ self.emit_u16(Opcode::NewArray, 1);
1973
+ self.emit(Opcode::ConcatArray);
1974
+ }
1975
+ CallArg::Spread(expr) => {
1976
+ self.compile_expr(expr)?;
1977
+ self.emit(Opcode::ConcatArray);
1978
+ }
1979
+ }
1980
+ }
1981
+ self.compile_expr(callee)?;
1982
+ self.emit(Opcode::CallSpread);
1983
+ } else {
1984
+ // Self-recursion fast path: `name(args)` where `name` is this function's own
1985
+ // provably-stable binding → `SelfCall` (no callee LoadVar, no closure dispatch;
1986
+ // the JIT lowers it to a native recursive call). `self_fn_name` is only `Some`
1987
+ // when the compiler proved `name` isn't shadowed or rebound (see FunDecl).
1988
+ let is_self_call = matches!(
1989
+ callee.as_ref(),
1990
+ Expr::Ident { name, .. } if self.self_fn_name.as_deref() == Some(name.as_ref())
1991
+ );
1992
+ if is_self_call {
1993
+ for arg in args {
1994
+ if let CallArg::Expr(e) = arg {
1995
+ self.compile_expr(e)?;
1996
+ }
1997
+ }
1998
+ self.emit_u16(Opcode::SelfCall, args.len() as u16);
1999
+ } else {
2000
+ self.compile_expr(callee)?;
2001
+ for arg in args {
2002
+ if let CallArg::Expr(e) = arg {
2003
+ self.compile_expr(e)?;
2004
+ }
2005
+ }
2006
+ self.emit_u16(Opcode::Call, args.len() as u16);
2007
+ }
2008
+ }
2009
+ }
2010
+ Expr::Member {
2011
+ object,
2012
+ prop,
2013
+ optional,
2014
+ ..
2015
+ } => {
2016
+ self.compile_expr(object)?;
2017
+ if *optional {
2018
+ self.emit(Opcode::Dup);
2019
+ let null_idx = self.constant_idx(Constant::Null);
2020
+ self.emit(Opcode::LoadConst);
2021
+ self.chunk.write_u16(null_idx);
2022
+ self.emit_u8(Opcode::BinOp, 8);
2023
+ let jump_to_null = self.emit_jump(Opcode::JumpIfFalse);
2024
+ let jump_to_get_instr = self.chunk.code.len();
2025
+ let jump_to_get = self.emit_jump(Opcode::Jump);
2026
+ self.patch_jump(jump_to_null, jump_to_get_instr);
2027
+ self.emit(Opcode::Pop);
2028
+ self.emit(Opcode::LoadConst);
2029
+ self.chunk.write_u16(null_idx);
2030
+ let jump_end = self.emit_jump(Opcode::Jump);
2031
+ self.patch_jump(jump_to_get, self.chunk.code.len());
2032
+ match prop {
2033
+ MemberProp::Name { name: key, .. } => {
2034
+ let idx = self.name_idx(key);
2035
+ self.emit_u16(Opcode::GetMemberOptional, idx);
2036
+ }
2037
+ MemberProp::Expr(e) => {
2038
+ self.compile_expr(e)?;
2039
+ self.emit(Opcode::GetIndex);
2040
+ }
2041
+ }
2042
+ self.patch_jump(jump_end, self.chunk.code.len());
2043
+ } else {
2044
+ match prop {
2045
+ MemberProp::Name { name: key, .. } => {
2046
+ let idx = self.name_idx(key);
2047
+ self.emit_u16(Opcode::GetMember, idx);
2048
+ }
2049
+ MemberProp::Expr(e) => {
2050
+ self.compile_expr(e)?;
2051
+ self.emit(Opcode::GetIndex);
2052
+ }
2053
+ }
2054
+ }
2055
+ }
2056
+ Expr::Index { object, index, .. } => {
2057
+ self.compile_expr(object)?;
2058
+ self.compile_expr(index)?;
2059
+ self.emit(Opcode::GetIndex);
2060
+ }
2061
+ Expr::Conditional {
2062
+ cond,
2063
+ then_branch,
2064
+ else_branch,
2065
+ ..
2066
+ } => {
2067
+ self.compile_expr(cond)?;
2068
+ let jump_else = self.emit_jump(Opcode::JumpIfFalse);
2069
+ // JumpIfFalse pops condition when taking then; when taking else it also pops
2070
+ self.compile_expr(then_branch)?;
2071
+ let jump_end = self.emit_jump(Opcode::Jump);
2072
+ self.patch_jump(jump_else, self.chunk.code.len());
2073
+ // no Pop: condition was already popped by JumpIfFalse
2074
+ self.compile_expr(else_branch)?;
2075
+ self.patch_jump(jump_end, self.chunk.code.len());
2076
+ }
2077
+ Expr::NullishCoalesce { left, right, .. } => {
2078
+ self.compile_expr(left)?;
2079
+ self.emit(Opcode::Dup);
2080
+ let idx = self.constant_idx(Constant::Null);
2081
+ self.emit(Opcode::LoadConst);
2082
+ self.chunk.write_u16(idx);
2083
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictNe));
2084
+ let jump_to_right = self.emit_jump(Opcode::JumpIfFalse);
2085
+ let jump_end = self.emit_jump(Opcode::Jump);
2086
+ self.patch_jump(jump_to_right, self.chunk.code.len());
2087
+ self.emit(Opcode::Pop);
2088
+ self.compile_expr(right)?;
2089
+ self.patch_jump(jump_end, self.chunk.code.len());
2090
+ }
2091
+ Expr::Array { elements, .. } => {
2092
+ let has_spread = elements
2093
+ .iter()
2094
+ .any(|e| matches!(e, ArrayElement::Spread(_)));
2095
+ if has_spread {
2096
+ // Build array incrementally: start with [], concat each element
2097
+ self.emit_u16(Opcode::NewArray, 0);
2098
+ for elem in elements {
2099
+ match elem {
2100
+ ArrayElement::Expr(e) => {
2101
+ self.compile_expr(e)?;
2102
+ self.emit_u16(Opcode::NewArray, 1);
2103
+ self.emit(Opcode::ConcatArray);
2104
+ }
2105
+ ArrayElement::Spread(expr) => {
2106
+ self.compile_expr(expr)?;
2107
+ self.emit(Opcode::ConcatArray);
2108
+ }
2109
+ }
2110
+ }
2111
+ } else {
2112
+ for elem in elements {
2113
+ if let ArrayElement::Expr(e) = elem {
2114
+ self.compile_expr(e)?;
2115
+ }
2116
+ }
2117
+ self.emit_u16(Opcode::NewArray, elements.len() as u16);
2118
+ }
2119
+ }
2120
+ Expr::Object { props, .. } => {
2121
+ let has_spread = props.iter().any(|p| matches!(p, ObjectProp::Spread(_)));
2122
+ if has_spread {
2123
+ self.emit_u16(Opcode::NewObject, 0); // start with {}
2124
+ for prop in props {
2125
+ match prop {
2126
+ ObjectProp::KeyValue(k, v) => {
2127
+ let idx = self.constant_idx(Constant::String(Arc::clone(k)));
2128
+ self.emit(Opcode::LoadConst);
2129
+ self.chunk.write_u16(idx);
2130
+ self.compile_expr(v)?;
2131
+ self.emit_u16(Opcode::NewObject, 1);
2132
+ self.emit(Opcode::MergeObject);
2133
+ }
2134
+ ObjectProp::Spread(expr) => {
2135
+ self.compile_expr(expr)?;
2136
+ self.emit(Opcode::MergeObject);
2137
+ }
2138
+ }
2139
+ }
2140
+ } else {
2141
+ for prop in props {
2142
+ if let ObjectProp::KeyValue(k, v) = prop {
2143
+ let idx = self.constant_idx(Constant::String(Arc::clone(k)));
2144
+ self.emit(Opcode::LoadConst);
2145
+ self.chunk.write_u16(idx);
2146
+ self.compile_expr(v)?;
2147
+ }
2148
+ }
2149
+ self.emit_u16(Opcode::NewObject, props.len() as u16);
2150
+ }
2151
+ }
2152
+ Expr::Assign { name, value, .. } => {
2153
+ self.compile_expr(value)?;
2154
+ self.emit_var_store(name);
2155
+ self.emit_var_load(name); // assign yields value
2156
+ }
2157
+ Expr::TypeOf { operand, .. } => {
2158
+ let typeof_idx = self.name_idx(&Arc::from("typeof"));
2159
+ self.emit_u16(Opcode::LoadGlobal, typeof_idx);
2160
+ self.compile_expr(operand)?;
2161
+ self.emit_u16(Opcode::Call, 1);
2162
+ }
2163
+ Expr::ArrowFunction { params, body, .. } => {
2164
+ let formal_len = params.len();
2165
+ let (param_names, slots) = Self::plan_function_params(params)?;
2166
+ let simple_slots = simple_fn_slots(params, false, |pset| match body {
2167
+ ArrowBody::Expr(e) => expr_is_param_only(e, pset),
2168
+ ArrowBody::Block(s) => stmt_is_param_only(s, pset),
2169
+ });
2170
+ let mut inner = Chunk::new();
2171
+ inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
2172
+ for p in &param_names {
2173
+ inner.add_name(Arc::clone(p));
2174
+ }
2175
+ inner.param_count = param_names.len() as u16;
2176
+ if simple_slots.is_some() {
2177
+ inner.slot_based = true;
2178
+ inner.num_slots = param_names.len() as u16;
2179
+ }
2180
+ let mut inner_comp = Compiler::new(&mut inner, false);
2181
+ if let Some(map) = simple_slots {
2182
+ inner_comp.slot_ctx = Some(map);
2183
+ } else {
2184
+ inner_comp.scope = vec![param_names
2185
+ .iter()
2186
+ .map(|n| (Arc::clone(n), false))
2187
+ .collect::<HashMap<_, _>>()];
2188
+ inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2189
+ }
2190
+ inner_comp.emit_param_defaults_prologue(params)?;
2191
+ match body {
2192
+ ArrowBody::Expr(e) => {
2193
+ inner_comp.compile_expr(e)?;
2194
+ inner_comp.emit(Opcode::Return);
2195
+ }
2196
+ ArrowBody::Block(s) => {
2197
+ inner_comp.compile_statement(s)?;
2198
+ let idx = inner_comp.constant_idx(Constant::Null);
2199
+ inner_comp.emit(Opcode::LoadConst);
2200
+ inner_comp.chunk.write_u16(idx);
2201
+ inner_comp.emit(Opcode::Return);
2202
+ }
2203
+ }
2204
+ let nested_idx = self.chunk.add_nested(inner);
2205
+ let idx = self.constant_idx(Constant::Closure(nested_idx));
2206
+ self.emit(Opcode::LoadConst);
2207
+ self.chunk.write_u16(idx);
2208
+ }
2209
+ Expr::TemplateLiteral { quasis, exprs, .. } => {
2210
+ if exprs.is_empty() {
2211
+ let s = quasis[0].to_string();
2212
+ let idx = self.constant_idx(Constant::String(Arc::from(s)));
2213
+ self.emit(Opcode::LoadConst);
2214
+ self.chunk.write_u16(idx);
2215
+ } else {
2216
+ // Interleave quasis and exprs: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + quasi[n]
2217
+ let first = quasis[0].to_string();
2218
+ let idx = self.constant_idx(Constant::String(Arc::from(first)));
2219
+ self.emit(Opcode::LoadConst);
2220
+ self.chunk.write_u16(idx);
2221
+ for (i, expr) in exprs.iter().enumerate() {
2222
+ self.compile_expr(expr)?;
2223
+ self.emit_u8(Opcode::BinOp, 0); // Add (string concat)
2224
+ let quasi_s = quasis[i + 1].to_string();
2225
+ let qidx = self.constant_idx(Constant::String(Arc::from(quasi_s)));
2226
+ self.emit(Opcode::LoadConst);
2227
+ self.chunk.write_u16(qidx);
2228
+ self.emit_u8(Opcode::BinOp, 0); // Add
2229
+ }
2230
+ }
2231
+ }
2232
+ Expr::PostfixInc { name, .. } => {
2233
+ let one = self.constant_idx(Constant::Number(1.0));
2234
+ self.emit_var_load(name);
2235
+ self.emit(Opcode::Dup);
2236
+ self.emit(Opcode::LoadConst);
2237
+ self.chunk.write_u16(one);
2238
+ self.emit_u8(Opcode::BinOp, 0);
2239
+ self.emit_var_store(name);
2240
+ }
2241
+ Expr::PostfixDec { name, .. } => {
2242
+ let one = self.constant_idx(Constant::Number(1.0));
2243
+ self.emit_var_load(name);
2244
+ self.emit(Opcode::Dup);
2245
+ self.emit(Opcode::LoadConst);
2246
+ self.chunk.write_u16(one);
2247
+ self.emit_u8(Opcode::BinOp, 1);
2248
+ self.emit_var_store(name);
2249
+ }
2250
+ Expr::PrefixInc { name, .. } => {
2251
+ let one = self.constant_idx(Constant::Number(1.0));
2252
+ self.emit_var_load(name);
2253
+ self.emit(Opcode::LoadConst);
2254
+ self.chunk.write_u16(one);
2255
+ self.emit_u8(Opcode::BinOp, 0);
2256
+ self.emit(Opcode::Dup);
2257
+ self.emit_var_store(name);
2258
+ }
2259
+ Expr::PrefixDec { name, .. } => {
2260
+ let one = self.constant_idx(Constant::Number(1.0));
2261
+ self.emit_var_load(name);
2262
+ self.emit(Opcode::LoadConst);
2263
+ self.chunk.write_u16(one);
2264
+ self.emit_u8(Opcode::BinOp, 1);
2265
+ self.emit(Opcode::Dup);
2266
+ self.emit_var_store(name);
2267
+ }
2268
+ Expr::CompoundAssign {
2269
+ name, op, value, ..
2270
+ } => {
2271
+ self.emit_var_load(name);
2272
+ self.compile_expr(value)?;
2273
+ self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
2274
+ self.emit(Opcode::Dup);
2275
+ self.emit_var_store(name);
2276
+ }
2277
+ Expr::MemberAssign {
2278
+ object,
2279
+ prop,
2280
+ value,
2281
+ ..
2282
+ } => {
2283
+ self.compile_expr(object)?;
2284
+ self.compile_expr(value)?;
2285
+ let idx = self.name_idx(prop);
2286
+ self.emit_u16(Opcode::SetMember, idx); // SetMember pops obj, val and pushes val back
2287
+ }
2288
+ Expr::IndexAssign {
2289
+ object,
2290
+ index,
2291
+ value,
2292
+ ..
2293
+ } => {
2294
+ self.compile_expr(object)?;
2295
+ self.compile_expr(index)?;
2296
+ self.compile_expr(value)?;
2297
+ self.emit(Opcode::Dup); // leave copy for assignment expression result
2298
+ self.emit(Opcode::SetIndex);
2299
+ }
2300
+ Expr::NativeModuleLoad {
2301
+ spec, export_name, ..
2302
+ } => {
2303
+ let spec_idx = self.constant_idx(Constant::String(Arc::clone(spec)));
2304
+ let export_idx = self.constant_idx(Constant::String(Arc::clone(export_name)));
2305
+ self.emit(Opcode::LoadNativeExport);
2306
+ self.chunk.write_u16(spec_idx);
2307
+ self.chunk.write_u16(export_idx);
2308
+ }
2309
+ Expr::JsxElement {
2310
+ tag,
2311
+ props,
2312
+ children,
2313
+ ..
2314
+ } => {
2315
+ self.compile_jsx_element(tag, props, children)?;
2316
+ }
2317
+ Expr::JsxFragment { children, .. } => {
2318
+ self.compile_jsx_fragment(children)?;
2319
+ }
2320
+ Expr::Await { operand, .. } => {
2321
+ // await expr => evaluate operand, then VM Opcode::AwaitPromise (throw on reject).
2322
+ self.compile_expr(operand)?;
2323
+ self.emit(Opcode::AwaitPromise);
2324
+ }
2325
+ Expr::Delete { target, .. } => {
2326
+ // `delete obj.prop` / `delete obj[key]` → push [obj, key], then DeleteIndex
2327
+ // pops both, removes the property, and pushes `true`. Deleting anything that
2328
+ // isn't a property reference is a no-op that still yields `true` (JS).
2329
+ match target.as_ref() {
2330
+ Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
2331
+ self.compile_expr(object)?;
2332
+ let idx = self.constant_idx(Constant::String(Arc::clone(name)));
2333
+ self.emit(Opcode::LoadConst);
2334
+ self.chunk.write_u16(idx);
2335
+ self.emit(Opcode::DeleteIndex);
2336
+ }
2337
+ Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
2338
+ self.compile_expr(object)?;
2339
+ self.compile_expr(key)?;
2340
+ self.emit(Opcode::DeleteIndex);
2341
+ }
2342
+ Expr::Index { object, index, .. } => {
2343
+ self.compile_expr(object)?;
2344
+ self.compile_expr(index)?;
2345
+ self.emit(Opcode::DeleteIndex);
2346
+ }
2347
+ _ => {
2348
+ let idx = self.constant_idx(Constant::Bool(true));
2349
+ self.emit(Opcode::LoadConst);
2350
+ self.chunk.write_u16(idx);
2351
+ }
2352
+ }
2353
+ }
2354
+ Expr::LogicalAssign {
2355
+ name, op, value, ..
2356
+ } => {
2357
+ match op {
2358
+ LogicalAssignOp::OrOr => {
2359
+ // ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
2360
+ self.emit_var_load(name);
2361
+ self.emit(Opcode::Dup);
2362
+ let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
2363
+ let j_end = self.emit_jump(Opcode::Jump);
2364
+ self.patch_jump(j_rhs, self.chunk.code.len());
2365
+ self.emit(Opcode::Pop);
2366
+ self.compile_expr(value)?;
2367
+ self.emit_var_store(name);
2368
+ self.emit_var_load(name);
2369
+ let end = self.chunk.code.len();
2370
+ self.patch_jump(j_end, end);
2371
+ }
2372
+ LogicalAssignOp::AndAnd => {
2373
+ // &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
2374
+ self.emit_var_load(name);
2375
+ self.emit(Opcode::Dup);
2376
+ let j_short = self.emit_jump(Opcode::JumpIfFalse);
2377
+ self.emit(Opcode::Pop);
2378
+ self.compile_expr(value)?;
2379
+ self.emit_var_store(name);
2380
+ self.emit_var_load(name);
2381
+ let j_end = self.emit_jump(Opcode::Jump);
2382
+ let end = self.chunk.code.len();
2383
+ self.patch_jump(j_short, end);
2384
+ self.patch_jump(j_end, end);
2385
+ }
2386
+ LogicalAssignOp::Nullish => {
2387
+ // ??= : assign only when current === null (matches interpreter)
2388
+ let null_c = self.constant_idx(Constant::Null);
2389
+ self.emit_var_load(name);
2390
+ self.emit(Opcode::Dup);
2391
+ self.emit(Opcode::LoadConst);
2392
+ self.chunk.write_u16(null_c);
2393
+ self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictEq));
2394
+ let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
2395
+ self.emit(Opcode::Pop);
2396
+ self.compile_expr(value)?;
2397
+ self.emit_var_store(name);
2398
+ self.emit_var_load(name);
2399
+ let j_end = self.emit_jump(Opcode::Jump);
2400
+ let end = self.chunk.code.len();
2401
+ self.patch_jump(j_not_null, end);
2402
+ self.patch_jump(j_end, end);
2403
+ }
2404
+ }
2405
+ }
2406
+ Expr::New { callee, args, .. } => {
2407
+ let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
2408
+ if has_spread {
2409
+ self.emit_u16(Opcode::NewArray, 0);
2410
+ for arg in args {
2411
+ match arg {
2412
+ CallArg::Expr(e) => {
2413
+ self.compile_expr(e)?;
2414
+ self.emit_u16(Opcode::NewArray, 1);
2415
+ self.emit(Opcode::ConcatArray);
2416
+ }
2417
+ CallArg::Spread(expr) => {
2418
+ self.compile_expr(expr)?;
2419
+ self.emit(Opcode::ConcatArray);
2420
+ }
2421
+ }
2422
+ }
2423
+ self.compile_expr(callee)?;
2424
+ self.emit(Opcode::ConstructSpread);
2425
+ } else {
2426
+ self.compile_expr(callee)?;
2427
+ for arg in args {
2428
+ if let CallArg::Expr(e) = arg {
2429
+ self.compile_expr(e)?;
2430
+ }
2431
+ }
2432
+ self.emit_u16(Opcode::Construct, args.len() as u16);
2433
+ }
2434
+ }
2435
+ }
2436
+ Ok(())
2437
+ }
2438
+
2439
+ fn compile_jsx_element(
2440
+ &mut self,
2441
+ tag: &Arc<str>,
2442
+ props: &[JsxProp],
2443
+ children: &[JsxChild],
2444
+ ) -> Result<(), CompileError> {
2445
+ let h_idx = self.name_idx(&Arc::from("h"));
2446
+ self.emit_u16(Opcode::LoadGlobal, h_idx);
2447
+ let tag_str = tag.as_ref();
2448
+ let is_component = tag_str
2449
+ .chars()
2450
+ .next()
2451
+ .map(|c| c.is_uppercase())
2452
+ .unwrap_or(false);
2453
+ if is_component {
2454
+ let tag_idx = self.name_idx(tag);
2455
+ self.emit_u16(Opcode::LoadGlobal, tag_idx);
2456
+ } else {
2457
+ let tag_const = self.constant_idx(Constant::String(Arc::from(tag_str)));
2458
+ self.emit(Opcode::LoadConst);
2459
+ self.chunk.write_u16(tag_const);
2460
+ }
2461
+ self.compile_jsx_props(props)?;
2462
+ self.compile_jsx_children(children)?;
2463
+ self.emit_u16(Opcode::Call, 3);
2464
+ Ok(())
2465
+ }
2466
+
2467
+ fn compile_jsx_fragment(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
2468
+ let h_idx = self.name_idx(&Arc::from("h"));
2469
+ self.emit_u16(Opcode::LoadGlobal, h_idx);
2470
+ let fragment_idx = self.name_idx(&Arc::from("Fragment"));
2471
+ self.emit_u16(Opcode::LoadGlobal, fragment_idx);
2472
+ let null_idx = self.constant_idx(Constant::Null);
2473
+ self.emit(Opcode::LoadConst);
2474
+ self.chunk.write_u16(null_idx);
2475
+ self.compile_jsx_children(children)?;
2476
+ self.emit_u16(Opcode::Call, 3);
2477
+ Ok(())
2478
+ }
2479
+
2480
+ fn compile_jsx_props(&mut self, props: &[JsxProp]) -> Result<(), CompileError> {
2481
+ if props.is_empty() {
2482
+ let null_idx = self.constant_idx(Constant::Null);
2483
+ self.emit(Opcode::LoadConst);
2484
+ self.chunk.write_u16(null_idx);
2485
+ return Ok(());
2486
+ }
2487
+ let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
2488
+ if has_spread {
2489
+ self.emit_u16(Opcode::NewObject, 0);
2490
+ for prop in props {
2491
+ match prop {
2492
+ JsxProp::Attr { name, value } => {
2493
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
2494
+ self.emit(Opcode::LoadConst);
2495
+ self.chunk.write_u16(key_idx);
2496
+ match value {
2497
+ JsxAttrValue::String(s) => {
2498
+ let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
2499
+ self.emit(Opcode::LoadConst);
2500
+ self.chunk.write_u16(val_idx);
2501
+ }
2502
+ JsxAttrValue::Expr(e) => self.compile_expr(e)?,
2503
+ JsxAttrValue::ImplicitTrue => {
2504
+ let true_idx = self.constant_idx(Constant::Bool(true));
2505
+ self.emit(Opcode::LoadConst);
2506
+ self.chunk.write_u16(true_idx);
2507
+ }
2508
+ }
2509
+ self.emit_u16(Opcode::NewObject, 1);
2510
+ self.emit(Opcode::MergeObject);
2511
+ }
2512
+ JsxProp::Spread(expr) => {
2513
+ self.compile_expr(expr)?;
2514
+ self.emit(Opcode::MergeObject);
2515
+ }
2516
+ }
2517
+ }
2518
+ } else {
2519
+ for prop in props {
2520
+ if let JsxProp::Attr { name, value } = prop {
2521
+ let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
2522
+ self.emit(Opcode::LoadConst);
2523
+ self.chunk.write_u16(key_idx);
2524
+ match value {
2525
+ JsxAttrValue::String(s) => {
2526
+ let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
2527
+ self.emit(Opcode::LoadConst);
2528
+ self.chunk.write_u16(val_idx);
2529
+ }
2530
+ JsxAttrValue::Expr(e) => self.compile_expr(e)?,
2531
+ JsxAttrValue::ImplicitTrue => {
2532
+ let true_idx = self.constant_idx(Constant::Bool(true));
2533
+ self.emit(Opcode::LoadConst);
2534
+ self.chunk.write_u16(true_idx);
2535
+ }
2536
+ }
2537
+ }
2538
+ }
2539
+ self.emit_u16(Opcode::NewObject, props.len() as u16);
2540
+ }
2541
+ Ok(())
2542
+ }
2543
+
2544
+ fn compile_jsx_children(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
2545
+ for child in children {
2546
+ match child {
2547
+ JsxChild::Text(s) => {
2548
+ let idx = self.constant_idx(Constant::String(Arc::clone(s)));
2549
+ self.emit(Opcode::LoadConst);
2550
+ self.chunk.write_u16(idx);
2551
+ }
2552
+ JsxChild::Expr(e) => self.compile_expr(e)?,
2553
+ }
2554
+ }
2555
+ self.emit_u16(Opcode::NewArray, children.len() as u16);
2556
+ Ok(())
2557
+ }
2558
+ }
2559
+
2560
+ /// Compile a Tish program to bytecode (with peephole optimizations).
2561
+ pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
2562
+ compile_internal(program, true, false, None)
2563
+ }
2564
+
2565
+ /// Compile, tagging the chunk with a source file path so runtime errors can report
2566
+ /// `file:line` (issue #74). The line table is built during compilation and survives the
2567
+ /// in-place peephole pass; it is not serialized.
2568
+ pub fn compile_with_source(
2569
+ program: &Program,
2570
+ source: Option<std::sync::Arc<str>>,
2571
+ ) -> Result<Chunk, CompileError> {
2572
+ compile_internal(program, true, false, source)
2573
+ }
2574
+
2575
+ /// Compile without peephole optimizations (for --no-optimize).
2576
+ pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
2577
+ compile_internal(program, false, false, None)
2578
+ }
2579
+
2580
+ /// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
2581
+ pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
2582
+ compile_internal(program, true, true, None)
2583
+ }
2584
+
2585
+ /// Compile for REPL without peephole optimizations.
2586
+ pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
2587
+ compile_internal(program, false, true, None)
2588
+ }
2589
+
2590
+ fn compile_internal(
2591
+ program: &Program,
2592
+ peephole: bool,
2593
+ retain_last_expr: bool,
2594
+ source: Option<std::sync::Arc<str>>,
2595
+ ) -> Result<Chunk, CompileError> {
2596
+ let mut chunk = Chunk::new();
2597
+ chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
2598
+ let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
2599
+ compiler.compile_program(program)?;
2600
+ if peephole {
2601
+ crate::peephole::optimize(&mut chunk);
2602
+ }
2603
+ Ok(chunk)
2604
+ }