@tishlang/tish-format 2.12.0 → 2.35.0

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 (88) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  4. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  5. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  6. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/integration_test.rs +80 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +84 -0
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +724 -72
  13. package/crates/tish_builtins/src/collections.rs +149 -40
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +263 -23
  18. package/crates/tish_bytecode/src/chunk.rs +7 -0
  19. package/crates/tish_bytecode/src/compiler.rs +611 -63
  20. package/crates/tish_bytecode/src/lib.rs +1 -1
  21. package/crates/tish_bytecode/src/opcode.rs +156 -2
  22. package/crates/tish_bytecode/src/serialize.rs +2 -0
  23. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  24. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  25. package/crates/tish_compile/src/codegen.rs +18219 -5283
  26. package/crates/tish_compile/src/infer.rs +1562 -30
  27. package/crates/tish_compile/src/lib.rs +29 -4
  28. package/crates/tish_compile/src/resolve.rs +151 -13
  29. package/crates/tish_compile/src/types.rs +224 -28
  30. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  31. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  32. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  33. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  34. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  35. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  36. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  37. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  38. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  39. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  40. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  41. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  42. package/crates/tish_compile_js/src/codegen.rs +94 -3
  43. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  44. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
  45. package/crates/tish_core/Cargo.toml +4 -0
  46. package/crates/tish_core/src/json.rs +109 -6
  47. package/crates/tish_core/src/lib.rs +222 -2
  48. package/crates/tish_core/src/shape.rs +4 -2
  49. package/crates/tish_core/src/uri.rs +45 -0
  50. package/crates/tish_core/src/value.rs +571 -35
  51. package/crates/tish_core/src/vmref.rs +14 -0
  52. package/crates/tish_eval/Cargo.toml +2 -1
  53. package/crates/tish_eval/src/eval.rs +1328 -101
  54. package/crates/tish_eval/src/natives.rs +283 -18
  55. package/crates/tish_eval/src/regex.rs +47 -1
  56. package/crates/tish_eval/src/value.rs +76 -21
  57. package/crates/tish_ffi/src/lib.rs +11 -1
  58. package/crates/tish_ffi/tests/double_free.rs +35 -0
  59. package/crates/tish_fmt/Cargo.toml +1 -1
  60. package/crates/tish_fmt/src/lib.rs +94 -1
  61. package/crates/tish_lexer/src/lib.rs +76 -0
  62. package/crates/tish_lexer/src/token.rs +4 -0
  63. package/crates/tish_lint/src/lib.rs +126 -0
  64. package/crates/tish_lsp/README.md +2 -1
  65. package/crates/tish_lsp/src/main.rs +378 -28
  66. package/crates/tish_native/src/build.rs +41 -0
  67. package/crates/tish_opt/src/lib.rs +20 -1
  68. package/crates/tish_parser/Cargo.toml +4 -0
  69. package/crates/tish_parser/src/lib.rs +68 -0
  70. package/crates/tish_parser/src/parser.rs +479 -28
  71. package/crates/tish_resolve/src/lib.rs +75 -5
  72. package/crates/tish_runtime/Cargo.toml +10 -1
  73. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  74. package/crates/tish_runtime/src/http.rs +28 -10
  75. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  76. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  77. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  78. package/crates/tish_runtime/src/lib.rs +651 -51
  79. package/crates/tish_runtime/src/timers.rs +49 -2
  80. package/crates/tish_ui/src/jsx.rs +21 -3
  81. package/crates/tish_vm/src/jit.rs +2514 -117
  82. package/crates/tish_vm/src/vm.rs +1479 -224
  83. package/package.json +1 -1
  84. package/platform/darwin-arm64/tish-fmt +0 -0
  85. package/platform/darwin-x64/tish-fmt +0 -0
  86. package/platform/linux-arm64/tish-fmt +0 -0
  87. package/platform/linux-x64/tish-fmt +0 -0
  88. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -4,20 +4,41 @@ use std::collections::{HashMap, HashSet};
4
4
  use std::sync::Arc;
5
5
 
6
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,
7
+ ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern,
8
+ ExportDeclaration, Expr, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp,
9
+ MemberProp, ObjectProp, Program, Span, Statement,
10
10
  };
11
11
 
12
12
  use crate::chunk::{Chunk, Constant};
13
13
  use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
14
- use crate::opcode::Opcode;
14
+ use crate::opcode::{MathUnaryFn, Opcode};
15
15
 
16
16
  enum SimpleMapResult {
17
17
  Identity,
18
18
  BinOp(BinOp, Constant, bool), // op, constant, param_on_left
19
19
  }
20
20
 
21
+ /// Provably evaluates to a `String` at runtime — the safety gate for the plain-assign `AppendLocal`
22
+ /// builder fast path (#186). A string literal, a template literal, or an `+` chain containing one
23
+ /// (JS `+` string-coerces the whole expression). Conservative: an unknown/numeric operand returns
24
+ /// `false`, so a numeric accumulator never routes through the builder.
25
+ fn is_string_typed(expr: &Expr) -> bool {
26
+ match expr {
27
+ Expr::Literal {
28
+ value: Literal::String(_),
29
+ ..
30
+ } => true,
31
+ Expr::TemplateLiteral { .. } => true,
32
+ Expr::Binary {
33
+ left,
34
+ op: BinOp::Add,
35
+ right,
36
+ ..
37
+ } => is_string_typed(left) || is_string_typed(right),
38
+ _ => false,
39
+ }
40
+ }
41
+
21
42
  fn literal_to_constant(expr: &Expr) -> Option<Constant> {
22
43
  if let Expr::Literal { value, .. } = expr {
23
44
  Some(match value {
@@ -110,6 +131,16 @@ struct Compiler<'a> {
110
131
  /// JIT lowers it to a native recursive call). `None` for anonymous fns, top-level, or anywhere the
111
132
  /// self-binding can't be proven stable.
112
133
  self_fn_name: Option<Arc<str>>,
134
+ /// #186 — `Math` is provably the global builtin: it is never rebound/shadowed anywhere in the
135
+ /// program (via the conservative [`stmt_rebinds`] scan). Only then may `Math.<fn>(arg)` lower to
136
+ /// the [`Opcode::MathUnary`] intrinsic, which the numeric JIT can compile without a shape guard.
137
+ /// `false` on nested-fn compilers and whenever the scan can't prove stability.
138
+ math_is_global: bool,
139
+ /// #187: top-level function names provably stable across the whole program (see
140
+ /// [`compute_stable_globals`]). A top-level `function N` in this set gets its chunk stamped with
141
+ /// `global_name = Some(N)`, which lets the numeric JIT register it as a directly-callable callee.
142
+ /// Threaded unchanged into every nested compiler (shared `Arc`).
143
+ stable_globals: Arc<std::collections::HashSet<Arc<str>>>,
113
144
  }
114
145
 
115
146
  /// Does `e` reference only the given params (no free/global vars, no nested
@@ -160,9 +191,7 @@ fn expr_is_param_only(e: &Expr, params: &HashSet<&str>) -> bool {
160
191
  ObjectProp::KeyValue(_, x, _) => expr_is_param_only(x, params),
161
192
  ObjectProp::Spread(_) => false,
162
193
  }),
163
- Expr::TemplateLiteral { exprs, .. } => {
164
- exprs.iter().all(|x| expr_is_param_only(x, params))
165
- }
194
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().all(|x| expr_is_param_only(x, params)),
166
195
  Expr::TypeOf { operand, .. } => expr_is_param_only(operand, params),
167
196
  // Mutation, nested fns, async, jsx, native, `new` — not eligible.
168
197
  _ => false,
@@ -234,7 +263,9 @@ fn simple_fn_slots(
234
263
  /// **Default ON** — validated across the full cross-backend suite + the compute micros (−22..27%) +
235
264
  /// the `main.tish` bundle (−22%). Set `TISH_VM_SLOTS=0` to disable (name-based, the old path).
236
265
  fn slots_enabled() -> bool {
237
- std::env::var("TISH_VM_SLOTS").map(|v| v != "0").unwrap_or(true)
266
+ std::env::var("TISH_VM_SLOTS")
267
+ .map(|v| v != "0")
268
+ .unwrap_or(true)
238
269
  }
239
270
 
240
271
  /// Is `name` bound by one of `params` (so it would shadow a function's own name)? Conservative:
@@ -246,6 +277,19 @@ fn params_bind_name(params: &[FunParam], name: &str) -> bool {
246
277
  })
247
278
  }
248
279
 
280
+ /// #187: does any parameter DEFAULT expression rebind `name`? A default like `(y = (foo = evil))`
281
+ /// reassigns `foo` when the function is called, so a callee reached through it is NOT stable. The
282
+ /// stability walk must scan these (they are otherwise invisible to the body scan).
283
+ fn params_default_rebinds(params: &[FunParam], name: &str) -> bool {
284
+ params.iter().any(|p| {
285
+ let default = match p {
286
+ FunParam::Simple(tp) => &tp.default,
287
+ FunParam::Destructure { default, .. } => default,
288
+ };
289
+ default.as_ref().is_some_and(|e| expr_rebinds(e, name))
290
+ })
291
+ }
292
+
249
293
  /// Conservative scan: does `name` get REBOUND (assigned `=`, `+=`, `??=`, `++`/`--`, or re-declared
250
294
  /// via `let`/`for-of`) anywhere in `s`? Returns `true` on a rebind OR on any node it can't fully
251
295
  /// analyze. Used to decide whether `fn NAME`'s body may emit `SelfCall` for `NAME(...)`: only when
@@ -262,23 +306,50 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
262
306
  Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
263
307
  Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
264
308
  Statement::Throw { value, .. } => expr_rebinds(value, name),
265
- Statement::If { cond, then_branch, else_branch, .. } => {
309
+ Statement::If {
310
+ cond,
311
+ then_branch,
312
+ else_branch,
313
+ ..
314
+ } => {
266
315
  expr_rebinds(cond, name)
267
316
  || stmt_rebinds(then_branch, name)
268
317
  || else_branch.as_ref().is_some_and(|s| stmt_rebinds(s, name))
269
318
  }
270
319
  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, .. } => {
320
+ Statement::DoWhile { body, cond, .. } => {
321
+ stmt_rebinds(body, name) || expr_rebinds(cond, name)
322
+ }
323
+ Statement::For {
324
+ init,
325
+ cond,
326
+ update,
327
+ body,
328
+ ..
329
+ } => {
273
330
  init.as_ref().is_some_and(|s| stmt_rebinds(s, name))
274
331
  || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
275
332
  || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
276
333
  || stmt_rebinds(body, name)
277
334
  }
278
- Statement::ForOf { name: n, iterable, body, .. } => {
279
- n.as_ref() == name || expr_rebinds(iterable, name) || stmt_rebinds(body, name)
335
+ Statement::ForOf {
336
+ name: n,
337
+ iterable: e,
338
+ body,
339
+ ..
280
340
  }
281
- Statement::Switch { expr, cases, default_body, .. } => {
341
+ | Statement::ForIn {
342
+ name: n,
343
+ object: e,
344
+ body,
345
+ ..
346
+ } => n.as_ref() == name || expr_rebinds(e, name) || stmt_rebinds(body, name),
347
+ Statement::Switch {
348
+ expr,
349
+ cases,
350
+ default_body,
351
+ ..
352
+ } => {
282
353
  expr_rebinds(expr, name)
283
354
  || cases.iter().any(|(t, body)| {
284
355
  t.as_ref().is_some_and(|e| expr_rebinds(e, name))
@@ -288,7 +359,12 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
288
359
  .as_ref()
289
360
  .is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
290
361
  }
291
- Statement::Try { body, catch_body, finally_body, .. } => {
362
+ Statement::Try {
363
+ body,
364
+ catch_body,
365
+ finally_body,
366
+ ..
367
+ } => {
292
368
  stmt_rebinds(body, name)
293
369
  || catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
294
370
  || finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
@@ -305,7 +381,9 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
305
381
  match e {
306
382
  Expr::Assign { name: n, value, .. }
307
383
  | Expr::CompoundAssign { name: n, value, .. }
308
- | Expr::LogicalAssign { name: n, value, .. } => n.as_ref() == name || expr_rebinds(value, name),
384
+ | Expr::LogicalAssign { name: n, value, .. } => {
385
+ n.as_ref() == name || expr_rebinds(value, name)
386
+ }
309
387
  Expr::PostfixInc { name: n, .. }
310
388
  | Expr::PostfixDec { name: n, .. }
311
389
  | Expr::PrefixInc { name: n, .. }
@@ -314,11 +392,18 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
314
392
  Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
315
393
  expr_rebinds(left, name) || expr_rebinds(right, name)
316
394
  }
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)
395
+ Expr::Unary { operand, .. }
396
+ | Expr::TypeOf { operand, .. }
397
+ | Expr::Await { operand, .. } => expr_rebinds(operand, name),
398
+ Expr::Conditional {
399
+ cond,
400
+ then_branch,
401
+ else_branch,
402
+ ..
403
+ } => {
404
+ expr_rebinds(cond, name)
405
+ || expr_rebinds(then_branch, name)
406
+ || expr_rebinds(else_branch, name)
322
407
  }
323
408
  Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
324
409
  expr_rebinds(callee, name)
@@ -327,29 +412,203 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
327
412
  })
328
413
  }
329
414
  Expr::Member { object, .. } => expr_rebinds(object, name),
330
- Expr::Index { object, index, .. } => expr_rebinds(object, name) || expr_rebinds(index, name),
415
+ Expr::Index { object, index, .. } => {
416
+ expr_rebinds(object, name) || expr_rebinds(index, name)
417
+ }
331
418
  Expr::Array { elements, .. } => elements.iter().any(|el| match el {
332
419
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
333
420
  }),
334
421
  Expr::Object { props, .. } => props.iter().any(|p| match p {
335
422
  ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_rebinds(e, name),
336
423
  }),
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)
424
+ Expr::MemberAssign { object, value, .. } => {
425
+ expr_rebinds(object, name) || expr_rebinds(value, name)
340
426
  }
427
+ Expr::IndexAssign {
428
+ object,
429
+ index,
430
+ value,
431
+ ..
432
+ } => expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name),
341
433
  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
- },
434
+ // A nested closure could reassign the outer `name` — in its body OR a param default (which
435
+ // evaluates in the enclosing scope). Recurse into both (over-conservative if it shadows, which
436
+ // only costs the optimization). #187: the param-default scan closes the `(a = (foo = x)) => …` hole.
437
+ Expr::ArrowFunction { params, body, .. } => {
438
+ params_default_rebinds(params, name)
439
+ || match body {
440
+ ArrowBody::Expr(e) => expr_rebinds(e, name),
441
+ ArrowBody::Block(s) => stmt_rebinds(s, name),
442
+ }
443
+ }
348
444
  // Jsx, NativeModuleLoad, and anything unknown → conservative.
349
445
  _ => true,
350
446
  }
351
447
  }
352
448
 
449
+ /// #187: whole-program scan — does `name` get REBOUND (assigned/updated, redeclared via `let`/`const`/
450
+ /// `for-of`/`catch`, shadowed by a param, or (re)declared via a `function name`) anywhere in `s`,
451
+ /// INCLUDING inside every function body? Unlike [`stmt_rebinds`] (which returns `true` on any `FunDecl`
452
+ /// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
453
+ /// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
454
+ /// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
455
+ fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
456
+ match s {
457
+ Statement::FunDecl {
458
+ name: n,
459
+ params,
460
+ rest_param,
461
+ body,
462
+ ..
463
+ } => {
464
+ n.as_ref() == name
465
+ || params_bind_name(params, name)
466
+ || params_default_rebinds(params, name)
467
+ || rest_param
468
+ .as_ref()
469
+ .is_some_and(|rp| rp.name.as_ref() == name)
470
+ || name_rebinds_in_stmt(body, name)
471
+ }
472
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
473
+ statements.iter().any(|s| name_rebinds_in_stmt(s, name))
474
+ }
475
+ Statement::VarDecl { name: n, init, .. } => {
476
+ n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
477
+ }
478
+ Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
479
+ Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
480
+ Statement::Throw { value, .. } => expr_rebinds(value, name),
481
+ Statement::If {
482
+ cond,
483
+ then_branch,
484
+ else_branch,
485
+ ..
486
+ } => {
487
+ expr_rebinds(cond, name)
488
+ || name_rebinds_in_stmt(then_branch, name)
489
+ || else_branch
490
+ .as_ref()
491
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
492
+ }
493
+ Statement::While { cond, body, .. } => {
494
+ expr_rebinds(cond, name) || name_rebinds_in_stmt(body, name)
495
+ }
496
+ Statement::DoWhile { body, cond, .. } => {
497
+ name_rebinds_in_stmt(body, name) || expr_rebinds(cond, name)
498
+ }
499
+ Statement::For {
500
+ init,
501
+ cond,
502
+ update,
503
+ body,
504
+ ..
505
+ } => {
506
+ init.as_ref().is_some_and(|s| name_rebinds_in_stmt(s, name))
507
+ || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
508
+ || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
509
+ || name_rebinds_in_stmt(body, name)
510
+ }
511
+ Statement::ForOf {
512
+ name: n,
513
+ iterable: e,
514
+ body,
515
+ ..
516
+ }
517
+ | Statement::ForIn {
518
+ name: n,
519
+ object: e,
520
+ body,
521
+ ..
522
+ } => n.as_ref() == name || expr_rebinds(e, name) || name_rebinds_in_stmt(body, name),
523
+ Statement::Switch {
524
+ expr,
525
+ cases,
526
+ default_body,
527
+ ..
528
+ } => {
529
+ expr_rebinds(expr, name)
530
+ || cases.iter().any(|(t, body)| {
531
+ t.as_ref().is_some_and(|e| expr_rebinds(e, name))
532
+ || body.iter().any(|s| name_rebinds_in_stmt(s, name))
533
+ })
534
+ || default_body
535
+ .as_ref()
536
+ .is_some_and(|b| b.iter().any(|s| name_rebinds_in_stmt(s, name)))
537
+ }
538
+ Statement::Try {
539
+ body,
540
+ catch_body,
541
+ finally_body,
542
+ ..
543
+ } => {
544
+ name_rebinds_in_stmt(body, name)
545
+ || catch_body
546
+ .as_ref()
547
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
548
+ || finally_body
549
+ .as_ref()
550
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
551
+ }
552
+ Statement::Break { .. } | Statement::Continue { .. } => false,
553
+ // VarDeclDestructure (could bind `name`) + any unknown construct → conservative.
554
+ _ => true,
555
+ }
556
+ }
557
+
558
+ /// #187: the set of top-level function names that are PROVABLY stable across the whole program — each
559
+ /// declared exactly once as a top-level `function N`, never also bound by a top-level `let`/`var`/
560
+ /// `const`, and never reassigned/redeclared/shadowed anywhere (via [`name_rebinds_in_stmt`], skipping
561
+ /// the single defining declaration but still recursing into its body). A direct native call to such a
562
+ /// callee can never dispatch a stale function, because the binding can never change.
563
+ fn compute_stable_globals(program: &Program) -> std::collections::HashSet<Arc<str>> {
564
+ let mut fn_count: HashMap<Arc<str>, usize> = HashMap::new();
565
+ let mut nonfn_toplevel: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
566
+ for s in &program.statements {
567
+ match s {
568
+ Statement::FunDecl { name, .. } => *fn_count.entry(Arc::clone(name)).or_insert(0) += 1,
569
+ Statement::VarDecl { name, .. } => {
570
+ nonfn_toplevel.insert(Arc::clone(name));
571
+ }
572
+ _ => {}
573
+ }
574
+ }
575
+ let mut stable: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
576
+ 'cand: for (name, &count) in &fn_count {
577
+ if count != 1 || nonfn_toplevel.contains(name) {
578
+ continue;
579
+ }
580
+ for s in &program.statements {
581
+ match s {
582
+ // The single defining `function name`: not a rebind, but its body/params still count.
583
+ Statement::FunDecl {
584
+ name: n,
585
+ params,
586
+ rest_param,
587
+ body,
588
+ ..
589
+ } if n == name => {
590
+ if params_bind_name(params, name)
591
+ || params_default_rebinds(params, name)
592
+ || rest_param
593
+ .as_ref()
594
+ .is_some_and(|rp| rp.name.as_ref() == name.as_ref())
595
+ || name_rebinds_in_stmt(body, name)
596
+ {
597
+ continue 'cand;
598
+ }
599
+ }
600
+ other => {
601
+ if name_rebinds_in_stmt(other, name) {
602
+ continue 'cand;
603
+ }
604
+ }
605
+ }
606
+ }
607
+ stable.insert(Arc::clone(name));
608
+ }
609
+ stable
610
+ }
611
+
353
612
  /// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
354
613
  /// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
355
614
  /// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
@@ -367,19 +626,42 @@ struct SlotScan {
367
626
  impl SlotScan {
368
627
  fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
369
628
  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)),
629
+ Statement::Block { statements, .. } => {
630
+ statements.iter().all(|s| self.stmt(s, in_closure))
631
+ }
632
+ Statement::Multi { statements, .. } => {
633
+ statements.iter().all(|s| self.stmt(s, in_closure))
634
+ }
635
+ Statement::VarDecl { init, .. } => {
636
+ init.as_ref().is_none_or(|e| self.expr(e, in_closure))
637
+ }
373
638
  Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
374
639
  Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
375
- Statement::If { cond, then_branch, else_branch, .. } => {
640
+ Statement::If {
641
+ cond,
642
+ then_branch,
643
+ else_branch,
644
+ ..
645
+ } => {
376
646
  self.expr(cond, in_closure)
377
647
  && self.stmt(then_branch, in_closure)
378
- && else_branch.as_ref().is_none_or(|s| self.stmt(s, in_closure))
648
+ && else_branch
649
+ .as_ref()
650
+ .is_none_or(|s| self.stmt(s, in_closure))
651
+ }
652
+ Statement::While { cond, body, .. } => {
653
+ self.expr(cond, in_closure) && self.stmt(body, in_closure)
379
654
  }
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, .. } => {
655
+ Statement::DoWhile { body, cond, .. } => {
656
+ self.stmt(body, in_closure) && self.expr(cond, in_closure)
657
+ }
658
+ Statement::For {
659
+ init,
660
+ cond,
661
+ update,
662
+ body,
663
+ ..
664
+ } => {
383
665
  init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
384
666
  && cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
385
667
  && update.as_ref().is_none_or(|e| self.expr(e, in_closure))
@@ -388,10 +670,17 @@ impl SlotScan {
388
670
  Statement::ForOf { iterable, body, .. } => {
389
671
  self.expr(iterable, in_closure) && self.stmt(body, in_closure)
390
672
  }
391
- Statement::Return { value, .. } => value.as_ref().is_none_or(|e| self.expr(e, in_closure)),
673
+ Statement::Return { value, .. } => {
674
+ value.as_ref().is_none_or(|e| self.expr(e, in_closure))
675
+ }
392
676
  Statement::Throw { value, .. } => self.expr(value, in_closure),
393
677
  Statement::Break { .. } | Statement::Continue { .. } => true,
394
- Statement::Switch { expr, cases, default_body, .. } => {
678
+ Statement::Switch {
679
+ expr,
680
+ cases,
681
+ default_body,
682
+ ..
683
+ } => {
395
684
  if !self.expr(expr, in_closure) {
396
685
  return false;
397
686
  }
@@ -409,10 +698,17 @@ impl SlotScan {
409
698
  .as_ref()
410
699
  .is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
411
700
  }
412
- Statement::Try { body, catch_body, finally_body, .. } => {
701
+ Statement::Try {
702
+ body,
703
+ catch_body,
704
+ finally_body,
705
+ ..
706
+ } => {
413
707
  self.stmt(body, in_closure)
414
708
  && 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))
709
+ && finally_body
710
+ .as_ref()
711
+ .is_none_or(|s| self.stmt(s, in_closure))
416
712
  }
417
713
  // A nested named function: its param defaults (enclosing-scope) + whole body capture.
418
714
  Statement::FunDecl { params, body, .. } => {
@@ -432,14 +728,25 @@ impl SlotScan {
432
728
  }
433
729
  true
434
730
  }
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)
731
+ Expr::Binary { left, right, .. } => {
732
+ self.expr(left, in_closure) && self.expr(right, in_closure)
438
733
  }
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)
734
+ Expr::Unary { operand, .. }
735
+ | Expr::TypeOf { operand, .. }
736
+ | Expr::Await { operand, .. } => self.expr(operand, in_closure),
737
+ Expr::Conditional {
738
+ cond,
739
+ then_branch,
740
+ else_branch,
741
+ ..
742
+ } => {
743
+ self.expr(cond, in_closure)
744
+ && self.expr(then_branch, in_closure)
745
+ && self.expr(else_branch, in_closure)
746
+ }
747
+ Expr::NullishCoalesce { left, right, .. } => {
748
+ self.expr(left, in_closure) && self.expr(right, in_closure)
441
749
  }
442
- Expr::NullishCoalesce { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
443
750
  Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
444
751
  self.expr(callee, in_closure)
445
752
  && args.iter().all(|a| match a {
@@ -447,7 +754,9 @@ impl SlotScan {
447
754
  })
448
755
  }
449
756
  Expr::Member { object, .. } => self.expr(object, in_closure),
450
- Expr::Index { object, index, .. } => self.expr(object, in_closure) && self.expr(index, in_closure),
757
+ Expr::Index { object, index, .. } => {
758
+ self.expr(object, in_closure) && self.expr(index, in_closure)
759
+ }
451
760
  Expr::Array { elements, .. } => elements.iter().all(|el| match el {
452
761
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
453
762
  }),
@@ -462,9 +771,18 @@ impl SlotScan {
462
771
  }
463
772
  self.expr(value, in_closure)
464
773
  }
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)
774
+ Expr::MemberAssign { object, value, .. } => {
775
+ self.expr(object, in_closure) && self.expr(value, in_closure)
776
+ }
777
+ Expr::IndexAssign {
778
+ object,
779
+ index,
780
+ value,
781
+ ..
782
+ } => {
783
+ self.expr(object, in_closure)
784
+ && self.expr(index, in_closure)
785
+ && self.expr(value, in_closure)
468
786
  }
469
787
  Expr::PostfixInc { name, .. }
470
788
  | Expr::PostfixDec { name, .. }
@@ -511,7 +829,11 @@ impl SlotScan {
511
829
  /// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
512
830
  /// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
513
831
  /// (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>>> {
832
+ fn slot_analyze(
833
+ params: &[FunParam],
834
+ has_rest: bool,
835
+ body: &Statement,
836
+ ) -> Option<HashSet<Arc<str>>> {
515
837
  if !slots_enabled() || has_rest {
516
838
  return None;
517
839
  }
@@ -559,7 +881,10 @@ impl<'a> Compiler<'a> {
559
881
  return Some(*s);
560
882
  }
561
883
  }
562
- self.slot_scopes.iter().rev().find_map(|m| m.get(name).copied())
884
+ self.slot_scopes
885
+ .iter()
886
+ .rev()
887
+ .find_map(|m| m.get(name).copied())
563
888
  }
564
889
 
565
890
  /// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
@@ -598,6 +923,8 @@ impl<'a> Compiler<'a> {
598
923
  general_slots: false,
599
924
  finally_stack: Vec::new(),
600
925
  self_fn_name: None,
926
+ math_is_global: false,
927
+ stable_globals: Arc::new(std::collections::HashSet::new()),
601
928
  }
602
929
  }
603
930
 
@@ -842,10 +1169,7 @@ impl<'a> Compiler<'a> {
842
1169
  /// condition shape stays eligible for the cranelift JIT (#167). Returns the `JumpIfFalse` patch
843
1170
  /// sites the caller must point at its "condition is false" target. The condition value itself is
844
1171
  /// never observed here, so truthiness-only lowering is exact; short-circuit order is preserved.
845
- fn compile_condition_jump_if_false(
846
- &mut self,
847
- cond: &Expr,
848
- ) -> Result<Vec<usize>, CompileError> {
1172
+ fn compile_condition_jump_if_false(&mut self, cond: &Expr) -> Result<Vec<usize>, CompileError> {
849
1173
  match cond {
850
1174
  // `a && b` is false if EITHER operand is false — test each in order; both exit to the
851
1175
  // same false-target, so concatenate their patch sites.
@@ -1232,6 +1556,48 @@ impl<'a> Compiler<'a> {
1232
1556
  self.compile_destructure(pattern, false, false)?;
1233
1557
  }
1234
1558
  Statement::ExprStmt { expr, .. } => {
1559
+ // String-builder fast path: statement-position `acc += rhs` on a frame-slot local
1560
+ // compiles to `<rhs>; AppendLocal slot` (no LoadLocal/Dup/StoreLocal/Pop), letting
1561
+ // the VM append in amortized O(1) without materializing the discarded result. Only
1562
+ // for a simple slot-resolved identifier with the `+` compound op; everything else
1563
+ // (name-based vars, other ops) keeps the generic path.
1564
+ if let Expr::CompoundAssign {
1565
+ name,
1566
+ op: CompoundOp::Add,
1567
+ value,
1568
+ ..
1569
+ } = expr
1570
+ {
1571
+ if let Some(slot) = self.resolve_slot(name) {
1572
+ self.compile_expr(value)?;
1573
+ self.emit_u16(Opcode::AppendLocal, slot);
1574
+ return Ok(());
1575
+ }
1576
+ }
1577
+ // Same builder fast path for the plain-assign spelling `s = s + <str>` — but ONLY when
1578
+ // the appended operand is PROVABLY a string. A numeric accumulator (`i = i + 1`) must
1579
+ // NOT builder-ize: the VM keeps a single string builder slot, so a second builder-ized
1580
+ // slot flushes the first every iteration → O(n²) (#186). Restricting to string-typed
1581
+ // RHS keeps `s = s + "x"` fast (string_concat) while `i = i + 1` stays a plain store.
1582
+ if let Expr::Assign { name, value, .. } = expr {
1583
+ if let Expr::Binary {
1584
+ left,
1585
+ op: BinOp::Add,
1586
+ right,
1587
+ ..
1588
+ } = value.as_ref()
1589
+ {
1590
+ if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln == name)
1591
+ && is_string_typed(right)
1592
+ {
1593
+ if let Some(slot) = self.resolve_slot(name) {
1594
+ self.compile_expr(right)?;
1595
+ self.emit_u16(Opcode::AppendLocal, slot);
1596
+ return Ok(());
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1235
1601
  self.compile_expr(expr)?;
1236
1602
  self.emit(Opcode::Pop);
1237
1603
  }
@@ -1449,6 +1815,46 @@ impl<'a> Compiler<'a> {
1449
1815
  self.emit(Opcode::LoopVarsEnd);
1450
1816
  self.exit_block_scope();
1451
1817
  }
1818
+ Statement::ForIn {
1819
+ name,
1820
+ name_span,
1821
+ object,
1822
+ body,
1823
+ span,
1824
+ } => {
1825
+ // Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413): Object.keys
1826
+ // yields exactly the own enumerable keys the interpreter enumerates — insertion order
1827
+ // for objects, index strings for arrays, and `[]` for a non-object (so `for (k in
1828
+ // null)` no-ops). Reuses the whole for-of iteration path.
1829
+ let dummy = Span {
1830
+ start: (0, 0),
1831
+ end: (0, 0),
1832
+ };
1833
+ let keys_call = Expr::Call {
1834
+ callee: Box::new(Expr::Member {
1835
+ object: Box::new(Expr::Ident {
1836
+ name: Arc::from("Object"),
1837
+ span: dummy,
1838
+ }),
1839
+ prop: MemberProp::Name {
1840
+ name: Arc::from("keys"),
1841
+ span: dummy,
1842
+ },
1843
+ optional: false,
1844
+ span: dummy,
1845
+ }),
1846
+ args: vec![CallArg::Expr(object.clone())],
1847
+ span: dummy,
1848
+ };
1849
+ let lowered = Statement::ForOf {
1850
+ name: Arc::clone(name),
1851
+ name_span: *name_span,
1852
+ iterable: keys_call,
1853
+ body: body.clone(),
1854
+ span: *span,
1855
+ };
1856
+ self.compile_statement(&lowered)?;
1857
+ }
1452
1858
  Statement::Return { value, .. } => {
1453
1859
  // Evaluate the return value first (JS order), then run any enclosing `finally`
1454
1860
  // blocks (they're stack-neutral, so the value stays on top), then return.
@@ -1552,7 +1958,16 @@ impl<'a> Compiler<'a> {
1552
1958
  inner.slot_based = true;
1553
1959
  inner.num_slots = param_names.len() as u16;
1554
1960
  }
1961
+ // #187: stamp the chunk with its global name so the JIT can register it as a directly-
1962
+ // callable callee — but ONLY when it is a provably-stable top-level function. `self`
1963
+ // being the ROOT compiler (top-level) is indicated by `self.self_fn_name.is_none()` and
1964
+ // an empty `slot_scopes`; simplest sound check: the name is in `stable_globals` (which
1965
+ // by construction contains only the unique top-level `function name`).
1966
+ if self.stable_globals.contains(name) {
1967
+ inner.global_name = Some(Arc::clone(name));
1968
+ }
1555
1969
  let mut inner_comp = Compiler::new(&mut inner, false);
1970
+ inner_comp.stable_globals = Arc::clone(&self.stable_globals);
1556
1971
  // Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
1557
1972
  // param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
1558
1973
  // compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
@@ -1583,7 +1998,8 @@ impl<'a> Compiler<'a> {
1583
1998
  .iter()
1584
1999
  .map(|n| (Arc::clone(n), false))
1585
2000
  .collect::<HashMap<_, _>>()];
1586
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2001
+ inner_comp
2002
+ .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
1587
2003
  inner_comp.emit_param_defaults_prologue(params)?;
1588
2004
  inner_comp.compile_statement(body)?;
1589
2005
  }
@@ -1792,6 +2208,7 @@ impl<'a> Compiler<'a> {
1792
2208
  message: "export default is not supported in bytecode".to_string(),
1793
2209
  });
1794
2210
  }
2211
+ ExportDeclaration::ReExport { .. } => {}
1795
2212
  },
1796
2213
  Statement::TypeAlias { .. }
1797
2214
  | Statement::DeclareVar { .. }
@@ -1958,6 +2375,30 @@ impl<'a> Compiler<'a> {
1958
2375
  self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
1959
2376
  }
1960
2377
  Expr::Call { callee, args, .. } => {
2378
+ // #186: `Math.<unaryfn>(arg)` → `<arg>; MathUnary(id)` when `Math` is provably the
2379
+ // global builtin (unshadowed program-wide) and there is exactly one argument. Lets the
2380
+ // numeric JIT lower the intrinsic (math_trig; a Math.* prereq for other kernels).
2381
+ if self.math_is_global && args.len() == 1 {
2382
+ if let Expr::Member {
2383
+ object,
2384
+ prop: MemberProp::Name { name: fname, .. },
2385
+ optional: false,
2386
+ ..
2387
+ } = callee.as_ref()
2388
+ {
2389
+ if let (Expr::Ident { name: obj, .. }, CallArg::Expr(arg)) =
2390
+ (object.as_ref(), &args[0])
2391
+ {
2392
+ if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
2393
+ if let Some(mfn) = MathUnaryFn::from_name(fname.as_ref()) {
2394
+ self.compile_expr(arg)?;
2395
+ self.emit_u16(Opcode::MathUnary, mfn as u16);
2396
+ return Ok(());
2397
+ }
2398
+ }
2399
+ }
2400
+ }
2401
+ }
1961
2402
  // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
1962
2403
  if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
1963
2404
  && args.len() == 1
@@ -2244,6 +2685,7 @@ impl<'a> Compiler<'a> {
2244
2685
  inner.num_slots = param_names.len() as u16;
2245
2686
  }
2246
2687
  let mut inner_comp = Compiler::new(&mut inner, false);
2688
+ inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
2247
2689
  if let Some(map) = simple_slots {
2248
2690
  inner_comp.slot_ctx = Some(map);
2249
2691
  } else {
@@ -2251,7 +2693,8 @@ impl<'a> Compiler<'a> {
2251
2693
  .iter()
2252
2694
  .map(|n| (Arc::clone(n), false))
2253
2695
  .collect::<HashMap<_, _>>()];
2254
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2696
+ inner_comp
2697
+ .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2255
2698
  }
2256
2699
  inner_comp.emit_param_defaults_prologue(params)?;
2257
2700
  match body {
@@ -2393,14 +2836,22 @@ impl<'a> Compiler<'a> {
2393
2836
  // pops both, removes the property, and pushes `true`. Deleting anything that
2394
2837
  // isn't a property reference is a no-op that still yields `true` (JS).
2395
2838
  match target.as_ref() {
2396
- Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
2839
+ Expr::Member {
2840
+ object,
2841
+ prop: MemberProp::Name { name, .. },
2842
+ ..
2843
+ } => {
2397
2844
  self.compile_expr(object)?;
2398
2845
  let idx = self.constant_idx(Constant::String(Arc::clone(name)));
2399
2846
  self.emit(Opcode::LoadConst);
2400
2847
  self.chunk.write_u16(idx);
2401
2848
  self.emit(Opcode::DeleteIndex);
2402
2849
  }
2403
- Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
2850
+ Expr::Member {
2851
+ object,
2852
+ prop: MemberProp::Expr(key),
2853
+ ..
2854
+ } => {
2404
2855
  self.compile_expr(object)?;
2405
2856
  self.compile_expr(key)?;
2406
2857
  self.emit(Opcode::DeleteIndex);
@@ -2662,9 +3113,106 @@ fn compile_internal(
2662
3113
  let mut chunk = Chunk::new();
2663
3114
  chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
2664
3115
  let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
3116
+ // #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
3117
+ // `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
3118
+ // `false` here only forgoes the optimization, never risks a miscompile.
3119
+ compiler.math_is_global = !program.statements.iter().any(|s| stmt_rebinds(s, "Math"));
3120
+ // #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
3121
+ // shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
3122
+ compiler.stable_globals = Arc::new(compute_stable_globals(program));
2665
3123
  compiler.compile_program(program)?;
2666
3124
  if peephole {
2667
3125
  crate::peephole::optimize(&mut chunk);
2668
3126
  }
2669
3127
  Ok(chunk)
2670
3128
  }
3129
+
3130
+ #[cfg(test)]
3131
+ mod stable_globals_tests {
3132
+ use super::compute_stable_globals;
3133
+
3134
+ fn stable(src: &str) -> std::collections::HashSet<String> {
3135
+ let prog = tishlang_parser::parse(src).expect("parse");
3136
+ compute_stable_globals(&prog)
3137
+ .into_iter()
3138
+ .map(|n| n.to_string())
3139
+ .collect()
3140
+ }
3141
+
3142
+ /// #187 gate: the exact spectral_norm shape — five top-level functions, none reassigned — must ALL
3143
+ /// be provably stable (this is what lets `multiplyAv` directly call `evalA`). The naive
3144
+ /// `stmt_rebinds`-based scan would return the empty set here (it treats every `FunDecl` as a
3145
+ /// rebind), so this is the regression tripwire for the purpose-built analysis.
3146
+ #[test]
3147
+ fn all_unreassigned_top_level_fns_are_stable() {
3148
+ let s = stable(
3149
+ "function evalA(i, j) { return 1.0 / (i + j) }\n\
3150
+ function multiplyAv(n, v, av) { let i = 0; while (i < n) { av[i] = evalA(i, i) * v[i]; i = i + 1 } }\n\
3151
+ function spectralNorm(n) { multiplyAv(n, n, n); return n }\n",
3152
+ );
3153
+ assert!(
3154
+ s.contains("evalA"),
3155
+ "evalA (called by multiplyAv) must be stable"
3156
+ );
3157
+ assert!(s.contains("multiplyAv"));
3158
+ assert!(s.contains("spectralNorm"));
3159
+ }
3160
+
3161
+ /// A reassigned function is NOT stable (a direct call could hit a stale binding) — but a sibling
3162
+ /// that is never reassigned still is.
3163
+ #[test]
3164
+ fn reassigned_function_is_excluded() {
3165
+ let s = stable(
3166
+ "function f(x) { return x + 1 }\n\
3167
+ function g(x) { return f(x) * 2 }\n\
3168
+ f = (x) => x + 100\n",
3169
+ );
3170
+ assert!(!s.contains("f"), "f is reassigned → must NOT be stable");
3171
+ assert!(s.contains("g"), "g is never reassigned → stable");
3172
+ }
3173
+
3174
+ /// Reassignment/redeclaration/shadowing INSIDE another function body must disqualify the name —
3175
+ /// the whole-program walk has to recurse into every body (the whole point vs `stmt_rebinds`).
3176
+ #[test]
3177
+ fn cross_body_rebind_and_shadow_and_redecl_excluded() {
3178
+ // reassigned inside another function's body
3179
+ assert!(!stable(
3180
+ "function h(x) { return x }\n\
3181
+ function k() { h = 5; return h }\n"
3182
+ )
3183
+ .contains("h"));
3184
+ // shadowed by a param in another function
3185
+ assert!(!stable(
3186
+ "function p(x) { return x }\n\
3187
+ function q(p) { return p }\n"
3188
+ )
3189
+ .contains("p"));
3190
+ // declared twice at top level
3191
+ assert!(!stable("function d(x) { return x }\nfunction d(y) { return y }\n").contains("d"));
3192
+ // also bound by a top-level let
3193
+ assert!(!stable("function e(x) { return x }\nlet e = 3\n").contains("e"));
3194
+ }
3195
+
3196
+ /// A global reassigned inside a PARAMETER DEFAULT (of a function or arrow) must disqualify it — the
3197
+ /// default runs in the enclosing scope when the function is called, so a direct call would be stale.
3198
+ #[test]
3199
+ fn param_default_rebind_excluded() {
3200
+ // `foo` reassigned in another function's param default
3201
+ assert!(!stable(
3202
+ "function evil(x) { return x + 1000 }\n\
3203
+ function foo(x) { return x + 1 }\n\
3204
+ function resetFoo(y = (foo = evil)) { return y }\n"
3205
+ )
3206
+ .contains("foo"));
3207
+ // `bar` reassigned in an arrow's param default
3208
+ assert!(!stable(
3209
+ "function bar(x) { return x }\n\
3210
+ let f = (z = (bar = 5)) => z\n"
3211
+ )
3212
+ .contains("bar"));
3213
+ // a param default that does NOT touch the name leaves it stable
3214
+ assert!(
3215
+ stable("function ok(x) { return x }\nfunction u(a = 1) { return a }\n").contains("ok")
3216
+ );
3217
+ }
3218
+ }