@tishlang/tish 1.13.2 → 2.0.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 (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 +43 -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
@@ -9,13 +9,13 @@
9
9
  //! - Comparison of two `number` expressions → `boolean`
10
10
  //! - Already-annotated vars are left unchanged.
11
11
 
12
- use std::collections::HashMap;
12
+ use std::collections::{HashMap, HashSet};
13
13
  use tishlang_ast::{
14
14
  ArrowBody, BinOp, CallArg, Expr, FunParam, Literal, Program, Statement, TypeAnnotation,
15
15
  };
16
16
 
17
17
  /// Scoped type environment used during inference.
18
- #[derive(Default)]
18
+ #[derive(Default, Clone)]
19
19
  pub struct InferCtx {
20
20
  scopes: Vec<HashMap<String, TypeAnnotation>>,
21
21
  }
@@ -55,6 +55,41 @@ fn is_number(ann: &TypeAnnotation) -> bool {
55
55
  matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "number")
56
56
  }
57
57
 
58
+ fn is_string(ann: &TypeAnnotation) -> bool {
59
+ matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "string")
60
+ }
61
+
62
+ fn is_bool(ann: &TypeAnnotation) -> bool {
63
+ matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "boolean")
64
+ }
65
+
66
+ /// Element type of an array literal of uniform native scalars (number/string/boolean), for
67
+ /// `let xs = [1, 2, 3]` -> `number[]`. Bails (None) on empty, spread, mixed, or non-scalar
68
+ /// elements so the binding stays a boxed array.
69
+ fn infer_array_elem(elements: &[tishlang_ast::ArrayElement], ctx: &InferCtx) -> Option<TypeAnnotation> {
70
+ use tishlang_ast::ArrayElement;
71
+ if elements.is_empty() {
72
+ return None;
73
+ }
74
+ let mut elem: Option<TypeAnnotation> = None;
75
+ for el in elements {
76
+ let e = match el {
77
+ ArrayElement::Expr(e) => e,
78
+ ArrayElement::Spread(_) => return None,
79
+ };
80
+ let t = infer_expr_type(e, ctx)?;
81
+ if !(is_number(&t) || is_string(&t) || is_bool(&t)) {
82
+ return None;
83
+ }
84
+ match &elem {
85
+ None => elem = Some(t),
86
+ Some(prev) if prev != &t => return None,
87
+ _ => {}
88
+ }
89
+ }
90
+ elem
91
+ }
92
+
58
93
  fn number_ann() -> TypeAnnotation {
59
94
  TypeAnnotation::Simple("number".into())
60
95
  }
@@ -84,9 +119,10 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
84
119
  let rt = infer_expr_type(right, ctx)?;
85
120
  if is_number(&lt) && is_number(&rt) {
86
121
  match op {
87
- BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
88
- Some(number_ann())
89
- }
122
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
123
+ // Bitwise/shift coerce to int32 and yield a Number.
124
+ | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor
125
+ | BinOp::Shl | BinOp::Shr | BinOp::UShr => Some(number_ann()),
90
126
  BinOp::Lt
91
127
  | BinOp::Le
92
128
  | BinOp::Gt
@@ -95,6 +131,14 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
95
131
  | BinOp::StrictNe => Some(bool_ann()),
96
132
  _ => None,
97
133
  }
134
+ } else if is_string(&lt) && is_string(&rt) {
135
+ // M2: `string + string` concatenates → string; `===`/`!==` → boolean. Relational
136
+ // comparisons stay boxed (UTF-16 vs UTF-8 ordering differs outside the BMP).
137
+ match op {
138
+ BinOp::Add => Some(string_ann()),
139
+ BinOp::StrictEq | BinOp::StrictNe => Some(bool_ann()),
140
+ _ => None,
141
+ }
98
142
  } else {
99
143
  None
100
144
  }
@@ -111,9 +155,19 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
111
155
  }
112
156
  }
113
157
  UnaryOp::Not => Some(bool_ann()),
158
+ // `~x` is a Number.
159
+ UnaryOp::BitNot => {
160
+ let t = infer_expr_type(operand, ctx)?;
161
+ is_number(&t).then(number_ann)
162
+ }
114
163
  _ => None,
115
164
  }
116
165
  }
166
+ // Index of a typed array yields its element type (`a[i]` where `a: T[]` → `T`).
167
+ Expr::Index { object, .. } => match infer_expr_type(object, ctx) {
168
+ Some(TypeAnnotation::Array(elem)) => Some(*elem),
169
+ _ => None,
170
+ },
117
171
  _ => None,
118
172
  }
119
173
  }
@@ -121,9 +175,1286 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
121
175
  /// Run inference over a program, returning a modified Program with additional
122
176
  /// type annotations filled in on `VarDecl` nodes.
123
177
  pub fn infer_program(program: &Program) -> Program {
178
+ // M4 (opt-in via TISH_PARAM_INFER) runs FIRST: give unannotated params used PURELY
179
+ // numerically a synthetic `: number`. Doing this *before* local/struct inference is what
180
+ // lets derived numeric locals (`let x0 = (px / w) * 3`) be proven numeric off the now-known
181
+ // param types — otherwise they fall back to boxed `Value` and the whole hot loop boxes with
182
+ // them (the difference between an idiomatic numeric fn going native vs staying boxed).
183
+ // Conservative — any non-numeric / write / escape use bails (param stays boxed Value).
184
+ let p = if std::env::var("TISH_PARAM_INFER").map(|v| v != "0").unwrap_or(false) {
185
+ param_infer_program(program.clone())
186
+ } else {
187
+ program.clone()
188
+ };
189
+ // Base + local-numeric inference (annotates `let` nodes), now seeing M4's param types.
124
190
  let mut ctx = InferCtx::new();
191
+ let p = Program {
192
+ statements: infer_statements(&p.statements, &mut ctx),
193
+ };
194
+ // Automatic struct inference (opt-in via TISH_STRUCT_INFER until proven):
195
+ // give unannotated object literals a concrete struct type so the Rust
196
+ // backend emits unboxed structs with direct field access. Conservative —
197
+ // only applies when every use of the binding is a literal-key field read,
198
+ // so it can never miscompile (any uncertainty falls back to boxed Value).
199
+ if std::env::var("TISH_STRUCT_INFER").map(|v| v != "0").unwrap_or(false) {
200
+ struct_infer_program(p)
201
+ } else {
202
+ p
203
+ }
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // M4: parameter type inference (conservative, sound, opt-in)
208
+ // ---------------------------------------------------------------------------
209
+
210
+ fn param_infer_program(program: Program) -> Program {
125
211
  Program {
126
- statements: infer_statements(&program.statements, &mut ctx),
212
+ statements: program.statements.into_iter().map(pi_stmt).collect(),
213
+ }
214
+ }
215
+
216
+ fn pi_stmt(s: Statement) -> Statement {
217
+ if let Statement::FunDecl {
218
+ async_,
219
+ name,
220
+ name_span,
221
+ params,
222
+ rest_param,
223
+ return_type,
224
+ body,
225
+ span,
226
+ } = s
227
+ {
228
+ // Locals provably numeric in this body (annotated, incl. base-inferred `let i = 0`), so a
229
+ // bare loop counter `i` counts as a numeric operand and `i < n` can prove the param `n`.
230
+ let mut nums = HashSet::new();
231
+ collect_numeric_locals(&body, &mut nums);
232
+ let new_params = params
233
+ .into_iter()
234
+ .map(|p| match p {
235
+ FunParam::Simple(mut tp) => {
236
+ if tp.type_ann.is_none()
237
+ && tp.default.is_none()
238
+ && nus_stmt(&body, tp.name.as_ref(), &nums)
239
+ {
240
+ tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number")));
241
+ }
242
+ FunParam::Simple(tp)
243
+ }
244
+ other => other,
245
+ })
246
+ .collect();
247
+ Statement::FunDecl {
248
+ async_,
249
+ name,
250
+ name_span,
251
+ params: new_params,
252
+ rest_param,
253
+ return_type,
254
+ body,
255
+ span,
256
+ }
257
+ } else {
258
+ s
259
+ }
260
+ }
261
+
262
+ /// Names of locals in `s` annotated (or base-inferred) `: number`. Consulted by `numeric_provable`
263
+ /// so a bare numeric local (e.g. a `let i: number` loop counter) counts as a numeric operand —
264
+ /// letting `i < n` / `i * n + k` prove the *param* `n` numeric. Flat across nested scopes; at worst
265
+ /// it over-includes a shadowed name, which only widens inference (still bounded by the same
266
+ /// caller-passes-a-number assumption M4 already makes).
267
+ fn collect_numeric_locals(s: &Statement, out: &mut HashSet<String>) {
268
+ use Statement::*;
269
+ match s {
270
+ VarDecl {
271
+ name,
272
+ type_ann,
273
+ init,
274
+ ..
275
+ } => {
276
+ // Annotated `: number`, OR **base-inferred** from a numeric-literal initializer
277
+ // (`let i = 0`, `let x = 0.0`) — the common loop-counter / accumulator pattern. A
278
+ // numeric literal is unambiguously a number at init; this is what lets `i < n` prove
279
+ // the *param* `n` numeric. Over-inclusion (if the local is later reassigned to a
280
+ // non-number) only *widens* param inference, still bounded by M4's
281
+ // caller-passes-a-number / NaN-coercion soundness and the corpus/gauntlet guards.
282
+ let numeric = type_ann.as_ref().is_some_and(is_number)
283
+ || matches!(
284
+ init,
285
+ Some(tishlang_ast::Expr::Literal {
286
+ value: tishlang_ast::Literal::Number(_),
287
+ ..
288
+ })
289
+ );
290
+ if numeric {
291
+ out.insert(name.to_string());
292
+ }
293
+ }
294
+ Block { statements, .. } | Multi { statements, .. } => {
295
+ statements.iter().for_each(|x| collect_numeric_locals(x, out))
296
+ }
297
+ If {
298
+ then_branch,
299
+ else_branch,
300
+ ..
301
+ } => {
302
+ collect_numeric_locals(then_branch, out);
303
+ if let Some(e) = else_branch {
304
+ collect_numeric_locals(e, out);
305
+ }
306
+ }
307
+ For { init, body, .. } => {
308
+ if let Some(i) = init {
309
+ collect_numeric_locals(i, out);
310
+ }
311
+ collect_numeric_locals(body, out);
312
+ }
313
+ While { body, .. } => collect_numeric_locals(body, out),
314
+ _ => {}
315
+ }
316
+ }
317
+
318
+ /// One side of an OVERLOADED binop (`+`, comparisons). If `operand` is bare `name`, the `other`
319
+ /// side must be PROVABLY numeric (else `name + x` / `name < x` could be string ops, and `name`
320
+ /// a string). If `operand` is a sub-expr, recurse (its own context decides).
321
+ fn nus_overloaded(operand: &Expr, other: &Expr, name: &str, nums: &HashSet<String>) -> bool {
322
+ if matches!(operand, Expr::Ident { name: n, .. } if n.as_ref() == name) {
323
+ return numeric_provable(other, nums);
324
+ }
325
+ nus_expr(operand, name, nums)
326
+ }
327
+
328
+ /// `e` is PROVABLY a number: a number literal, arithmetic (`-`/`*`/`/`/`%`/`**`), numeric unary,
329
+ /// or a Math intrinsic. Bare variables and `+`/comparisons are NOT provable (could be strings).
330
+ fn numeric_provable(e: &Expr, nums: &HashSet<String>) -> bool {
331
+ use Expr::*;
332
+ match e {
333
+ Literal {
334
+ value: tishlang_ast::Literal::Number(_),
335
+ ..
336
+ } => true,
337
+ // A local proven numeric in this function (annotated `: number`, incl. base-inferred
338
+ // `let i = 0`) — so `i` as the OTHER operand of `i < n` / `i * n` proves `n` numeric.
339
+ Ident { name: n, .. } => nums.contains(n.as_ref()),
340
+ Binary {
341
+ left, op, right, ..
342
+ } => {
343
+ use tishlang_ast::BinOp::*;
344
+ matches!(
345
+ op,
346
+ Sub | Mul | Div | Mod | Pow | BitAnd | BitOr | BitXor | Shl | Shr | UShr
347
+ ) && numeric_provable(left, nums)
348
+ && numeric_provable(right, nums)
349
+ }
350
+ Unary { op, operand, .. } => {
351
+ matches!(
352
+ op,
353
+ tishlang_ast::UnaryOp::Neg | tishlang_ast::UnaryOp::Pos | tishlang_ast::UnaryOp::BitNot
354
+ ) && numeric_provable(operand, nums)
355
+ }
356
+ Call { callee, .. } => matches!(callee.as_ref(),
357
+ Expr::Member { object, prop: tishlang_ast::MemberProp::Name { name: m, .. }, .. }
358
+ if matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
359
+ && matches!(m.as_ref(),
360
+ "sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp" | "trunc" | "log")),
361
+ _ => false,
362
+ }
363
+ }
364
+
365
+ /// Every use of `name` within `s` is a numeric-operand use (so `name` can lower to `f64`).
366
+ fn nus_stmt(s: &Statement, name: &str, nums: &HashSet<String>) -> bool {
367
+ use Statement::*;
368
+ match s {
369
+ Block { statements, .. } => statements.iter().all(|x| nus_stmt(x, name, nums)),
370
+ Return { value, .. } => value.as_ref().is_none_or(|e| nus_num_operand(e, name, nums)),
371
+ If {
372
+ cond,
373
+ then_branch,
374
+ else_branch,
375
+ ..
376
+ } => {
377
+ nus_expr(cond, name, nums)
378
+ && nus_stmt(then_branch, name, nums)
379
+ && else_branch.as_ref().is_none_or(|e| nus_stmt(e, name, nums))
380
+ }
381
+ ExprStmt { expr, .. } => nus_expr(expr, name, nums),
382
+ While { cond, body, .. } => nus_expr(cond, name, nums) && nus_stmt(body, name, nums),
383
+ For {
384
+ init,
385
+ cond,
386
+ update,
387
+ body,
388
+ ..
389
+ } => {
390
+ init.as_ref().is_none_or(|x| nus_stmt(x, name, nums))
391
+ && cond.as_ref().is_none_or(|e| nus_expr(e, name, nums))
392
+ && update.as_ref().is_none_or(|e| nus_expr(e, name, nums))
393
+ && nus_stmt(body, name, nums)
394
+ }
395
+ VarDecl {
396
+ name: vn, init, ..
397
+ } => vn.as_ref() != name && init.as_ref().is_none_or(|e| nus_expr(e, name, nums)),
398
+ Break { .. } | Continue { .. } => true,
399
+ // Any other statement (switch/throw/try/nested fn/...) -> bail (don't infer this param).
400
+ _ => false,
401
+ }
402
+ }
403
+
404
+ /// `e` with `name` used only as a numeric operand. A bare `Ident(name)` at THIS level is not a
405
+ /// numeric operand (only valid inside a numeric parent), so it returns false here.
406
+ fn nus_expr(e: &Expr, name: &str, nums: &HashSet<String>) -> bool {
407
+ use Expr::*;
408
+ match e {
409
+ Literal { .. } => true,
410
+ Ident { name: n, .. } => n.as_ref() != name,
411
+ Binary {
412
+ left, op, right, ..
413
+ } => {
414
+ use tishlang_ast::BinOp::*;
415
+ match op {
416
+ // Unambiguously numeric — `name` as either operand is definitely a number.
417
+ // Includes the bitwise/shift family: JS coerces both sides to int32, so
418
+ // `name & x` / `name >>> x` proves `name` numeric just like `name * x`.
419
+ Sub | Mul | Div | Mod | Pow | BitAnd | BitOr | BitXor | Shl | Shr | UShr => {
420
+ nus_num_operand(left, name, nums) && nus_num_operand(right, name, nums)
421
+ }
422
+ // OVERLOADED: `+` is also string concat, `<`/`===` also compare strings. If
423
+ // `name` is a DIRECT operand here, the OTHER side must be PROVABLY numeric to
424
+ // conclude `name` is a number — this is what stops `first + ":"` typing `first`.
425
+ Add | Lt | Le | Gt | Ge | StrictEq | StrictNe => {
426
+ nus_overloaded(left, right, name, nums)
427
+ && nus_overloaded(right, left, name, nums)
428
+ }
429
+ // Logical `&&`/`||`: recurse — a param used numerically *inside* a condition
430
+ // operand (e.g. `iter < maxIter && x*x + y*y <= 4`) is a numeric use; a bare
431
+ // `name && x` is not (the recursion's `Ident(name)` case returns false).
432
+ And | Or => nus_expr(left, name, nums) && nus_expr(right, name, nums),
433
+ // Anything else (`Eq`/`Ne`/`In`): `name` must be absent.
434
+ _ => !pi_mentions(left, name) && !pi_mentions(right, name),
435
+ }
436
+ }
437
+ Unary { op, operand, .. } => {
438
+ if matches!(
439
+ op,
440
+ tishlang_ast::UnaryOp::Neg | tishlang_ast::UnaryOp::Pos | tishlang_ast::UnaryOp::BitNot
441
+ ) {
442
+ nus_num_operand(operand, name, nums)
443
+ } else {
444
+ !pi_mentions(operand, name)
445
+ }
446
+ }
447
+ Index { object, index, .. } => {
448
+ !pi_mentions(object, name) && nus_num_operand(index, name, nums)
449
+ }
450
+ Call { callee, args, .. } => {
451
+ !pi_mentions(callee, name)
452
+ && args.iter().all(|a| match a {
453
+ tishlang_ast::CallArg::Expr(x) => nus_arg(x, name, nums),
454
+ tishlang_ast::CallArg::Spread(_) => false,
455
+ })
456
+ }
457
+ Conditional {
458
+ cond,
459
+ then_branch,
460
+ else_branch,
461
+ ..
462
+ } => {
463
+ nus_expr(cond, name, nums)
464
+ && nus_num_operand(then_branch, name, nums)
465
+ && nus_num_operand(else_branch, name, nums)
466
+ }
467
+ // Assignment to a DIFFERENT var, where the RHS may use `name` numerically (e.g.
468
+ // `sum = sum + a[i*N+k]`). Writing to `name` itself bails (its type could change).
469
+ Assign { name: an, value, .. }
470
+ | CompoundAssign { name: an, value, .. }
471
+ | LogicalAssign { name: an, value, .. } => {
472
+ an.as_ref() != name && nus_expr(value, name, nums)
473
+ }
474
+ PostfixInc { name: n, .. }
475
+ | PostfixDec { name: n, .. }
476
+ | PrefixInc { name: n, .. }
477
+ | PrefixDec { name: n, .. } => n.as_ref() != name,
478
+ // `c[i*N+j] = sum`: index is a numeric operand, RHS may use `name` numerically.
479
+ IndexAssign {
480
+ object,
481
+ index,
482
+ value,
483
+ ..
484
+ } => {
485
+ !pi_mentions(object, name)
486
+ && nus_num_operand(index, name, nums)
487
+ && nus_expr(value, name, nums)
488
+ }
489
+ MemberAssign { object, value, .. } => {
490
+ !pi_mentions(object, name) && nus_expr(value, name, nums)
491
+ }
492
+ // any other context where `name` could appear non-numerically -> require it absent.
493
+ _ => !pi_mentions(e, name),
494
+ }
495
+ }
496
+
497
+ /// A numeric-operand position: `name` may appear directly, or as a numeric sub-expr.
498
+ fn nus_num_operand(e: &Expr, name: &str, nums: &HashSet<String>) -> bool {
499
+ if matches!(e, Expr::Ident { name: n, .. } if n.as_ref() == name) {
500
+ return true;
501
+ }
502
+ nus_expr(e, name, nums)
503
+ }
504
+
505
+ /// A call argument: passing `name` BARE bails (callee param type unknown); a numeric sub-expr ok.
506
+ fn nus_arg(e: &Expr, name: &str, nums: &HashSet<String>) -> bool {
507
+ if matches!(e, Expr::Ident { name: n, .. } if n.as_ref() == name) {
508
+ return false;
509
+ }
510
+ nus_expr(e, name, nums)
511
+ }
512
+
513
+ /// Does `name` appear anywhere in `e`? Conservative: unhandled forms -> true (assume present).
514
+ pub(crate) fn pi_mentions(e: &Expr, name: &str) -> bool {
515
+ use Expr::*;
516
+ match e {
517
+ Literal { .. } => false,
518
+ Ident { name: n, .. } => n.as_ref() == name,
519
+ Binary { left, right, .. } | NullishCoalesce { left, right, .. } => {
520
+ pi_mentions(left, name) || pi_mentions(right, name)
521
+ }
522
+ Unary { operand, .. } | TypeOf { operand, .. } | Await { operand, .. } => {
523
+ pi_mentions(operand, name)
524
+ }
525
+ Member { object, prop, .. } => {
526
+ pi_mentions(object, name)
527
+ || matches!(prop, tishlang_ast::MemberProp::Expr(p) if pi_mentions(p, name))
528
+ }
529
+ Index { object, index, .. } => pi_mentions(object, name) || pi_mentions(index, name),
530
+ Call { callee, args, .. } | New { callee, args, .. } => {
531
+ pi_mentions(callee, name)
532
+ || args.iter().any(|a| match a {
533
+ tishlang_ast::CallArg::Expr(x) | tishlang_ast::CallArg::Spread(x) => {
534
+ pi_mentions(x, name)
535
+ }
536
+ })
537
+ }
538
+ Conditional {
539
+ cond,
540
+ then_branch,
541
+ else_branch,
542
+ ..
543
+ } => {
544
+ pi_mentions(cond, name)
545
+ || pi_mentions(then_branch, name)
546
+ || pi_mentions(else_branch, name)
547
+ }
548
+ Assign { name: n, value, .. }
549
+ | CompoundAssign { name: n, value, .. }
550
+ | LogicalAssign { name: n, value, .. } => n.as_ref() == name || pi_mentions(value, name),
551
+ PostfixInc { name: n, .. }
552
+ | PostfixDec { name: n, .. }
553
+ | PrefixInc { name: n, .. }
554
+ | PrefixDec { name: n, .. } => n.as_ref() == name,
555
+ Array { elements, .. } => elements.iter().any(|el| match el {
556
+ tishlang_ast::ArrayElement::Expr(x) | tishlang_ast::ArrayElement::Spread(x) => {
557
+ pi_mentions(x, name)
558
+ }
559
+ }),
560
+ Object { props, .. } => props.iter().any(|p| match p {
561
+ tishlang_ast::ObjectProp::KeyValue(_, v) => pi_mentions(v, name),
562
+ tishlang_ast::ObjectProp::Spread(x) => pi_mentions(x, name),
563
+ }),
564
+ TemplateLiteral { exprs, .. } => exprs.iter().any(|x| pi_mentions(x, name)),
565
+ MemberAssign { object, value, .. } => {
566
+ pi_mentions(object, name) || pi_mentions(value, name)
567
+ }
568
+ IndexAssign {
569
+ object,
570
+ index,
571
+ value,
572
+ ..
573
+ } => pi_mentions(object, name) || pi_mentions(index, name) || pi_mentions(value, name),
574
+ // ArrowFunction (could capture), Jsx, native loads, etc. -> assume present (bail).
575
+ _ => true,
576
+ }
577
+ }
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // Automatic struct inference (conservative, sound, opt-in)
581
+ // ---------------------------------------------------------------------------
582
+
583
+ /// One emitted `type` decl: alias name plus its field list.
584
+ type StructDecl = (String, Vec<(std::sync::Arc<str>, TypeAnnotation)>);
585
+
586
+ /// Registry of distinct inferred object shapes → synthetic alias name, so
587
+ /// identical shapes share one generated struct.
588
+ #[derive(Default)]
589
+ struct StructRegistry {
590
+ /// canonical "k1:ty1;k2:ty2;…" → alias name
591
+ by_shape: HashMap<String, String>,
592
+ /// alias name → field list (for emitting the `type` decls)
593
+ decls: Vec<StructDecl>,
594
+ }
595
+
596
+ impl StructRegistry {
597
+ fn intern(&mut self, fields: &[(std::sync::Arc<str>, TypeAnnotation)]) -> String {
598
+ let canon = fields
599
+ .iter()
600
+ .map(|(k, t)| format!("{}:{}", k, type_canon(t)))
601
+ .collect::<Vec<_>>()
602
+ .join(";");
603
+ if let Some(name) = self.by_shape.get(&canon) {
604
+ return name.clone();
605
+ }
606
+ let name = format!("TishAnon_{}", self.decls.len());
607
+ self.by_shape.insert(canon, name.clone());
608
+ self.decls.push((name.clone(), fields.to_vec()));
609
+ name
610
+ }
611
+ }
612
+
613
+ fn type_canon(t: &TypeAnnotation) -> String {
614
+ match t {
615
+ TypeAnnotation::Simple(s) => s.to_string(),
616
+ TypeAnnotation::Object(fields) => format!(
617
+ "{{{}}}",
618
+ fields
619
+ .iter()
620
+ .map(|(k, t)| format!("{}:{}", k, type_canon(t)))
621
+ .collect::<Vec<_>>()
622
+ .join(";")
623
+ ),
624
+ _ => "?".to_string(),
625
+ }
626
+ }
627
+
628
+ /// Infer a concrete object shape from an object literal, or `None` if any field
629
+ /// can't be typed concretely / there's a spread.
630
+ fn infer_object_shape(
631
+ props: &[tishlang_ast::ObjectProp],
632
+ ctx: &InferCtx,
633
+ ) -> Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>> {
634
+ let mut fields = Vec::with_capacity(props.len());
635
+ for p in props {
636
+ match p {
637
+ tishlang_ast::ObjectProp::KeyValue(k, v) => {
638
+ let ty = infer_expr_type(v, ctx)?;
639
+ // Only primitive field types in this conservative version.
640
+ if !matches!(&ty, TypeAnnotation::Simple(s)
641
+ if matches!(s.as_ref(), "number" | "string" | "boolean"))
642
+ {
643
+ return None;
644
+ }
645
+ fields.push((k.clone(), ty));
646
+ }
647
+ tishlang_ast::ObjectProp::Spread(_) => return None,
648
+ }
649
+ }
650
+ if fields.is_empty() {
651
+ return None;
652
+ }
653
+ Some(fields)
654
+ }
655
+
656
+ fn struct_infer_program(program: Program) -> Program {
657
+ let mut reg = StructRegistry::default();
658
+ let mut ctx = InferCtx::new();
659
+ let mut stmts = si_block(program.statements, &mut reg, &mut ctx);
660
+ // Prepend the generated struct `type` aliases so codegen synthesizes them.
661
+ let mut out: Vec<Statement> = Vec::with_capacity(stmts.len() + reg.decls.len());
662
+ let span = stmts.first().map(stmt_span).unwrap_or_else(zero_span);
663
+ for (name, fields) in reg.decls.drain(..) {
664
+ out.push(Statement::TypeAlias {
665
+ name: name.as_str().into(),
666
+ name_span: span,
667
+ ty: TypeAnnotation::Object(fields),
668
+ span,
669
+ });
670
+ }
671
+ out.append(&mut stmts);
672
+ Program { statements: out }
673
+ }
674
+
675
+ fn stmt_span(s: &Statement) -> tishlang_ast::Span {
676
+ match s {
677
+ Statement::VarDecl { span, .. }
678
+ | Statement::Block { span, .. }
679
+ | Statement::ExprStmt { span, .. }
680
+ | Statement::If { span, .. }
681
+ | Statement::For { span, .. }
682
+ | Statement::ForOf { span, .. }
683
+ | Statement::While { span, .. }
684
+ | Statement::Return { span, .. }
685
+ | Statement::FunDecl { span, .. }
686
+ | Statement::TypeAlias { span, .. } => *span,
687
+ _ => zero_span(),
688
+ }
689
+ }
690
+
691
+ fn zero_span() -> tishlang_ast::Span {
692
+ tishlang_ast::Span {
693
+ start: (0, 0),
694
+ end: (0, 0),
695
+ }
696
+ }
697
+
698
+ /// Transform a block: annotate struct-safe object `let` bindings, recursing
699
+ /// into nested blocks. `ctx` provides field-type inference for initializers.
700
+ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx) -> Vec<Statement> {
701
+ ctx.push_scope();
702
+ // Co-infer mutable `number[]` locals across the whole block (cross-referencing arrays) up front.
703
+ // Co-infer mutable native arrays (number[] and boolean[]) across the block; an array of the
704
+ // wrong element type simply fails its run and stays boxed.
705
+ let native_num_arrays = block_native_arrays(&stmts, ctx, &number_ann());
706
+ let native_bool_arrays = block_native_arrays(&stmts, ctx, &bool_ann());
707
+ let n = stmts.len();
708
+ let mut out: Vec<Statement> = Vec::with_capacity(n);
709
+ for (i, stmt) in stmts.iter().enumerate() {
710
+ // Candidate: `let xs = [ ...uniform native scalars... ]` with no annotation -> native `T[]`,
711
+ // but only when every later use is read-only (`uses_are_array_safe`), so a `Vec<f64>`
712
+ // assumption can't be violated by a later `push`/`xs[i] = …`.
713
+ if let Statement::VarDecl {
714
+ name,
715
+ name_span,
716
+ mutable,
717
+ type_ann: None,
718
+ init: Some(Expr::Array { elements, .. }),
719
+ span,
720
+ } = stmt
721
+ {
722
+ // (a) Read-only typed array: uniform scalar literals, every later use read-only.
723
+ if let Some(elem) = infer_array_elem(elements, ctx) {
724
+ if uses_are_array_safe(name.as_ref(), &stmts[i + 1..]) {
725
+ let arr_ann = TypeAnnotation::Array(Box::new(elem));
726
+ ctx.define(name.as_ref(), arr_ann.clone());
727
+ out.push(Statement::VarDecl {
728
+ name: name.clone(),
729
+ name_span: *name_span,
730
+ mutable: *mutable,
731
+ type_ann: Some(arr_ann),
732
+ init: stmt_init_clone(stmt),
733
+ span: *span,
734
+ });
735
+ continue;
736
+ }
737
+ }
738
+ // (b) Mutable native `number[]` / `boolean[]`: `let a = []` / `[lits]` driven by push +
739
+ // index read/write (`fannkuch`/`queens`/`nsieve`, incl. cross-referencing siblings).
740
+ // Sound via the block-level fixpoint above (every accepted array's elements are provably
741
+ // of the chosen element type). `number[]` wins over `boolean[]` if somehow in both.
742
+ let native_elem = if native_num_arrays.contains(name.as_ref()) {
743
+ Some(number_ann())
744
+ } else if native_bool_arrays.contains(name.as_ref()) {
745
+ Some(bool_ann())
746
+ } else {
747
+ None
748
+ };
749
+ if let Some(elem) = native_elem {
750
+ let arr_ann = TypeAnnotation::Array(Box::new(elem));
751
+ ctx.define(name.as_ref(), arr_ann.clone());
752
+ out.push(Statement::VarDecl {
753
+ name: name.clone(),
754
+ name_span: *name_span,
755
+ mutable: *mutable,
756
+ type_ann: Some(arr_ann),
757
+ init: stmt_init_clone(stmt),
758
+ span: *span,
759
+ });
760
+ continue;
761
+ }
762
+ }
763
+ // Candidate: `let o = { ...object literal... }` with no annotation.
764
+ if let Statement::VarDecl {
765
+ name,
766
+ name_span,
767
+ mutable,
768
+ type_ann: None,
769
+ init: Some(Expr::Object { props, .. }),
770
+ span,
771
+ } = stmt
772
+ {
773
+ if let Some(fields) = infer_object_shape(props, ctx) {
774
+ let keys: std::collections::HashSet<&str> =
775
+ fields.iter().map(|(k, _)| k.as_ref()).collect();
776
+ // Sound: every later use in this block must be a literal-key read.
777
+ if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
778
+ let alias = reg.intern(&fields);
779
+ ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into()));
780
+ out.push(Statement::VarDecl {
781
+ name: name.clone(),
782
+ name_span: *name_span,
783
+ mutable: *mutable,
784
+ type_ann: Some(TypeAnnotation::Simple(alias.as_str().into())),
785
+ init: stmt_init_clone(stmt),
786
+ span: *span,
787
+ });
788
+ continue;
789
+ }
790
+ }
791
+ }
792
+ // Record a typed plain local (e.g. `let i = 0`) so a LATER object literal can type its
793
+ // fields from it (`{ x: i }` → struct). The first inference pass may already have annotated
794
+ // it (`let i: number`), so read the annotation OR infer from the init. Object-literal lets
795
+ // defined their struct alias above and `continue`d; this runs for the rest. Without it,
796
+ // `{ x: i }` can't resolve `i`'s type and the object stays a boxed `PropMap` (object_sum gap).
797
+ if let Statement::VarDecl {
798
+ name,
799
+ type_ann,
800
+ init,
801
+ ..
802
+ } = stmt
803
+ {
804
+ let t = type_ann
805
+ .clone()
806
+ .or_else(|| init.as_ref().and_then(|e| infer_expr_type(e, ctx)));
807
+ if let Some(t) = t {
808
+ ctx.define(name.as_ref(), t);
809
+ }
810
+ }
811
+ out.push(si_recurse(stmt, reg, ctx));
812
+ }
813
+ ctx.pop_scope();
814
+ out
815
+ }
816
+
817
+ fn stmt_init_clone(stmt: &Statement) -> Option<Expr> {
818
+ if let Statement::VarDecl { init, .. } = stmt {
819
+ init.clone()
820
+ } else {
821
+ None
822
+ }
823
+ }
824
+
825
+ /// Recurse struct inference into a statement's nested blocks (function bodies,
826
+ /// loop/if bodies). Non-block statements pass through unchanged.
827
+ fn si_recurse(stmt: &Statement, reg: &mut StructRegistry, ctx: &mut InferCtx) -> Statement {
828
+ match stmt {
829
+ Statement::Block { statements, span } => Statement::Block {
830
+ statements: si_block(statements.clone(), reg, ctx),
831
+ span: *span,
832
+ },
833
+ Statement::For {
834
+ init,
835
+ cond,
836
+ update,
837
+ body,
838
+ span,
839
+ } => Statement::For {
840
+ init: init.clone(),
841
+ cond: cond.clone(),
842
+ update: update.clone(),
843
+ body: Box::new(si_recurse(body, reg, ctx)),
844
+ span: *span,
845
+ },
846
+ Statement::ForOf {
847
+ name,
848
+ name_span,
849
+ iterable,
850
+ body,
851
+ span,
852
+ } => Statement::ForOf {
853
+ name: name.clone(),
854
+ name_span: *name_span,
855
+ iterable: iterable.clone(),
856
+ body: Box::new(si_recurse(body, reg, ctx)),
857
+ span: *span,
858
+ },
859
+ Statement::While { cond, body, span } => Statement::While {
860
+ cond: cond.clone(),
861
+ body: Box::new(si_recurse(body, reg, ctx)),
862
+ span: *span,
863
+ },
864
+ Statement::DoWhile { body, cond, span } => Statement::DoWhile {
865
+ body: Box::new(si_recurse(body, reg, ctx)),
866
+ cond: cond.clone(),
867
+ span: *span,
868
+ },
869
+ Statement::If {
870
+ cond,
871
+ then_branch,
872
+ else_branch,
873
+ span,
874
+ } => Statement::If {
875
+ cond: cond.clone(),
876
+ then_branch: Box::new(si_recurse(then_branch, reg, ctx)),
877
+ else_branch: else_branch.as_ref().map(|e| Box::new(si_recurse(e, reg, ctx))),
878
+ span: *span,
879
+ },
880
+ Statement::FunDecl {
881
+ async_,
882
+ name,
883
+ name_span,
884
+ params,
885
+ rest_param,
886
+ return_type,
887
+ body,
888
+ span,
889
+ } => {
890
+ // Define the (annotated or M4-inferred) scalar params in a body scope so the body's
891
+ // local-type + mutable-array inference can use them (`let r = n` ⇒ `r: number`).
892
+ ctx.push_scope();
893
+ for p in params {
894
+ if let FunParam::Simple(tp) = p {
895
+ if let Some(ann) = &tp.type_ann {
896
+ ctx.define(tp.name.as_ref(), ann.clone());
897
+ }
898
+ }
899
+ }
900
+ let new_body = Box::new(si_recurse(body, reg, ctx));
901
+ ctx.pop_scope();
902
+ Statement::FunDecl {
903
+ async_: *async_,
904
+ name: name.clone(),
905
+ name_span: *name_span,
906
+ params: params.clone(),
907
+ rest_param: rest_param.clone(),
908
+ return_type: return_type.clone(),
909
+ body: new_body,
910
+ span: *span,
911
+ }
912
+ }
913
+ other => other.clone(),
914
+ }
915
+ }
916
+
917
+ /// Sound check: in `tail`, every use of `name` is a literal-key field READ
918
+ /// (`name.field` with field ∈ `keys`). Any other occurrence — write, computed
919
+ /// access, reassignment, escape into a call/return/array/object/closure, or a
920
+ /// rebinding of `name` — returns false (bail to boxed Value). Unhandled AST
921
+ /// shapes also return false, so this can never wrongly green-light.
922
+ fn uses_are_struct_safe(name: &str, keys: &std::collections::HashSet<&str>, tail: &[Statement]) -> bool {
923
+ tail.iter().all(|s| stmt_name_safe(s, name, keys))
924
+ }
925
+
926
+ /// Define every provably-numeric block-local into `hyp` (flat across nested scopes): annotated
927
+ /// `: number`, a number-literal init, or any init `infer_expr_type` proves `number` *under the
928
+ /// current hypothesis* — so `let temp = perm[i]` becomes `number` once `perm` is hypothesized
929
+ /// `number[]`. Run a few times to resolve derived chains (`let b = temp + 1`).
930
+ fn seed_numeric_locals(s: &Statement, hyp: &mut InferCtx) {
931
+ use Statement::*;
932
+ match s {
933
+ VarDecl { name, type_ann, init, .. } => {
934
+ let numeric = type_ann.as_ref().is_some_and(is_number)
935
+ || init
936
+ .as_ref()
937
+ .and_then(|e| infer_expr_type(e, hyp))
938
+ .as_ref()
939
+ .is_some_and(is_number);
940
+ if numeric {
941
+ hyp.define(name, number_ann());
942
+ }
943
+ }
944
+ Block { statements, .. } | Multi { statements, .. } => {
945
+ statements.iter().for_each(|x| seed_numeric_locals(x, hyp))
946
+ }
947
+ If { then_branch, else_branch, .. } => {
948
+ seed_numeric_locals(then_branch, hyp);
949
+ if let Some(e) = else_branch {
950
+ seed_numeric_locals(e, hyp);
951
+ }
952
+ }
953
+ For { init, body, .. } => {
954
+ if let Some(i) = init {
955
+ seed_numeric_locals(i, hyp);
956
+ }
957
+ seed_numeric_locals(body, hyp);
958
+ }
959
+ While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
960
+ seed_numeric_locals(body, hyp)
961
+ }
962
+ _ => {}
963
+ }
964
+ }
965
+
966
+ /// Block-level co-inference of mutable native arrays of element type `elem` (`number[]` or
967
+ /// `boolean[]`) — handles cross-referencing arrays (`perm[i] = perm1[i]`) that a per-array pass can't.
968
+ /// Collects every top-level `let X = []` / `[elem literals]` candidate, then runs a monotone
969
+ /// **fixpoint**: hypothesize ALL candidates are `elem[]`, verify each (a candidate fails if any
970
+ /// value written/pushed isn't provably `elem` under the hypothesis, or it escapes), drop the
971
+ /// failures, repeat until stable. The stable set is self-consistent, so it's sound. Run once per
972
+ /// element type; an array of the wrong type simply fails this run (its pushes aren't `elem`).
973
+ fn block_native_arrays(
974
+ stmts: &[Statement],
975
+ outer_ctx: &InferCtx,
976
+ elem: &TypeAnnotation,
977
+ ) -> std::collections::HashSet<String> {
978
+ use std::collections::HashSet;
979
+ let mut cands: Vec<(usize, String)> = Vec::new();
980
+ for (i, s) in stmts.iter().enumerate() {
981
+ if let Statement::VarDecl {
982
+ name,
983
+ type_ann: None,
984
+ init: Some(Expr::Array { elements, .. }),
985
+ ..
986
+ } = s
987
+ {
988
+ // Empty `[]` is a candidate for either element type; literal elements must match `elem`.
989
+ let lit_ok = elements.is_empty()
990
+ || matches!(infer_array_elem(elements, outer_ctx), Some(t) if &t == elem);
991
+ if lit_ok {
992
+ cands.push((i, name.to_string()));
993
+ }
994
+ }
995
+ }
996
+ if cands.is_empty() {
997
+ return HashSet::new();
998
+ }
999
+ let mut accepted: HashSet<String> = cands.iter().map(|c| c.1.clone()).collect();
1000
+ loop {
1001
+ let mut hyp = outer_ctx.clone();
1002
+ for n in &accepted {
1003
+ hyp.define(n, TypeAnnotation::Array(Box::new(elem.clone())));
1004
+ }
1005
+ // Seed numeric block-locals under the array hypothesis so values like `temp` in
1006
+ // `let temp = perm[i]; perm[k-i] = temp` are known (`perm[i]` is `elem` because `perm` is
1007
+ // hypothesized `elem[]`). A few passes resolve derived chains (`let b = temp + 1`).
1008
+ for _ in 0..4 {
1009
+ for s in stmts {
1010
+ seed_numeric_locals(s, &mut hyp);
1011
+ }
1012
+ }
1013
+ let mut removed = false;
1014
+ for (idx, name) in &cands {
1015
+ if accepted.contains(name)
1016
+ && !stmts[idx + 1..].iter().all(|s| mut_arr_stmt_ok(s, name, elem, &hyp))
1017
+ {
1018
+ accepted.remove(name);
1019
+ removed = true;
1020
+ }
1021
+ }
1022
+ if !removed {
1023
+ break;
1024
+ }
1025
+ }
1026
+ accepted
1027
+ }
1028
+
1029
+ /// A value written into / pushed onto `name` must have the array's element type `elem`: a `name[_]`
1030
+ /// self-read (already `elem` by hypothesis) or any expression `infer_expr_type` proves is `elem`.
1031
+ fn mut_arr_value_ok(v: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx) -> bool {
1032
+ if let Expr::Index { object, .. } = v {
1033
+ if matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name) {
1034
+ return true;
1035
+ }
1036
+ }
1037
+ infer_expr_type(v, hyp).as_ref() == Some(elem)
1038
+ }
1039
+
1040
+ fn mut_arr_stmt_ok(s: &Statement, name: &str, elem: &TypeAnnotation, hyp: &InferCtx) -> bool {
1041
+ use Statement::*;
1042
+ match s {
1043
+ VarDecl { name: n, init, .. } => {
1044
+ n.as_ref() != name && init.as_ref().is_none_or(|e| mut_arr_expr_ok(e, name, elem, hyp))
1045
+ }
1046
+ ExprStmt { expr, .. } => mut_arr_expr_ok(expr, name, elem, hyp),
1047
+ Block { statements, .. } | Multi { statements, .. } => {
1048
+ statements.iter().all(|s| mut_arr_stmt_ok(s, name, elem, hyp))
1049
+ }
1050
+ If { cond, then_branch, else_branch, .. } => {
1051
+ mut_arr_expr_ok(cond, name, elem, hyp)
1052
+ && mut_arr_stmt_ok(then_branch, name, elem, hyp)
1053
+ && else_branch.as_ref().is_none_or(|e| mut_arr_stmt_ok(e, name, elem, hyp))
1054
+ }
1055
+ While { cond, body, .. } => {
1056
+ mut_arr_expr_ok(cond, name, elem, hyp) && mut_arr_stmt_ok(body, name, elem, hyp)
1057
+ }
1058
+ DoWhile { body, cond, .. } => {
1059
+ mut_arr_stmt_ok(body, name, elem, hyp) && mut_arr_expr_ok(cond, name, elem, hyp)
1060
+ }
1061
+ For { init, cond, update, body, .. } => {
1062
+ init.as_ref().is_none_or(|i| mut_arr_stmt_ok(i, name, elem, hyp))
1063
+ && cond.as_ref().is_none_or(|c| mut_arr_expr_ok(c, name, elem, hyp))
1064
+ && update.as_ref().is_none_or(|u| mut_arr_expr_ok(u, name, elem, hyp))
1065
+ && mut_arr_stmt_ok(body, name, elem, hyp)
1066
+ }
1067
+ ForOf { name: n, iterable, body, .. } => {
1068
+ if n.as_ref() == name {
1069
+ return false;
1070
+ }
1071
+ let iter_ok = matches!(iterable, Expr::Ident { name: it, .. } if it.as_ref() == name)
1072
+ || mut_arr_expr_ok(iterable, name, elem, hyp);
1073
+ iter_ok && mut_arr_stmt_ok(body, name, elem, hyp)
1074
+ }
1075
+ Return { value, .. } => value.as_ref().is_none_or(|e| mut_arr_expr_ok(e, name, elem, hyp)),
1076
+ Throw { value, .. } => mut_arr_expr_ok(value, name, elem, hyp),
1077
+ Break { .. } | Continue { .. } | TypeAlias { .. } => true,
1078
+ _ => false, // switch / try / nested fn / etc: bail
1079
+ }
1080
+ }
1081
+
1082
+ fn mut_arr_expr_ok(e: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx) -> bool {
1083
+ use Expr::*;
1084
+ let is_name = |x: &Expr| matches!(x, Expr::Ident { name: n, .. } if n.as_ref() == name);
1085
+ match e {
1086
+ Literal { .. } => true,
1087
+ Ident { name: n, .. } => n.as_ref() != name, // bare escape is unsafe
1088
+ Index { object, index, .. } => {
1089
+ (is_name(object) || mut_arr_expr_ok(object, name, elem, hyp))
1090
+ && mut_arr_expr_ok(index, name, elem, hyp)
1091
+ }
1092
+ // `name[i] = v`: OK iff `v` has type `elem`; else recurse (writing to a DIFFERENT array).
1093
+ IndexAssign { object, index, value, .. } => {
1094
+ if is_name(object) {
1095
+ mut_arr_expr_ok(index, name, elem, hyp)
1096
+ && mut_arr_value_ok(value, name, elem, hyp)
1097
+ && mut_arr_expr_ok(value, name, elem, hyp)
1098
+ } else {
1099
+ mut_arr_expr_ok(object, name, elem, hyp)
1100
+ && mut_arr_expr_ok(index, name, elem, hyp)
1101
+ && mut_arr_expr_ok(value, name, elem, hyp)
1102
+ }
1103
+ }
1104
+ // `name.push(v…)`: OK iff each `v` has type `elem`. Any other method on `name`: bail.
1105
+ Call { callee, args, .. } => {
1106
+ if let Member { object, prop: tishlang_ast::MemberProp::Name { name: m, .. }, .. } =
1107
+ callee.as_ref()
1108
+ {
1109
+ if is_name(object) {
1110
+ return m.as_ref() == "push"
1111
+ && args.iter().all(|a| match a {
1112
+ tishlang_ast::CallArg::Expr(v) => {
1113
+ mut_arr_value_ok(v, name, elem, hyp) && mut_arr_expr_ok(v, name, elem, hyp)
1114
+ }
1115
+ tishlang_ast::CallArg::Spread(_) => false,
1116
+ });
1117
+ }
1118
+ }
1119
+ mut_arr_expr_ok(callee, name, elem, hyp)
1120
+ && args.iter().all(|a| match a {
1121
+ tishlang_ast::CallArg::Expr(v) => mut_arr_expr_ok(v, name, elem, hyp),
1122
+ tishlang_ast::CallArg::Spread(_) => false,
1123
+ })
1124
+ }
1125
+ Member { object, prop, .. } => {
1126
+ if is_name(object) {
1127
+ matches!(prop, tishlang_ast::MemberProp::Name { name: p, .. } if p.as_ref() == "length")
1128
+ } else {
1129
+ mut_arr_expr_ok(object, name, elem, hyp)
1130
+ }
1131
+ }
1132
+ Binary { left, right, .. } => {
1133
+ mut_arr_expr_ok(left, name, elem, hyp) && mut_arr_expr_ok(right, name, elem, hyp)
1134
+ }
1135
+ Unary { operand, .. } => mut_arr_expr_ok(operand, name, elem, hyp),
1136
+ Conditional { cond, then_branch, else_branch, .. } => {
1137
+ mut_arr_expr_ok(cond, name, elem, hyp)
1138
+ && mut_arr_expr_ok(then_branch, name, elem, hyp)
1139
+ && mut_arr_expr_ok(else_branch, name, elem, hyp)
1140
+ }
1141
+ Assign { name: an, value, .. }
1142
+ | CompoundAssign { name: an, value, .. }
1143
+ | LogicalAssign { name: an, value, .. } => {
1144
+ an.as_ref() != name && mut_arr_expr_ok(value, name, elem, hyp)
1145
+ }
1146
+ PostfixInc { name: n, .. }
1147
+ | PostfixDec { name: n, .. }
1148
+ | PrefixInc { name: n, .. }
1149
+ | PrefixDec { name: n, .. } => n.as_ref() != name,
1150
+ MemberAssign { object, value, .. } => {
1151
+ mut_arr_expr_ok(object, name, elem, hyp) && mut_arr_expr_ok(value, name, elem, hyp)
1152
+ }
1153
+ // Anything else: `name` must be absent (would alias the Vec into a boxed context).
1154
+ _ => !pi_mentions(e, name),
1155
+ }
1156
+ }
1157
+
1158
+ fn uses_are_array_safe(name: &str, tail: &[Statement]) -> bool {
1159
+ tail.iter().all(|s| arr_stmt_safe(s, name))
1160
+ }
1161
+
1162
+ fn arr_opt_expr_safe(e: &Option<Expr>, name: &str) -> bool {
1163
+ e.as_ref().map(|e| arr_expr_safe(e, name)).unwrap_or(true)
1164
+ }
1165
+
1166
+ fn arr_stmt_safe(s: &Statement, name: &str) -> bool {
1167
+ use Statement::*;
1168
+ match s {
1169
+ VarDecl { name: n, init, .. } => n.as_ref() != name && arr_opt_expr_safe(init, name),
1170
+ ExprStmt { expr, .. } => arr_expr_safe(expr, name),
1171
+ Block { statements, .. } => statements.iter().all(|s| arr_stmt_safe(s, name)),
1172
+ If {
1173
+ cond,
1174
+ then_branch,
1175
+ else_branch,
1176
+ ..
1177
+ } => {
1178
+ arr_expr_safe(cond, name)
1179
+ && arr_stmt_safe(then_branch, name)
1180
+ && else_branch.as_ref().map(|e| arr_stmt_safe(e, name)).unwrap_or(true)
1181
+ }
1182
+ While { cond, body, .. } => arr_expr_safe(cond, name) && arr_stmt_safe(body, name),
1183
+ DoWhile { body, cond, .. } => arr_stmt_safe(body, name) && arr_expr_safe(cond, name),
1184
+ For {
1185
+ init,
1186
+ cond,
1187
+ update,
1188
+ body,
1189
+ ..
1190
+ } => {
1191
+ init.as_ref().map(|i| arr_stmt_safe(i, name)).unwrap_or(true)
1192
+ && cond.as_ref().map(|c| arr_expr_safe(c, name)).unwrap_or(true)
1193
+ && update.as_ref().map(|u| arr_expr_safe(u, name)).unwrap_or(true)
1194
+ && arr_stmt_safe(body, name)
1195
+ }
1196
+ // `for (_ of name)` is the key read-only use; rebinding `name` bails.
1197
+ ForOf {
1198
+ name: n,
1199
+ iterable,
1200
+ body,
1201
+ ..
1202
+ } => {
1203
+ if n.as_ref() == name {
1204
+ return false;
1205
+ }
1206
+ let iter_ok = matches!(iterable, Expr::Ident { name: it, .. } if it.as_ref() == name)
1207
+ || arr_expr_safe(iterable, name);
1208
+ iter_ok && arr_stmt_safe(body, name)
1209
+ }
1210
+ Return { value, .. } => arr_opt_expr_safe(value, name),
1211
+ Throw { value, .. } => arr_expr_safe(value, name),
1212
+ Break { .. } | Continue { .. } | TypeAlias { .. } => true,
1213
+ // Nested fn could capture+mutate; switch/try and anything else: be safe, bail.
1214
+ _ => false,
1215
+ }
1216
+ }
1217
+
1218
+ fn arr_expr_safe(e: &Expr, name: &str) -> bool {
1219
+ use Expr::*;
1220
+ match e {
1221
+ Literal { .. } => true,
1222
+ Ident { name: n, .. } => n.as_ref() != name, // bare escape is unsafe
1223
+ // `name[i]` lowers to a native `Vec` index that PANICS out-of-bounds, whereas the boxed
1224
+ // array yields `undefined` — so an index read of `name` is unsound for inference. (Only
1225
+ // `for (_ of name)` and `name.length` are safe reads.) Indexing a DIFFERENT array is fine.
1226
+ Index { object, index, .. } => {
1227
+ !matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
1228
+ && arr_expr_safe(object, name)
1229
+ && arr_expr_safe(index, name)
1230
+ }
1231
+ // `name.length` READ is safe; any other `name.<prop>` (incl. a method receiver) bails.
1232
+ Member {
1233
+ object,
1234
+ prop,
1235
+ optional,
1236
+ ..
1237
+ } => {
1238
+ if let Ident { name: n, .. } = object.as_ref() {
1239
+ if n.as_ref() == name {
1240
+ return !optional
1241
+ && matches!(prop, tishlang_ast::MemberProp::Name { name: k, .. } if k.as_ref() == "length");
1242
+ }
1243
+ }
1244
+ arr_expr_safe(object, name)
1245
+ && match prop {
1246
+ tishlang_ast::MemberProp::Expr(p) => arr_expr_safe(p, name),
1247
+ tishlang_ast::MemberProp::Name { .. } => true,
1248
+ }
1249
+ }
1250
+ Binary { left, right, .. } | NullishCoalesce { left, right, .. } => {
1251
+ arr_expr_safe(left, name) && arr_expr_safe(right, name)
1252
+ }
1253
+ Unary { operand, .. } | TypeOf { operand, .. } | Await { operand, .. } => {
1254
+ arr_expr_safe(operand, name)
1255
+ }
1256
+ Conditional {
1257
+ cond,
1258
+ then_branch,
1259
+ else_branch,
1260
+ ..
1261
+ } => {
1262
+ arr_expr_safe(cond, name)
1263
+ && arr_expr_safe(then_branch, name)
1264
+ && arr_expr_safe(else_branch, name)
1265
+ }
1266
+ Call { callee, args, .. } | New { callee, args, .. } => {
1267
+ arr_expr_safe(callee, name)
1268
+ && args.iter().all(|a| match a {
1269
+ tishlang_ast::CallArg::Expr(x) | tishlang_ast::CallArg::Spread(x) => {
1270
+ arr_expr_safe(x, name)
1271
+ }
1272
+ })
1273
+ }
1274
+ Assign { name: an, value, .. }
1275
+ | CompoundAssign { name: an, value, .. }
1276
+ | LogicalAssign { name: an, value, .. } => {
1277
+ an.as_ref() != name && arr_expr_safe(value, name) // reassigning `name` bails
1278
+ }
1279
+ // Mutating `name` via index/member assignment bails; otherwise recurse.
1280
+ IndexAssign {
1281
+ object,
1282
+ index,
1283
+ value,
1284
+ ..
1285
+ } => {
1286
+ !matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
1287
+ && arr_expr_safe(object, name)
1288
+ && arr_expr_safe(index, name)
1289
+ && arr_expr_safe(value, name)
1290
+ }
1291
+ MemberAssign { object, value, .. } => {
1292
+ !matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
1293
+ && arr_expr_safe(object, name)
1294
+ && arr_expr_safe(value, name)
1295
+ }
1296
+ PostfixInc { name: n, .. }
1297
+ | PostfixDec { name: n, .. }
1298
+ | PrefixInc { name: n, .. }
1299
+ | PrefixDec { name: n, .. } => n.as_ref() != name,
1300
+ Array { elements, .. } => elements.iter().all(|el| match el {
1301
+ tishlang_ast::ArrayElement::Expr(x) | tishlang_ast::ArrayElement::Spread(x) => {
1302
+ arr_expr_safe(x, name)
1303
+ }
1304
+ }),
1305
+ Object { props, .. } => props.iter().all(|p| match p {
1306
+ tishlang_ast::ObjectProp::KeyValue(_, v) => arr_expr_safe(v, name),
1307
+ tishlang_ast::ObjectProp::Spread(v) => arr_expr_safe(v, name),
1308
+ }),
1309
+ // Anything else that mentions `name`: be safe, bail.
1310
+ _ => !pi_mentions(e, name),
1311
+ }
1312
+ }
1313
+
1314
+ fn opt_expr_safe(e: &Option<Expr>, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
1315
+ e.as_ref().map(|e| expr_name_safe(e, name, keys)).unwrap_or(true)
1316
+ }
1317
+
1318
+ fn stmt_name_safe(s: &Statement, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
1319
+ match s {
1320
+ // A rebinding of `name` in scope is too subtle to track — bail.
1321
+ Statement::VarDecl { name: n, init, .. } => {
1322
+ if n.as_ref() == name {
1323
+ return false;
1324
+ }
1325
+ opt_expr_safe(init, name, keys)
1326
+ }
1327
+ Statement::VarDeclDestructure { init, .. } => expr_name_safe(init, name, keys),
1328
+ Statement::ExprStmt { expr, .. } => expr_name_safe(expr, name, keys),
1329
+ Statement::Block { statements, .. } => statements.iter().all(|s| stmt_name_safe(s, name, keys)),
1330
+ Statement::If { cond, then_branch, else_branch, .. } => {
1331
+ expr_name_safe(cond, name, keys)
1332
+ && stmt_name_safe(then_branch, name, keys)
1333
+ && else_branch.as_ref().map(|e| stmt_name_safe(e, name, keys)).unwrap_or(true)
1334
+ }
1335
+ Statement::While { cond, body, .. } => {
1336
+ expr_name_safe(cond, name, keys) && stmt_name_safe(body, name, keys)
1337
+ }
1338
+ Statement::DoWhile { body, cond, .. } => {
1339
+ stmt_name_safe(body, name, keys) && expr_name_safe(cond, name, keys)
1340
+ }
1341
+ Statement::For { init, cond, update, body, .. } => {
1342
+ init.as_ref().map(|i| stmt_name_safe(i, name, keys)).unwrap_or(true)
1343
+ && cond.as_ref().map(|c| expr_name_safe(c, name, keys)).unwrap_or(true)
1344
+ && update.as_ref().map(|u| expr_name_safe(u, name, keys)).unwrap_or(true)
1345
+ && stmt_name_safe(body, name, keys)
1346
+ }
1347
+ Statement::ForOf { name: n, iterable, body, .. } => {
1348
+ if n.as_ref() == name {
1349
+ return false; // rebinding
1350
+ }
1351
+ expr_name_safe(iterable, name, keys) && stmt_name_safe(body, name, keys)
1352
+ }
1353
+ Statement::Return { value, .. } => opt_expr_safe(value, name, keys),
1354
+ Statement::Throw { value, .. } => expr_name_safe(value, name, keys),
1355
+ Statement::Switch { expr, cases, default_body, .. } => {
1356
+ expr_name_safe(expr, name, keys)
1357
+ && cases.iter().all(|(g, body)| {
1358
+ g.as_ref().map(|e| expr_name_safe(e, name, keys)).unwrap_or(true)
1359
+ && body.iter().all(|s| stmt_name_safe(s, name, keys))
1360
+ })
1361
+ && default_body
1362
+ .as_ref()
1363
+ .map(|b| b.iter().all(|s| stmt_name_safe(s, name, keys)))
1364
+ .unwrap_or(true)
1365
+ }
1366
+ Statement::Try { body, catch_body, finally_body, .. } => {
1367
+ stmt_name_safe(body, name, keys)
1368
+ && catch_body.as_ref().map(|b| stmt_name_safe(b, name, keys)).unwrap_or(true)
1369
+ && finally_body.as_ref().map(|b| stmt_name_safe(b, name, keys)).unwrap_or(true)
1370
+ }
1371
+ // A nested function that closes over `name` could mutate it — bail.
1372
+ Statement::FunDecl { .. } => false,
1373
+ Statement::Break { .. } | Statement::Continue { .. } | Statement::TypeAlias { .. } => true,
1374
+ // Anything not explicitly handled: be safe, bail.
1375
+ _ => false,
1376
+ }
1377
+ }
1378
+
1379
+ fn expr_name_safe(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
1380
+ use Expr::*;
1381
+ match e {
1382
+ Literal { .. } => true,
1383
+ Delete { target, .. } => expr_name_safe(target, name, keys),
1384
+ Ident { name: n, .. } => n.as_ref() != name, // bare use of `name` is unsafe
1385
+ Member { object, prop, optional, .. } => {
1386
+ if let Ident { name: n, .. } = object.as_ref() {
1387
+ if n.as_ref() == name {
1388
+ // `name.<prop>` — safe only as a non-optional literal-key read.
1389
+ return !optional
1390
+ && matches!(prop, tishlang_ast::MemberProp::Name { name: k, .. }
1391
+ if keys.contains(k.as_ref()));
1392
+ }
1393
+ }
1394
+ expr_name_safe(object, name, keys)
1395
+ && match prop {
1396
+ tishlang_ast::MemberProp::Expr(p) => expr_name_safe(p, name, keys),
1397
+ tishlang_ast::MemberProp::Name { .. } => true,
1398
+ }
1399
+ }
1400
+ Binary { left, right, .. } => {
1401
+ expr_name_safe(left, name, keys) && expr_name_safe(right, name, keys)
1402
+ }
1403
+ Unary { operand, .. } | TypeOf { operand, .. } | Await { operand, .. } => {
1404
+ expr_name_safe(operand, name, keys)
1405
+ }
1406
+ Call { callee, args, .. } | New { callee, args, .. } => {
1407
+ expr_name_safe(callee, name, keys) && args.iter().all(|a| call_arg_safe(a, name, keys))
1408
+ }
1409
+ Index { object, index, .. } => {
1410
+ expr_name_safe(object, name, keys) && expr_name_safe(index, name, keys)
1411
+ }
1412
+ Conditional { cond, then_branch, else_branch, .. } => {
1413
+ expr_name_safe(cond, name, keys)
1414
+ && expr_name_safe(then_branch, name, keys)
1415
+ && expr_name_safe(else_branch, name, keys)
1416
+ }
1417
+ NullishCoalesce { left, right, .. } => {
1418
+ expr_name_safe(left, name, keys) && expr_name_safe(right, name, keys)
1419
+ }
1420
+ Array { elements, .. } => elements.iter().all(|el| match el {
1421
+ tishlang_ast::ArrayElement::Expr(e) | tishlang_ast::ArrayElement::Spread(e) => {
1422
+ expr_name_safe(e, name, keys)
1423
+ }
1424
+ }),
1425
+ Object { props, .. } => props.iter().all(|p| match p {
1426
+ tishlang_ast::ObjectProp::KeyValue(_, v) => expr_name_safe(v, name, keys),
1427
+ tishlang_ast::ObjectProp::Spread(e) => expr_name_safe(e, name, keys),
1428
+ }),
1429
+ TemplateLiteral { exprs, .. } => exprs.iter().all(|e| expr_name_safe(e, name, keys)),
1430
+ // Reassignment / mutation referencing `name` by identifier → unsafe.
1431
+ Assign { name: n, value, .. }
1432
+ | CompoundAssign { name: n, value, .. }
1433
+ | LogicalAssign { name: n, value, .. } => {
1434
+ n.as_ref() != name && expr_name_safe(value, name, keys)
1435
+ }
1436
+ PostfixInc { name: n, .. } | PostfixDec { name: n, .. } | PrefixInc { name: n, .. }
1437
+ | PrefixDec { name: n, .. } => n.as_ref() != name,
1438
+ MemberAssign { object, value, .. } => {
1439
+ // A write to `name.x` (even a literal key) is excluded in this
1440
+ // read-only version → object being `name` makes it unsafe.
1441
+ expr_name_safe(object, name, keys) && expr_name_safe(value, name, keys)
1442
+ }
1443
+ IndexAssign { object, index, value, .. } => {
1444
+ expr_name_safe(object, name, keys)
1445
+ && expr_name_safe(index, name, keys)
1446
+ && expr_name_safe(value, name, keys)
1447
+ }
1448
+ // Closures could capture+mutate `name`; JSX/native — bail conservatively.
1449
+ ArrowFunction { .. } | JsxElement { .. } | JsxFragment { .. } | NativeModuleLoad { .. } => {
1450
+ false
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ fn call_arg_safe(a: &CallArg, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
1456
+ match a {
1457
+ CallArg::Expr(e) | CallArg::Spread(e) => expr_name_safe(e, name, keys),
127
1458
  }
128
1459
  }
129
1460
 
@@ -290,3 +1621,61 @@ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
290
1621
  fn _uses_call_arg(_: &CallArg) {}
291
1622
  #[allow(dead_code)]
292
1623
  fn _uses_arrow_body(_: &ArrowBody) {}
1624
+
1625
+ #[cfg(test)]
1626
+ mod param_infer_tests {
1627
+ use super::*;
1628
+ use tishlang_parser::parse;
1629
+
1630
+ /// Run base inference, then M4 param inference, and return the inferred annotation name (if
1631
+ /// any) for parameter `param` of `fn <fn_name>`.
1632
+ fn inferred_param(src: &str, fn_name: &str, param: &str) -> Option<String> {
1633
+ let parsed = parse(src).unwrap();
1634
+ let base = Program {
1635
+ statements: infer_statements(&parsed.statements, &mut InferCtx::new()),
1636
+ };
1637
+ let prog = param_infer_program(base);
1638
+ for s in &prog.statements {
1639
+ if let Statement::FunDecl { name, params, .. } = s {
1640
+ if name.as_ref() == fn_name {
1641
+ for p in params {
1642
+ if let FunParam::Simple(tp) = p {
1643
+ if tp.name.as_ref() == param {
1644
+ return tp.type_ann.as_ref().map(|a| match a {
1645
+ TypeAnnotation::Simple(s) => s.to_string(),
1646
+ _ => "<complex>".to_string(),
1647
+ });
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ }
1653
+ }
1654
+ None
1655
+ }
1656
+
1657
+ #[test]
1658
+ fn infers_loop_bound_param_via_numeric_local() {
1659
+ // `n` is the bare operand of `i < n`; `i` (`let i = 0`, base-inferred numeric) makes the
1660
+ // OTHER operand provably numeric, so `n` is inferred `number` (the numeric-locals fix).
1661
+ let src = "fn countUp(n) { let total = 0; for (let i = 0; i < n; i = i + 1) { total = total + i } return total }";
1662
+ assert_eq!(inferred_param(src, "countUp", "n").as_deref(), Some("number"));
1663
+ }
1664
+
1665
+ #[test]
1666
+ fn does_not_infer_string_concat_param() {
1667
+ // `x` is the bare operand of `+` against a string literal — NOT provably numeric, so `x`
1668
+ // must stay dynamic (else `label("hi")` would mistype to f64 and panic at runtime).
1669
+ let src = "fn label(x) { return \"v=\" + x }";
1670
+ assert_eq!(inferred_param(src, "label", "x"), None);
1671
+ }
1672
+
1673
+ #[test]
1674
+ fn does_not_treat_other_param_as_numeric_local() {
1675
+ // `a < b`: neither operand is a known numeric *local* (both are params), so neither is
1676
+ // provable and neither param is inferred — the relaxation is locals-only.
1677
+ let src = "fn cmp(a, b) { if (a < b) { return 1 } return 0 }";
1678
+ assert_eq!(inferred_param(src, "cmp", "a"), None);
1679
+ assert_eq!(inferred_param(src, "cmp", "b"), None);
1680
+ }
1681
+ }