@tishlang/tish 1.13.2 → 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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +61 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -1,6 +1,6 @@
1
1
  //! AST to bytecode compiler.
2
2
 
3
- use std::collections::HashMap;
3
+ use std::collections::{HashMap, HashSet};
4
4
  use std::sync::Arc;
5
5
 
6
6
  use tishlang_ast::{
@@ -83,9 +83,505 @@ struct Compiler<'a> {
83
83
  block_depth: usize,
84
84
  /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
85
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)
86
549
  }
87
550
 
88
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
+
89
585
  fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
90
586
  Self {
91
587
  chunk,
@@ -95,9 +591,56 @@ impl<'a> Compiler<'a> {
95
591
  breakable_stack: Vec::new(),
96
592
  block_depth: 0,
97
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,
98
601
  }
99
602
  }
100
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
+
101
644
  fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
102
645
  let n = self.block_depth.saturating_sub(target_depth);
103
646
  for _ in 0..n {
@@ -107,6 +650,7 @@ impl<'a> Compiler<'a> {
107
650
 
108
651
  /// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
109
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
110
654
  fn plan_function_params(
111
655
  params: &[FunParam],
112
656
  ) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
@@ -154,6 +698,56 @@ impl<'a> Compiler<'a> {
154
698
  Ok(())
155
699
  }
156
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
+
157
751
  fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
158
752
  match stmt {
159
753
  Statement::VarDecl {
@@ -169,12 +763,17 @@ impl<'a> Compiler<'a> {
169
763
  self.emit(Opcode::LoadConst);
170
764
  self.chunk.write_u16(idx);
171
765
  }
172
- let idx = self.name_idx(name);
173
- self.emit_u16(Opcode::DeclareVarPlain, idx);
174
- self.scope
175
- .last_mut()
176
- .unwrap()
177
- .insert(Arc::clone(name), false);
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
+ }
178
777
  }
179
778
  Statement::VarDeclDestructure { pattern, init, .. } => {
180
779
  self.compile_expr(init)?;
@@ -197,6 +796,13 @@ impl<'a> Compiler<'a> {
197
796
  self.chunk.write_u8(op as u8);
198
797
  }
199
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
+
200
806
  fn emit_u8(&mut self, op: Opcode, v: u8) {
201
807
  self.chunk.write_u8(op as u8);
202
808
  self.chunk.write_u16(v as u16);
@@ -487,6 +1093,16 @@ impl<'a> Compiler<'a> {
487
1093
 
488
1094
  fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
489
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
+ }
490
1106
  let last_is_expr = self.retain_last_expr
491
1107
  && stmts
492
1108
  .last()
@@ -509,22 +1125,35 @@ impl<'a> Compiler<'a> {
509
1125
  self.emit(Opcode::LoadConst);
510
1126
  self.chunk.write_u16(idx);
511
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
+ }
512
1133
  Ok(())
513
1134
  }
514
1135
 
515
1136
  fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
1137
+ self.mark_line(stmt.span());
516
1138
  match stmt {
517
1139
  Statement::Block { statements, .. } => {
518
1140
  self.emit(Opcode::EnterBlock);
519
1141
  self.block_depth += 1;
520
- self.scope.push(HashMap::new());
1142
+ self.enter_block_scope();
521
1143
  for s in statements {
522
1144
  self.compile_statement(s)?;
523
1145
  }
524
- self.scope.pop();
1146
+ self.exit_block_scope();
525
1147
  self.emit(Opcode::ExitBlock);
526
1148
  self.block_depth -= 1;
527
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
+ }
528
1157
  Statement::VarDecl {
529
1158
  name,
530
1159
  init,
@@ -538,12 +1167,18 @@ impl<'a> Compiler<'a> {
538
1167
  self.emit(Opcode::LoadConst);
539
1168
  self.chunk.write_u16(idx);
540
1169
  }
541
- let idx = self.name_idx(name);
542
- self.emit_u16(Opcode::DeclareVar, idx);
543
- self.scope
544
- .last_mut()
545
- .unwrap()
546
- .insert(Arc::clone(name), false);
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
+ }
547
1182
  }
548
1183
  Statement::VarDeclDestructure { pattern, init, .. } => {
549
1184
  self.compile_expr(init)?;
@@ -570,6 +1205,14 @@ impl<'a> Compiler<'a> {
570
1205
  self.patch_jump(jump_end, self.chunk.code.len());
571
1206
  }
572
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
+ }
573
1216
  let start = self.chunk.code.len();
574
1217
  self.loop_stack.push(LoopInfo {
575
1218
  break_patches: Vec::new(),
@@ -595,6 +1238,9 @@ impl<'a> Compiler<'a> {
595
1238
  for p in info.break_patches {
596
1239
  self.patch_jump(p, end);
597
1240
  }
1241
+ for _ in &body_lets {
1242
+ self.emit(Opcode::LoopVarsEnd);
1243
+ }
598
1244
  }
599
1245
  Statement::For {
600
1246
  init,
@@ -603,10 +1249,22 @@ impl<'a> Compiler<'a> {
603
1249
  body,
604
1250
  ..
605
1251
  } => {
606
- self.scope.push(HashMap::new());
1252
+ self.enter_block_scope();
607
1253
  if let Some(i) = init {
608
1254
  self.compile_for_init_statement(i.as_ref())?;
609
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
+ }
610
1268
  let cond_start = self.chunk.code.len();
611
1269
  if let Some(c) = cond {
612
1270
  self.compile_expr(c)?;
@@ -641,8 +1299,13 @@ impl<'a> Compiler<'a> {
641
1299
  for p in info.break_patches {
642
1300
  self.patch_jump(p, end);
643
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
+ }
644
1307
  self.breakable_stack.pop();
645
- self.scope.pop();
1308
+ self.exit_block_scope();
646
1309
  }
647
1310
  Statement::ForOf {
648
1311
  name,
@@ -651,7 +1314,10 @@ impl<'a> Compiler<'a> {
651
1314
  ..
652
1315
  } => {
653
1316
  self.compile_expr(iterable)?;
654
- self.scope.push(HashMap::new());
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();
655
1321
  let arr_name = Arc::from("__forof_arr__");
656
1322
  let i_name = Arc::from("__forof_i__");
657
1323
  let len_name = Arc::from("__forof_len__");
@@ -677,11 +1343,21 @@ impl<'a> Compiler<'a> {
677
1343
  self.chunk.write_u16(zero_idx);
678
1344
  self.emit_u16(Opcode::DeclareVar, i_idx);
679
1345
  self.scope.last_mut().unwrap().insert(i_name.clone(), false);
680
- let loop_start = self.chunk.code.len();
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);
681
1357
  self.loop_stack.push(LoopInfo {
682
1358
  break_patches: Vec::new(),
683
1359
  continue_patches: Vec::new(),
684
- continue_is_forward_jump: false,
1360
+ continue_is_forward_jump: true,
685
1361
  });
686
1362
  self.breakable_stack.push(Breakable::Loop {
687
1363
  unwind_depth: self.block_depth,
@@ -695,31 +1371,32 @@ impl<'a> Compiler<'a> {
695
1371
  .unwrap()
696
1372
  .insert(Arc::clone(name), false);
697
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();
698
1376
  self.emit_u16(Opcode::LoadVar, i_idx);
699
1377
  let one_idx = self.constant_idx(Constant::Number(1.0));
700
1378
  self.emit(Opcode::LoadConst);
701
1379
  self.chunk.write_u16(one_idx);
702
1380
  self.emit_u8(Opcode::BinOp, 0);
703
1381
  self.emit_u16(Opcode::StoreVar, i_idx);
704
- self.emit_u16(Opcode::LoadVar, i_idx);
705
- self.emit_u16(Opcode::LoadVar, len_idx);
706
- self.emit_u8(Opcode::BinOp, 10);
707
- let jump_out = self.emit_jump(Opcode::JumpIfFalse);
708
- let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(loop_start);
709
- self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
710
- let end = self.chunk.code.len();
711
- self.patch_jump(jump_out, end);
712
1382
  let info = self.loop_stack.pop().unwrap();
713
1383
  self.breakable_stack.pop();
714
1384
  for p in info.continue_patches {
715
- self.patch_jump_back(p, loop_start);
1385
+ self.patch_jump(p, update_start);
716
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);
717
1391
  for p in info.break_patches {
718
1392
  self.patch_jump(p, end);
719
1393
  }
720
- self.scope.pop();
1394
+ self.emit(Opcode::LoopVarsEnd);
1395
+ self.exit_block_scope();
721
1396
  }
722
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.
723
1400
  if let Some(v) = value {
724
1401
  self.compile_expr(v)?;
725
1402
  } else {
@@ -727,6 +1404,7 @@ impl<'a> Compiler<'a> {
727
1404
  self.emit(Opcode::LoadConst);
728
1405
  self.chunk.write_u16(idx);
729
1406
  }
1407
+ self.emit_pending_finallys()?;
730
1408
  self.emit(Opcode::Return);
731
1409
  }
732
1410
  Statement::Break { .. } => {
@@ -794,7 +1472,19 @@ impl<'a> Compiler<'a> {
794
1472
  } => {
795
1473
  let formal_len = params.len();
796
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
+ };
797
1486
  let mut inner = Chunk::new();
1487
+ inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
798
1488
  if let Some(rp) = rest_param {
799
1489
  param_names.push(Arc::clone(&rp.name));
800
1490
  inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
@@ -803,17 +1493,53 @@ impl<'a> Compiler<'a> {
803
1493
  inner.add_name(Arc::clone(p));
804
1494
  }
805
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
+ }
806
1500
  let mut inner_comp = Compiler::new(&mut inner, false);
807
- inner_comp.scope = vec![param_names
808
- .iter()
809
- .map(|n| (Arc::clone(n), false))
810
- .collect::<HashMap<_, _>>()];
811
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
812
- inner_comp.compile_statement(body)?;
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
+ }
813
1535
  inner_comp.emit(Opcode::LoadConst);
814
1536
  let idx = inner_comp.constant_idx(Constant::Null);
815
1537
  inner_comp.chunk.write_u16(idx);
816
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
+ }
817
1543
  let nested_idx = self.chunk.add_nested(inner);
818
1544
  self.emit(Opcode::LoadConst);
819
1545
  let idx = self.constant_idx(Constant::Closure(nested_idx));
@@ -826,6 +1552,11 @@ impl<'a> Compiler<'a> {
826
1552
  .insert(Arc::clone(name), false);
827
1553
  }
828
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
+ }
829
1560
  let start = self.chunk.code.len();
830
1561
  self.loop_stack.push(LoopInfo {
831
1562
  break_patches: Vec::new(),
@@ -851,6 +1582,9 @@ impl<'a> Compiler<'a> {
851
1582
  for p in info.break_patches {
852
1583
  self.patch_jump(p, end);
853
1584
  }
1585
+ for _ in &body_lets {
1586
+ self.emit(Opcode::LoopVarsEnd);
1587
+ }
854
1588
  }
855
1589
  Statement::Switch {
856
1590
  expr,
@@ -931,6 +1665,10 @@ impl<'a> Compiler<'a> {
931
1665
  let catch_offset_pos = self.chunk.code.len();
932
1666
  self.emit(Opcode::EnterTry);
933
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
+ }
934
1672
  self.compile_statement(body)?;
935
1673
  self.emit(Opcode::ExitTry);
936
1674
  let jump_over_catch = self.emit_jump(Opcode::Jump);
@@ -939,7 +1677,7 @@ impl<'a> Compiler<'a> {
939
1677
  if let Some(param) = catch_param {
940
1678
  self.emit(Opcode::EnterBlock);
941
1679
  self.block_depth += 1;
942
- self.scope.push(HashMap::new());
1680
+ self.enter_block_scope();
943
1681
  let param_idx = self.name_idx(param);
944
1682
  self.emit_u16(Opcode::DeclareVar, param_idx);
945
1683
  self.scope
@@ -947,7 +1685,7 @@ impl<'a> Compiler<'a> {
947
1685
  .unwrap()
948
1686
  .insert(Arc::clone(param), false);
949
1687
  self.compile_statement(catch_stmt)?;
950
- self.scope.pop();
1688
+ self.exit_block_scope();
951
1689
  self.emit(Opcode::ExitBlock);
952
1690
  self.block_depth -= 1;
953
1691
  } else {
@@ -955,10 +1693,19 @@ impl<'a> Compiler<'a> {
955
1693
  self.compile_statement(catch_stmt)?;
956
1694
  }
957
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
+ }
958
1700
  self.emit(Opcode::Throw);
959
1701
  }
960
1702
  let after_catch = self.chunk.code.len();
961
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
+ }
962
1709
  if let Some(finally) = finally_body {
963
1710
  self.compile_statement(finally)?;
964
1711
  }
@@ -1017,10 +1764,36 @@ impl<'a> Compiler<'a> {
1017
1764
  .unwrap()
1018
1765
  .insert(Arc::clone(name), false);
1019
1766
  }
1020
- _ => {
1021
- return Err(CompileError {
1022
- message: "Complex destructuring not yet supported".to_string(),
1023
- });
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);
1024
1797
  }
1025
1798
  }
1026
1799
  }
@@ -1044,10 +1817,15 @@ impl<'a> Compiler<'a> {
1044
1817
  .insert(Arc::clone(name), false);
1045
1818
  }
1046
1819
  }
1047
- _ => {
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(_, _) => {
1048
1827
  return Err(CompileError {
1049
- message: "Nested object destructuring not yet supported"
1050
- .to_string(),
1828
+ message: "Object rest destructuring not yet supported".to_string(),
1051
1829
  });
1052
1830
  }
1053
1831
  }
@@ -1059,6 +1837,7 @@ impl<'a> Compiler<'a> {
1059
1837
  }
1060
1838
 
1061
1839
  fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
1840
+ self.mark_line(expr.span());
1062
1841
  match expr {
1063
1842
  Expr::Literal { value, .. } => {
1064
1843
  let c = match value {
@@ -1072,8 +1851,8 @@ impl<'a> Compiler<'a> {
1072
1851
  self.chunk.write_u16(idx);
1073
1852
  }
1074
1853
  Expr::Ident { name, .. } => {
1075
- let idx = self.name_idx(name);
1076
- self.emit_u16(Opcode::LoadVar, idx);
1854
+ // `resolve_slot` checks BOTH the simple param-only map and the general scope stack.
1855
+ self.emit_var_load(name);
1077
1856
  }
1078
1857
  Expr::Binary {
1079
1858
  left, op, right, ..
@@ -1202,13 +1981,30 @@ impl<'a> Compiler<'a> {
1202
1981
  self.compile_expr(callee)?;
1203
1982
  self.emit(Opcode::CallSpread);
1204
1983
  } else {
1205
- self.compile_expr(callee)?;
1206
- for arg in args {
1207
- if let CallArg::Expr(e) = arg {
1208
- self.compile_expr(e)?;
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
+ }
1209
2005
  }
2006
+ self.emit_u16(Opcode::Call, args.len() as u16);
1210
2007
  }
1211
- self.emit_u16(Opcode::Call, args.len() as u16);
1212
2008
  }
1213
2009
  }
1214
2010
  Expr::Member {
@@ -1355,9 +2151,8 @@ impl<'a> Compiler<'a> {
1355
2151
  }
1356
2152
  Expr::Assign { name, value, .. } => {
1357
2153
  self.compile_expr(value)?;
1358
- let idx = self.name_idx(name);
1359
- self.emit_u16(Opcode::StoreVar, idx);
1360
- self.emit_u16(Opcode::LoadVar, idx); // assign yields value
2154
+ self.emit_var_store(name);
2155
+ self.emit_var_load(name); // assign yields value
1361
2156
  }
1362
2157
  Expr::TypeOf { operand, .. } => {
1363
2158
  let typeof_idx = self.name_idx(&Arc::from("typeof"));
@@ -1368,17 +2163,31 @@ impl<'a> Compiler<'a> {
1368
2163
  Expr::ArrowFunction { params, body, .. } => {
1369
2164
  let formal_len = params.len();
1370
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
+ });
1371
2170
  let mut inner = Chunk::new();
2171
+ inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
1372
2172
  for p in &param_names {
1373
2173
  inner.add_name(Arc::clone(p));
1374
2174
  }
1375
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
+ }
1376
2180
  let mut inner_comp = Compiler::new(&mut inner, false);
1377
- inner_comp.scope = vec![param_names
1378
- .iter()
1379
- .map(|n| (Arc::clone(n), false))
1380
- .collect::<HashMap<_, _>>()];
1381
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
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)?;
1382
2191
  match body {
1383
2192
  ArrowBody::Expr(e) => {
1384
2193
  inner_comp.compile_expr(e)?;
@@ -1421,54 +2230,49 @@ impl<'a> Compiler<'a> {
1421
2230
  }
1422
2231
  }
1423
2232
  Expr::PostfixInc { name, .. } => {
1424
- let idx = self.name_idx(name);
1425
2233
  let one = self.constant_idx(Constant::Number(1.0));
1426
- self.emit_u16(Opcode::LoadVar, idx);
2234
+ self.emit_var_load(name);
1427
2235
  self.emit(Opcode::Dup);
1428
2236
  self.emit(Opcode::LoadConst);
1429
2237
  self.chunk.write_u16(one);
1430
2238
  self.emit_u8(Opcode::BinOp, 0);
1431
- self.emit_u16(Opcode::StoreVar, idx);
2239
+ self.emit_var_store(name);
1432
2240
  }
1433
2241
  Expr::PostfixDec { name, .. } => {
1434
- let idx = self.name_idx(name);
1435
2242
  let one = self.constant_idx(Constant::Number(1.0));
1436
- self.emit_u16(Opcode::LoadVar, idx);
2243
+ self.emit_var_load(name);
1437
2244
  self.emit(Opcode::Dup);
1438
2245
  self.emit(Opcode::LoadConst);
1439
2246
  self.chunk.write_u16(one);
1440
2247
  self.emit_u8(Opcode::BinOp, 1);
1441
- self.emit_u16(Opcode::StoreVar, idx);
2248
+ self.emit_var_store(name);
1442
2249
  }
1443
2250
  Expr::PrefixInc { name, .. } => {
1444
- let idx = self.name_idx(name);
1445
2251
  let one = self.constant_idx(Constant::Number(1.0));
1446
- self.emit_u16(Opcode::LoadVar, idx);
2252
+ self.emit_var_load(name);
1447
2253
  self.emit(Opcode::LoadConst);
1448
2254
  self.chunk.write_u16(one);
1449
2255
  self.emit_u8(Opcode::BinOp, 0);
1450
2256
  self.emit(Opcode::Dup);
1451
- self.emit_u16(Opcode::StoreVar, idx);
2257
+ self.emit_var_store(name);
1452
2258
  }
1453
2259
  Expr::PrefixDec { name, .. } => {
1454
- let idx = self.name_idx(name);
1455
2260
  let one = self.constant_idx(Constant::Number(1.0));
1456
- self.emit_u16(Opcode::LoadVar, idx);
2261
+ self.emit_var_load(name);
1457
2262
  self.emit(Opcode::LoadConst);
1458
2263
  self.chunk.write_u16(one);
1459
2264
  self.emit_u8(Opcode::BinOp, 1);
1460
2265
  self.emit(Opcode::Dup);
1461
- self.emit_u16(Opcode::StoreVar, idx);
2266
+ self.emit_var_store(name);
1462
2267
  }
1463
2268
  Expr::CompoundAssign {
1464
2269
  name, op, value, ..
1465
2270
  } => {
1466
- let idx = self.name_idx(name);
1467
- self.emit_u16(Opcode::LoadVar, idx);
2271
+ self.emit_var_load(name);
1468
2272
  self.compile_expr(value)?;
1469
2273
  self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
1470
2274
  self.emit(Opcode::Dup);
1471
- self.emit_u16(Opcode::StoreVar, idx);
2275
+ self.emit_var_store(name);
1472
2276
  }
1473
2277
  Expr::MemberAssign {
1474
2278
  object,
@@ -1518,34 +2322,62 @@ impl<'a> Compiler<'a> {
1518
2322
  self.compile_expr(operand)?;
1519
2323
  self.emit(Opcode::AwaitPromise);
1520
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
+ }
1521
2354
  Expr::LogicalAssign {
1522
2355
  name, op, value, ..
1523
2356
  } => {
1524
- let idx = self.name_idx(name);
1525
2357
  match op {
1526
2358
  LogicalAssignOp::OrOr => {
1527
2359
  // ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
1528
- self.emit_u16(Opcode::LoadVar, idx);
2360
+ self.emit_var_load(name);
1529
2361
  self.emit(Opcode::Dup);
1530
2362
  let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
1531
2363
  let j_end = self.emit_jump(Opcode::Jump);
1532
2364
  self.patch_jump(j_rhs, self.chunk.code.len());
1533
2365
  self.emit(Opcode::Pop);
1534
2366
  self.compile_expr(value)?;
1535
- self.emit_u16(Opcode::StoreVar, idx);
1536
- self.emit_u16(Opcode::LoadVar, idx);
2367
+ self.emit_var_store(name);
2368
+ self.emit_var_load(name);
1537
2369
  let end = self.chunk.code.len();
1538
2370
  self.patch_jump(j_end, end);
1539
2371
  }
1540
2372
  LogicalAssignOp::AndAnd => {
1541
2373
  // &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
1542
- self.emit_u16(Opcode::LoadVar, idx);
2374
+ self.emit_var_load(name);
1543
2375
  self.emit(Opcode::Dup);
1544
2376
  let j_short = self.emit_jump(Opcode::JumpIfFalse);
1545
2377
  self.emit(Opcode::Pop);
1546
2378
  self.compile_expr(value)?;
1547
- self.emit_u16(Opcode::StoreVar, idx);
1548
- self.emit_u16(Opcode::LoadVar, idx);
2379
+ self.emit_var_store(name);
2380
+ self.emit_var_load(name);
1549
2381
  let j_end = self.emit_jump(Opcode::Jump);
1550
2382
  let end = self.chunk.code.len();
1551
2383
  self.patch_jump(j_short, end);
@@ -1554,7 +2386,7 @@ impl<'a> Compiler<'a> {
1554
2386
  LogicalAssignOp::Nullish => {
1555
2387
  // ??= : assign only when current === null (matches interpreter)
1556
2388
  let null_c = self.constant_idx(Constant::Null);
1557
- self.emit_u16(Opcode::LoadVar, idx);
2389
+ self.emit_var_load(name);
1558
2390
  self.emit(Opcode::Dup);
1559
2391
  self.emit(Opcode::LoadConst);
1560
2392
  self.chunk.write_u16(null_c);
@@ -1562,8 +2394,8 @@ impl<'a> Compiler<'a> {
1562
2394
  let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
1563
2395
  self.emit(Opcode::Pop);
1564
2396
  self.compile_expr(value)?;
1565
- self.emit_u16(Opcode::StoreVar, idx);
1566
- self.emit_u16(Opcode::LoadVar, idx);
2397
+ self.emit_var_store(name);
2398
+ self.emit_var_load(name);
1567
2399
  let j_end = self.emit_jump(Opcode::Jump);
1568
2400
  let end = self.chunk.code.len();
1569
2401
  self.patch_jump(j_not_null, end);
@@ -1727,30 +2559,42 @@ impl<'a> Compiler<'a> {
1727
2559
 
1728
2560
  /// Compile a Tish program to bytecode (with peephole optimizations).
1729
2561
  pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
1730
- compile_internal(program, true, false)
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)
1731
2573
  }
1732
2574
 
1733
2575
  /// Compile without peephole optimizations (for --no-optimize).
1734
2576
  pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
1735
- compile_internal(program, false, false)
2577
+ compile_internal(program, false, false, None)
1736
2578
  }
1737
2579
 
1738
2580
  /// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
1739
2581
  pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
1740
- compile_internal(program, true, true)
2582
+ compile_internal(program, true, true, None)
1741
2583
  }
1742
2584
 
1743
2585
  /// Compile for REPL without peephole optimizations.
1744
2586
  pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
1745
- compile_internal(program, false, true)
2587
+ compile_internal(program, false, true, None)
1746
2588
  }
1747
2589
 
1748
2590
  fn compile_internal(
1749
2591
  program: &Program,
1750
2592
  peephole: bool,
1751
2593
  retain_last_expr: bool,
2594
+ source: Option<std::sync::Arc<str>>,
1752
2595
  ) -> Result<Chunk, CompileError> {
1753
2596
  let mut chunk = Chunk::new();
2597
+ chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
1754
2598
  let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
1755
2599
  compiler.compile_program(program)?;
1756
2600
  if peephole {