@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.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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(<) && is_number(&rt) {
|
|
86
121
|
match op {
|
|
87
|
-
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
88
|
-
|
|
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(<) && 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:
|
|
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
|
+
}
|