@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.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,44 @@ 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)
280
- }
281
- Statement::Switch { expr, cases, default_body, .. } => {
335
+ Statement::ForOf {
336
+ name: n,
337
+ iterable,
338
+ body,
339
+ ..
340
+ } => n.as_ref() == name || expr_rebinds(iterable, name) || stmt_rebinds(body, name),
341
+ Statement::Switch {
342
+ expr,
343
+ cases,
344
+ default_body,
345
+ ..
346
+ } => {
282
347
  expr_rebinds(expr, name)
283
348
  || cases.iter().any(|(t, body)| {
284
349
  t.as_ref().is_some_and(|e| expr_rebinds(e, name))
@@ -288,7 +353,12 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
288
353
  .as_ref()
289
354
  .is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
290
355
  }
291
- Statement::Try { body, catch_body, finally_body, .. } => {
356
+ Statement::Try {
357
+ body,
358
+ catch_body,
359
+ finally_body,
360
+ ..
361
+ } => {
292
362
  stmt_rebinds(body, name)
293
363
  || catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
294
364
  || finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
@@ -305,7 +375,9 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
305
375
  match e {
306
376
  Expr::Assign { name: n, value, .. }
307
377
  | Expr::CompoundAssign { name: n, value, .. }
308
- | Expr::LogicalAssign { name: n, value, .. } => n.as_ref() == name || expr_rebinds(value, name),
378
+ | Expr::LogicalAssign { name: n, value, .. } => {
379
+ n.as_ref() == name || expr_rebinds(value, name)
380
+ }
309
381
  Expr::PostfixInc { name: n, .. }
310
382
  | Expr::PostfixDec { name: n, .. }
311
383
  | Expr::PrefixInc { name: n, .. }
@@ -314,11 +386,18 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
314
386
  Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
315
387
  expr_rebinds(left, name) || expr_rebinds(right, name)
316
388
  }
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)
389
+ Expr::Unary { operand, .. }
390
+ | Expr::TypeOf { operand, .. }
391
+ | Expr::Await { operand, .. } => expr_rebinds(operand, name),
392
+ Expr::Conditional {
393
+ cond,
394
+ then_branch,
395
+ else_branch,
396
+ ..
397
+ } => {
398
+ expr_rebinds(cond, name)
399
+ || expr_rebinds(then_branch, name)
400
+ || expr_rebinds(else_branch, name)
322
401
  }
323
402
  Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
324
403
  expr_rebinds(callee, name)
@@ -327,29 +406,197 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
327
406
  })
328
407
  }
329
408
  Expr::Member { object, .. } => expr_rebinds(object, name),
330
- Expr::Index { object, index, .. } => expr_rebinds(object, name) || expr_rebinds(index, name),
409
+ Expr::Index { object, index, .. } => {
410
+ expr_rebinds(object, name) || expr_rebinds(index, name)
411
+ }
331
412
  Expr::Array { elements, .. } => elements.iter().any(|el| match el {
332
413
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
333
414
  }),
334
415
  Expr::Object { props, .. } => props.iter().any(|p| match p {
335
416
  ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_rebinds(e, name),
336
417
  }),
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)
418
+ Expr::MemberAssign { object, value, .. } => {
419
+ expr_rebinds(object, name) || expr_rebinds(value, name)
340
420
  }
421
+ Expr::IndexAssign {
422
+ object,
423
+ index,
424
+ value,
425
+ ..
426
+ } => expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name),
341
427
  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
- },
428
+ // A nested closure could reassign the outer `name` in its body OR a param default (which
429
+ // evaluates in the enclosing scope). Recurse into both (over-conservative if it shadows, which
430
+ // only costs the optimization). #187: the param-default scan closes the `(a = (foo = x)) => …` hole.
431
+ Expr::ArrowFunction { params, body, .. } => {
432
+ params_default_rebinds(params, name)
433
+ || match body {
434
+ ArrowBody::Expr(e) => expr_rebinds(e, name),
435
+ ArrowBody::Block(s) => stmt_rebinds(s, name),
436
+ }
437
+ }
348
438
  // Jsx, NativeModuleLoad, and anything unknown → conservative.
349
439
  _ => true,
350
440
  }
351
441
  }
352
442
 
443
+ /// #187: whole-program scan — does `name` get REBOUND (assigned/updated, redeclared via `let`/`const`/
444
+ /// `for-of`/`catch`, shadowed by a param, or (re)declared via a `function name`) anywhere in `s`,
445
+ /// INCLUDING inside every function body? Unlike [`stmt_rebinds`] (which returns `true` on any `FunDecl`
446
+ /// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
447
+ /// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
448
+ /// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
449
+ fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
450
+ match s {
451
+ Statement::FunDecl {
452
+ name: n,
453
+ params,
454
+ rest_param,
455
+ body,
456
+ ..
457
+ } => {
458
+ n.as_ref() == name
459
+ || params_bind_name(params, name)
460
+ || params_default_rebinds(params, name)
461
+ || rest_param
462
+ .as_ref()
463
+ .is_some_and(|rp| rp.name.as_ref() == name)
464
+ || name_rebinds_in_stmt(body, name)
465
+ }
466
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
467
+ statements.iter().any(|s| name_rebinds_in_stmt(s, name))
468
+ }
469
+ Statement::VarDecl { name: n, init, .. } => {
470
+ n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
471
+ }
472
+ Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
473
+ Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
474
+ Statement::Throw { value, .. } => expr_rebinds(value, name),
475
+ Statement::If {
476
+ cond,
477
+ then_branch,
478
+ else_branch,
479
+ ..
480
+ } => {
481
+ expr_rebinds(cond, name)
482
+ || name_rebinds_in_stmt(then_branch, name)
483
+ || else_branch
484
+ .as_ref()
485
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
486
+ }
487
+ Statement::While { cond, body, .. } => {
488
+ expr_rebinds(cond, name) || name_rebinds_in_stmt(body, name)
489
+ }
490
+ Statement::DoWhile { body, cond, .. } => {
491
+ name_rebinds_in_stmt(body, name) || expr_rebinds(cond, name)
492
+ }
493
+ Statement::For {
494
+ init,
495
+ cond,
496
+ update,
497
+ body,
498
+ ..
499
+ } => {
500
+ init.as_ref().is_some_and(|s| name_rebinds_in_stmt(s, name))
501
+ || cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
502
+ || update.as_ref().is_some_and(|e| expr_rebinds(e, name))
503
+ || name_rebinds_in_stmt(body, name)
504
+ }
505
+ Statement::ForOf {
506
+ name: n,
507
+ iterable,
508
+ body,
509
+ ..
510
+ } => n.as_ref() == name || expr_rebinds(iterable, name) || name_rebinds_in_stmt(body, name),
511
+ Statement::Switch {
512
+ expr,
513
+ cases,
514
+ default_body,
515
+ ..
516
+ } => {
517
+ expr_rebinds(expr, name)
518
+ || cases.iter().any(|(t, body)| {
519
+ t.as_ref().is_some_and(|e| expr_rebinds(e, name))
520
+ || body.iter().any(|s| name_rebinds_in_stmt(s, name))
521
+ })
522
+ || default_body
523
+ .as_ref()
524
+ .is_some_and(|b| b.iter().any(|s| name_rebinds_in_stmt(s, name)))
525
+ }
526
+ Statement::Try {
527
+ body,
528
+ catch_body,
529
+ finally_body,
530
+ ..
531
+ } => {
532
+ name_rebinds_in_stmt(body, name)
533
+ || catch_body
534
+ .as_ref()
535
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
536
+ || finally_body
537
+ .as_ref()
538
+ .is_some_and(|s| name_rebinds_in_stmt(s, name))
539
+ }
540
+ Statement::Break { .. } | Statement::Continue { .. } => false,
541
+ // VarDeclDestructure (could bind `name`) + any unknown construct → conservative.
542
+ _ => true,
543
+ }
544
+ }
545
+
546
+ /// #187: the set of top-level function names that are PROVABLY stable across the whole program — each
547
+ /// declared exactly once as a top-level `function N`, never also bound by a top-level `let`/`var`/
548
+ /// `const`, and never reassigned/redeclared/shadowed anywhere (via [`name_rebinds_in_stmt`], skipping
549
+ /// the single defining declaration but still recursing into its body). A direct native call to such a
550
+ /// callee can never dispatch a stale function, because the binding can never change.
551
+ fn compute_stable_globals(program: &Program) -> std::collections::HashSet<Arc<str>> {
552
+ let mut fn_count: HashMap<Arc<str>, usize> = HashMap::new();
553
+ let mut nonfn_toplevel: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
554
+ for s in &program.statements {
555
+ match s {
556
+ Statement::FunDecl { name, .. } => *fn_count.entry(Arc::clone(name)).or_insert(0) += 1,
557
+ Statement::VarDecl { name, .. } => {
558
+ nonfn_toplevel.insert(Arc::clone(name));
559
+ }
560
+ _ => {}
561
+ }
562
+ }
563
+ let mut stable: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
564
+ 'cand: for (name, &count) in &fn_count {
565
+ if count != 1 || nonfn_toplevel.contains(name) {
566
+ continue;
567
+ }
568
+ for s in &program.statements {
569
+ match s {
570
+ // The single defining `function name`: not a rebind, but its body/params still count.
571
+ Statement::FunDecl {
572
+ name: n,
573
+ params,
574
+ rest_param,
575
+ body,
576
+ ..
577
+ } if n == name => {
578
+ if params_bind_name(params, name)
579
+ || params_default_rebinds(params, name)
580
+ || rest_param
581
+ .as_ref()
582
+ .is_some_and(|rp| rp.name.as_ref() == name.as_ref())
583
+ || name_rebinds_in_stmt(body, name)
584
+ {
585
+ continue 'cand;
586
+ }
587
+ }
588
+ other => {
589
+ if name_rebinds_in_stmt(other, name) {
590
+ continue 'cand;
591
+ }
592
+ }
593
+ }
594
+ }
595
+ stable.insert(Arc::clone(name));
596
+ }
597
+ stable
598
+ }
599
+
353
600
  /// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
354
601
  /// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
355
602
  /// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
@@ -367,19 +614,42 @@ struct SlotScan {
367
614
  impl SlotScan {
368
615
  fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
369
616
  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)),
617
+ Statement::Block { statements, .. } => {
618
+ statements.iter().all(|s| self.stmt(s, in_closure))
619
+ }
620
+ Statement::Multi { statements, .. } => {
621
+ statements.iter().all(|s| self.stmt(s, in_closure))
622
+ }
623
+ Statement::VarDecl { init, .. } => {
624
+ init.as_ref().is_none_or(|e| self.expr(e, in_closure))
625
+ }
373
626
  Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
374
627
  Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
375
- Statement::If { cond, then_branch, else_branch, .. } => {
628
+ Statement::If {
629
+ cond,
630
+ then_branch,
631
+ else_branch,
632
+ ..
633
+ } => {
376
634
  self.expr(cond, in_closure)
377
635
  && self.stmt(then_branch, in_closure)
378
- && else_branch.as_ref().is_none_or(|s| self.stmt(s, in_closure))
636
+ && else_branch
637
+ .as_ref()
638
+ .is_none_or(|s| self.stmt(s, in_closure))
639
+ }
640
+ Statement::While { cond, body, .. } => {
641
+ self.expr(cond, in_closure) && self.stmt(body, in_closure)
379
642
  }
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, .. } => {
643
+ Statement::DoWhile { body, cond, .. } => {
644
+ self.stmt(body, in_closure) && self.expr(cond, in_closure)
645
+ }
646
+ Statement::For {
647
+ init,
648
+ cond,
649
+ update,
650
+ body,
651
+ ..
652
+ } => {
383
653
  init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
384
654
  && cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
385
655
  && update.as_ref().is_none_or(|e| self.expr(e, in_closure))
@@ -388,10 +658,17 @@ impl SlotScan {
388
658
  Statement::ForOf { iterable, body, .. } => {
389
659
  self.expr(iterable, in_closure) && self.stmt(body, in_closure)
390
660
  }
391
- Statement::Return { value, .. } => value.as_ref().is_none_or(|e| self.expr(e, in_closure)),
661
+ Statement::Return { value, .. } => {
662
+ value.as_ref().is_none_or(|e| self.expr(e, in_closure))
663
+ }
392
664
  Statement::Throw { value, .. } => self.expr(value, in_closure),
393
665
  Statement::Break { .. } | Statement::Continue { .. } => true,
394
- Statement::Switch { expr, cases, default_body, .. } => {
666
+ Statement::Switch {
667
+ expr,
668
+ cases,
669
+ default_body,
670
+ ..
671
+ } => {
395
672
  if !self.expr(expr, in_closure) {
396
673
  return false;
397
674
  }
@@ -409,10 +686,17 @@ impl SlotScan {
409
686
  .as_ref()
410
687
  .is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
411
688
  }
412
- Statement::Try { body, catch_body, finally_body, .. } => {
689
+ Statement::Try {
690
+ body,
691
+ catch_body,
692
+ finally_body,
693
+ ..
694
+ } => {
413
695
  self.stmt(body, in_closure)
414
696
  && 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))
697
+ && finally_body
698
+ .as_ref()
699
+ .is_none_or(|s| self.stmt(s, in_closure))
416
700
  }
417
701
  // A nested named function: its param defaults (enclosing-scope) + whole body capture.
418
702
  Statement::FunDecl { params, body, .. } => {
@@ -432,14 +716,25 @@ impl SlotScan {
432
716
  }
433
717
  true
434
718
  }
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)
719
+ Expr::Binary { left, right, .. } => {
720
+ self.expr(left, in_closure) && self.expr(right, in_closure)
721
+ }
722
+ Expr::Unary { operand, .. }
723
+ | Expr::TypeOf { operand, .. }
724
+ | Expr::Await { operand, .. } => self.expr(operand, in_closure),
725
+ Expr::Conditional {
726
+ cond,
727
+ then_branch,
728
+ else_branch,
729
+ ..
730
+ } => {
731
+ self.expr(cond, in_closure)
732
+ && self.expr(then_branch, in_closure)
733
+ && self.expr(else_branch, in_closure)
438
734
  }
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)
735
+ Expr::NullishCoalesce { left, right, .. } => {
736
+ self.expr(left, in_closure) && self.expr(right, in_closure)
441
737
  }
442
- Expr::NullishCoalesce { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
443
738
  Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
444
739
  self.expr(callee, in_closure)
445
740
  && args.iter().all(|a| match a {
@@ -447,7 +742,9 @@ impl SlotScan {
447
742
  })
448
743
  }
449
744
  Expr::Member { object, .. } => self.expr(object, in_closure),
450
- Expr::Index { object, index, .. } => self.expr(object, in_closure) && self.expr(index, in_closure),
745
+ Expr::Index { object, index, .. } => {
746
+ self.expr(object, in_closure) && self.expr(index, in_closure)
747
+ }
451
748
  Expr::Array { elements, .. } => elements.iter().all(|el| match el {
452
749
  ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
453
750
  }),
@@ -462,9 +759,18 @@ impl SlotScan {
462
759
  }
463
760
  self.expr(value, in_closure)
464
761
  }
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)
762
+ Expr::MemberAssign { object, value, .. } => {
763
+ self.expr(object, in_closure) && self.expr(value, in_closure)
764
+ }
765
+ Expr::IndexAssign {
766
+ object,
767
+ index,
768
+ value,
769
+ ..
770
+ } => {
771
+ self.expr(object, in_closure)
772
+ && self.expr(index, in_closure)
773
+ && self.expr(value, in_closure)
468
774
  }
469
775
  Expr::PostfixInc { name, .. }
470
776
  | Expr::PostfixDec { name, .. }
@@ -511,7 +817,11 @@ impl SlotScan {
511
817
  /// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
512
818
  /// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
513
819
  /// (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>>> {
820
+ fn slot_analyze(
821
+ params: &[FunParam],
822
+ has_rest: bool,
823
+ body: &Statement,
824
+ ) -> Option<HashSet<Arc<str>>> {
515
825
  if !slots_enabled() || has_rest {
516
826
  return None;
517
827
  }
@@ -559,7 +869,10 @@ impl<'a> Compiler<'a> {
559
869
  return Some(*s);
560
870
  }
561
871
  }
562
- self.slot_scopes.iter().rev().find_map(|m| m.get(name).copied())
872
+ self.slot_scopes
873
+ .iter()
874
+ .rev()
875
+ .find_map(|m| m.get(name).copied())
563
876
  }
564
877
 
565
878
  /// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
@@ -598,6 +911,8 @@ impl<'a> Compiler<'a> {
598
911
  general_slots: false,
599
912
  finally_stack: Vec::new(),
600
913
  self_fn_name: None,
914
+ math_is_global: false,
915
+ stable_globals: Arc::new(std::collections::HashSet::new()),
601
916
  }
602
917
  }
603
918
 
@@ -842,10 +1157,7 @@ impl<'a> Compiler<'a> {
842
1157
  /// condition shape stays eligible for the cranelift JIT (#167). Returns the `JumpIfFalse` patch
843
1158
  /// sites the caller must point at its "condition is false" target. The condition value itself is
844
1159
  /// 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> {
1160
+ fn compile_condition_jump_if_false(&mut self, cond: &Expr) -> Result<Vec<usize>, CompileError> {
849
1161
  match cond {
850
1162
  // `a && b` is false if EITHER operand is false — test each in order; both exit to the
851
1163
  // same false-target, so concatenate their patch sites.
@@ -1232,6 +1544,48 @@ impl<'a> Compiler<'a> {
1232
1544
  self.compile_destructure(pattern, false, false)?;
1233
1545
  }
1234
1546
  Statement::ExprStmt { expr, .. } => {
1547
+ // String-builder fast path: statement-position `acc += rhs` on a frame-slot local
1548
+ // compiles to `<rhs>; AppendLocal slot` (no LoadLocal/Dup/StoreLocal/Pop), letting
1549
+ // the VM append in amortized O(1) without materializing the discarded result. Only
1550
+ // for a simple slot-resolved identifier with the `+` compound op; everything else
1551
+ // (name-based vars, other ops) keeps the generic path.
1552
+ if let Expr::CompoundAssign {
1553
+ name,
1554
+ op: CompoundOp::Add,
1555
+ value,
1556
+ ..
1557
+ } = expr
1558
+ {
1559
+ if let Some(slot) = self.resolve_slot(name) {
1560
+ self.compile_expr(value)?;
1561
+ self.emit_u16(Opcode::AppendLocal, slot);
1562
+ return Ok(());
1563
+ }
1564
+ }
1565
+ // Same builder fast path for the plain-assign spelling `s = s + <str>` — but ONLY when
1566
+ // the appended operand is PROVABLY a string. A numeric accumulator (`i = i + 1`) must
1567
+ // NOT builder-ize: the VM keeps a single string builder slot, so a second builder-ized
1568
+ // slot flushes the first every iteration → O(n²) (#186). Restricting to string-typed
1569
+ // RHS keeps `s = s + "x"` fast (string_concat) while `i = i + 1` stays a plain store.
1570
+ if let Expr::Assign { name, value, .. } = expr {
1571
+ if let Expr::Binary {
1572
+ left,
1573
+ op: BinOp::Add,
1574
+ right,
1575
+ ..
1576
+ } = value.as_ref()
1577
+ {
1578
+ if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln == name)
1579
+ && is_string_typed(right)
1580
+ {
1581
+ if let Some(slot) = self.resolve_slot(name) {
1582
+ self.compile_expr(right)?;
1583
+ self.emit_u16(Opcode::AppendLocal, slot);
1584
+ return Ok(());
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1235
1589
  self.compile_expr(expr)?;
1236
1590
  self.emit(Opcode::Pop);
1237
1591
  }
@@ -1552,7 +1906,16 @@ impl<'a> Compiler<'a> {
1552
1906
  inner.slot_based = true;
1553
1907
  inner.num_slots = param_names.len() as u16;
1554
1908
  }
1909
+ // #187: stamp the chunk with its global name so the JIT can register it as a directly-
1910
+ // callable callee — but ONLY when it is a provably-stable top-level function. `self`
1911
+ // being the ROOT compiler (top-level) is indicated by `self.self_fn_name.is_none()` and
1912
+ // an empty `slot_scopes`; simplest sound check: the name is in `stable_globals` (which
1913
+ // by construction contains only the unique top-level `function name`).
1914
+ if self.stable_globals.contains(name) {
1915
+ inner.global_name = Some(Arc::clone(name));
1916
+ }
1555
1917
  let mut inner_comp = Compiler::new(&mut inner, false);
1918
+ inner_comp.stable_globals = Arc::clone(&self.stable_globals);
1556
1919
  // Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
1557
1920
  // param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
1558
1921
  // compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
@@ -1583,7 +1946,8 @@ impl<'a> Compiler<'a> {
1583
1946
  .iter()
1584
1947
  .map(|n| (Arc::clone(n), false))
1585
1948
  .collect::<HashMap<_, _>>()];
1586
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
1949
+ inner_comp
1950
+ .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
1587
1951
  inner_comp.emit_param_defaults_prologue(params)?;
1588
1952
  inner_comp.compile_statement(body)?;
1589
1953
  }
@@ -1792,6 +2156,7 @@ impl<'a> Compiler<'a> {
1792
2156
  message: "export default is not supported in bytecode".to_string(),
1793
2157
  });
1794
2158
  }
2159
+ ExportDeclaration::ReExport { .. } => {}
1795
2160
  },
1796
2161
  Statement::TypeAlias { .. }
1797
2162
  | Statement::DeclareVar { .. }
@@ -1958,6 +2323,30 @@ impl<'a> Compiler<'a> {
1958
2323
  self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
1959
2324
  }
1960
2325
  Expr::Call { callee, args, .. } => {
2326
+ // #186: `Math.<unaryfn>(arg)` → `<arg>; MathUnary(id)` when `Math` is provably the
2327
+ // global builtin (unshadowed program-wide) and there is exactly one argument. Lets the
2328
+ // numeric JIT lower the intrinsic (math_trig; a Math.* prereq for other kernels).
2329
+ if self.math_is_global && args.len() == 1 {
2330
+ if let Expr::Member {
2331
+ object,
2332
+ prop: MemberProp::Name { name: fname, .. },
2333
+ optional: false,
2334
+ ..
2335
+ } = callee.as_ref()
2336
+ {
2337
+ if let (Expr::Ident { name: obj, .. }, CallArg::Expr(arg)) =
2338
+ (object.as_ref(), &args[0])
2339
+ {
2340
+ if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
2341
+ if let Some(mfn) = MathUnaryFn::from_name(fname.as_ref()) {
2342
+ self.compile_expr(arg)?;
2343
+ self.emit_u16(Opcode::MathUnary, mfn as u16);
2344
+ return Ok(());
2345
+ }
2346
+ }
2347
+ }
2348
+ }
2349
+ }
1961
2350
  // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
1962
2351
  if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
1963
2352
  && args.len() == 1
@@ -2244,6 +2633,7 @@ impl<'a> Compiler<'a> {
2244
2633
  inner.num_slots = param_names.len() as u16;
2245
2634
  }
2246
2635
  let mut inner_comp = Compiler::new(&mut inner, false);
2636
+ inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
2247
2637
  if let Some(map) = simple_slots {
2248
2638
  inner_comp.slot_ctx = Some(map);
2249
2639
  } else {
@@ -2251,7 +2641,8 @@ impl<'a> Compiler<'a> {
2251
2641
  .iter()
2252
2642
  .map(|n| (Arc::clone(n), false))
2253
2643
  .collect::<HashMap<_, _>>()];
2254
- inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2644
+ inner_comp
2645
+ .emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
2255
2646
  }
2256
2647
  inner_comp.emit_param_defaults_prologue(params)?;
2257
2648
  match body {
@@ -2393,14 +2784,22 @@ impl<'a> Compiler<'a> {
2393
2784
  // pops both, removes the property, and pushes `true`. Deleting anything that
2394
2785
  // isn't a property reference is a no-op that still yields `true` (JS).
2395
2786
  match target.as_ref() {
2396
- Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
2787
+ Expr::Member {
2788
+ object,
2789
+ prop: MemberProp::Name { name, .. },
2790
+ ..
2791
+ } => {
2397
2792
  self.compile_expr(object)?;
2398
2793
  let idx = self.constant_idx(Constant::String(Arc::clone(name)));
2399
2794
  self.emit(Opcode::LoadConst);
2400
2795
  self.chunk.write_u16(idx);
2401
2796
  self.emit(Opcode::DeleteIndex);
2402
2797
  }
2403
- Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
2798
+ Expr::Member {
2799
+ object,
2800
+ prop: MemberProp::Expr(key),
2801
+ ..
2802
+ } => {
2404
2803
  self.compile_expr(object)?;
2405
2804
  self.compile_expr(key)?;
2406
2805
  self.emit(Opcode::DeleteIndex);
@@ -2662,9 +3061,106 @@ fn compile_internal(
2662
3061
  let mut chunk = Chunk::new();
2663
3062
  chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
2664
3063
  let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
3064
+ // #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
3065
+ // `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
3066
+ // `false` here only forgoes the optimization, never risks a miscompile.
3067
+ compiler.math_is_global = !program.statements.iter().any(|s| stmt_rebinds(s, "Math"));
3068
+ // #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
3069
+ // shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
3070
+ compiler.stable_globals = Arc::new(compute_stable_globals(program));
2665
3071
  compiler.compile_program(program)?;
2666
3072
  if peephole {
2667
3073
  crate::peephole::optimize(&mut chunk);
2668
3074
  }
2669
3075
  Ok(chunk)
2670
3076
  }
3077
+
3078
+ #[cfg(test)]
3079
+ mod stable_globals_tests {
3080
+ use super::compute_stable_globals;
3081
+
3082
+ fn stable(src: &str) -> std::collections::HashSet<String> {
3083
+ let prog = tishlang_parser::parse(src).expect("parse");
3084
+ compute_stable_globals(&prog)
3085
+ .into_iter()
3086
+ .map(|n| n.to_string())
3087
+ .collect()
3088
+ }
3089
+
3090
+ /// #187 gate: the exact spectral_norm shape — five top-level functions, none reassigned — must ALL
3091
+ /// be provably stable (this is what lets `multiplyAv` directly call `evalA`). The naive
3092
+ /// `stmt_rebinds`-based scan would return the empty set here (it treats every `FunDecl` as a
3093
+ /// rebind), so this is the regression tripwire for the purpose-built analysis.
3094
+ #[test]
3095
+ fn all_unreassigned_top_level_fns_are_stable() {
3096
+ let s = stable(
3097
+ "function evalA(i, j) { return 1.0 / (i + j) }\n\
3098
+ function multiplyAv(n, v, av) { let i = 0; while (i < n) { av[i] = evalA(i, i) * v[i]; i = i + 1 } }\n\
3099
+ function spectralNorm(n) { multiplyAv(n, n, n); return n }\n",
3100
+ );
3101
+ assert!(
3102
+ s.contains("evalA"),
3103
+ "evalA (called by multiplyAv) must be stable"
3104
+ );
3105
+ assert!(s.contains("multiplyAv"));
3106
+ assert!(s.contains("spectralNorm"));
3107
+ }
3108
+
3109
+ /// A reassigned function is NOT stable (a direct call could hit a stale binding) — but a sibling
3110
+ /// that is never reassigned still is.
3111
+ #[test]
3112
+ fn reassigned_function_is_excluded() {
3113
+ let s = stable(
3114
+ "function f(x) { return x + 1 }\n\
3115
+ function g(x) { return f(x) * 2 }\n\
3116
+ f = (x) => x + 100\n",
3117
+ );
3118
+ assert!(!s.contains("f"), "f is reassigned → must NOT be stable");
3119
+ assert!(s.contains("g"), "g is never reassigned → stable");
3120
+ }
3121
+
3122
+ /// Reassignment/redeclaration/shadowing INSIDE another function body must disqualify the name —
3123
+ /// the whole-program walk has to recurse into every body (the whole point vs `stmt_rebinds`).
3124
+ #[test]
3125
+ fn cross_body_rebind_and_shadow_and_redecl_excluded() {
3126
+ // reassigned inside another function's body
3127
+ assert!(!stable(
3128
+ "function h(x) { return x }\n\
3129
+ function k() { h = 5; return h }\n"
3130
+ )
3131
+ .contains("h"));
3132
+ // shadowed by a param in another function
3133
+ assert!(!stable(
3134
+ "function p(x) { return x }\n\
3135
+ function q(p) { return p }\n"
3136
+ )
3137
+ .contains("p"));
3138
+ // declared twice at top level
3139
+ assert!(!stable("function d(x) { return x }\nfunction d(y) { return y }\n").contains("d"));
3140
+ // also bound by a top-level let
3141
+ assert!(!stable("function e(x) { return x }\nlet e = 3\n").contains("e"));
3142
+ }
3143
+
3144
+ /// A global reassigned inside a PARAMETER DEFAULT (of a function or arrow) must disqualify it — the
3145
+ /// default runs in the enclosing scope when the function is called, so a direct call would be stale.
3146
+ #[test]
3147
+ fn param_default_rebind_excluded() {
3148
+ // `foo` reassigned in another function's param default
3149
+ assert!(!stable(
3150
+ "function evil(x) { return x + 1000 }\n\
3151
+ function foo(x) { return x + 1 }\n\
3152
+ function resetFoo(y = (foo = evil)) { return y }\n"
3153
+ )
3154
+ .contains("foo"));
3155
+ // `bar` reassigned in an arrow's param default
3156
+ assert!(!stable(
3157
+ "function bar(x) { return x }\n\
3158
+ let f = (z = (bar = 5)) => z\n"
3159
+ )
3160
+ .contains("bar"));
3161
+ // a param default that does NOT touch the name leaves it stable
3162
+ assert!(
3163
+ stable("function ok(x) { return x }\nfunction u(a = 1) { return a }\n").contains("ok")
3164
+ );
3165
+ }
3166
+ }