@tishlang/tish-format 2.12.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
//! - Already-annotated vars are left unchanged.
|
|
11
11
|
|
|
12
12
|
use std::collections::{HashMap, HashSet};
|
|
13
|
+
use std::sync::Arc;
|
|
13
14
|
use tishlang_ast::{
|
|
14
15
|
ArrowBody, BinOp, CallArg, Expr, FunParam, Literal, Program, Statement, TypeAnnotation,
|
|
15
16
|
};
|
|
@@ -18,15 +19,42 @@ use tishlang_ast::{
|
|
|
18
19
|
#[derive(Default, Clone)]
|
|
19
20
|
pub struct InferCtx {
|
|
20
21
|
scopes: Vec<HashMap<String, TypeAnnotation>>,
|
|
22
|
+
/// #175: fns de-virtualized to native-vec free fns → per-param "is an array param" flags. Lets
|
|
23
|
+
/// the mutable-array co-inference treat `f(arr)` as a native use (the callee takes `&/&mut Vec`),
|
|
24
|
+
/// not a boxing escape, so the caller's array stays an unboxed `Vec`.
|
|
25
|
+
native_vec_array_params: HashMap<String, Vec<bool>>,
|
|
26
|
+
/// #320: fns PROVEN to always return a `number` (every return is numeric and the body can't fall
|
|
27
|
+
/// through to an implicit `undefined`). Lets `infer_expr_type(f(...))` be `number`, so
|
|
28
|
+
/// `a.push(f(...))` infers `a: number[]` (e.g. k_nucleotide's `seq.push(nextBase())`).
|
|
29
|
+
number_returning_fns: std::collections::HashSet<String>,
|
|
30
|
+
/// #373: registered struct-alias shapes (alias → ordered fields), so `infer_expr_type` can
|
|
31
|
+
/// resolve `<expr: Simple(alias)>.field` → the field's type — needed to type a CROSS-ARRAY
|
|
32
|
+
/// struct-field read like `base[i].key` when building another record array (`rows`).
|
|
33
|
+
struct_shapes: HashMap<String, Vec<(std::sync::Arc<str>, TypeAnnotation)>>,
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
impl InferCtx {
|
|
24
37
|
pub fn new() -> Self {
|
|
25
38
|
Self {
|
|
26
39
|
scopes: vec![HashMap::new()],
|
|
40
|
+
native_vec_array_params: HashMap::new(),
|
|
41
|
+
number_returning_fns: std::collections::HashSet::new(),
|
|
42
|
+
struct_shapes: HashMap::new(),
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
fn define_struct(&mut self, alias: &str, fields: Vec<(std::sync::Arc<str>, TypeAnnotation)>) {
|
|
47
|
+
self.struct_shapes.insert(alias.to_string(), fields);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn struct_field(&self, alias: &str, key: &str) -> Option<&TypeAnnotation> {
|
|
51
|
+
self.struct_shapes
|
|
52
|
+
.get(alias)?
|
|
53
|
+
.iter()
|
|
54
|
+
.find(|(k, _)| k.as_ref() == key)
|
|
55
|
+
.map(|(_, t)| t)
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
fn push_scope(&mut self) {
|
|
31
59
|
self.scopes.push(HashMap::new());
|
|
32
60
|
}
|
|
@@ -115,6 +143,13 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
|
|
|
115
143
|
Expr::Binary {
|
|
116
144
|
left, op, right, ..
|
|
117
145
|
} => {
|
|
146
|
+
// Always-numeric ops coerce BOTH operands to Number (JS ToNumber) → result is Number
|
|
147
|
+
// regardless of operand types, even when an operand type is unknown (e.g. a call result).
|
|
148
|
+
// Excludes `+` (string concat) and comparisons. Lets `{ key: r % 65536 }` type as number.
|
|
149
|
+
if matches!(op, BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
|
|
150
|
+
| BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr | BinOp::UShr) {
|
|
151
|
+
return Some(number_ann());
|
|
152
|
+
}
|
|
118
153
|
let lt = infer_expr_type(left, ctx)?;
|
|
119
154
|
let rt = infer_expr_type(right, ctx)?;
|
|
120
155
|
if is_number(<) && is_number(&rt) {
|
|
@@ -164,10 +199,29 @@ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
|
|
|
164
199
|
}
|
|
165
200
|
}
|
|
166
201
|
// Index of a typed array yields its element type (`a[i]` where `a: T[]` → `T`).
|
|
202
|
+
// `<expr: Simple(alias)>.field` — struct field read → the field's type (alias shape must be
|
|
203
|
+
// registered in ctx). Enables cross-array reads like `base[i].key` (#373). Non-struct
|
|
204
|
+
// receivers (or `Simple("number")` etc.) resolve to None, unchanged.
|
|
205
|
+
Expr::Member {
|
|
206
|
+
object,
|
|
207
|
+
prop: tishlang_ast::MemberProp::Name { name: k, .. },
|
|
208
|
+
..
|
|
209
|
+
} => match infer_expr_type(object, ctx)? {
|
|
210
|
+
TypeAnnotation::Simple(alias, _) => ctx.struct_field(alias.as_ref(), k.as_ref()).cloned(),
|
|
211
|
+
_ => None,
|
|
212
|
+
},
|
|
167
213
|
Expr::Index { object, .. } => match infer_expr_type(object, ctx) {
|
|
168
214
|
Some(TypeAnnotation::Array(elem)) => Some(*elem),
|
|
169
215
|
_ => None,
|
|
170
216
|
},
|
|
217
|
+
// #320: a call to a fn PROVEN to always return a number is itself a number — so
|
|
218
|
+
// `a.push(f(...))` can infer `a: number[]` (e.g. `seq.push(nextBase())`).
|
|
219
|
+
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
220
|
+
Expr::Ident { name, .. } if ctx.number_returning_fns.contains(name.as_ref()) => {
|
|
221
|
+
Some(number_ann())
|
|
222
|
+
}
|
|
223
|
+
_ => None,
|
|
224
|
+
},
|
|
171
225
|
_ => None,
|
|
172
226
|
}
|
|
173
227
|
}
|
|
@@ -181,7 +235,7 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
181
235
|
// param types — otherwise they fall back to boxed `Value` and the whole hot loop boxes with
|
|
182
236
|
// them (the difference between an idiomatic numeric fn going native vs staying boxed).
|
|
183
237
|
// Conservative — any non-numeric / write / escape use bails (param stays boxed Value).
|
|
184
|
-
let p = if
|
|
238
|
+
let p = if crate::native_opts_enabled() {
|
|
185
239
|
param_infer_program(program.clone())
|
|
186
240
|
} else {
|
|
187
241
|
program.clone()
|
|
@@ -196,7 +250,7 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
196
250
|
// backend emits unboxed structs with direct field access. Conservative —
|
|
197
251
|
// only applies when every use of the binding is a literal-key field read,
|
|
198
252
|
// so it can never miscompile (any uncertainty falls back to boxed Value).
|
|
199
|
-
let p = if
|
|
253
|
+
let p = if crate::native_opts_enabled() {
|
|
200
254
|
struct_infer_program(p)
|
|
201
255
|
} else {
|
|
202
256
|
p
|
|
@@ -212,7 +266,7 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
212
266
|
// until those land this pass only emits the annotations the existing codegen backs SOUNDLY
|
|
213
267
|
// (the S-0 scalar `: number` params, identical to the M4 mechanism) plus the inert struct
|
|
214
268
|
// alias decls. See `aggregate_infer_program`.
|
|
215
|
-
if
|
|
269
|
+
if crate::native_opts_enabled() {
|
|
216
270
|
aggregate_infer_program(p)
|
|
217
271
|
} else {
|
|
218
272
|
p
|
|
@@ -224,12 +278,22 @@ pub fn infer_program(program: &Program) -> Program {
|
|
|
224
278
|
// ---------------------------------------------------------------------------
|
|
225
279
|
|
|
226
280
|
fn param_infer_program(program: Program) -> Program {
|
|
281
|
+
// Soundness: a fn called ANYWHERE with a provably-non-numeric arg (a string/bool/null literal,
|
|
282
|
+
// template, array, or object) must NOT get a `: number` param shadow — the body may "look
|
|
283
|
+
// numeric" (`return SHARED`) yet the caller passes e.g. `"arg"`, which the f64 coercion would
|
|
284
|
+
// panic on / turn to NaN where JS keeps the value. Skip those fns (they stay fully boxed). Same
|
|
285
|
+
// set `collect_native_fns` uses to refuse the M5 native form.
|
|
286
|
+
let nonnumeric = crate::codegen::Codegen::fns_called_with_nonnumeric_arg(&program.statements);
|
|
227
287
|
Program {
|
|
228
|
-
statements: program
|
|
288
|
+
statements: program
|
|
289
|
+
.statements
|
|
290
|
+
.into_iter()
|
|
291
|
+
.map(|s| pi_stmt(s, &nonnumeric))
|
|
292
|
+
.collect(),
|
|
229
293
|
}
|
|
230
294
|
}
|
|
231
295
|
|
|
232
|
-
fn pi_stmt(s: Statement) -> Statement {
|
|
296
|
+
fn pi_stmt(s: Statement, nonnumeric: &HashSet<String>) -> Statement {
|
|
233
297
|
if let Statement::FunDecl {
|
|
234
298
|
async_,
|
|
235
299
|
name,
|
|
@@ -241,6 +305,19 @@ fn pi_stmt(s: Statement) -> Statement {
|
|
|
241
305
|
span,
|
|
242
306
|
} = s
|
|
243
307
|
{
|
|
308
|
+
// A fn reached with a non-numeric arg somewhere: leave its params dynamic (boxed).
|
|
309
|
+
if nonnumeric.contains(name.as_ref()) {
|
|
310
|
+
return Statement::FunDecl {
|
|
311
|
+
async_,
|
|
312
|
+
name,
|
|
313
|
+
name_span,
|
|
314
|
+
params,
|
|
315
|
+
rest_param,
|
|
316
|
+
return_type,
|
|
317
|
+
body,
|
|
318
|
+
span,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
244
321
|
// Locals provably numeric in this body (annotated, incl. base-inferred `let i = 0`), so a
|
|
245
322
|
// bare loop counter `i` counts as a numeric operand and `i < n` can prove the param `n`.
|
|
246
323
|
// Fixpoint-closed so copy-chains of numeric literals (`let a = 0; let b = a`) propagate.
|
|
@@ -871,6 +948,11 @@ struct StructRegistry {
|
|
|
871
948
|
by_shape: HashMap<String, String>,
|
|
872
949
|
/// alias name → field list (for emitting the `type` decls)
|
|
873
950
|
decls: Vec<StructDecl>,
|
|
951
|
+
/// #179 Stage B: canonical union key ("shape0|shape1|…") → union alias name.
|
|
952
|
+
by_union: HashMap<String, String>,
|
|
953
|
+
/// #179 Stage B: union alias → its variant shapes. Emitted as `type TishAnonUnion_N =
|
|
954
|
+
/// Union([Object(s0), Object(s1), …])`; codegen synthesizes the enum + per-variant structs.
|
|
955
|
+
union_decls: Vec<(String, Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>>)>,
|
|
874
956
|
}
|
|
875
957
|
|
|
876
958
|
impl StructRegistry {
|
|
@@ -888,6 +970,37 @@ impl StructRegistry {
|
|
|
888
970
|
self.decls.push((name.clone(), fields.to_vec()));
|
|
889
971
|
name
|
|
890
972
|
}
|
|
973
|
+
|
|
974
|
+
/// #179 Stage B: register a closed set of distinct record shapes as a shape-union alias
|
|
975
|
+
/// (`TishAnonUnion_N`). Identical sets (order-insensitive per the caller's dedup) share one alias.
|
|
976
|
+
fn intern_union(&mut self, shapes: &[Vec<(std::sync::Arc<str>, TypeAnnotation)>]) -> String {
|
|
977
|
+
let canon = shapes
|
|
978
|
+
.iter()
|
|
979
|
+
.map(|s| {
|
|
980
|
+
s.iter()
|
|
981
|
+
.map(|(k, t)| format!("{}:{}", k, type_canon(t)))
|
|
982
|
+
.collect::<Vec<_>>()
|
|
983
|
+
.join(";")
|
|
984
|
+
})
|
|
985
|
+
.collect::<Vec<_>>()
|
|
986
|
+
.join("|");
|
|
987
|
+
if let Some(name) = self.by_union.get(&canon) {
|
|
988
|
+
return name.clone();
|
|
989
|
+
}
|
|
990
|
+
let name = format!("TishAnonUnion_{}", self.union_decls.len());
|
|
991
|
+
self.by_union.insert(canon, name.clone());
|
|
992
|
+
self.union_decls.push((name.clone(), shapes.to_vec()));
|
|
993
|
+
name
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/// The field list registered under an alias (`TishAnon_N` or a user `type`), for inlining a
|
|
997
|
+
/// struct-typed spread's fields into a merged object shape.
|
|
998
|
+
fn fields_of(&self, alias: &str) -> Option<&[(std::sync::Arc<str>, TypeAnnotation)]> {
|
|
999
|
+
self.decls
|
|
1000
|
+
.iter()
|
|
1001
|
+
.find(|(n, _)| n == alias)
|
|
1002
|
+
.map(|(_, f)| f.as_slice())
|
|
1003
|
+
}
|
|
891
1004
|
}
|
|
892
1005
|
|
|
893
1006
|
fn type_canon(t: &TypeAnnotation) -> String {
|
|
@@ -905,6 +1018,79 @@ fn type_canon(t: &TypeAnnotation) -> String {
|
|
|
905
1018
|
}
|
|
906
1019
|
}
|
|
907
1020
|
|
|
1021
|
+
/// If `props` is `{ ...base, <explicit KeyValue props> }` where `base` is a struct-typed local and
|
|
1022
|
+
/// the explicit keys don't collide with base's fields, return (merged struct field list, spread-free
|
|
1023
|
+
/// rewritten props: `base`'s fields as `base.field` reads, then the explicit props). This lets a
|
|
1024
|
+
/// `{ ...base, k: v }` immutable-update / config-merge lower to an unboxed struct via the normal
|
|
1025
|
+
/// struct-from-object codegen — no codegen change. Conservative — `None` (stays a boxed PropMap)
|
|
1026
|
+
/// unless: exactly one spread, it is the FIRST prop, over a plain struct-typed ident; every explicit
|
|
1027
|
+
/// prop is a typeable `KeyValue`; and no explicit key collides with a base field (keeps spread
|
|
1028
|
+
/// override-order semantics out of scope). `base` is already a struct ⇒ proven read-only, so
|
|
1029
|
+
/// inlining its field reads is sound.
|
|
1030
|
+
fn inline_struct_spread(
|
|
1031
|
+
props: &[tishlang_ast::ObjectProp],
|
|
1032
|
+
ctx: &InferCtx,
|
|
1033
|
+
reg: &StructRegistry,
|
|
1034
|
+
) -> Option<(
|
|
1035
|
+
Vec<(std::sync::Arc<str>, TypeAnnotation)>,
|
|
1036
|
+
Vec<tishlang_ast::ObjectProp>,
|
|
1037
|
+
)> {
|
|
1038
|
+
use tishlang_ast::{Expr, MemberProp, ObjectProp};
|
|
1039
|
+
if props
|
|
1040
|
+
.iter()
|
|
1041
|
+
.filter(|p| matches!(p, ObjectProp::Spread(_)))
|
|
1042
|
+
.count()
|
|
1043
|
+
!= 1
|
|
1044
|
+
{
|
|
1045
|
+
return None;
|
|
1046
|
+
}
|
|
1047
|
+
let (base_name, base_span) = match props.first() {
|
|
1048
|
+
Some(ObjectProp::Spread(Expr::Ident { name, span })) => (name.clone(), *span),
|
|
1049
|
+
_ => return None, // spread not first, or not a plain ident
|
|
1050
|
+
};
|
|
1051
|
+
let alias = match ctx.lookup(base_name.as_ref()) {
|
|
1052
|
+
Some(TypeAnnotation::Simple(a, _)) => a.clone(),
|
|
1053
|
+
_ => return None, // base is not a struct-typed local
|
|
1054
|
+
};
|
|
1055
|
+
let base_fields = reg.fields_of(&alias)?.to_vec();
|
|
1056
|
+
let explicit = &props[1..];
|
|
1057
|
+
// Explicit props must be spread-free + all typeable (infer_object_shape bails otherwise).
|
|
1058
|
+
let explicit_fields = infer_object_shape(explicit, ctx)?;
|
|
1059
|
+
let base_keys: std::collections::HashSet<&str> =
|
|
1060
|
+
base_fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
1061
|
+
if explicit_fields
|
|
1062
|
+
.iter()
|
|
1063
|
+
.any(|(k, _)| base_keys.contains(k.as_ref()))
|
|
1064
|
+
{
|
|
1065
|
+
return None;
|
|
1066
|
+
}
|
|
1067
|
+
let mut merged = base_fields.clone();
|
|
1068
|
+
merged.extend(explicit_fields.iter().cloned());
|
|
1069
|
+
let mut rewritten: Vec<ObjectProp> = base_fields
|
|
1070
|
+
.iter()
|
|
1071
|
+
.map(|(k, _)| {
|
|
1072
|
+
ObjectProp::KeyValue(
|
|
1073
|
+
k.clone(),
|
|
1074
|
+
Expr::Member {
|
|
1075
|
+
object: Box::new(Expr::Ident {
|
|
1076
|
+
name: base_name.clone(),
|
|
1077
|
+
span: base_span,
|
|
1078
|
+
}),
|
|
1079
|
+
prop: MemberProp::Name {
|
|
1080
|
+
name: k.clone(),
|
|
1081
|
+
span: base_span,
|
|
1082
|
+
},
|
|
1083
|
+
optional: false,
|
|
1084
|
+
span: base_span,
|
|
1085
|
+
},
|
|
1086
|
+
base_span,
|
|
1087
|
+
)
|
|
1088
|
+
})
|
|
1089
|
+
.collect();
|
|
1090
|
+
rewritten.extend_from_slice(explicit);
|
|
1091
|
+
Some((merged, rewritten))
|
|
1092
|
+
}
|
|
1093
|
+
|
|
908
1094
|
/// Infer a concrete object shape from an object literal, or `None` if any field
|
|
909
1095
|
/// can't be typed concretely / there's a spread.
|
|
910
1096
|
fn infer_object_shape(
|
|
@@ -915,6 +1101,12 @@ fn infer_object_shape(
|
|
|
915
1101
|
for p in props {
|
|
916
1102
|
match p {
|
|
917
1103
|
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
1104
|
+
// Security #379: an object key that is not a valid Rust identifier must not become a
|
|
1105
|
+
// native struct field (it would be interpolated verbatim into generated Rust — code
|
|
1106
|
+
// injection). Decline the shape → the object stays on the boxed `Value` path.
|
|
1107
|
+
if !crate::types::is_struct_field_safe(k) {
|
|
1108
|
+
return None;
|
|
1109
|
+
}
|
|
918
1110
|
let ty = infer_expr_type(v, ctx)?;
|
|
919
1111
|
// Only primitive field types in this conservative version.
|
|
920
1112
|
if !matches!(&ty, TypeAnnotation::Simple(s, _)
|
|
@@ -930,12 +1122,760 @@ fn infer_object_shape(
|
|
|
930
1122
|
if fields.is_empty() {
|
|
931
1123
|
return None;
|
|
932
1124
|
}
|
|
933
|
-
Some(fields)
|
|
1125
|
+
Some(fields)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/// #320: the set of top-level fns PROVEN to ALWAYS return a `number` — every `return` carries a
|
|
1129
|
+
/// numeric value and the body can't fall through to an implicit `undefined`. Lets `infer_expr_type`
|
|
1130
|
+
/// type a call `f(...)` as `number`, so `seq.push(nextBase())` infers `seq: number[]` (native key
|
|
1131
|
+
/// arithmetic over a `Vec<f64>` instead of a boxed `Value[]`). Conservative + sound: any fn we
|
|
1132
|
+
/// can't prove is left out, and the Call arm then declines to type its calls. Small fixpoint so a
|
|
1133
|
+
/// number-fn may call another already-accepted number-fn.
|
|
1134
|
+
fn collect_number_returning_fns(
|
|
1135
|
+
stmts: &[Statement],
|
|
1136
|
+
base: &InferCtx,
|
|
1137
|
+
) -> std::collections::HashSet<String> {
|
|
1138
|
+
use std::collections::HashSet;
|
|
1139
|
+
let mut accepted: HashSet<String> = HashSet::new();
|
|
1140
|
+
loop {
|
|
1141
|
+
let mut changed = false;
|
|
1142
|
+
for s in stmts {
|
|
1143
|
+
if let Statement::FunDecl {
|
|
1144
|
+
async_: false,
|
|
1145
|
+
name,
|
|
1146
|
+
params,
|
|
1147
|
+
rest_param: None,
|
|
1148
|
+
body,
|
|
1149
|
+
..
|
|
1150
|
+
} = s
|
|
1151
|
+
{
|
|
1152
|
+
if accepted.contains(name.as_ref()) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
// Fn-local ctx: numeric params (param-infer already annotated them), numeric locals,
|
|
1156
|
+
// and the number-fns accepted so far (so a numeric call inside resolves).
|
|
1157
|
+
let mut fctx = base.clone();
|
|
1158
|
+
fctx.number_returning_fns = accepted.clone();
|
|
1159
|
+
fctx.push_scope();
|
|
1160
|
+
for p in params {
|
|
1161
|
+
if let FunParam::Simple(tp) = p {
|
|
1162
|
+
if tp.type_ann.as_ref().is_some_and(is_number) {
|
|
1163
|
+
fctx.define(&tp.name, number_ann());
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// A few passes so chained numeric locals settle (`let a = 0; let b = a * 2`).
|
|
1168
|
+
for _ in 0..4 {
|
|
1169
|
+
seed_numeric_locals(body, &mut fctx);
|
|
1170
|
+
}
|
|
1171
|
+
if fn_always_returns_number(body, &fctx) {
|
|
1172
|
+
accepted.insert(name.to_string());
|
|
1173
|
+
changed = true;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if !changed {
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
accepted
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/// A fn always returns a number iff EVERY `return` in its body carries a value that infers to
|
|
1185
|
+
/// `number` AND the body can't fall through to an implicit `undefined` (conservatively: its last
|
|
1186
|
+
/// statement is an unconditional `return <value>`).
|
|
1187
|
+
fn fn_always_returns_number(body: &Statement, ctx: &InferCtx) -> bool {
|
|
1188
|
+
let mut ok = true;
|
|
1189
|
+
check_returns_numeric(body, ctx, &mut ok);
|
|
1190
|
+
ok && body_ends_in_return(body)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/// Visit EVERY `return` reachable in THIS fn (descends all statement-nesting constructs but NOT
|
|
1194
|
+
/// nested fn bodies, whose returns belong to the closure). Sets `ok = false` on a bare `return;` or
|
|
1195
|
+
/// a `return <e>` whose value doesn't provably infer to `number`. Missing a construct here would be
|
|
1196
|
+
/// unsound, so every statement-nesting variant is enumerated explicitly.
|
|
1197
|
+
fn check_returns_numeric(s: &Statement, ctx: &InferCtx, ok: &mut bool) {
|
|
1198
|
+
use Statement::*;
|
|
1199
|
+
if !*ok {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
match s {
|
|
1203
|
+
Return { value, .. } => match value {
|
|
1204
|
+
Some(e) => {
|
|
1205
|
+
if infer_expr_type(e, ctx).as_ref().map_or(true, |t| !is_number(t)) {
|
|
1206
|
+
*ok = false;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
None => *ok = false,
|
|
1210
|
+
},
|
|
1211
|
+
FunDecl { .. } => {} // nested fn: its returns are not this fn's
|
|
1212
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
1213
|
+
for c in statements {
|
|
1214
|
+
check_returns_numeric(c, ctx, ok);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
If { then_branch, else_branch, .. } => {
|
|
1218
|
+
check_returns_numeric(then_branch, ctx, ok);
|
|
1219
|
+
if let Some(e) = else_branch {
|
|
1220
|
+
check_returns_numeric(e, ctx, ok);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1224
|
+
check_returns_numeric(body, ctx, ok);
|
|
1225
|
+
}
|
|
1226
|
+
For { init, body, .. } => {
|
|
1227
|
+
if let Some(i) = init {
|
|
1228
|
+
check_returns_numeric(i, ctx, ok);
|
|
1229
|
+
}
|
|
1230
|
+
check_returns_numeric(body, ctx, ok);
|
|
1231
|
+
}
|
|
1232
|
+
Switch { cases, default_body, .. } => {
|
|
1233
|
+
for (_, body) in cases {
|
|
1234
|
+
for c in body {
|
|
1235
|
+
check_returns_numeric(c, ctx, ok);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if let Some(d) = default_body {
|
|
1239
|
+
for c in d {
|
|
1240
|
+
check_returns_numeric(c, ctx, ok);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1245
|
+
check_returns_numeric(body, ctx, ok);
|
|
1246
|
+
if let Some(c) = catch_body {
|
|
1247
|
+
check_returns_numeric(c, ctx, ok);
|
|
1248
|
+
}
|
|
1249
|
+
if let Some(f) = finally_body {
|
|
1250
|
+
check_returns_numeric(f, ctx, ok);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
_ => {}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/// Conservative "the body always reaches a return": the last statement is an unconditional
|
|
1258
|
+
/// `return <value>` (or a nested block that itself ends in one). Anything else (trailing `if`,
|
|
1259
|
+
/// loop, fall-off) is treated as a possible implicit-`undefined` exit and rejects the fn.
|
|
1260
|
+
fn body_ends_in_return(s: &Statement) -> bool {
|
|
1261
|
+
match s {
|
|
1262
|
+
Statement::Return { value: Some(_), .. } => true,
|
|
1263
|
+
Statement::Block { statements, .. } => statements.last().is_some_and(body_ends_in_return),
|
|
1264
|
+
_ => false,
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ---------------------------------------------------------------------------
|
|
1269
|
+
// #179 Stage A: single-call nullary-factory inlining (fail-closed)
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
|
|
1272
|
+
/// Count uses of function name `f` across `stmts`. Returns `(direct_calls, other_uses)`:
|
|
1273
|
+
/// a `direct_call` is `f(...)` (an `Ident(f)` in callee position); `other_uses` is EVERY other
|
|
1274
|
+
/// occurrence of the name — as a value, an assignment/inc-dec target, etc. Exhaustive over the
|
|
1275
|
+
/// whole AST (walks fn bodies, switch, try — which the shared `walk_exprs_stmt` skips) so it can
|
|
1276
|
+
/// back a SOUND escape check: if `other_uses > 0` the function escapes and must not be inlined.
|
|
1277
|
+
fn fn_name_uses(stmts: &[Statement], f: &str) -> (usize, usize) {
|
|
1278
|
+
let mut calls = 0usize;
|
|
1279
|
+
let mut others = 0usize;
|
|
1280
|
+
for s in stmts {
|
|
1281
|
+
count_uses_stmt(s, f, &mut calls, &mut others);
|
|
1282
|
+
}
|
|
1283
|
+
(calls, others)
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
fn count_uses_stmt(s: &Statement, f: &str, calls: &mut usize, others: &mut usize) {
|
|
1287
|
+
use Statement::*;
|
|
1288
|
+
match s {
|
|
1289
|
+
VarDecl { init, .. } => {
|
|
1290
|
+
if let Some(e) = init {
|
|
1291
|
+
count_uses_expr(e, f, calls, others);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
VarDeclDestructure { init, .. } => count_uses_expr(init, f, calls, others),
|
|
1295
|
+
Multi { statements, .. } | Block { statements, .. } => {
|
|
1296
|
+
statements.iter().for_each(|x| count_uses_stmt(x, f, calls, others))
|
|
1297
|
+
}
|
|
1298
|
+
ExprStmt { expr, .. } => count_uses_expr(expr, f, calls, others),
|
|
1299
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
1300
|
+
count_uses_expr(cond, f, calls, others);
|
|
1301
|
+
count_uses_stmt(then_branch, f, calls, others);
|
|
1302
|
+
if let Some(e) = else_branch {
|
|
1303
|
+
count_uses_stmt(e, f, calls, others);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
While { cond, body, .. } | DoWhile { cond, body, .. } => {
|
|
1307
|
+
count_uses_expr(cond, f, calls, others);
|
|
1308
|
+
count_uses_stmt(body, f, calls, others);
|
|
1309
|
+
}
|
|
1310
|
+
For { init, cond, update, body, .. } => {
|
|
1311
|
+
if let Some(i) = init {
|
|
1312
|
+
count_uses_stmt(i, f, calls, others);
|
|
1313
|
+
}
|
|
1314
|
+
if let Some(c) = cond {
|
|
1315
|
+
count_uses_expr(c, f, calls, others);
|
|
1316
|
+
}
|
|
1317
|
+
if let Some(u) = update {
|
|
1318
|
+
count_uses_expr(u, f, calls, others);
|
|
1319
|
+
}
|
|
1320
|
+
count_uses_stmt(body, f, calls, others);
|
|
1321
|
+
}
|
|
1322
|
+
ForOf { iterable, body, .. }
|
|
1323
|
+
| ForIn {
|
|
1324
|
+
object: iterable,
|
|
1325
|
+
body,
|
|
1326
|
+
..
|
|
1327
|
+
} => {
|
|
1328
|
+
count_uses_expr(iterable, f, calls, others);
|
|
1329
|
+
count_uses_stmt(body, f, calls, others);
|
|
1330
|
+
}
|
|
1331
|
+
Return { value: Some(e), .. } => count_uses_expr(e, f, calls, others),
|
|
1332
|
+
Throw { value, .. } => count_uses_expr(value, f, calls, others),
|
|
1333
|
+
FunDecl { body, .. } => count_uses_stmt(body, f, calls, others),
|
|
1334
|
+
Switch { expr, cases, default_body, .. } => {
|
|
1335
|
+
count_uses_expr(expr, f, calls, others);
|
|
1336
|
+
for (test, body) in cases {
|
|
1337
|
+
if let Some(t) = test {
|
|
1338
|
+
count_uses_expr(t, f, calls, others);
|
|
1339
|
+
}
|
|
1340
|
+
body.iter().for_each(|x| count_uses_stmt(x, f, calls, others));
|
|
1341
|
+
}
|
|
1342
|
+
if let Some(body) = default_body {
|
|
1343
|
+
body.iter().for_each(|x| count_uses_stmt(x, f, calls, others));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1347
|
+
count_uses_stmt(body, f, calls, others);
|
|
1348
|
+
if let Some(cb) = catch_body {
|
|
1349
|
+
count_uses_stmt(cb, f, calls, others);
|
|
1350
|
+
}
|
|
1351
|
+
if let Some(fb) = finally_body {
|
|
1352
|
+
count_uses_stmt(fb, f, calls, others);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
Export { declaration, .. } => match declaration.as_ref() {
|
|
1356
|
+
tishlang_ast::ExportDeclaration::Named(inner) => {
|
|
1357
|
+
count_uses_stmt(inner, f, calls, others)
|
|
1358
|
+
}
|
|
1359
|
+
tishlang_ast::ExportDeclaration::Default(e) => count_uses_expr(e, f, calls, others),
|
|
1360
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => {}
|
|
1361
|
+
},
|
|
1362
|
+
Return { value: None, .. }
|
|
1363
|
+
| Break { .. }
|
|
1364
|
+
| Continue { .. }
|
|
1365
|
+
| Import { .. }
|
|
1366
|
+
| TypeAlias { .. }
|
|
1367
|
+
| DeclareVar { .. }
|
|
1368
|
+
| DeclareFun { .. } => {}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
fn count_uses_expr(e: &Expr, f: &str, calls: &mut usize, others: &mut usize) {
|
|
1373
|
+
use Expr::*;
|
|
1374
|
+
match e {
|
|
1375
|
+
Ident { name, .. } => {
|
|
1376
|
+
if name.as_ref() == f {
|
|
1377
|
+
*others += 1;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
Call { callee, args, .. } => {
|
|
1381
|
+
// `f(...)` in callee position is a direct call — do NOT count the callee ident as an
|
|
1382
|
+
// "other use". Any other callee shape (member call, computed) recurses normally.
|
|
1383
|
+
match callee.as_ref() {
|
|
1384
|
+
Ident { name, .. } if name.as_ref() == f => *calls += 1,
|
|
1385
|
+
_ => count_uses_expr(callee, f, calls, others),
|
|
1386
|
+
}
|
|
1387
|
+
for a in args {
|
|
1388
|
+
match a {
|
|
1389
|
+
CallArg::Expr(x) | CallArg::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
New { callee, args, .. } => {
|
|
1394
|
+
count_uses_expr(callee, f, calls, others);
|
|
1395
|
+
for a in args {
|
|
1396
|
+
match a {
|
|
1397
|
+
CallArg::Expr(x) | CallArg::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
Assign { name, value, .. }
|
|
1402
|
+
| CompoundAssign { name, value, .. }
|
|
1403
|
+
| LogicalAssign { name, value, .. } => {
|
|
1404
|
+
if name.as_ref() == f {
|
|
1405
|
+
*others += 1; // reassigning the function name — an escape
|
|
1406
|
+
}
|
|
1407
|
+
count_uses_expr(value, f, calls, others);
|
|
1408
|
+
}
|
|
1409
|
+
PostfixInc { name, .. } | PostfixDec { name, .. } | PrefixInc { name, .. }
|
|
1410
|
+
| PrefixDec { name, .. } => {
|
|
1411
|
+
if name.as_ref() == f {
|
|
1412
|
+
*others += 1;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
Binary { left, right, .. } | NullishCoalesce { left, right, .. } => {
|
|
1416
|
+
count_uses_expr(left, f, calls, others);
|
|
1417
|
+
count_uses_expr(right, f, calls, others);
|
|
1418
|
+
}
|
|
1419
|
+
Unary { operand, .. } | TypeOf { operand, .. } | Await { operand, .. }
|
|
1420
|
+
| Delete { target: operand, .. } => count_uses_expr(operand, f, calls, others),
|
|
1421
|
+
Member { object, prop, .. } => {
|
|
1422
|
+
count_uses_expr(object, f, calls, others);
|
|
1423
|
+
if let tishlang_ast::MemberProp::Expr(p) = prop {
|
|
1424
|
+
count_uses_expr(p, f, calls, others);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
Index { object, index, .. } => {
|
|
1428
|
+
count_uses_expr(object, f, calls, others);
|
|
1429
|
+
count_uses_expr(index, f, calls, others);
|
|
1430
|
+
}
|
|
1431
|
+
Conditional { cond, then_branch, else_branch, .. } => {
|
|
1432
|
+
count_uses_expr(cond, f, calls, others);
|
|
1433
|
+
count_uses_expr(then_branch, f, calls, others);
|
|
1434
|
+
count_uses_expr(else_branch, f, calls, others);
|
|
1435
|
+
}
|
|
1436
|
+
Array { elements, .. } => {
|
|
1437
|
+
for el in elements {
|
|
1438
|
+
match el {
|
|
1439
|
+
tishlang_ast::ArrayElement::Expr(x)
|
|
1440
|
+
| tishlang_ast::ArrayElement::Spread(x) => {
|
|
1441
|
+
count_uses_expr(x, f, calls, others)
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
Object { props, .. } => {
|
|
1447
|
+
for p in props {
|
|
1448
|
+
match p {
|
|
1449
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => {
|
|
1450
|
+
count_uses_expr(v, f, calls, others)
|
|
1451
|
+
}
|
|
1452
|
+
tishlang_ast::ObjectProp::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
MemberAssign { object, value, .. } => {
|
|
1457
|
+
count_uses_expr(object, f, calls, others);
|
|
1458
|
+
count_uses_expr(value, f, calls, others);
|
|
1459
|
+
}
|
|
1460
|
+
IndexAssign { object, index, value, .. } => {
|
|
1461
|
+
count_uses_expr(object, f, calls, others);
|
|
1462
|
+
count_uses_expr(index, f, calls, others);
|
|
1463
|
+
count_uses_expr(value, f, calls, others);
|
|
1464
|
+
}
|
|
1465
|
+
ArrowFunction { body, .. } => match body {
|
|
1466
|
+
tishlang_ast::ArrowBody::Expr(x) => count_uses_expr(x, f, calls, others),
|
|
1467
|
+
tishlang_ast::ArrowBody::Block(s) => count_uses_stmt(s, f, calls, others),
|
|
1468
|
+
},
|
|
1469
|
+
TemplateLiteral { exprs, .. } => {
|
|
1470
|
+
exprs.iter().for_each(|x| count_uses_expr(x, f, calls, others))
|
|
1471
|
+
}
|
|
1472
|
+
JsxElement { props, children, .. } => {
|
|
1473
|
+
for prop in props {
|
|
1474
|
+
match prop {
|
|
1475
|
+
tishlang_ast::JsxProp::Attr { value, .. } => {
|
|
1476
|
+
if let tishlang_ast::JsxAttrValue::Expr(x) = value {
|
|
1477
|
+
count_uses_expr(x, f, calls, others);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
tishlang_ast::JsxProp::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
for c in children {
|
|
1484
|
+
if let tishlang_ast::JsxChild::Expr(x) = c {
|
|
1485
|
+
count_uses_expr(x, f, calls, others);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
JsxFragment { children, .. } => {
|
|
1490
|
+
for c in children {
|
|
1491
|
+
if let tishlang_ast::JsxChild::Expr(x) = c {
|
|
1492
|
+
count_uses_expr(x, f, calls, others);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
Literal { .. } | NativeModuleLoad { .. } => {}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/// Does statement `s` (or anything nested) contain a closure (`FunDecl`/`ArrowFunction`) or a
|
|
1501
|
+
/// `this`/`arguments`/`super` reference? Such a body is NOT safely alpha-renameable by the flat
|
|
1502
|
+
/// splice below (a closure could capture a renamed local), so we refuse to inline it.
|
|
1503
|
+
fn body_has_closure_or_dynamic(s: &Statement) -> bool {
|
|
1504
|
+
let mut bad = false;
|
|
1505
|
+
walk_exprs_stmt_full(s, &mut |e| {
|
|
1506
|
+
match e {
|
|
1507
|
+
Expr::ArrowFunction { .. } => bad = true,
|
|
1508
|
+
Expr::Ident { name, .. }
|
|
1509
|
+
if matches!(name.as_ref(), "this" | "arguments" | "super") =>
|
|
1510
|
+
{
|
|
1511
|
+
bad = true
|
|
1512
|
+
}
|
|
1513
|
+
_ => {}
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
bad || stmt_contains_fundecl(s)
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
fn stmt_contains_fundecl(s: &Statement) -> bool {
|
|
1520
|
+
use Statement::*;
|
|
1521
|
+
match s {
|
|
1522
|
+
FunDecl { .. } => true,
|
|
1523
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
1524
|
+
statements.iter().any(stmt_contains_fundecl)
|
|
1525
|
+
}
|
|
1526
|
+
If { then_branch, else_branch, .. } => {
|
|
1527
|
+
stmt_contains_fundecl(then_branch)
|
|
1528
|
+
|| else_branch.as_ref().is_some_and(|e| stmt_contains_fundecl(e))
|
|
1529
|
+
}
|
|
1530
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1531
|
+
stmt_contains_fundecl(body)
|
|
1532
|
+
}
|
|
1533
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1534
|
+
stmt_contains_fundecl(body)
|
|
1535
|
+
|| catch_body.as_ref().is_some_and(|b| stmt_contains_fundecl(b))
|
|
1536
|
+
|| finally_body.as_ref().is_some_and(|b| stmt_contains_fundecl(b))
|
|
1537
|
+
}
|
|
1538
|
+
Switch { cases, default_body, .. } => {
|
|
1539
|
+
cases.iter().any(|(_, b)| b.iter().any(stmt_contains_fundecl))
|
|
1540
|
+
|| default_body
|
|
1541
|
+
.as_ref()
|
|
1542
|
+
.is_some_and(|b| b.iter().any(stmt_contains_fundecl))
|
|
1543
|
+
}
|
|
1544
|
+
_ => false,
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/// Exhaustive expr-visitor over a statement (unlike the shared `walk_exprs_stmt`, this also
|
|
1549
|
+
/// descends fn bodies, switch, and try). Visits every `Expr` node via `f`.
|
|
1550
|
+
fn walk_exprs_stmt_full(s: &Statement, f: &mut impl FnMut(&Expr)) {
|
|
1551
|
+
use Statement::*;
|
|
1552
|
+
match s {
|
|
1553
|
+
VarDecl { init: Some(e), .. } => walk_exprs_expr(e, f),
|
|
1554
|
+
VarDeclDestructure { init, .. } => walk_exprs_expr(init, f),
|
|
1555
|
+
Multi { statements, .. } | Block { statements, .. } => {
|
|
1556
|
+
statements.iter().for_each(|x| walk_exprs_stmt_full(x, f))
|
|
1557
|
+
}
|
|
1558
|
+
ExprStmt { expr, .. } => walk_exprs_expr(expr, f),
|
|
1559
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
1560
|
+
walk_exprs_expr(cond, f);
|
|
1561
|
+
walk_exprs_stmt_full(then_branch, f);
|
|
1562
|
+
if let Some(e) = else_branch {
|
|
1563
|
+
walk_exprs_stmt_full(e, f);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
While { cond, body, .. } | DoWhile { cond, body, .. } => {
|
|
1567
|
+
walk_exprs_expr(cond, f);
|
|
1568
|
+
walk_exprs_stmt_full(body, f);
|
|
1569
|
+
}
|
|
1570
|
+
For { init, cond, update, body, .. } => {
|
|
1571
|
+
if let Some(i) = init {
|
|
1572
|
+
walk_exprs_stmt_full(i, f);
|
|
1573
|
+
}
|
|
1574
|
+
if let Some(c) = cond {
|
|
1575
|
+
walk_exprs_expr(c, f);
|
|
1576
|
+
}
|
|
1577
|
+
if let Some(u) = update {
|
|
1578
|
+
walk_exprs_expr(u, f);
|
|
1579
|
+
}
|
|
1580
|
+
walk_exprs_stmt_full(body, f);
|
|
1581
|
+
}
|
|
1582
|
+
ForOf { iterable, body, .. }
|
|
1583
|
+
| ForIn {
|
|
1584
|
+
object: iterable,
|
|
1585
|
+
body,
|
|
1586
|
+
..
|
|
1587
|
+
} => {
|
|
1588
|
+
walk_exprs_expr(iterable, f);
|
|
1589
|
+
walk_exprs_stmt_full(body, f);
|
|
1590
|
+
}
|
|
1591
|
+
Return { value: Some(e), .. } => walk_exprs_expr(e, f),
|
|
1592
|
+
Throw { value, .. } => walk_exprs_expr(value, f),
|
|
1593
|
+
FunDecl { body, .. } => walk_exprs_stmt_full(body, f),
|
|
1594
|
+
Switch { expr, cases, default_body, .. } => {
|
|
1595
|
+
walk_exprs_expr(expr, f);
|
|
1596
|
+
for (test, body) in cases {
|
|
1597
|
+
if let Some(t) = test {
|
|
1598
|
+
walk_exprs_expr(t, f);
|
|
1599
|
+
}
|
|
1600
|
+
body.iter().for_each(|x| walk_exprs_stmt_full(x, f));
|
|
1601
|
+
}
|
|
1602
|
+
if let Some(body) = default_body {
|
|
1603
|
+
body.iter().for_each(|x| walk_exprs_stmt_full(x, f));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1607
|
+
walk_exprs_stmt_full(body, f);
|
|
1608
|
+
if let Some(cb) = catch_body {
|
|
1609
|
+
walk_exprs_stmt_full(cb, f);
|
|
1610
|
+
}
|
|
1611
|
+
if let Some(fb) = finally_body {
|
|
1612
|
+
walk_exprs_stmt_full(fb, f);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
Export { declaration, .. } => match declaration.as_ref() {
|
|
1616
|
+
tishlang_ast::ExportDeclaration::Named(inner) => walk_exprs_stmt_full(inner, f),
|
|
1617
|
+
tishlang_ast::ExportDeclaration::Default(e) => walk_exprs_expr(e, f),
|
|
1618
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => {}
|
|
1619
|
+
},
|
|
1620
|
+
_ => {}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/// Collect the names declared by DIRECT (top-level-of-body) `let`/`const` VarDecls in `body_stmts`.
|
|
1625
|
+
/// These are the locals that would leak into the caller's scope when the body is spliced there, so
|
|
1626
|
+
/// they must be alpha-renamed. Nested-scope declarations (inside loops/blocks) stay properly scoped
|
|
1627
|
+
/// after splicing and are left alone. Destructuring decls make the factory ineligible (returns None).
|
|
1628
|
+
fn direct_body_locals(body_stmts: &[Statement]) -> Option<Vec<Arc<str>>> {
|
|
1629
|
+
let mut out = Vec::new();
|
|
1630
|
+
for s in body_stmts {
|
|
1631
|
+
match s {
|
|
1632
|
+
Statement::VarDecl { name, .. } => out.push(name.clone()),
|
|
1633
|
+
Statement::VarDeclDestructure { .. } => return None,
|
|
1634
|
+
_ => {}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
Some(out)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/// Count `return` statements anywhere in `s`.
|
|
1641
|
+
fn count_returns(s: &Statement) -> usize {
|
|
1642
|
+
use Statement::*;
|
|
1643
|
+
match s {
|
|
1644
|
+
Return { .. } => 1,
|
|
1645
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
1646
|
+
statements.iter().map(count_returns).sum()
|
|
1647
|
+
}
|
|
1648
|
+
If { then_branch, else_branch, .. } => {
|
|
1649
|
+
count_returns(then_branch)
|
|
1650
|
+
+ else_branch.as_ref().map_or(0, |e| count_returns(e))
|
|
1651
|
+
}
|
|
1652
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1653
|
+
count_returns(body)
|
|
1654
|
+
}
|
|
1655
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1656
|
+
count_returns(body)
|
|
1657
|
+
+ catch_body.as_ref().map_or(0, |b| count_returns(b))
|
|
1658
|
+
+ finally_body.as_ref().map_or(0, |b| count_returns(b))
|
|
1659
|
+
}
|
|
1660
|
+
Switch { cases, default_body, .. } => {
|
|
1661
|
+
cases.iter().map(|(_, b)| b.iter().map(count_returns).sum::<usize>()).sum::<usize>()
|
|
1662
|
+
+ default_body
|
|
1663
|
+
.as_ref()
|
|
1664
|
+
.map_or(0, |b| b.iter().map(count_returns).sum())
|
|
1665
|
+
}
|
|
1666
|
+
_ => 0,
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/// #179 Stage A driver: for each eligible nullary factory, splice its body at its single top-level
|
|
1671
|
+
/// call site (alpha-renamed) and drop the FunDecl. Fail-closed: any shape not proven safe is left
|
|
1672
|
+
/// untouched.
|
|
1673
|
+
fn inline_single_call_factories(stmts: Vec<Statement>) -> Vec<Statement> {
|
|
1674
|
+
// Snapshot eligible (name → FunDecl body, is_mutable-array) candidates. We do at most one
|
|
1675
|
+
// inlining pass over top-level statements; a factory that itself calls another eligible factory
|
|
1676
|
+
// is handled on the next compile-invariant since we only splice direct top-level call sites.
|
|
1677
|
+
let mut result = stmts;
|
|
1678
|
+
// Collect candidate factory names first (immutable borrow), then transform.
|
|
1679
|
+
let candidates: Vec<Arc<str>> = result
|
|
1680
|
+
.iter()
|
|
1681
|
+
.filter_map(|s| match s {
|
|
1682
|
+
Statement::FunDecl {
|
|
1683
|
+
async_: false,
|
|
1684
|
+
name,
|
|
1685
|
+
params,
|
|
1686
|
+
rest_param: None,
|
|
1687
|
+
body,
|
|
1688
|
+
..
|
|
1689
|
+
} if params.is_empty() => {
|
|
1690
|
+
// body must be a Block/Multi ending in a single tail `return E`.
|
|
1691
|
+
let body_stmts = match body.as_ref() {
|
|
1692
|
+
Statement::Block { statements, .. }
|
|
1693
|
+
| Statement::Multi { statements, .. } => statements.as_slice(),
|
|
1694
|
+
_ => return None,
|
|
1695
|
+
};
|
|
1696
|
+
if count_returns(body) != 1 {
|
|
1697
|
+
return None;
|
|
1698
|
+
}
|
|
1699
|
+
match body_stmts.last() {
|
|
1700
|
+
Some(Statement::Return { value: Some(_), .. }) => {}
|
|
1701
|
+
_ => return None,
|
|
1702
|
+
}
|
|
1703
|
+
if body_has_closure_or_dynamic(body) {
|
|
1704
|
+
return None;
|
|
1705
|
+
}
|
|
1706
|
+
if direct_body_locals(body_stmts).is_none() {
|
|
1707
|
+
return None;
|
|
1708
|
+
}
|
|
1709
|
+
Some(name.clone())
|
|
1710
|
+
}
|
|
1711
|
+
_ => None,
|
|
1712
|
+
})
|
|
1713
|
+
.collect();
|
|
1714
|
+
|
|
1715
|
+
for fname in candidates {
|
|
1716
|
+
// Re-check use counts against the CURRENT program (a prior inline may have changed them).
|
|
1717
|
+
let (calls, others) = fn_name_uses(&result, fname.as_ref());
|
|
1718
|
+
if others != 0 || calls != 1 {
|
|
1719
|
+
continue; // escapes, unused, or called more than once → not inlinable
|
|
1720
|
+
}
|
|
1721
|
+
// Find the single top-level call site index + form.
|
|
1722
|
+
let mut site: Option<(usize, Option<Arc<str>>)> = None; // (idx, Some(target)=VarDecl / None=ExprStmt)
|
|
1723
|
+
let mut ambiguous = false;
|
|
1724
|
+
for (i, s) in result.iter().enumerate() {
|
|
1725
|
+
let hit = match s {
|
|
1726
|
+
Statement::VarDecl {
|
|
1727
|
+
name,
|
|
1728
|
+
type_ann: None,
|
|
1729
|
+
init: Some(Expr::Call { callee, args, .. }),
|
|
1730
|
+
..
|
|
1731
|
+
} if args.is_empty()
|
|
1732
|
+
&& matches!(callee.as_ref(), Expr::Ident { name: c, .. } if c.as_ref() == fname.as_ref()) =>
|
|
1733
|
+
{
|
|
1734
|
+
Some(Some(name.clone()))
|
|
1735
|
+
}
|
|
1736
|
+
Statement::ExprStmt { expr: Expr::Call { callee, args, .. }, .. }
|
|
1737
|
+
if args.is_empty()
|
|
1738
|
+
&& matches!(callee.as_ref(), Expr::Ident { name: c, .. } if c.as_ref() == fname.as_ref()) =>
|
|
1739
|
+
{
|
|
1740
|
+
Some(None)
|
|
1741
|
+
}
|
|
1742
|
+
_ => None,
|
|
1743
|
+
};
|
|
1744
|
+
if let Some(target) = hit {
|
|
1745
|
+
if site.is_some() {
|
|
1746
|
+
ambiguous = true;
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
site = Some((i, target));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
// The single call must be a TOP-LEVEL statement (calls==1 guarantees no other call exists;
|
|
1753
|
+
// if we didn't find it at top level it is nested — skip, fail-closed).
|
|
1754
|
+
let (site_idx, target) = match site {
|
|
1755
|
+
Some(s) if !ambiguous => s,
|
|
1756
|
+
_ => continue,
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// Pull the factory body out of its FunDecl.
|
|
1760
|
+
let fdecl_idx = result.iter().position(|s| {
|
|
1761
|
+
matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == fname.as_ref())
|
|
1762
|
+
});
|
|
1763
|
+
let Some(fdecl_idx) = fdecl_idx else { continue };
|
|
1764
|
+
let body_stmts: Vec<Statement> = match &result[fdecl_idx] {
|
|
1765
|
+
Statement::FunDecl { body, .. } => match body.as_ref() {
|
|
1766
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
1767
|
+
statements.clone()
|
|
1768
|
+
}
|
|
1769
|
+
_ => continue,
|
|
1770
|
+
},
|
|
1771
|
+
_ => continue,
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1774
|
+
// Split off the trailing `return E`.
|
|
1775
|
+
let (prefix, ret_expr) = match body_stmts.split_last() {
|
|
1776
|
+
Some((Statement::Return { value: Some(e), .. }, head)) => (head.to_vec(), e.clone()),
|
|
1777
|
+
_ => continue,
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
// Build the alpha-rename map for the leaked (direct) body locals.
|
|
1781
|
+
let Some(leaked) = direct_body_locals(&prefix) else { continue };
|
|
1782
|
+
// NARROW to the push-built factory shape ONLY: `return R` where R is a leaked (body-top-level)
|
|
1783
|
+
// local built via `let R = []` + pushes, consumed as `let X = f()`. A literal-array return
|
|
1784
|
+
// (`return [a, b]`), a call return, or a discarded `f()` is left intact — those are the #177
|
|
1785
|
+
// aggregate machinery's territory (nbody's `makeBodies` returns `[sun, jupiter, …]`, and
|
|
1786
|
+
// inlining it removes the function the aggregate ABI expects → nbody BUILD-FAIL under
|
|
1787
|
+
// TISH_AGGREGATE_INFER). This keeps the inliner to exactly the Stage A/B bridge target.
|
|
1788
|
+
let return_ident = match &ret_expr {
|
|
1789
|
+
Expr::Ident { name, .. } if leaked.iter().any(|l| l.as_ref() == name.as_ref()) => {
|
|
1790
|
+
name.clone()
|
|
1791
|
+
}
|
|
1792
|
+
_ => continue,
|
|
1793
|
+
};
|
|
1794
|
+
let Some(tgt) = target.clone() else { continue }; // only `let X = f()`, not a discarded call
|
|
1795
|
+
let mut active: std::collections::HashMap<String, Arc<str>> =
|
|
1796
|
+
std::collections::HashMap::new();
|
|
1797
|
+
// Map the returned array var R → the call-site target X, so the spliced `let R = []` becomes
|
|
1798
|
+
// `let X = []` and the record-array/ShapeUnion machinery types X directly. Every other leaked
|
|
1799
|
+
// local gets a fresh, collision-proof name.
|
|
1800
|
+
for (n, l) in leaked.iter().enumerate() {
|
|
1801
|
+
if l.as_ref() == return_ident.as_ref() {
|
|
1802
|
+
active.insert(l.to_string(), tgt.clone());
|
|
1803
|
+
} else {
|
|
1804
|
+
let fresh: Arc<str> = format!("__inl_{}_{}_{}", fname, n, l).into();
|
|
1805
|
+
active.insert(l.to_string(), fresh);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Alpha-rename the prefix (declarations + references) using the resolution-tested renamer.
|
|
1810
|
+
// The trailing `return R` is dropped: R was renamed to X, so X is already the array binding.
|
|
1811
|
+
let mut renamed_prefix = prefix;
|
|
1812
|
+
for s in &mut renamed_prefix {
|
|
1813
|
+
let mut a = active.clone();
|
|
1814
|
+
crate::resolve::rewrite_stmt_scope(s, &mut a, true);
|
|
1815
|
+
}
|
|
1816
|
+
let spliced: Vec<Statement> = renamed_prefix;
|
|
1817
|
+
|
|
1818
|
+
// Rebuild the program: replace the call site with the spliced body, drop the FunDecl.
|
|
1819
|
+
let mut out: Vec<Statement> = Vec::with_capacity(result.len() + spliced.len());
|
|
1820
|
+
for (i, s) in result.into_iter().enumerate() {
|
|
1821
|
+
if i == fdecl_idx {
|
|
1822
|
+
continue; // drop the now-inlined factory
|
|
1823
|
+
}
|
|
1824
|
+
if i == site_idx {
|
|
1825
|
+
out.extend(spliced.iter().cloned());
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
out.push(s);
|
|
1829
|
+
}
|
|
1830
|
+
result = out;
|
|
1831
|
+
}
|
|
1832
|
+
result
|
|
934
1833
|
}
|
|
935
1834
|
|
|
936
1835
|
fn struct_infer_program(program: Program) -> Program {
|
|
1836
|
+
// #179 Stage A: inline a nullary function that is called EXACTLY ONCE (as a top-level
|
|
1837
|
+
// statement) and never used as a value, splicing its body at the call site. This bridges a
|
|
1838
|
+
// build-then-return record-array factory (`function mk(){ let a=[]; a.push({..}); return a }`;
|
|
1839
|
+
// `let xs = mk()`) into a TOP-LEVEL push-built array, which the record-array / ShapeUnion
|
|
1840
|
+
// machinery can then type. Sound by construction: a single-call inline substitutes the whole
|
|
1841
|
+
// call, so evaluation order and side effects are preserved exactly (a captured-global write in
|
|
1842
|
+
// the body becomes an ordinary top-level statement; a factory used as a value is left alone).
|
|
1843
|
+
let program = if crate::native_opts_enabled() {
|
|
1844
|
+
Program { statements: inline_single_call_factories(program.statements) }
|
|
1845
|
+
} else {
|
|
1846
|
+
program
|
|
1847
|
+
};
|
|
937
1848
|
let mut reg = StructRegistry::default();
|
|
938
1849
|
let mut ctx = InferCtx::new();
|
|
1850
|
+
// #320: fns proven to always return a number, so `infer_expr_type(f(...))` is `number` and a
|
|
1851
|
+
// `seq.push(nextBase())` keeps `seq` a native `number[]`. Computed off the param-inferred
|
|
1852
|
+
// program so numeric params are already typed.
|
|
1853
|
+
ctx.number_returning_fns = collect_number_returning_fns(&program.statements, &ctx);
|
|
1854
|
+
// #175: tell the mutable-array co-inference which fns take native `&/&mut Vec` array params, so
|
|
1855
|
+
// forwarding an array into one isn't treated as a boxing escape (lets the caller's array stay
|
|
1856
|
+
// unboxed). Same AST-only detection codegen uses, so the two never disagree.
|
|
1857
|
+
if crate::native_opts_enabled() {
|
|
1858
|
+
for (fname, sig) in crate::codegen::Codegen::detect_native_vec_fns(&program) {
|
|
1859
|
+
let flags: Vec<bool> = sig
|
|
1860
|
+
.params
|
|
1861
|
+
.iter()
|
|
1862
|
+
.map(|(_, k)| matches!(k, crate::codegen::VecParamKind::Array { .. }))
|
|
1863
|
+
.collect();
|
|
1864
|
+
ctx.native_vec_array_params.insert(fname, flags);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
// #320: mark a read-only `number[]`-param fn's array args as non-escapes so the caller's array
|
|
1868
|
+
// stays `number[]` (e.g. `kNucleotide(seq, k)`). This uses the READ-ONLY criterion only — sound
|
|
1869
|
+
// regardless of the arg type, since passing a native `Vec` to a boxed closure copies it (a
|
|
1870
|
+
// read-only callee can't mutate the caller's array through the copy). Codegen separately proves,
|
|
1871
|
+
// via the call sites, which of these it may actually UNBOX (`native_arr_param_fns`); a fn it
|
|
1872
|
+
// can't prove simply stays a fully boxed closure. Runs pre-`si_block`, so it must not rely on
|
|
1873
|
+
// `number[]` stamps — `arr_param_readonly_fns` only inspects each fn's own body.
|
|
1874
|
+
if crate::native_opts_enabled() {
|
|
1875
|
+
for (fname, flags) in crate::codegen::Codegen::arr_param_readonly_fns(&program) {
|
|
1876
|
+
ctx.native_vec_array_params.insert(fname, flags);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
939
1879
|
let mut stmts = si_block(program.statements, &mut reg, &mut ctx);
|
|
940
1880
|
// Prepend the generated struct `type` aliases so codegen synthesizes them.
|
|
941
1881
|
let mut out: Vec<Statement> = Vec::with_capacity(stmts.len() + reg.decls.len());
|
|
@@ -948,6 +1888,20 @@ fn struct_infer_program(program: Program) -> Program {
|
|
|
948
1888
|
span,
|
|
949
1889
|
});
|
|
950
1890
|
}
|
|
1891
|
+
// #179 Stage B: emit each shape-union alias as `type TishAnonUnion_N = Union([Object(s0), …])`;
|
|
1892
|
+
// codegen recognizes the reserved-prefix alias and synthesizes the enum + per-variant structs.
|
|
1893
|
+
for (name, shapes) in reg.union_decls.drain(..) {
|
|
1894
|
+
let members: Vec<TypeAnnotation> = shapes
|
|
1895
|
+
.into_iter()
|
|
1896
|
+
.map(TypeAnnotation::Object)
|
|
1897
|
+
.collect();
|
|
1898
|
+
out.push(Statement::TypeAlias {
|
|
1899
|
+
name: name.as_str().into(),
|
|
1900
|
+
name_span: span,
|
|
1901
|
+
ty: TypeAnnotation::Union(members),
|
|
1902
|
+
span,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
951
1905
|
out.append(&mut stmts);
|
|
952
1906
|
Program { statements: out }
|
|
953
1907
|
}
|
|
@@ -1001,7 +1955,11 @@ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx)
|
|
|
1001
1955
|
{
|
|
1002
1956
|
// (a) Read-only typed array: uniform scalar literals, every later use read-only.
|
|
1003
1957
|
if let Some(elem) = infer_array_elem(elements, ctx) {
|
|
1004
|
-
|
|
1958
|
+
// A constant OOB read (`let a=[1,2,3]; a[5]`) yields NaN under Vec<f64> but `null`
|
|
1959
|
+
// boxed, so disqualify it even though the use is "read-only".
|
|
1960
|
+
if uses_are_array_safe(name.as_ref(), &stmts[i + 1..])
|
|
1961
|
+
&& !array_oob_const_access(&stmts[i + 1..], name.as_ref(), elements.len())
|
|
1962
|
+
{
|
|
1005
1963
|
let arr_ann = TypeAnnotation::Array(Box::new(elem));
|
|
1006
1964
|
ctx.define(name.as_ref(), arr_ann.clone());
|
|
1007
1965
|
out.push(Statement::VarDecl {
|
|
@@ -1039,6 +1997,78 @@ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx)
|
|
|
1039
1997
|
});
|
|
1040
1998
|
continue;
|
|
1041
1999
|
}
|
|
2000
|
+
// (d) Array of uniform records: `let rows = []` (or `[{lits}]`) built by
|
|
2001
|
+
// `rows.push({uniform primitive object literal})` and used only as `rows[i].field`
|
|
2002
|
+
// reads / `rows.length`. Lower to a native `Vec<struct>` — the struct path emits direct
|
|
2003
|
+
// field access and (post-#350) allocation-free construction. Conservative: any other use
|
|
2004
|
+
// of the binding (escape, element/field write, for-of, bare ref) leaves it boxed.
|
|
2005
|
+
if let Some(fields) = record_array_fields(name.as_ref(), elements, &stmts[i + 1..], ctx) {
|
|
2006
|
+
let keys: std::collections::HashSet<&str> =
|
|
2007
|
+
fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
2008
|
+
if rec_arr_uses_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
2009
|
+
let alias = reg.intern(&fields);
|
|
2010
|
+
let arr_ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple(
|
|
2011
|
+
alias.as_str().into(),
|
|
2012
|
+
tishlang_ast::Span::default(),
|
|
2013
|
+
)));
|
|
2014
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
2015
|
+
ctx.define_struct(alias.as_str(), fields.clone()); // #373: enable cross-array field reads
|
|
2016
|
+
out.push(Statement::VarDecl {
|
|
2017
|
+
name: name.clone(),
|
|
2018
|
+
name_span: *name_span,
|
|
2019
|
+
mutable: *mutable,
|
|
2020
|
+
type_ann: Some(arr_ann),
|
|
2021
|
+
init: stmt_init_clone(stmt),
|
|
2022
|
+
span: *span,
|
|
2023
|
+
});
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
// (e) #179 Stage B — array of a CLOSED SET of heterogeneous record shapes ("megamorphic":
|
|
2028
|
+
// `objs.push({value,a})`, `objs.push({b,value,c})`, … all read as `objs[i].value`). Lower
|
|
2029
|
+
// to a native `Vec<enum>` (ShapeUnion): a `.field` present-and-same-type in EVERY variant
|
|
2030
|
+
// becomes a direct `match` load. Opt-in (TISH_SHAPE_UNION) until Stage C's integer modulo
|
|
2031
|
+
// makes it a net win. The safety walk uses ONLY the common keys, so a read of a
|
|
2032
|
+
// not-in-every-variant field disqualifies (stays boxed → undefined/null preserved).
|
|
2033
|
+
if shape_union_enabled() {
|
|
2034
|
+
if let Some(shapes) =
|
|
2035
|
+
record_array_shape_set(name.as_ref(), elements, &stmts[i + 1..], ctx)
|
|
2036
|
+
{
|
|
2037
|
+
let common = shape_set_common_keys(&shapes);
|
|
2038
|
+
let common_refs: std::collections::HashSet<&str> =
|
|
2039
|
+
common.iter().map(|k| k.as_ref()).collect();
|
|
2040
|
+
if !common_refs.is_empty()
|
|
2041
|
+
&& rec_arr_uses_safe(name.as_ref(), &common_refs, &stmts[i + 1..])
|
|
2042
|
+
{
|
|
2043
|
+
let union_alias = reg.intern_union(&shapes);
|
|
2044
|
+
let arr_ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple(
|
|
2045
|
+
union_alias.as_str().into(),
|
|
2046
|
+
tishlang_ast::Span::default(),
|
|
2047
|
+
)));
|
|
2048
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
2049
|
+
// Register the union's COMMON fields as a struct shape so `infer_expr_type`
|
|
2050
|
+
// types `objs[i].field` as its (number) type — otherwise a numeric
|
|
2051
|
+
// accumulator fed by the read (`sum = sum + objs[i].value`) stays boxed.
|
|
2052
|
+
// All common fields share one type across variants (guaranteed above), so the
|
|
2053
|
+
// first shape's types are correct.
|
|
2054
|
+
let common_fields: Vec<(std::sync::Arc<str>, TypeAnnotation)> = shapes[0]
|
|
2055
|
+
.iter()
|
|
2056
|
+
.filter(|(k, _)| common.contains(k))
|
|
2057
|
+
.cloned()
|
|
2058
|
+
.collect();
|
|
2059
|
+
ctx.define_struct(union_alias.as_str(), common_fields);
|
|
2060
|
+
out.push(Statement::VarDecl {
|
|
2061
|
+
name: name.clone(),
|
|
2062
|
+
name_span: *name_span,
|
|
2063
|
+
mutable: *mutable,
|
|
2064
|
+
type_ann: Some(arr_ann),
|
|
2065
|
+
init: stmt_init_clone(stmt),
|
|
2066
|
+
span: *span,
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
1042
2072
|
}
|
|
1043
2073
|
// Candidate: `let o = { ...object literal... }` with no annotation.
|
|
1044
2074
|
if let Statement::VarDecl {
|
|
@@ -1046,23 +2076,37 @@ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx)
|
|
|
1046
2076
|
name_span,
|
|
1047
2077
|
mutable,
|
|
1048
2078
|
type_ann: None,
|
|
1049
|
-
init: Some(Expr::Object { props,
|
|
2079
|
+
init: Some(Expr::Object { props, span: obj_span }),
|
|
1050
2080
|
span,
|
|
1051
2081
|
} = stmt
|
|
1052
2082
|
{
|
|
1053
|
-
|
|
2083
|
+
// A struct-typed leading spread (`{ ...base, k: v }`) is inlined to a plain
|
|
2084
|
+
// `{ a: base.a, …, k: v }` literal so it lowers to an unboxed struct; otherwise the plain
|
|
2085
|
+
// literal path applies (a spread it can't inline leaves `infer_object_shape` → None → boxed).
|
|
2086
|
+
let (shape, rewritten_props) = match inline_struct_spread(props, ctx, reg) {
|
|
2087
|
+
Some((f, rp)) => (Some(f), Some(rp)),
|
|
2088
|
+
None => (infer_object_shape(props, ctx), None),
|
|
2089
|
+
};
|
|
2090
|
+
if let Some(fields) = shape {
|
|
1054
2091
|
let keys: std::collections::HashSet<&str> =
|
|
1055
2092
|
fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
1056
2093
|
// Sound: every later use in this block must be a literal-key read.
|
|
1057
2094
|
if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
1058
2095
|
let alias = reg.intern(&fields);
|
|
1059
2096
|
ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default()));
|
|
2097
|
+
let new_init = match rewritten_props {
|
|
2098
|
+
Some(rp) => Some(Expr::Object {
|
|
2099
|
+
props: rp,
|
|
2100
|
+
span: *obj_span,
|
|
2101
|
+
}),
|
|
2102
|
+
None => stmt_init_clone(stmt),
|
|
2103
|
+
};
|
|
1060
2104
|
out.push(Statement::VarDecl {
|
|
1061
2105
|
name: name.clone(),
|
|
1062
2106
|
name_span: *name_span,
|
|
1063
2107
|
mutable: *mutable,
|
|
1064
2108
|
type_ann: Some(TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default())),
|
|
1065
|
-
init:
|
|
2109
|
+
init: new_init,
|
|
1066
2110
|
span: *span,
|
|
1067
2111
|
});
|
|
1068
2112
|
continue;
|
|
@@ -1126,6 +2170,26 @@ fn stmt_init_clone(stmt: &Statement) -> Option<Expr> {
|
|
|
1126
2170
|
}
|
|
1127
2171
|
}
|
|
1128
2172
|
|
|
2173
|
+
/// Define a `let`/`const` binding's name+type in `ctx` from its annotation or inferred init type, so
|
|
2174
|
+
/// inference in an inner scope can resolve references to it. Used to bind `for`-init loop variables
|
|
2175
|
+
/// before recursing into the loop body (enables struct inference of loop-local objects that read the
|
|
2176
|
+
/// counter). Only the simple `let x = <expr>` shape; anything else is skipped (sound — the body just
|
|
2177
|
+
/// keeps today's behavior).
|
|
2178
|
+
fn si_bind_decl(stmt: &Statement, ctx: &mut InferCtx) {
|
|
2179
|
+
if let Statement::VarDecl {
|
|
2180
|
+
name,
|
|
2181
|
+
type_ann,
|
|
2182
|
+
init: Some(e),
|
|
2183
|
+
..
|
|
2184
|
+
} = stmt
|
|
2185
|
+
{
|
|
2186
|
+
let t = type_ann.clone().or_else(|| infer_expr_type(e, ctx));
|
|
2187
|
+
if let Some(t) = t {
|
|
2188
|
+
ctx.define(name.as_ref(), t);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1129
2193
|
/// Recurse struct inference into a statement's nested blocks (function bodies,
|
|
1130
2194
|
/// loop/if bodies). Non-block statements pass through unchanged.
|
|
1131
2195
|
fn si_recurse(stmt: &Statement, reg: &mut StructRegistry, ctx: &mut InferCtx) -> Statement {
|
|
@@ -1140,13 +2204,26 @@ fn si_recurse(stmt: &Statement, reg: &mut StructRegistry, ctx: &mut InferCtx) ->
|
|
|
1140
2204
|
update,
|
|
1141
2205
|
body,
|
|
1142
2206
|
span,
|
|
1143
|
-
} =>
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
2207
|
+
} => {
|
|
2208
|
+
// Bind the loop variable(s) from `init` into a fresh scope so the body's struct inference
|
|
2209
|
+
// can type object-literal fields that reference the counter — e.g. `{ id: i, v: i * 2 }`
|
|
2210
|
+
// inside `for (let i = 0; …)`. Without this, `infer_object_shape` can't type those fields
|
|
2211
|
+
// and a loop-local object stays a boxed `PropMap`. (A loop-local object that reads only
|
|
2212
|
+
// outer/literal values already lowers to an unboxed struct; this closes the loop-var gap.)
|
|
2213
|
+
ctx.push_scope();
|
|
2214
|
+
if let Some(init_stmt) = init.as_deref() {
|
|
2215
|
+
si_bind_decl(init_stmt, ctx);
|
|
2216
|
+
}
|
|
2217
|
+
let new_body = Box::new(si_recurse(body, reg, ctx));
|
|
2218
|
+
ctx.pop_scope();
|
|
2219
|
+
Statement::For {
|
|
2220
|
+
init: init.clone(),
|
|
2221
|
+
cond: cond.clone(),
|
|
2222
|
+
update: update.clone(),
|
|
2223
|
+
body: new_body,
|
|
2224
|
+
span: *span,
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
1150
2227
|
Statement::ForOf {
|
|
1151
2228
|
name,
|
|
1152
2229
|
name_span,
|
|
@@ -1227,6 +2304,373 @@ fn uses_are_struct_safe(name: &str, keys: &std::collections::HashSet<&str>, tail
|
|
|
1227
2304
|
tail.iter().all(|s| stmt_name_safe(s, name, keys))
|
|
1228
2305
|
}
|
|
1229
2306
|
|
|
2307
|
+
// ── array-of-records inference (`let rows = []; rows.push({…}); rows[i].field`) ───────────────────
|
|
2308
|
+
// Lowers an untyped local array of uniform primitive records to a native `Vec<struct>`, so the
|
|
2309
|
+
// struct codegen emits direct field access + (post-#350) allocation-free construction. Conservative:
|
|
2310
|
+
// only fires when the binding is used solely as `push({uniform literal})`, `name[i].<key>` reads, and
|
|
2311
|
+
// `name.length` — any other use leaves it a boxed `Value[]`.
|
|
2312
|
+
|
|
2313
|
+
/// Compare two inferred record shapes for equality, order-independently (same keys + field types).
|
|
2314
|
+
fn fields_eq(
|
|
2315
|
+
a: &[(std::sync::Arc<str>, TypeAnnotation)],
|
|
2316
|
+
b: &[(std::sync::Arc<str>, TypeAnnotation)],
|
|
2317
|
+
) -> bool {
|
|
2318
|
+
if a.len() != b.len() {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
let mut av: Vec<(String, String)> =
|
|
2322
|
+
a.iter().map(|(k, t)| (k.to_string(), type_canon(t))).collect();
|
|
2323
|
+
let mut bv: Vec<(String, String)> =
|
|
2324
|
+
b.iter().map(|(k, t)| (k.to_string(), type_canon(t))).collect();
|
|
2325
|
+
av.sort();
|
|
2326
|
+
bv.sort();
|
|
2327
|
+
av == bv
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/// Fold one object literal's shape into the running record shape, flagging `*ok=false` on any
|
|
2331
|
+
/// non-uniform or non-primitive literal.
|
|
2332
|
+
fn record_shape_consider(
|
|
2333
|
+
shape: &mut Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>>,
|
|
2334
|
+
ok: &mut bool,
|
|
2335
|
+
props: &[tishlang_ast::ObjectProp],
|
|
2336
|
+
ctx: &InferCtx,
|
|
2337
|
+
) {
|
|
2338
|
+
match infer_object_shape(props, ctx) {
|
|
2339
|
+
Some(f) => match shape {
|
|
2340
|
+
None => *shape = Some(f),
|
|
2341
|
+
Some(prev) => {
|
|
2342
|
+
if !fields_eq(prev, &f) {
|
|
2343
|
+
*ok = false;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
None => *ok = false,
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/// Infer the common record shape of candidate array-of-records `name`: unify the shapes of its
|
|
2352
|
+
/// initial object-literal elements and every `name.push({…})` in `tail`. Returns the field list, or
|
|
2353
|
+
/// None if there is no object shape, the literals disagree, or a push arg is not a uniform primitive
|
|
2354
|
+
/// object literal. Loop vars in the literals (`{ x: i }`) are typed by seeding numeric locals first.
|
|
2355
|
+
fn record_array_fields(
|
|
2356
|
+
name: &str,
|
|
2357
|
+
init_elements: &[tishlang_ast::ArrayElement],
|
|
2358
|
+
tail: &[Statement],
|
|
2359
|
+
ctx: &InferCtx,
|
|
2360
|
+
) -> Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>> {
|
|
2361
|
+
// Seed numeric block-locals (incl. enclosing loop vars) so `{ x: i, y: i * 2 }` field values type.
|
|
2362
|
+
let mut sctx = ctx.clone();
|
|
2363
|
+
for _ in 0..4 {
|
|
2364
|
+
for s in tail {
|
|
2365
|
+
seed_numeric_locals(s, &mut sctx);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
let mut shape: Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>> = None;
|
|
2369
|
+
let mut ok = true;
|
|
2370
|
+
for el in init_elements {
|
|
2371
|
+
match el {
|
|
2372
|
+
tishlang_ast::ArrayElement::Expr(Expr::Object { props, .. }) => {
|
|
2373
|
+
record_shape_consider(&mut shape, &mut ok, props, &sctx);
|
|
2374
|
+
}
|
|
2375
|
+
_ => return None,
|
|
2376
|
+
}
|
|
2377
|
+
if !ok {
|
|
2378
|
+
return None;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
for s in tail {
|
|
2382
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2383
|
+
if let Expr::Call { callee, args, .. } = e {
|
|
2384
|
+
if let Expr::Member {
|
|
2385
|
+
object,
|
|
2386
|
+
prop: tishlang_ast::MemberProp::Name { name: m, .. },
|
|
2387
|
+
..
|
|
2388
|
+
} = callee.as_ref()
|
|
2389
|
+
{
|
|
2390
|
+
if m.as_ref() == "push"
|
|
2391
|
+
&& matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
|
|
2392
|
+
{
|
|
2393
|
+
if let [CallArg::Expr(Expr::Object { props, .. })] = args.as_slice() {
|
|
2394
|
+
record_shape_consider(&mut shape, &mut ok, props, &sctx);
|
|
2395
|
+
} else {
|
|
2396
|
+
ok = false;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
if !ok {
|
|
2403
|
+
return None;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
shape
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
/// True if every use of array-of-records `name` in `tail` is `name.push({uniform literal})`,
|
|
2410
|
+
/// `name[i].<key>` (field read, `key` in the shape), or `name.length`. Mirrors the #177 S-D safety
|
|
2411
|
+
/// walk; any other use of the binding (bare ref, element/field write, escape into a call / return /
|
|
2412
|
+
/// spread, for-of, method other than push) returns false → the array stays boxed.
|
|
2413
|
+
fn rec_arr_uses_safe(name: &str, keys: &std::collections::HashSet<&str>, tail: &[Statement]) -> bool {
|
|
2414
|
+
tail.iter().all(|s| ra_stmt(s, name, keys))
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/// #179 Stage B/C: shape-union (megamorphic) inference — a heterogeneous closed-shape-set array
|
|
2418
|
+
/// lowers to a native `Vec<enum>` with direct enum-match field reads. DEFAULT-ON as of Stage C
|
|
2419
|
+
/// (the native-length fix made megamorphic beat node: 485→~16ms vs node ~58ms). Part of the native
|
|
2420
|
+
/// stack, so `TISH_NATIVE_OPT=0` disables it (the boxed baseline); `TISH_SHAPE_UNION=0` is a
|
|
2421
|
+
/// dedicated escape hatch.
|
|
2422
|
+
fn shape_union_enabled() -> bool {
|
|
2423
|
+
crate::native_opts_enabled() && std::env::var("TISH_SHAPE_UNION").as_deref() != Ok("0")
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/// #179 Stage B: like [`record_array_fields`] but for a HETEROGENEOUS array — accumulate the set of
|
|
2427
|
+
/// DISTINCT primitive-record shapes across the initial elements + every `name.push({…})`. Returns
|
|
2428
|
+
/// the deduped shapes when there are 2..=8 of them (a genuine union; 1 shape is the existing
|
|
2429
|
+
/// single-struct path), or None if a push arg isn't a uniform primitive object literal, a non-object
|
|
2430
|
+
/// element appears, or the set exceeds 8 shapes.
|
|
2431
|
+
fn record_array_shape_set(
|
|
2432
|
+
name: &str,
|
|
2433
|
+
init_elements: &[tishlang_ast::ArrayElement],
|
|
2434
|
+
tail: &[Statement],
|
|
2435
|
+
ctx: &InferCtx,
|
|
2436
|
+
) -> Option<Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>>> {
|
|
2437
|
+
let mut sctx = ctx.clone();
|
|
2438
|
+
for _ in 0..4 {
|
|
2439
|
+
for s in tail {
|
|
2440
|
+
seed_numeric_locals(s, &mut sctx);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
let mut shapes: Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>> = Vec::new();
|
|
2444
|
+
let mut ok = true;
|
|
2445
|
+
for el in init_elements {
|
|
2446
|
+
match el {
|
|
2447
|
+
tishlang_ast::ArrayElement::Expr(Expr::Object { props, .. }) => {
|
|
2448
|
+
shape_set_consider(&mut shapes, &mut ok, props, &sctx);
|
|
2449
|
+
}
|
|
2450
|
+
_ => return None,
|
|
2451
|
+
}
|
|
2452
|
+
if !ok {
|
|
2453
|
+
return None;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
for s in tail {
|
|
2457
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2458
|
+
if let Expr::Call { callee, args, .. } = e {
|
|
2459
|
+
if let Expr::Member {
|
|
2460
|
+
object,
|
|
2461
|
+
prop: tishlang_ast::MemberProp::Name { name: m, .. },
|
|
2462
|
+
..
|
|
2463
|
+
} = callee.as_ref()
|
|
2464
|
+
{
|
|
2465
|
+
if m.as_ref() == "push"
|
|
2466
|
+
&& matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
|
|
2467
|
+
{
|
|
2468
|
+
if let [CallArg::Expr(Expr::Object { props, .. })] = args.as_slice() {
|
|
2469
|
+
shape_set_consider(&mut shapes, &mut ok, props, &sctx);
|
|
2470
|
+
} else {
|
|
2471
|
+
ok = false;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
if !ok {
|
|
2478
|
+
return None;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if !(2..=8).contains(&shapes.len()) {
|
|
2482
|
+
return None;
|
|
2483
|
+
}
|
|
2484
|
+
// Every variant must have a DISTINCT key set — codegen selects the variant for an object literal
|
|
2485
|
+
// by its key set, so a same-keys/different-types collision would be ambiguous → stay boxed.
|
|
2486
|
+
let mut keysets: Vec<Vec<&str>> = shapes
|
|
2487
|
+
.iter()
|
|
2488
|
+
.map(|s| {
|
|
2489
|
+
let mut k: Vec<&str> = s.iter().map(|(k, _)| k.as_ref()).collect();
|
|
2490
|
+
k.sort_unstable();
|
|
2491
|
+
k
|
|
2492
|
+
})
|
|
2493
|
+
.collect();
|
|
2494
|
+
let n = keysets.len();
|
|
2495
|
+
keysets.sort();
|
|
2496
|
+
keysets.dedup();
|
|
2497
|
+
if keysets.len() != n {
|
|
2498
|
+
return None;
|
|
2499
|
+
}
|
|
2500
|
+
Some(shapes)
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/// Accumulate a distinct primitive-record shape into the set (dedup by `fields_eq`). Sets `ok=false`
|
|
2504
|
+
/// if the literal is not a primitive record or the set would exceed 8 shapes.
|
|
2505
|
+
fn shape_set_consider(
|
|
2506
|
+
shapes: &mut Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>>,
|
|
2507
|
+
ok: &mut bool,
|
|
2508
|
+
props: &[tishlang_ast::ObjectProp],
|
|
2509
|
+
ctx: &InferCtx,
|
|
2510
|
+
) {
|
|
2511
|
+
match infer_object_shape(props, ctx) {
|
|
2512
|
+
Some(f) => {
|
|
2513
|
+
if !shapes.iter().any(|s| fields_eq(s, &f)) {
|
|
2514
|
+
if shapes.len() >= 8 {
|
|
2515
|
+
*ok = false;
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
shapes.push(f);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
None => *ok = false,
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
/// #179 Stage B: the fields present with an IDENTICAL type in EVERY variant of a shape set — the only
|
|
2526
|
+
/// fields a `union[i].field` read may soundly lower to a native `match` (a field missing from, or
|
|
2527
|
+
/// type-divergent across, some variant must stay boxed → undefined/null semantics preserved).
|
|
2528
|
+
fn shape_set_common_keys(
|
|
2529
|
+
shapes: &[Vec<(std::sync::Arc<str>, TypeAnnotation)>],
|
|
2530
|
+
) -> std::collections::HashSet<std::sync::Arc<str>> {
|
|
2531
|
+
let mut common = std::collections::HashSet::new();
|
|
2532
|
+
let Some(first) = shapes.first() else {
|
|
2533
|
+
return common;
|
|
2534
|
+
};
|
|
2535
|
+
'field: for (k, ty) in first {
|
|
2536
|
+
for other in &shapes[1..] {
|
|
2537
|
+
match other.iter().find(|(ok, _)| ok.as_ref() == k.as_ref()) {
|
|
2538
|
+
Some((_, oty)) if type_canon(oty) == type_canon(ty) => {}
|
|
2539
|
+
_ => continue 'field, // absent or type-divergent in some variant
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
common.insert(k.clone());
|
|
2543
|
+
}
|
|
2544
|
+
common
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
fn ra_stmt(s: &Statement, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
|
|
2548
|
+
use Statement::*;
|
|
2549
|
+
match s {
|
|
2550
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
2551
|
+
statements.iter().all(|x| ra_stmt(x, name, keys))
|
|
2552
|
+
}
|
|
2553
|
+
VarDecl { name: n, init, .. } => {
|
|
2554
|
+
n.as_ref() != name && init.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2555
|
+
}
|
|
2556
|
+
ExprStmt { expr, .. } => ra_expr(expr, name, keys),
|
|
2557
|
+
Return { value, .. } => value.as_ref().is_none_or(|e| ra_expr(e, name, keys)),
|
|
2558
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
2559
|
+
ra_expr(cond, name, keys)
|
|
2560
|
+
&& ra_stmt(then_branch, name, keys)
|
|
2561
|
+
&& else_branch.as_ref().is_none_or(|e| ra_stmt(e, name, keys))
|
|
2562
|
+
}
|
|
2563
|
+
While { cond, body, .. } => ra_expr(cond, name, keys) && ra_stmt(body, name, keys),
|
|
2564
|
+
DoWhile { cond, body, .. } => ra_expr(cond, name, keys) && ra_stmt(body, name, keys),
|
|
2565
|
+
For { init, cond, update, body, .. } => {
|
|
2566
|
+
init.as_ref().is_none_or(|x| ra_stmt(x, name, keys))
|
|
2567
|
+
&& cond.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2568
|
+
&& update.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2569
|
+
&& ra_stmt(body, name, keys)
|
|
2570
|
+
}
|
|
2571
|
+
Break { .. } | Continue { .. } => true,
|
|
2572
|
+
_ => false,
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
fn ra_expr(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
|
|
2577
|
+
use Expr::*;
|
|
2578
|
+
match e {
|
|
2579
|
+
Literal { .. } => true,
|
|
2580
|
+
Ident { name: n, .. } => n.as_ref() != name,
|
|
2581
|
+
Member { object, prop, optional: false, .. } => {
|
|
2582
|
+
let mname = match prop {
|
|
2583
|
+
tishlang_ast::MemberProp::Name { name: m, .. } => m.as_ref(),
|
|
2584
|
+
tishlang_ast::MemberProp::Expr(_) => return false,
|
|
2585
|
+
};
|
|
2586
|
+
match object.as_ref() {
|
|
2587
|
+
Ident { name: n, .. } if n.as_ref() == name => mname == "length",
|
|
2588
|
+
// `name[i].<key>` field read of THIS array. Guard on `io == name` so a DIFFERENT
|
|
2589
|
+
// record array's `other[i].field` falls through to the safe `_` recursion (bug: the
|
|
2590
|
+
// old unguarded arm returned false for it, failing any 2+-record-array program).
|
|
2591
|
+
Index { object: io, index, .. }
|
|
2592
|
+
if matches!(io.as_ref(), Ident { name: n, .. } if n.as_ref() == name) =>
|
|
2593
|
+
{
|
|
2594
|
+
keys.contains(mname) && ra_expr(index, name, keys)
|
|
2595
|
+
}
|
|
2596
|
+
_ => ra_expr(object, name, keys),
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
Member { .. } => false,
|
|
2600
|
+
Index { object, index, .. } => {
|
|
2601
|
+
if matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name) {
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2604
|
+
ra_expr(object, name, keys) && ra_expr(index, name, keys)
|
|
2605
|
+
}
|
|
2606
|
+
Binary { left, right, .. } => ra_expr(left, name, keys) && ra_expr(right, name, keys),
|
|
2607
|
+
Unary { operand, .. } => ra_expr(operand, name, keys),
|
|
2608
|
+
Conditional { cond, then_branch, else_branch, .. } => {
|
|
2609
|
+
ra_expr(cond, name, keys)
|
|
2610
|
+
&& ra_expr(then_branch, name, keys)
|
|
2611
|
+
&& ra_expr(else_branch, name, keys)
|
|
2612
|
+
}
|
|
2613
|
+
Assign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2614
|
+
CompoundAssign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2615
|
+
LogicalAssign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2616
|
+
MemberAssign { object, value, .. } => {
|
|
2617
|
+
ra_expr(object, name, keys) && ra_expr(value, name, keys)
|
|
2618
|
+
}
|
|
2619
|
+
IndexAssign { object, index, value, .. } => {
|
|
2620
|
+
!matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name)
|
|
2621
|
+
&& ra_expr(object, name, keys)
|
|
2622
|
+
&& ra_expr(index, name, keys)
|
|
2623
|
+
&& ra_expr(value, name, keys)
|
|
2624
|
+
}
|
|
2625
|
+
Call { callee, args, .. } => {
|
|
2626
|
+
if let Member { object, prop, optional: false, .. } = callee.as_ref() {
|
|
2627
|
+
if matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name) {
|
|
2628
|
+
if let tishlang_ast::MemberProp::Name { name: m, .. } = prop {
|
|
2629
|
+
if m.as_ref() == "push" {
|
|
2630
|
+
return matches!(
|
|
2631
|
+
args.as_slice(),
|
|
2632
|
+
[CallArg::Expr(Object { props, .. })]
|
|
2633
|
+
if props.iter().all(|pr| matches!(pr,
|
|
2634
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) if ra_expr(v, name, keys)))
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
// `.sort(cmp)` is an in-place PERMUTATION, not an escape (shape + element
|
|
2638
|
+
// values unchanged). Safe iff the comparator does not escape `name`.
|
|
2639
|
+
if m.as_ref() == "sort" {
|
|
2640
|
+
return args
|
|
2641
|
+
.iter()
|
|
2642
|
+
.all(|a| matches!(a, CallArg::Expr(x) if ra_expr(x, name, keys)));
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return false;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
ra_expr(callee, name, keys)
|
|
2649
|
+
&& args
|
|
2650
|
+
.iter()
|
|
2651
|
+
.all(|a| matches!(a, CallArg::Expr(x) if ra_expr(x, name, keys)))
|
|
2652
|
+
}
|
|
2653
|
+
TemplateLiteral { exprs, .. } => exprs.iter().all(|x| ra_expr(x, name, keys)),
|
|
2654
|
+
PostfixInc { name: n, .. }
|
|
2655
|
+
| PostfixDec { name: n, .. }
|
|
2656
|
+
| PrefixInc { name: n, .. }
|
|
2657
|
+
| PrefixDec { name: n, .. } => n.as_ref() != name,
|
|
2658
|
+
Array { elements, .. } => elements
|
|
2659
|
+
.iter()
|
|
2660
|
+
.all(|el| matches!(el, tishlang_ast::ArrayElement::Expr(x) if ra_expr(x, name, keys))),
|
|
2661
|
+
Object { props, .. } => props
|
|
2662
|
+
.iter()
|
|
2663
|
+
.all(|pr| matches!(pr, tishlang_ast::ObjectProp::KeyValue(_, v, _) if ra_expr(v, name, keys))),
|
|
2664
|
+
// HOF callbacks (e.g. the `.sort` comparator): recurse into the body precisely — a comparator
|
|
2665
|
+
// over `a.key`/`b.key` never touches `name` (pi_mentions is too conservative on closures).
|
|
2666
|
+
ArrowFunction { body, .. } => match body {
|
|
2667
|
+
tishlang_ast::ArrowBody::Expr(e) => ra_expr(e, name, keys),
|
|
2668
|
+
tishlang_ast::ArrowBody::Block(st) => ra_stmt(st, name, keys),
|
|
2669
|
+
},
|
|
2670
|
+
_ => false,
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
1230
2674
|
/// Define every provably-numeric block-local into `hyp` (flat across nested scopes): annotated
|
|
1231
2675
|
/// `: number`, a number-literal init, or any init `infer_expr_type` proves `number` *under the
|
|
1232
2676
|
/// current hypothesis* — so `let temp = perm[i]` becomes `number` once `perm` is hypothesized
|
|
@@ -1260,7 +2704,7 @@ fn seed_numeric_locals(s: &Statement, hyp: &mut InferCtx) {
|
|
|
1260
2704
|
}
|
|
1261
2705
|
seed_numeric_locals(body, hyp);
|
|
1262
2706
|
}
|
|
1263
|
-
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
2707
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
1264
2708
|
seed_numeric_locals(body, hyp)
|
|
1265
2709
|
}
|
|
1266
2710
|
_ => {}
|
|
@@ -1292,7 +2736,12 @@ fn block_native_arrays(
|
|
|
1292
2736
|
// Empty `[]` is a candidate for either element type; literal elements must match `elem`.
|
|
1293
2737
|
let lit_ok = elements.is_empty()
|
|
1294
2738
|
|| matches!(infer_array_elem(elements, outer_ctx), Some(t) if &t == elem);
|
|
1295
|
-
|
|
2739
|
+
// Soundness: a non-empty literal array accessed at a constant index past its initial
|
|
2740
|
+
// length is sparse (`let arr=[1,2,3]; arr[5]=100`) — the native Vec pads holes with NaN
|
|
2741
|
+
// and OOB reads yield NaN, diverging from boxed `null`. Keep it boxed.
|
|
2742
|
+
let sparse = !elements.is_empty()
|
|
2743
|
+
&& array_oob_const_access(&stmts[i + 1..], name.as_ref(), elements.len());
|
|
2744
|
+
if lit_ok && !sparse {
|
|
1296
2745
|
cands.push((i, name.to_string()));
|
|
1297
2746
|
}
|
|
1298
2747
|
}
|
|
@@ -1330,6 +2779,40 @@ fn block_native_arrays(
|
|
|
1330
2779
|
accepted
|
|
1331
2780
|
}
|
|
1332
2781
|
|
|
2782
|
+
/// True if `stmts` access `name` at a **constant** non-negative integer index `>= init_len`
|
|
2783
|
+
/// (`name[K]` read or `name[K] = v`). For a non-empty literal-initialized array, such an index is
|
|
2784
|
+
/// out of the initial bounds: the native `Vec<elem>` representation pads sparse writes with
|
|
2785
|
+
/// `NaN`/`0`/`false` and yields those on OOB reads, whereas the boxed array grows with `null` holes
|
|
2786
|
+
/// and reads `null`. So an array with such an access must stay boxed to preserve semantics. Only
|
|
2787
|
+
/// meaningful for non-empty literal inits — an empty `[]` grows dynamically (length unknown here),
|
|
2788
|
+
/// and its holes are typically only truth-tested (`NaN`/`null` are both falsy), so it is left alone.
|
|
2789
|
+
fn array_oob_const_access(stmts: &[Statement], name: &str, init_len: usize) -> bool {
|
|
2790
|
+
let mut found = false;
|
|
2791
|
+
for s in stmts {
|
|
2792
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2793
|
+
let idx = match e {
|
|
2794
|
+
Expr::Index { object, index, .. } | Expr::IndexAssign { object, index, .. } => {
|
|
2795
|
+
if matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name) {
|
|
2796
|
+
index
|
|
2797
|
+
} else {
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
_ => return,
|
|
2802
|
+
};
|
|
2803
|
+
if let Expr::Literal { value: Literal::Number(k), .. } = idx.as_ref() {
|
|
2804
|
+
if *k >= 0.0 && k.fract() == 0.0 && (*k as usize) >= init_len {
|
|
2805
|
+
found = true;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
if found {
|
|
2810
|
+
return true;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
found
|
|
2814
|
+
}
|
|
2815
|
+
|
|
1333
2816
|
/// A value written into / pushed onto `name` must have the array's element type `elem`: a `name[_]`
|
|
1334
2817
|
/// self-read (already `elem` by hypothesis) or any expression `infer_expr_type` proves is `elem`.
|
|
1335
2818
|
fn mut_arr_value_ok(v: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx) -> bool {
|
|
@@ -1379,7 +2862,8 @@ fn mut_arr_stmt_ok(s: &Statement, name: &str, elem: &TypeAnnotation, hyp: &Infer
|
|
|
1379
2862
|
Return { value, .. } => value.as_ref().is_none_or(|e| mut_arr_expr_ok(e, name, elem, hyp)),
|
|
1380
2863
|
Throw { value, .. } => mut_arr_expr_ok(value, name, elem, hyp),
|
|
1381
2864
|
Break { .. } | Continue { .. } | TypeAlias { .. } => true,
|
|
1382
|
-
|
|
2865
|
+
FunDecl { body, .. } => mut_arr_stmt_ok(body, name, elem, hyp),
|
|
2866
|
+
_ => false, // switch / try / etc: bail
|
|
1383
2867
|
}
|
|
1384
2868
|
}
|
|
1385
2869
|
|
|
@@ -1420,6 +2904,20 @@ fn mut_arr_expr_ok(e: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx)
|
|
|
1420
2904
|
});
|
|
1421
2905
|
}
|
|
1422
2906
|
}
|
|
2907
|
+
// #175: forwarding `name` into a native-vec fn's ARRAY param is a native use, not a
|
|
2908
|
+
// boxing escape — the callee takes it by `&/&mut Vec<elem>`, so the array stays unboxed.
|
|
2909
|
+
if let Ident { name: fname, .. } = callee.as_ref() {
|
|
2910
|
+
if let Some(is_arr) = hyp.native_vec_array_params.get(fname.as_ref()) {
|
|
2911
|
+
if args.len() == is_arr.len() {
|
|
2912
|
+
return args.iter().enumerate().all(|(i, a)| match a {
|
|
2913
|
+
tishlang_ast::CallArg::Expr(v) => {
|
|
2914
|
+
(is_name(v) && is_arr[i]) || mut_arr_expr_ok(v, name, elem, hyp)
|
|
2915
|
+
}
|
|
2916
|
+
tishlang_ast::CallArg::Spread(_) => false,
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
1423
2921
|
mut_arr_expr_ok(callee, name, elem, hyp)
|
|
1424
2922
|
&& args.iter().all(|a| match a {
|
|
1425
2923
|
tishlang_ast::CallArg::Expr(v) => mut_arr_expr_ok(v, name, elem, hyp),
|
|
@@ -1514,6 +3012,7 @@ fn arr_stmt_safe(s: &Statement, name: &str) -> bool {
|
|
|
1514
3012
|
Return { value, .. } => arr_opt_expr_safe(value, name),
|
|
1515
3013
|
Throw { value, .. } => arr_expr_safe(value, name),
|
|
1516
3014
|
Break { .. } | Continue { .. } | TypeAlias { .. } => true,
|
|
3015
|
+
FunDecl { body, .. } => arr_stmt_safe(body, name),
|
|
1517
3016
|
// Nested fn could capture+mutate; switch/try and anything else: be safe, bail.
|
|
1518
3017
|
_ => false,
|
|
1519
3018
|
}
|
|
@@ -1726,10 +3225,33 @@ fn expr_name_safe(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>)
|
|
|
1726
3225
|
expr_name_safe(e, name, keys)
|
|
1727
3226
|
}
|
|
1728
3227
|
}),
|
|
1729
|
-
Object { props, .. } =>
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
3228
|
+
Object { props, .. } => {
|
|
3229
|
+
// A `{ ...name, k: v, … }` immutable-update spreads `name` — a read-only bulk field read,
|
|
3230
|
+
// not an escape — and `inline_struct_spread` will lower it to a plain struct literal
|
|
3231
|
+
// (`{ a: name.a, …, k: v }`). Treat a leading self-spread as SAFE iff it matches that
|
|
3232
|
+
// inlining's eligibility EXACTLY: first prop, the only spread, and every other prop is a
|
|
3233
|
+
// `KeyValue` whose key doesn't collide with `name`'s keys (and whose value is name-safe).
|
|
3234
|
+
// Consistency: `name` then structifies iff the spread is inlined, so there's never a
|
|
3235
|
+
// struct spread into a boxed object (which would miscompile).
|
|
3236
|
+
let leading_self_spread = matches!(
|
|
3237
|
+
props.first(),
|
|
3238
|
+
Some(tishlang_ast::ObjectProp::Spread(Expr::Ident { name: n, .. })) if n.as_ref() == name
|
|
3239
|
+
);
|
|
3240
|
+
if leading_self_spread
|
|
3241
|
+
&& props[1..].iter().all(|p| match p {
|
|
3242
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
3243
|
+
!keys.contains(k.as_ref()) && expr_name_safe(v, name, keys)
|
|
3244
|
+
}
|
|
3245
|
+
tishlang_ast::ObjectProp::Spread(_) => false,
|
|
3246
|
+
})
|
|
3247
|
+
{
|
|
3248
|
+
return true;
|
|
3249
|
+
}
|
|
3250
|
+
props.iter().all(|p| match p {
|
|
3251
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => expr_name_safe(v, name, keys),
|
|
3252
|
+
tishlang_ast::ObjectProp::Spread(e) => expr_name_safe(e, name, keys),
|
|
3253
|
+
})
|
|
3254
|
+
}
|
|
1733
3255
|
TemplateLiteral { exprs, .. } => exprs.iter().all(|e| expr_name_safe(e, name, keys)),
|
|
1734
3256
|
// Reassignment / mutation referencing `name` by identifier → unsafe.
|
|
1735
3257
|
Assign { name: n, value, .. }
|
|
@@ -2137,7 +3659,7 @@ fn collect_return_shapes(
|
|
|
2137
3659
|
}
|
|
2138
3660
|
}
|
|
2139
3661
|
}
|
|
2140
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
3662
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
2141
3663
|
if !collect_return_shapes(body, pctx, found, ok) {
|
|
2142
3664
|
return false;
|
|
2143
3665
|
}
|
|
@@ -2153,6 +3675,11 @@ fn collect_return_shapes(
|
|
|
2153
3675
|
pub fn analyze_aggregate(program: &Program) -> AggregateAnalysis {
|
|
2154
3676
|
let mut analysis = AggregateAnalysis::default();
|
|
2155
3677
|
let mut reg = StructRegistry::default();
|
|
3678
|
+
// Soundness: a fn called ANYWHERE with a provably-non-numeric arg must not get S-0 `: number`
|
|
3679
|
+
// params either — `fn makeRecord(id, name){ return {id, name} }` called `makeRecord(1, "Alice")`
|
|
3680
|
+
// would otherwise emit a `match … panic!("expected number")` coercion on the String. Same set the
|
|
3681
|
+
// M4 `param_infer` path uses (infer.rs:273).
|
|
3682
|
+
let nonnumeric = crate::codegen::Codegen::fns_called_with_nonnumeric_arg(&program.statements);
|
|
2156
3683
|
|
|
2157
3684
|
// ---- S-0 + S-A: per top-level function, find numeric params and a return shape.
|
|
2158
3685
|
for s in &program.statements {
|
|
@@ -2204,7 +3731,7 @@ pub fn analyze_aggregate(program: &Program) -> AggregateAnalysis {
|
|
|
2204
3731
|
}
|
|
2205
3732
|
}
|
|
2206
3733
|
}
|
|
2207
|
-
if !numeric_params.is_empty() {
|
|
3734
|
+
if !numeric_params.is_empty() && !nonnumeric.contains(name.as_ref()) {
|
|
2208
3735
|
analysis.fn_numeric_params.insert(name.to_string(), numeric_params);
|
|
2209
3736
|
}
|
|
2210
3737
|
// S-A: a single all-f64 object-literal return shape ⇒ a struct alias.
|
|
@@ -2399,7 +3926,7 @@ fn collect_element_aliases(s: &Statement, p: &str, out: &mut HashSet<String>) {
|
|
|
2399
3926
|
}
|
|
2400
3927
|
collect_element_aliases(body, p, out);
|
|
2401
3928
|
}
|
|
2402
|
-
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
3929
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
2403
3930
|
collect_element_aliases(body, p, out)
|
|
2404
3931
|
}
|
|
2405
3932
|
_ => {}
|
|
@@ -2724,7 +4251,12 @@ fn walk_exprs_stmt(s: &Statement, f: &mut impl FnMut(&Expr)) {
|
|
|
2724
4251
|
}
|
|
2725
4252
|
walk_exprs_stmt(body, f);
|
|
2726
4253
|
}
|
|
2727
|
-
ForOf { iterable, body, .. }
|
|
4254
|
+
ForOf { iterable, body, .. }
|
|
4255
|
+
| ForIn {
|
|
4256
|
+
object: iterable,
|
|
4257
|
+
body,
|
|
4258
|
+
..
|
|
4259
|
+
} => {
|
|
2728
4260
|
walk_exprs_expr(iterable, f);
|
|
2729
4261
|
walk_exprs_stmt(body, f);
|
|
2730
4262
|
}
|
|
@@ -2837,7 +4369,7 @@ fn collect_body_struct_locals(
|
|
|
2837
4369
|
collect_body_struct_locals(e, analysis, out);
|
|
2838
4370
|
}
|
|
2839
4371
|
}
|
|
2840
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
4372
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
2841
4373
|
collect_body_struct_locals(body, analysis, out)
|
|
2842
4374
|
}
|
|
2843
4375
|
_ => {}
|
|
@@ -2871,7 +4403,7 @@ fn find_array_return(s: &Statement, locals: &HashMap<String, String>) -> Option<
|
|
|
2871
4403
|
}
|
|
2872
4404
|
If { then_branch, else_branch, .. } => find_array_return(then_branch, locals)
|
|
2873
4405
|
.or_else(|| else_branch.as_ref().and_then(|e| find_array_return(e, locals))),
|
|
2874
|
-
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } => {
|
|
4406
|
+
For { body, .. } | While { body, .. } | DoWhile { body, .. } | ForOf { body, .. } | ForIn { body, .. } => {
|
|
2875
4407
|
find_array_return(body, locals)
|
|
2876
4408
|
}
|
|
2877
4409
|
_ => None,
|