@tishlang/tish 2.12.0 → 2.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -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 +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- 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 +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- 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/src/lib.rs +75 -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_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -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_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -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, _)
|
|
@@ -933,196 +1125,1058 @@ fn infer_object_shape(
|
|
|
933
1125
|
Some(fields)
|
|
934
1126
|
}
|
|
935
1127
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
+
}
|
|
950
1180
|
}
|
|
951
|
-
|
|
952
|
-
Program { statements: out }
|
|
1181
|
+
accepted
|
|
953
1182
|
}
|
|
954
1183
|
|
|
955
|
-
fn
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
| Statement::ForOf { span, .. }
|
|
963
|
-
| Statement::While { span, .. }
|
|
964
|
-
| Statement::Return { span, .. }
|
|
965
|
-
| Statement::FunDecl { span, .. }
|
|
966
|
-
| Statement::TypeAlias { span, .. } => *span,
|
|
967
|
-
_ => zero_span(),
|
|
968
|
-
}
|
|
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)
|
|
969
1191
|
}
|
|
970
1192
|
|
|
971
|
-
fn
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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;
|
|
975
1201
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
ctx.push_scope();
|
|
982
|
-
// Co-infer mutable `number[]` locals across the whole block (cross-referencing arrays) up front.
|
|
983
|
-
// Co-infer mutable native arrays (number[] and boolean[]) across the block; an array of the
|
|
984
|
-
// wrong element type simply fails its run and stays boxed.
|
|
985
|
-
let native_num_arrays = block_native_arrays(&stmts, ctx, &number_ann());
|
|
986
|
-
let native_bool_arrays = block_native_arrays(&stmts, ctx, &bool_ann());
|
|
987
|
-
let n = stmts.len();
|
|
988
|
-
let mut out: Vec<Statement> = Vec::with_capacity(n);
|
|
989
|
-
for (i, stmt) in stmts.iter().enumerate() {
|
|
990
|
-
// Candidate: `let xs = [ ...uniform native scalars... ]` with no annotation -> native `T[]`,
|
|
991
|
-
// but only when every later use is read-only (`uses_are_array_safe`), so a `Vec<f64>`
|
|
992
|
-
// assumption can't be violated by a later `push`/`xs[i] = …`.
|
|
993
|
-
if let Statement::VarDecl {
|
|
994
|
-
name,
|
|
995
|
-
name_span,
|
|
996
|
-
mutable,
|
|
997
|
-
type_ann: None,
|
|
998
|
-
init: Some(Expr::Array { elements, .. }),
|
|
999
|
-
span,
|
|
1000
|
-
} = stmt
|
|
1001
|
-
{
|
|
1002
|
-
// (a) Read-only typed array: uniform scalar literals, every later use read-only.
|
|
1003
|
-
if let Some(elem) = infer_array_elem(elements, ctx) {
|
|
1004
|
-
if uses_are_array_safe(name.as_ref(), &stmts[i + 1..]) {
|
|
1005
|
-
let arr_ann = TypeAnnotation::Array(Box::new(elem));
|
|
1006
|
-
ctx.define(name.as_ref(), arr_ann.clone());
|
|
1007
|
-
out.push(Statement::VarDecl {
|
|
1008
|
-
name: name.clone(),
|
|
1009
|
-
name_span: *name_span,
|
|
1010
|
-
mutable: *mutable,
|
|
1011
|
-
type_ann: Some(arr_ann),
|
|
1012
|
-
init: stmt_init_clone(stmt),
|
|
1013
|
-
span: *span,
|
|
1014
|
-
});
|
|
1015
|
-
continue;
|
|
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;
|
|
1016
1207
|
}
|
|
1017
1208
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
} else if native_bool_arrays.contains(name.as_ref()) {
|
|
1025
|
-
Some(bool_ann())
|
|
1026
|
-
} else {
|
|
1027
|
-
None
|
|
1028
|
-
};
|
|
1029
|
-
if let Some(elem) = native_elem {
|
|
1030
|
-
let arr_ann = TypeAnnotation::Array(Box::new(elem));
|
|
1031
|
-
ctx.define(name.as_ref(), arr_ann.clone());
|
|
1032
|
-
out.push(Statement::VarDecl {
|
|
1033
|
-
name: name.clone(),
|
|
1034
|
-
name_span: *name_span,
|
|
1035
|
-
mutable: *mutable,
|
|
1036
|
-
type_ann: Some(arr_ann),
|
|
1037
|
-
init: stmt_init_clone(stmt),
|
|
1038
|
-
span: *span,
|
|
1039
|
-
});
|
|
1040
|
-
continue;
|
|
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);
|
|
1041
1215
|
}
|
|
1042
1216
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
mutable,
|
|
1048
|
-
type_ann: None,
|
|
1049
|
-
init: Some(Expr::Object { props, .. }),
|
|
1050
|
-
span,
|
|
1051
|
-
} = stmt
|
|
1052
|
-
{
|
|
1053
|
-
if let Some(fields) = infer_object_shape(props, ctx) {
|
|
1054
|
-
let keys: std::collections::HashSet<&str> =
|
|
1055
|
-
fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
1056
|
-
// Sound: every later use in this block must be a literal-key read.
|
|
1057
|
-
if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
1058
|
-
let alias = reg.intern(&fields);
|
|
1059
|
-
ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default()));
|
|
1060
|
-
out.push(Statement::VarDecl {
|
|
1061
|
-
name: name.clone(),
|
|
1062
|
-
name_span: *name_span,
|
|
1063
|
-
mutable: *mutable,
|
|
1064
|
-
type_ann: Some(TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default())),
|
|
1065
|
-
init: stmt_init_clone(stmt),
|
|
1066
|
-
span: *span,
|
|
1067
|
-
});
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
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);
|
|
1070
1221
|
}
|
|
1071
1222
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
name,
|
|
1079
|
-
name_span,
|
|
1080
|
-
mutable,
|
|
1081
|
-
type_ann,
|
|
1082
|
-
init,
|
|
1083
|
-
span,
|
|
1084
|
-
} = stmt
|
|
1085
|
-
{
|
|
1086
|
-
let inferred = type_ann
|
|
1087
|
-
.clone()
|
|
1088
|
-
.or_else(|| init.as_ref().and_then(|e| infer_expr_type(e, ctx)));
|
|
1089
|
-
if let Some(t) = &inferred {
|
|
1090
|
-
ctx.define(name.as_ref(), t.clone());
|
|
1223
|
+
While { body, .. } | DoWhile { body, .. } | ForOf { 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);
|
|
1091
1229
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
name: name.clone(),
|
|
1104
|
-
name_span: *name_span,
|
|
1105
|
-
mutable: *mutable,
|
|
1106
|
-
type_ann: Some(t.clone()),
|
|
1107
|
-
init: stmt_init_clone(stmt),
|
|
1108
|
-
span: *span,
|
|
1109
|
-
});
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
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);
|
|
1112
1241
|
}
|
|
1113
1242
|
}
|
|
1114
1243
|
}
|
|
1115
|
-
|
|
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
|
+
_ => {}
|
|
1116
1254
|
}
|
|
1117
|
-
ctx.pop_scope();
|
|
1118
|
-
out
|
|
1119
1255
|
}
|
|
1120
1256
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
+
count_uses_expr(iterable, f, calls, others);
|
|
1324
|
+
count_uses_stmt(body, f, calls, others);
|
|
1325
|
+
}
|
|
1326
|
+
Return { value: Some(e), .. } => count_uses_expr(e, f, calls, others),
|
|
1327
|
+
Throw { value, .. } => count_uses_expr(value, f, calls, others),
|
|
1328
|
+
FunDecl { body, .. } => count_uses_stmt(body, f, calls, others),
|
|
1329
|
+
Switch { expr, cases, default_body, .. } => {
|
|
1330
|
+
count_uses_expr(expr, f, calls, others);
|
|
1331
|
+
for (test, body) in cases {
|
|
1332
|
+
if let Some(t) = test {
|
|
1333
|
+
count_uses_expr(t, f, calls, others);
|
|
1334
|
+
}
|
|
1335
|
+
body.iter().for_each(|x| count_uses_stmt(x, f, calls, others));
|
|
1336
|
+
}
|
|
1337
|
+
if let Some(body) = default_body {
|
|
1338
|
+
body.iter().for_each(|x| count_uses_stmt(x, f, calls, others));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1342
|
+
count_uses_stmt(body, f, calls, others);
|
|
1343
|
+
if let Some(cb) = catch_body {
|
|
1344
|
+
count_uses_stmt(cb, f, calls, others);
|
|
1345
|
+
}
|
|
1346
|
+
if let Some(fb) = finally_body {
|
|
1347
|
+
count_uses_stmt(fb, f, calls, others);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
Export { declaration, .. } => match declaration.as_ref() {
|
|
1351
|
+
tishlang_ast::ExportDeclaration::Named(inner) => {
|
|
1352
|
+
count_uses_stmt(inner, f, calls, others)
|
|
1353
|
+
}
|
|
1354
|
+
tishlang_ast::ExportDeclaration::Default(e) => count_uses_expr(e, f, calls, others),
|
|
1355
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => {}
|
|
1356
|
+
},
|
|
1357
|
+
Return { value: None, .. }
|
|
1358
|
+
| Break { .. }
|
|
1359
|
+
| Continue { .. }
|
|
1360
|
+
| Import { .. }
|
|
1361
|
+
| TypeAlias { .. }
|
|
1362
|
+
| DeclareVar { .. }
|
|
1363
|
+
| DeclareFun { .. } => {}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
fn count_uses_expr(e: &Expr, f: &str, calls: &mut usize, others: &mut usize) {
|
|
1368
|
+
use Expr::*;
|
|
1369
|
+
match e {
|
|
1370
|
+
Ident { name, .. } => {
|
|
1371
|
+
if name.as_ref() == f {
|
|
1372
|
+
*others += 1;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
Call { callee, args, .. } => {
|
|
1376
|
+
// `f(...)` in callee position is a direct call — do NOT count the callee ident as an
|
|
1377
|
+
// "other use". Any other callee shape (member call, computed) recurses normally.
|
|
1378
|
+
match callee.as_ref() {
|
|
1379
|
+
Ident { name, .. } if name.as_ref() == f => *calls += 1,
|
|
1380
|
+
_ => count_uses_expr(callee, f, calls, others),
|
|
1381
|
+
}
|
|
1382
|
+
for a in args {
|
|
1383
|
+
match a {
|
|
1384
|
+
CallArg::Expr(x) | CallArg::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
New { callee, args, .. } => {
|
|
1389
|
+
count_uses_expr(callee, f, calls, others);
|
|
1390
|
+
for a in args {
|
|
1391
|
+
match a {
|
|
1392
|
+
CallArg::Expr(x) | CallArg::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
Assign { name, value, .. }
|
|
1397
|
+
| CompoundAssign { name, value, .. }
|
|
1398
|
+
| LogicalAssign { name, value, .. } => {
|
|
1399
|
+
if name.as_ref() == f {
|
|
1400
|
+
*others += 1; // reassigning the function name — an escape
|
|
1401
|
+
}
|
|
1402
|
+
count_uses_expr(value, f, calls, others);
|
|
1403
|
+
}
|
|
1404
|
+
PostfixInc { name, .. } | PostfixDec { name, .. } | PrefixInc { name, .. }
|
|
1405
|
+
| PrefixDec { name, .. } => {
|
|
1406
|
+
if name.as_ref() == f {
|
|
1407
|
+
*others += 1;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
Binary { left, right, .. } | NullishCoalesce { left, right, .. } => {
|
|
1411
|
+
count_uses_expr(left, f, calls, others);
|
|
1412
|
+
count_uses_expr(right, f, calls, others);
|
|
1413
|
+
}
|
|
1414
|
+
Unary { operand, .. } | TypeOf { operand, .. } | Await { operand, .. }
|
|
1415
|
+
| Delete { target: operand, .. } => count_uses_expr(operand, f, calls, others),
|
|
1416
|
+
Member { object, prop, .. } => {
|
|
1417
|
+
count_uses_expr(object, f, calls, others);
|
|
1418
|
+
if let tishlang_ast::MemberProp::Expr(p) = prop {
|
|
1419
|
+
count_uses_expr(p, f, calls, others);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
Index { object, index, .. } => {
|
|
1423
|
+
count_uses_expr(object, f, calls, others);
|
|
1424
|
+
count_uses_expr(index, f, calls, others);
|
|
1425
|
+
}
|
|
1426
|
+
Conditional { cond, then_branch, else_branch, .. } => {
|
|
1427
|
+
count_uses_expr(cond, f, calls, others);
|
|
1428
|
+
count_uses_expr(then_branch, f, calls, others);
|
|
1429
|
+
count_uses_expr(else_branch, f, calls, others);
|
|
1430
|
+
}
|
|
1431
|
+
Array { elements, .. } => {
|
|
1432
|
+
for el in elements {
|
|
1433
|
+
match el {
|
|
1434
|
+
tishlang_ast::ArrayElement::Expr(x)
|
|
1435
|
+
| tishlang_ast::ArrayElement::Spread(x) => {
|
|
1436
|
+
count_uses_expr(x, f, calls, others)
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
Object { props, .. } => {
|
|
1442
|
+
for p in props {
|
|
1443
|
+
match p {
|
|
1444
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => {
|
|
1445
|
+
count_uses_expr(v, f, calls, others)
|
|
1446
|
+
}
|
|
1447
|
+
tishlang_ast::ObjectProp::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
MemberAssign { object, value, .. } => {
|
|
1452
|
+
count_uses_expr(object, f, calls, others);
|
|
1453
|
+
count_uses_expr(value, f, calls, others);
|
|
1454
|
+
}
|
|
1455
|
+
IndexAssign { object, index, value, .. } => {
|
|
1456
|
+
count_uses_expr(object, f, calls, others);
|
|
1457
|
+
count_uses_expr(index, f, calls, others);
|
|
1458
|
+
count_uses_expr(value, f, calls, others);
|
|
1459
|
+
}
|
|
1460
|
+
ArrowFunction { body, .. } => match body {
|
|
1461
|
+
tishlang_ast::ArrowBody::Expr(x) => count_uses_expr(x, f, calls, others),
|
|
1462
|
+
tishlang_ast::ArrowBody::Block(s) => count_uses_stmt(s, f, calls, others),
|
|
1463
|
+
},
|
|
1464
|
+
TemplateLiteral { exprs, .. } => {
|
|
1465
|
+
exprs.iter().for_each(|x| count_uses_expr(x, f, calls, others))
|
|
1466
|
+
}
|
|
1467
|
+
JsxElement { props, children, .. } => {
|
|
1468
|
+
for prop in props {
|
|
1469
|
+
match prop {
|
|
1470
|
+
tishlang_ast::JsxProp::Attr { value, .. } => {
|
|
1471
|
+
if let tishlang_ast::JsxAttrValue::Expr(x) = value {
|
|
1472
|
+
count_uses_expr(x, f, calls, others);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
tishlang_ast::JsxProp::Spread(x) => count_uses_expr(x, f, calls, others),
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
for c in children {
|
|
1479
|
+
if let tishlang_ast::JsxChild::Expr(x) = c {
|
|
1480
|
+
count_uses_expr(x, f, calls, others);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
JsxFragment { children, .. } => {
|
|
1485
|
+
for c in children {
|
|
1486
|
+
if let tishlang_ast::JsxChild::Expr(x) = c {
|
|
1487
|
+
count_uses_expr(x, f, calls, others);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
Literal { .. } | NativeModuleLoad { .. } => {}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/// Does statement `s` (or anything nested) contain a closure (`FunDecl`/`ArrowFunction`) or a
|
|
1496
|
+
/// `this`/`arguments`/`super` reference? Such a body is NOT safely alpha-renameable by the flat
|
|
1497
|
+
/// splice below (a closure could capture a renamed local), so we refuse to inline it.
|
|
1498
|
+
fn body_has_closure_or_dynamic(s: &Statement) -> bool {
|
|
1499
|
+
let mut bad = false;
|
|
1500
|
+
walk_exprs_stmt_full(s, &mut |e| {
|
|
1501
|
+
match e {
|
|
1502
|
+
Expr::ArrowFunction { .. } => bad = true,
|
|
1503
|
+
Expr::Ident { name, .. }
|
|
1504
|
+
if matches!(name.as_ref(), "this" | "arguments" | "super") =>
|
|
1505
|
+
{
|
|
1506
|
+
bad = true
|
|
1507
|
+
}
|
|
1508
|
+
_ => {}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
bad || stmt_contains_fundecl(s)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
fn stmt_contains_fundecl(s: &Statement) -> bool {
|
|
1515
|
+
use Statement::*;
|
|
1516
|
+
match s {
|
|
1517
|
+
FunDecl { .. } => true,
|
|
1518
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
1519
|
+
statements.iter().any(stmt_contains_fundecl)
|
|
1520
|
+
}
|
|
1521
|
+
If { then_branch, else_branch, .. } => {
|
|
1522
|
+
stmt_contains_fundecl(then_branch)
|
|
1523
|
+
|| else_branch.as_ref().is_some_and(|e| stmt_contains_fundecl(e))
|
|
1524
|
+
}
|
|
1525
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } => {
|
|
1526
|
+
stmt_contains_fundecl(body)
|
|
1527
|
+
}
|
|
1528
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1529
|
+
stmt_contains_fundecl(body)
|
|
1530
|
+
|| catch_body.as_ref().is_some_and(|b| stmt_contains_fundecl(b))
|
|
1531
|
+
|| finally_body.as_ref().is_some_and(|b| stmt_contains_fundecl(b))
|
|
1532
|
+
}
|
|
1533
|
+
Switch { cases, default_body, .. } => {
|
|
1534
|
+
cases.iter().any(|(_, b)| b.iter().any(stmt_contains_fundecl))
|
|
1535
|
+
|| default_body
|
|
1536
|
+
.as_ref()
|
|
1537
|
+
.is_some_and(|b| b.iter().any(stmt_contains_fundecl))
|
|
1538
|
+
}
|
|
1539
|
+
_ => false,
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/// Exhaustive expr-visitor over a statement (unlike the shared `walk_exprs_stmt`, this also
|
|
1544
|
+
/// descends fn bodies, switch, and try). Visits every `Expr` node via `f`.
|
|
1545
|
+
fn walk_exprs_stmt_full(s: &Statement, f: &mut impl FnMut(&Expr)) {
|
|
1546
|
+
use Statement::*;
|
|
1547
|
+
match s {
|
|
1548
|
+
VarDecl { init: Some(e), .. } => walk_exprs_expr(e, f),
|
|
1549
|
+
VarDeclDestructure { init, .. } => walk_exprs_expr(init, f),
|
|
1550
|
+
Multi { statements, .. } | Block { statements, .. } => {
|
|
1551
|
+
statements.iter().for_each(|x| walk_exprs_stmt_full(x, f))
|
|
1552
|
+
}
|
|
1553
|
+
ExprStmt { expr, .. } => walk_exprs_expr(expr, f),
|
|
1554
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
1555
|
+
walk_exprs_expr(cond, f);
|
|
1556
|
+
walk_exprs_stmt_full(then_branch, f);
|
|
1557
|
+
if let Some(e) = else_branch {
|
|
1558
|
+
walk_exprs_stmt_full(e, f);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
While { cond, body, .. } | DoWhile { cond, body, .. } => {
|
|
1562
|
+
walk_exprs_expr(cond, f);
|
|
1563
|
+
walk_exprs_stmt_full(body, f);
|
|
1564
|
+
}
|
|
1565
|
+
For { init, cond, update, body, .. } => {
|
|
1566
|
+
if let Some(i) = init {
|
|
1567
|
+
walk_exprs_stmt_full(i, f);
|
|
1568
|
+
}
|
|
1569
|
+
if let Some(c) = cond {
|
|
1570
|
+
walk_exprs_expr(c, f);
|
|
1571
|
+
}
|
|
1572
|
+
if let Some(u) = update {
|
|
1573
|
+
walk_exprs_expr(u, f);
|
|
1574
|
+
}
|
|
1575
|
+
walk_exprs_stmt_full(body, f);
|
|
1576
|
+
}
|
|
1577
|
+
ForOf { iterable, body, .. } => {
|
|
1578
|
+
walk_exprs_expr(iterable, f);
|
|
1579
|
+
walk_exprs_stmt_full(body, f);
|
|
1580
|
+
}
|
|
1581
|
+
Return { value: Some(e), .. } => walk_exprs_expr(e, f),
|
|
1582
|
+
Throw { value, .. } => walk_exprs_expr(value, f),
|
|
1583
|
+
FunDecl { body, .. } => walk_exprs_stmt_full(body, f),
|
|
1584
|
+
Switch { expr, cases, default_body, .. } => {
|
|
1585
|
+
walk_exprs_expr(expr, f);
|
|
1586
|
+
for (test, body) in cases {
|
|
1587
|
+
if let Some(t) = test {
|
|
1588
|
+
walk_exprs_expr(t, f);
|
|
1589
|
+
}
|
|
1590
|
+
body.iter().for_each(|x| walk_exprs_stmt_full(x, f));
|
|
1591
|
+
}
|
|
1592
|
+
if let Some(body) = default_body {
|
|
1593
|
+
body.iter().for_each(|x| walk_exprs_stmt_full(x, f));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1597
|
+
walk_exprs_stmt_full(body, f);
|
|
1598
|
+
if let Some(cb) = catch_body {
|
|
1599
|
+
walk_exprs_stmt_full(cb, f);
|
|
1600
|
+
}
|
|
1601
|
+
if let Some(fb) = finally_body {
|
|
1602
|
+
walk_exprs_stmt_full(fb, f);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
Export { declaration, .. } => match declaration.as_ref() {
|
|
1606
|
+
tishlang_ast::ExportDeclaration::Named(inner) => walk_exprs_stmt_full(inner, f),
|
|
1607
|
+
tishlang_ast::ExportDeclaration::Default(e) => walk_exprs_expr(e, f),
|
|
1608
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => {}
|
|
1609
|
+
},
|
|
1610
|
+
_ => {}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/// Collect the names declared by DIRECT (top-level-of-body) `let`/`const` VarDecls in `body_stmts`.
|
|
1615
|
+
/// These are the locals that would leak into the caller's scope when the body is spliced there, so
|
|
1616
|
+
/// they must be alpha-renamed. Nested-scope declarations (inside loops/blocks) stay properly scoped
|
|
1617
|
+
/// after splicing and are left alone. Destructuring decls make the factory ineligible (returns None).
|
|
1618
|
+
fn direct_body_locals(body_stmts: &[Statement]) -> Option<Vec<Arc<str>>> {
|
|
1619
|
+
let mut out = Vec::new();
|
|
1620
|
+
for s in body_stmts {
|
|
1621
|
+
match s {
|
|
1622
|
+
Statement::VarDecl { name, .. } => out.push(name.clone()),
|
|
1623
|
+
Statement::VarDeclDestructure { .. } => return None,
|
|
1624
|
+
_ => {}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
Some(out)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/// Count `return` statements anywhere in `s`.
|
|
1631
|
+
fn count_returns(s: &Statement) -> usize {
|
|
1632
|
+
use Statement::*;
|
|
1633
|
+
match s {
|
|
1634
|
+
Return { .. } => 1,
|
|
1635
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
1636
|
+
statements.iter().map(count_returns).sum()
|
|
1637
|
+
}
|
|
1638
|
+
If { then_branch, else_branch, .. } => {
|
|
1639
|
+
count_returns(then_branch)
|
|
1640
|
+
+ else_branch.as_ref().map_or(0, |e| count_returns(e))
|
|
1641
|
+
}
|
|
1642
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. } => {
|
|
1643
|
+
count_returns(body)
|
|
1644
|
+
}
|
|
1645
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
1646
|
+
count_returns(body)
|
|
1647
|
+
+ catch_body.as_ref().map_or(0, |b| count_returns(b))
|
|
1648
|
+
+ finally_body.as_ref().map_or(0, |b| count_returns(b))
|
|
1649
|
+
}
|
|
1650
|
+
Switch { cases, default_body, .. } => {
|
|
1651
|
+
cases.iter().map(|(_, b)| b.iter().map(count_returns).sum::<usize>()).sum::<usize>()
|
|
1652
|
+
+ default_body
|
|
1653
|
+
.as_ref()
|
|
1654
|
+
.map_or(0, |b| b.iter().map(count_returns).sum())
|
|
1655
|
+
}
|
|
1656
|
+
_ => 0,
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/// #179 Stage A driver: for each eligible nullary factory, splice its body at its single top-level
|
|
1661
|
+
/// call site (alpha-renamed) and drop the FunDecl. Fail-closed: any shape not proven safe is left
|
|
1662
|
+
/// untouched.
|
|
1663
|
+
fn inline_single_call_factories(stmts: Vec<Statement>) -> Vec<Statement> {
|
|
1664
|
+
// Snapshot eligible (name → FunDecl body, is_mutable-array) candidates. We do at most one
|
|
1665
|
+
// inlining pass over top-level statements; a factory that itself calls another eligible factory
|
|
1666
|
+
// is handled on the next compile-invariant since we only splice direct top-level call sites.
|
|
1667
|
+
let mut result = stmts;
|
|
1668
|
+
// Collect candidate factory names first (immutable borrow), then transform.
|
|
1669
|
+
let candidates: Vec<Arc<str>> = result
|
|
1670
|
+
.iter()
|
|
1671
|
+
.filter_map(|s| match s {
|
|
1672
|
+
Statement::FunDecl {
|
|
1673
|
+
async_: false,
|
|
1674
|
+
name,
|
|
1675
|
+
params,
|
|
1676
|
+
rest_param: None,
|
|
1677
|
+
body,
|
|
1678
|
+
..
|
|
1679
|
+
} if params.is_empty() => {
|
|
1680
|
+
// body must be a Block/Multi ending in a single tail `return E`.
|
|
1681
|
+
let body_stmts = match body.as_ref() {
|
|
1682
|
+
Statement::Block { statements, .. }
|
|
1683
|
+
| Statement::Multi { statements, .. } => statements.as_slice(),
|
|
1684
|
+
_ => return None,
|
|
1685
|
+
};
|
|
1686
|
+
if count_returns(body) != 1 {
|
|
1687
|
+
return None;
|
|
1688
|
+
}
|
|
1689
|
+
match body_stmts.last() {
|
|
1690
|
+
Some(Statement::Return { value: Some(_), .. }) => {}
|
|
1691
|
+
_ => return None,
|
|
1692
|
+
}
|
|
1693
|
+
if body_has_closure_or_dynamic(body) {
|
|
1694
|
+
return None;
|
|
1695
|
+
}
|
|
1696
|
+
if direct_body_locals(body_stmts).is_none() {
|
|
1697
|
+
return None;
|
|
1698
|
+
}
|
|
1699
|
+
Some(name.clone())
|
|
1700
|
+
}
|
|
1701
|
+
_ => None,
|
|
1702
|
+
})
|
|
1703
|
+
.collect();
|
|
1704
|
+
|
|
1705
|
+
for fname in candidates {
|
|
1706
|
+
// Re-check use counts against the CURRENT program (a prior inline may have changed them).
|
|
1707
|
+
let (calls, others) = fn_name_uses(&result, fname.as_ref());
|
|
1708
|
+
if others != 0 || calls != 1 {
|
|
1709
|
+
continue; // escapes, unused, or called more than once → not inlinable
|
|
1710
|
+
}
|
|
1711
|
+
// Find the single top-level call site index + form.
|
|
1712
|
+
let mut site: Option<(usize, Option<Arc<str>>)> = None; // (idx, Some(target)=VarDecl / None=ExprStmt)
|
|
1713
|
+
let mut ambiguous = false;
|
|
1714
|
+
for (i, s) in result.iter().enumerate() {
|
|
1715
|
+
let hit = match s {
|
|
1716
|
+
Statement::VarDecl {
|
|
1717
|
+
name,
|
|
1718
|
+
type_ann: None,
|
|
1719
|
+
init: Some(Expr::Call { callee, args, .. }),
|
|
1720
|
+
..
|
|
1721
|
+
} if args.is_empty()
|
|
1722
|
+
&& matches!(callee.as_ref(), Expr::Ident { name: c, .. } if c.as_ref() == fname.as_ref()) =>
|
|
1723
|
+
{
|
|
1724
|
+
Some(Some(name.clone()))
|
|
1725
|
+
}
|
|
1726
|
+
Statement::ExprStmt { expr: Expr::Call { callee, args, .. }, .. }
|
|
1727
|
+
if args.is_empty()
|
|
1728
|
+
&& matches!(callee.as_ref(), Expr::Ident { name: c, .. } if c.as_ref() == fname.as_ref()) =>
|
|
1729
|
+
{
|
|
1730
|
+
Some(None)
|
|
1731
|
+
}
|
|
1732
|
+
_ => None,
|
|
1733
|
+
};
|
|
1734
|
+
if let Some(target) = hit {
|
|
1735
|
+
if site.is_some() {
|
|
1736
|
+
ambiguous = true;
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
site = Some((i, target));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
// The single call must be a TOP-LEVEL statement (calls==1 guarantees no other call exists;
|
|
1743
|
+
// if we didn't find it at top level it is nested — skip, fail-closed).
|
|
1744
|
+
let (site_idx, target) = match site {
|
|
1745
|
+
Some(s) if !ambiguous => s,
|
|
1746
|
+
_ => continue,
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// Pull the factory body out of its FunDecl.
|
|
1750
|
+
let fdecl_idx = result.iter().position(|s| {
|
|
1751
|
+
matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == fname.as_ref())
|
|
1752
|
+
});
|
|
1753
|
+
let Some(fdecl_idx) = fdecl_idx else { continue };
|
|
1754
|
+
let body_stmts: Vec<Statement> = match &result[fdecl_idx] {
|
|
1755
|
+
Statement::FunDecl { body, .. } => match body.as_ref() {
|
|
1756
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
1757
|
+
statements.clone()
|
|
1758
|
+
}
|
|
1759
|
+
_ => continue,
|
|
1760
|
+
},
|
|
1761
|
+
_ => continue,
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
// Split off the trailing `return E`.
|
|
1765
|
+
let (prefix, ret_expr) = match body_stmts.split_last() {
|
|
1766
|
+
Some((Statement::Return { value: Some(e), .. }, head)) => (head.to_vec(), e.clone()),
|
|
1767
|
+
_ => continue,
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// Build the alpha-rename map for the leaked (direct) body locals.
|
|
1771
|
+
let Some(leaked) = direct_body_locals(&prefix) else { continue };
|
|
1772
|
+
// NARROW to the push-built factory shape ONLY: `return R` where R is a leaked (body-top-level)
|
|
1773
|
+
// local built via `let R = []` + pushes, consumed as `let X = f()`. A literal-array return
|
|
1774
|
+
// (`return [a, b]`), a call return, or a discarded `f()` is left intact — those are the #177
|
|
1775
|
+
// aggregate machinery's territory (nbody's `makeBodies` returns `[sun, jupiter, …]`, and
|
|
1776
|
+
// inlining it removes the function the aggregate ABI expects → nbody BUILD-FAIL under
|
|
1777
|
+
// TISH_AGGREGATE_INFER). This keeps the inliner to exactly the Stage A/B bridge target.
|
|
1778
|
+
let return_ident = match &ret_expr {
|
|
1779
|
+
Expr::Ident { name, .. } if leaked.iter().any(|l| l.as_ref() == name.as_ref()) => {
|
|
1780
|
+
name.clone()
|
|
1781
|
+
}
|
|
1782
|
+
_ => continue,
|
|
1783
|
+
};
|
|
1784
|
+
let Some(tgt) = target.clone() else { continue }; // only `let X = f()`, not a discarded call
|
|
1785
|
+
let mut active: std::collections::HashMap<String, Arc<str>> =
|
|
1786
|
+
std::collections::HashMap::new();
|
|
1787
|
+
// Map the returned array var R → the call-site target X, so the spliced `let R = []` becomes
|
|
1788
|
+
// `let X = []` and the record-array/ShapeUnion machinery types X directly. Every other leaked
|
|
1789
|
+
// local gets a fresh, collision-proof name.
|
|
1790
|
+
for (n, l) in leaked.iter().enumerate() {
|
|
1791
|
+
if l.as_ref() == return_ident.as_ref() {
|
|
1792
|
+
active.insert(l.to_string(), tgt.clone());
|
|
1793
|
+
} else {
|
|
1794
|
+
let fresh: Arc<str> = format!("__inl_{}_{}_{}", fname, n, l).into();
|
|
1795
|
+
active.insert(l.to_string(), fresh);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Alpha-rename the prefix (declarations + references) using the resolution-tested renamer.
|
|
1800
|
+
// The trailing `return R` is dropped: R was renamed to X, so X is already the array binding.
|
|
1801
|
+
let mut renamed_prefix = prefix;
|
|
1802
|
+
for s in &mut renamed_prefix {
|
|
1803
|
+
let mut a = active.clone();
|
|
1804
|
+
crate::resolve::rewrite_stmt_scope(s, &mut a, true);
|
|
1805
|
+
}
|
|
1806
|
+
let spliced: Vec<Statement> = renamed_prefix;
|
|
1807
|
+
|
|
1808
|
+
// Rebuild the program: replace the call site with the spliced body, drop the FunDecl.
|
|
1809
|
+
let mut out: Vec<Statement> = Vec::with_capacity(result.len() + spliced.len());
|
|
1810
|
+
for (i, s) in result.into_iter().enumerate() {
|
|
1811
|
+
if i == fdecl_idx {
|
|
1812
|
+
continue; // drop the now-inlined factory
|
|
1813
|
+
}
|
|
1814
|
+
if i == site_idx {
|
|
1815
|
+
out.extend(spliced.iter().cloned());
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
out.push(s);
|
|
1819
|
+
}
|
|
1820
|
+
result = out;
|
|
1821
|
+
}
|
|
1822
|
+
result
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
fn struct_infer_program(program: Program) -> Program {
|
|
1826
|
+
// #179 Stage A: inline a nullary function that is called EXACTLY ONCE (as a top-level
|
|
1827
|
+
// statement) and never used as a value, splicing its body at the call site. This bridges a
|
|
1828
|
+
// build-then-return record-array factory (`function mk(){ let a=[]; a.push({..}); return a }`;
|
|
1829
|
+
// `let xs = mk()`) into a TOP-LEVEL push-built array, which the record-array / ShapeUnion
|
|
1830
|
+
// machinery can then type. Sound by construction: a single-call inline substitutes the whole
|
|
1831
|
+
// call, so evaluation order and side effects are preserved exactly (a captured-global write in
|
|
1832
|
+
// the body becomes an ordinary top-level statement; a factory used as a value is left alone).
|
|
1833
|
+
let program = if crate::native_opts_enabled() {
|
|
1834
|
+
Program { statements: inline_single_call_factories(program.statements) }
|
|
1835
|
+
} else {
|
|
1836
|
+
program
|
|
1837
|
+
};
|
|
1838
|
+
let mut reg = StructRegistry::default();
|
|
1839
|
+
let mut ctx = InferCtx::new();
|
|
1840
|
+
// #320: fns proven to always return a number, so `infer_expr_type(f(...))` is `number` and a
|
|
1841
|
+
// `seq.push(nextBase())` keeps `seq` a native `number[]`. Computed off the param-inferred
|
|
1842
|
+
// program so numeric params are already typed.
|
|
1843
|
+
ctx.number_returning_fns = collect_number_returning_fns(&program.statements, &ctx);
|
|
1844
|
+
// #175: tell the mutable-array co-inference which fns take native `&/&mut Vec` array params, so
|
|
1845
|
+
// forwarding an array into one isn't treated as a boxing escape (lets the caller's array stay
|
|
1846
|
+
// unboxed). Same AST-only detection codegen uses, so the two never disagree.
|
|
1847
|
+
if crate::native_opts_enabled() {
|
|
1848
|
+
for (fname, sig) in crate::codegen::Codegen::detect_native_vec_fns(&program) {
|
|
1849
|
+
let flags: Vec<bool> = sig
|
|
1850
|
+
.params
|
|
1851
|
+
.iter()
|
|
1852
|
+
.map(|(_, k)| matches!(k, crate::codegen::VecParamKind::Array { .. }))
|
|
1853
|
+
.collect();
|
|
1854
|
+
ctx.native_vec_array_params.insert(fname, flags);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// #320: mark a read-only `number[]`-param fn's array args as non-escapes so the caller's array
|
|
1858
|
+
// stays `number[]` (e.g. `kNucleotide(seq, k)`). This uses the READ-ONLY criterion only — sound
|
|
1859
|
+
// regardless of the arg type, since passing a native `Vec` to a boxed closure copies it (a
|
|
1860
|
+
// read-only callee can't mutate the caller's array through the copy). Codegen separately proves,
|
|
1861
|
+
// via the call sites, which of these it may actually UNBOX (`native_arr_param_fns`); a fn it
|
|
1862
|
+
// can't prove simply stays a fully boxed closure. Runs pre-`si_block`, so it must not rely on
|
|
1863
|
+
// `number[]` stamps — `arr_param_readonly_fns` only inspects each fn's own body.
|
|
1864
|
+
if crate::native_opts_enabled() {
|
|
1865
|
+
for (fname, flags) in crate::codegen::Codegen::arr_param_readonly_fns(&program) {
|
|
1866
|
+
ctx.native_vec_array_params.insert(fname, flags);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
let mut stmts = si_block(program.statements, &mut reg, &mut ctx);
|
|
1870
|
+
// Prepend the generated struct `type` aliases so codegen synthesizes them.
|
|
1871
|
+
let mut out: Vec<Statement> = Vec::with_capacity(stmts.len() + reg.decls.len());
|
|
1872
|
+
let span = stmts.first().map(stmt_span).unwrap_or_else(zero_span);
|
|
1873
|
+
for (name, fields) in reg.decls.drain(..) {
|
|
1874
|
+
out.push(Statement::TypeAlias {
|
|
1875
|
+
name: name.as_str().into(),
|
|
1876
|
+
name_span: span,
|
|
1877
|
+
ty: TypeAnnotation::Object(fields),
|
|
1878
|
+
span,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
// #179 Stage B: emit each shape-union alias as `type TishAnonUnion_N = Union([Object(s0), …])`;
|
|
1882
|
+
// codegen recognizes the reserved-prefix alias and synthesizes the enum + per-variant structs.
|
|
1883
|
+
for (name, shapes) in reg.union_decls.drain(..) {
|
|
1884
|
+
let members: Vec<TypeAnnotation> = shapes
|
|
1885
|
+
.into_iter()
|
|
1886
|
+
.map(TypeAnnotation::Object)
|
|
1887
|
+
.collect();
|
|
1888
|
+
out.push(Statement::TypeAlias {
|
|
1889
|
+
name: name.as_str().into(),
|
|
1890
|
+
name_span: span,
|
|
1891
|
+
ty: TypeAnnotation::Union(members),
|
|
1892
|
+
span,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
out.append(&mut stmts);
|
|
1896
|
+
Program { statements: out }
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
fn stmt_span(s: &Statement) -> tishlang_ast::Span {
|
|
1900
|
+
match s {
|
|
1901
|
+
Statement::VarDecl { span, .. }
|
|
1902
|
+
| Statement::Block { span, .. }
|
|
1903
|
+
| Statement::ExprStmt { span, .. }
|
|
1904
|
+
| Statement::If { span, .. }
|
|
1905
|
+
| Statement::For { span, .. }
|
|
1906
|
+
| Statement::ForOf { span, .. }
|
|
1907
|
+
| Statement::While { span, .. }
|
|
1908
|
+
| Statement::Return { span, .. }
|
|
1909
|
+
| Statement::FunDecl { span, .. }
|
|
1910
|
+
| Statement::TypeAlias { span, .. } => *span,
|
|
1911
|
+
_ => zero_span(),
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
fn zero_span() -> tishlang_ast::Span {
|
|
1916
|
+
tishlang_ast::Span {
|
|
1917
|
+
start: (0, 0),
|
|
1918
|
+
end: (0, 0),
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/// Transform a block: annotate struct-safe object `let` bindings, recursing
|
|
1923
|
+
/// into nested blocks. `ctx` provides field-type inference for initializers.
|
|
1924
|
+
fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx) -> Vec<Statement> {
|
|
1925
|
+
ctx.push_scope();
|
|
1926
|
+
// Co-infer mutable `number[]` locals across the whole block (cross-referencing arrays) up front.
|
|
1927
|
+
// Co-infer mutable native arrays (number[] and boolean[]) across the block; an array of the
|
|
1928
|
+
// wrong element type simply fails its run and stays boxed.
|
|
1929
|
+
let native_num_arrays = block_native_arrays(&stmts, ctx, &number_ann());
|
|
1930
|
+
let native_bool_arrays = block_native_arrays(&stmts, ctx, &bool_ann());
|
|
1931
|
+
let n = stmts.len();
|
|
1932
|
+
let mut out: Vec<Statement> = Vec::with_capacity(n);
|
|
1933
|
+
for (i, stmt) in stmts.iter().enumerate() {
|
|
1934
|
+
// Candidate: `let xs = [ ...uniform native scalars... ]` with no annotation -> native `T[]`,
|
|
1935
|
+
// but only when every later use is read-only (`uses_are_array_safe`), so a `Vec<f64>`
|
|
1936
|
+
// assumption can't be violated by a later `push`/`xs[i] = …`.
|
|
1937
|
+
if let Statement::VarDecl {
|
|
1938
|
+
name,
|
|
1939
|
+
name_span,
|
|
1940
|
+
mutable,
|
|
1941
|
+
type_ann: None,
|
|
1942
|
+
init: Some(Expr::Array { elements, .. }),
|
|
1943
|
+
span,
|
|
1944
|
+
} = stmt
|
|
1945
|
+
{
|
|
1946
|
+
// (a) Read-only typed array: uniform scalar literals, every later use read-only.
|
|
1947
|
+
if let Some(elem) = infer_array_elem(elements, ctx) {
|
|
1948
|
+
// A constant OOB read (`let a=[1,2,3]; a[5]`) yields NaN under Vec<f64> but `null`
|
|
1949
|
+
// boxed, so disqualify it even though the use is "read-only".
|
|
1950
|
+
if uses_are_array_safe(name.as_ref(), &stmts[i + 1..])
|
|
1951
|
+
&& !array_oob_const_access(&stmts[i + 1..], name.as_ref(), elements.len())
|
|
1952
|
+
{
|
|
1953
|
+
let arr_ann = TypeAnnotation::Array(Box::new(elem));
|
|
1954
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
1955
|
+
out.push(Statement::VarDecl {
|
|
1956
|
+
name: name.clone(),
|
|
1957
|
+
name_span: *name_span,
|
|
1958
|
+
mutable: *mutable,
|
|
1959
|
+
type_ann: Some(arr_ann),
|
|
1960
|
+
init: stmt_init_clone(stmt),
|
|
1961
|
+
span: *span,
|
|
1962
|
+
});
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
// (b) Mutable native `number[]` / `boolean[]`: `let a = []` / `[lits]` driven by push +
|
|
1967
|
+
// index read/write (`fannkuch`/`queens`/`nsieve`, incl. cross-referencing siblings).
|
|
1968
|
+
// Sound via the block-level fixpoint above (every accepted array's elements are provably
|
|
1969
|
+
// of the chosen element type). `number[]` wins over `boolean[]` if somehow in both.
|
|
1970
|
+
let native_elem = if native_num_arrays.contains(name.as_ref()) {
|
|
1971
|
+
Some(number_ann())
|
|
1972
|
+
} else if native_bool_arrays.contains(name.as_ref()) {
|
|
1973
|
+
Some(bool_ann())
|
|
1974
|
+
} else {
|
|
1975
|
+
None
|
|
1976
|
+
};
|
|
1977
|
+
if let Some(elem) = native_elem {
|
|
1978
|
+
let arr_ann = TypeAnnotation::Array(Box::new(elem));
|
|
1979
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
1980
|
+
out.push(Statement::VarDecl {
|
|
1981
|
+
name: name.clone(),
|
|
1982
|
+
name_span: *name_span,
|
|
1983
|
+
mutable: *mutable,
|
|
1984
|
+
type_ann: Some(arr_ann),
|
|
1985
|
+
init: stmt_init_clone(stmt),
|
|
1986
|
+
span: *span,
|
|
1987
|
+
});
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
// (d) Array of uniform records: `let rows = []` (or `[{lits}]`) built by
|
|
1991
|
+
// `rows.push({uniform primitive object literal})` and used only as `rows[i].field`
|
|
1992
|
+
// reads / `rows.length`. Lower to a native `Vec<struct>` — the struct path emits direct
|
|
1993
|
+
// field access and (post-#350) allocation-free construction. Conservative: any other use
|
|
1994
|
+
// of the binding (escape, element/field write, for-of, bare ref) leaves it boxed.
|
|
1995
|
+
if let Some(fields) = record_array_fields(name.as_ref(), elements, &stmts[i + 1..], ctx) {
|
|
1996
|
+
let keys: std::collections::HashSet<&str> =
|
|
1997
|
+
fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
1998
|
+
if rec_arr_uses_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
1999
|
+
let alias = reg.intern(&fields);
|
|
2000
|
+
let arr_ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple(
|
|
2001
|
+
alias.as_str().into(),
|
|
2002
|
+
tishlang_ast::Span::default(),
|
|
2003
|
+
)));
|
|
2004
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
2005
|
+
ctx.define_struct(alias.as_str(), fields.clone()); // #373: enable cross-array field reads
|
|
2006
|
+
out.push(Statement::VarDecl {
|
|
2007
|
+
name: name.clone(),
|
|
2008
|
+
name_span: *name_span,
|
|
2009
|
+
mutable: *mutable,
|
|
2010
|
+
type_ann: Some(arr_ann),
|
|
2011
|
+
init: stmt_init_clone(stmt),
|
|
2012
|
+
span: *span,
|
|
2013
|
+
});
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
// (e) #179 Stage B — array of a CLOSED SET of heterogeneous record shapes ("megamorphic":
|
|
2018
|
+
// `objs.push({value,a})`, `objs.push({b,value,c})`, … all read as `objs[i].value`). Lower
|
|
2019
|
+
// to a native `Vec<enum>` (ShapeUnion): a `.field` present-and-same-type in EVERY variant
|
|
2020
|
+
// becomes a direct `match` load. Opt-in (TISH_SHAPE_UNION) until Stage C's integer modulo
|
|
2021
|
+
// makes it a net win. The safety walk uses ONLY the common keys, so a read of a
|
|
2022
|
+
// not-in-every-variant field disqualifies (stays boxed → undefined/null preserved).
|
|
2023
|
+
if shape_union_enabled() {
|
|
2024
|
+
if let Some(shapes) =
|
|
2025
|
+
record_array_shape_set(name.as_ref(), elements, &stmts[i + 1..], ctx)
|
|
2026
|
+
{
|
|
2027
|
+
let common = shape_set_common_keys(&shapes);
|
|
2028
|
+
let common_refs: std::collections::HashSet<&str> =
|
|
2029
|
+
common.iter().map(|k| k.as_ref()).collect();
|
|
2030
|
+
if !common_refs.is_empty()
|
|
2031
|
+
&& rec_arr_uses_safe(name.as_ref(), &common_refs, &stmts[i + 1..])
|
|
2032
|
+
{
|
|
2033
|
+
let union_alias = reg.intern_union(&shapes);
|
|
2034
|
+
let arr_ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple(
|
|
2035
|
+
union_alias.as_str().into(),
|
|
2036
|
+
tishlang_ast::Span::default(),
|
|
2037
|
+
)));
|
|
2038
|
+
ctx.define(name.as_ref(), arr_ann.clone());
|
|
2039
|
+
// Register the union's COMMON fields as a struct shape so `infer_expr_type`
|
|
2040
|
+
// types `objs[i].field` as its (number) type — otherwise a numeric
|
|
2041
|
+
// accumulator fed by the read (`sum = sum + objs[i].value`) stays boxed.
|
|
2042
|
+
// All common fields share one type across variants (guaranteed above), so the
|
|
2043
|
+
// first shape's types are correct.
|
|
2044
|
+
let common_fields: Vec<(std::sync::Arc<str>, TypeAnnotation)> = shapes[0]
|
|
2045
|
+
.iter()
|
|
2046
|
+
.filter(|(k, _)| common.contains(k))
|
|
2047
|
+
.cloned()
|
|
2048
|
+
.collect();
|
|
2049
|
+
ctx.define_struct(union_alias.as_str(), common_fields);
|
|
2050
|
+
out.push(Statement::VarDecl {
|
|
2051
|
+
name: name.clone(),
|
|
2052
|
+
name_span: *name_span,
|
|
2053
|
+
mutable: *mutable,
|
|
2054
|
+
type_ann: Some(arr_ann),
|
|
2055
|
+
init: stmt_init_clone(stmt),
|
|
2056
|
+
span: *span,
|
|
2057
|
+
});
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
// Candidate: `let o = { ...object literal... }` with no annotation.
|
|
2064
|
+
if let Statement::VarDecl {
|
|
2065
|
+
name,
|
|
2066
|
+
name_span,
|
|
2067
|
+
mutable,
|
|
2068
|
+
type_ann: None,
|
|
2069
|
+
init: Some(Expr::Object { props, span: obj_span }),
|
|
2070
|
+
span,
|
|
2071
|
+
} = stmt
|
|
2072
|
+
{
|
|
2073
|
+
// A struct-typed leading spread (`{ ...base, k: v }`) is inlined to a plain
|
|
2074
|
+
// `{ a: base.a, …, k: v }` literal so it lowers to an unboxed struct; otherwise the plain
|
|
2075
|
+
// literal path applies (a spread it can't inline leaves `infer_object_shape` → None → boxed).
|
|
2076
|
+
let (shape, rewritten_props) = match inline_struct_spread(props, ctx, reg) {
|
|
2077
|
+
Some((f, rp)) => (Some(f), Some(rp)),
|
|
2078
|
+
None => (infer_object_shape(props, ctx), None),
|
|
2079
|
+
};
|
|
2080
|
+
if let Some(fields) = shape {
|
|
2081
|
+
let keys: std::collections::HashSet<&str> =
|
|
2082
|
+
fields.iter().map(|(k, _)| k.as_ref()).collect();
|
|
2083
|
+
// Sound: every later use in this block must be a literal-key read.
|
|
2084
|
+
if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
2085
|
+
let alias = reg.intern(&fields);
|
|
2086
|
+
ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default()));
|
|
2087
|
+
let new_init = match rewritten_props {
|
|
2088
|
+
Some(rp) => Some(Expr::Object {
|
|
2089
|
+
props: rp,
|
|
2090
|
+
span: *obj_span,
|
|
2091
|
+
}),
|
|
2092
|
+
None => stmt_init_clone(stmt),
|
|
2093
|
+
};
|
|
2094
|
+
out.push(Statement::VarDecl {
|
|
2095
|
+
name: name.clone(),
|
|
2096
|
+
name_span: *name_span,
|
|
2097
|
+
mutable: *mutable,
|
|
2098
|
+
type_ann: Some(TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default())),
|
|
2099
|
+
init: new_init,
|
|
2100
|
+
span: *span,
|
|
2101
|
+
});
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
// Record a typed plain local (e.g. `let i = 0`) so a LATER object literal can type its
|
|
2107
|
+
// fields from it (`{ x: i }` → struct). The first inference pass may already have annotated
|
|
2108
|
+
// it (`let i: number`), so read the annotation OR infer from the init. Object-literal lets
|
|
2109
|
+
// defined their struct alias above and `continue`d; this runs for the rest. Without it,
|
|
2110
|
+
// `{ x: i }` can't resolve `i`'s type and the object stays a boxed `PropMap` (object_sum gap).
|
|
2111
|
+
if let Statement::VarDecl {
|
|
2112
|
+
name,
|
|
2113
|
+
name_span,
|
|
2114
|
+
mutable,
|
|
2115
|
+
type_ann,
|
|
2116
|
+
init,
|
|
2117
|
+
span,
|
|
2118
|
+
} = stmt
|
|
2119
|
+
{
|
|
2120
|
+
let inferred = type_ann
|
|
2121
|
+
.clone()
|
|
2122
|
+
.or_else(|| init.as_ref().and_then(|e| infer_expr_type(e, ctx)));
|
|
2123
|
+
if let Some(t) = &inferred {
|
|
2124
|
+
ctx.define(name.as_ref(), t.clone());
|
|
2125
|
+
}
|
|
2126
|
+
// #170: the base inference pass ran before array types were known, so an unannotated
|
|
2127
|
+
// local whose init only types now that `perm: number[]` is known (`let k = perm[0]`,
|
|
2128
|
+
// `let temp = perm[i]`) was left `type_ann: None` — and codegen reads the NODE, not
|
|
2129
|
+
// `ctx`, so it stayed a boxed `Value` despite being provably `f64`. Persist a proven
|
|
2130
|
+
// `number` type back onto the node here (the second, type-aware pass). The codegen
|
|
2131
|
+
// demote-gate (`collect_demoted_numeric_locals`) re-boxes any such local whose later
|
|
2132
|
+
// reassignment can escape `number`, so writing the annotation can never miscompile.
|
|
2133
|
+
if type_ann.is_none() {
|
|
2134
|
+
if let Some(t @ TypeAnnotation::Simple(s, _)) = &inferred {
|
|
2135
|
+
if s.as_ref() == "number" {
|
|
2136
|
+
out.push(Statement::VarDecl {
|
|
2137
|
+
name: name.clone(),
|
|
2138
|
+
name_span: *name_span,
|
|
2139
|
+
mutable: *mutable,
|
|
2140
|
+
type_ann: Some(t.clone()),
|
|
2141
|
+
init: stmt_init_clone(stmt),
|
|
2142
|
+
span: *span,
|
|
2143
|
+
});
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
out.push(si_recurse(stmt, reg, ctx));
|
|
2150
|
+
}
|
|
2151
|
+
ctx.pop_scope();
|
|
2152
|
+
out
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
fn stmt_init_clone(stmt: &Statement) -> Option<Expr> {
|
|
2156
|
+
if let Statement::VarDecl { init, .. } = stmt {
|
|
2157
|
+
init.clone()
|
|
2158
|
+
} else {
|
|
2159
|
+
None
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/// Define a `let`/`const` binding's name+type in `ctx` from its annotation or inferred init type, so
|
|
2164
|
+
/// inference in an inner scope can resolve references to it. Used to bind `for`-init loop variables
|
|
2165
|
+
/// before recursing into the loop body (enables struct inference of loop-local objects that read the
|
|
2166
|
+
/// counter). Only the simple `let x = <expr>` shape; anything else is skipped (sound — the body just
|
|
2167
|
+
/// keeps today's behavior).
|
|
2168
|
+
fn si_bind_decl(stmt: &Statement, ctx: &mut InferCtx) {
|
|
2169
|
+
if let Statement::VarDecl {
|
|
2170
|
+
name,
|
|
2171
|
+
type_ann,
|
|
2172
|
+
init: Some(e),
|
|
2173
|
+
..
|
|
2174
|
+
} = stmt
|
|
2175
|
+
{
|
|
2176
|
+
let t = type_ann.clone().or_else(|| infer_expr_type(e, ctx));
|
|
2177
|
+
if let Some(t) = t {
|
|
2178
|
+
ctx.define(name.as_ref(), t);
|
|
2179
|
+
}
|
|
1126
2180
|
}
|
|
1127
2181
|
}
|
|
1128
2182
|
|
|
@@ -1140,13 +2194,26 @@ fn si_recurse(stmt: &Statement, reg: &mut StructRegistry, ctx: &mut InferCtx) ->
|
|
|
1140
2194
|
update,
|
|
1141
2195
|
body,
|
|
1142
2196
|
span,
|
|
1143
|
-
} =>
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
2197
|
+
} => {
|
|
2198
|
+
// Bind the loop variable(s) from `init` into a fresh scope so the body's struct inference
|
|
2199
|
+
// can type object-literal fields that reference the counter — e.g. `{ id: i, v: i * 2 }`
|
|
2200
|
+
// inside `for (let i = 0; …)`. Without this, `infer_object_shape` can't type those fields
|
|
2201
|
+
// and a loop-local object stays a boxed `PropMap`. (A loop-local object that reads only
|
|
2202
|
+
// outer/literal values already lowers to an unboxed struct; this closes the loop-var gap.)
|
|
2203
|
+
ctx.push_scope();
|
|
2204
|
+
if let Some(init_stmt) = init.as_deref() {
|
|
2205
|
+
si_bind_decl(init_stmt, ctx);
|
|
2206
|
+
}
|
|
2207
|
+
let new_body = Box::new(si_recurse(body, reg, ctx));
|
|
2208
|
+
ctx.pop_scope();
|
|
2209
|
+
Statement::For {
|
|
2210
|
+
init: init.clone(),
|
|
2211
|
+
cond: cond.clone(),
|
|
2212
|
+
update: update.clone(),
|
|
2213
|
+
body: new_body,
|
|
2214
|
+
span: *span,
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
1150
2217
|
Statement::ForOf {
|
|
1151
2218
|
name,
|
|
1152
2219
|
name_span,
|
|
@@ -1227,6 +2294,373 @@ fn uses_are_struct_safe(name: &str, keys: &std::collections::HashSet<&str>, tail
|
|
|
1227
2294
|
tail.iter().all(|s| stmt_name_safe(s, name, keys))
|
|
1228
2295
|
}
|
|
1229
2296
|
|
|
2297
|
+
// ── array-of-records inference (`let rows = []; rows.push({…}); rows[i].field`) ───────────────────
|
|
2298
|
+
// Lowers an untyped local array of uniform primitive records to a native `Vec<struct>`, so the
|
|
2299
|
+
// struct codegen emits direct field access + (post-#350) allocation-free construction. Conservative:
|
|
2300
|
+
// only fires when the binding is used solely as `push({uniform literal})`, `name[i].<key>` reads, and
|
|
2301
|
+
// `name.length` — any other use leaves it a boxed `Value[]`.
|
|
2302
|
+
|
|
2303
|
+
/// Compare two inferred record shapes for equality, order-independently (same keys + field types).
|
|
2304
|
+
fn fields_eq(
|
|
2305
|
+
a: &[(std::sync::Arc<str>, TypeAnnotation)],
|
|
2306
|
+
b: &[(std::sync::Arc<str>, TypeAnnotation)],
|
|
2307
|
+
) -> bool {
|
|
2308
|
+
if a.len() != b.len() {
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
let mut av: Vec<(String, String)> =
|
|
2312
|
+
a.iter().map(|(k, t)| (k.to_string(), type_canon(t))).collect();
|
|
2313
|
+
let mut bv: Vec<(String, String)> =
|
|
2314
|
+
b.iter().map(|(k, t)| (k.to_string(), type_canon(t))).collect();
|
|
2315
|
+
av.sort();
|
|
2316
|
+
bv.sort();
|
|
2317
|
+
av == bv
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/// Fold one object literal's shape into the running record shape, flagging `*ok=false` on any
|
|
2321
|
+
/// non-uniform or non-primitive literal.
|
|
2322
|
+
fn record_shape_consider(
|
|
2323
|
+
shape: &mut Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>>,
|
|
2324
|
+
ok: &mut bool,
|
|
2325
|
+
props: &[tishlang_ast::ObjectProp],
|
|
2326
|
+
ctx: &InferCtx,
|
|
2327
|
+
) {
|
|
2328
|
+
match infer_object_shape(props, ctx) {
|
|
2329
|
+
Some(f) => match shape {
|
|
2330
|
+
None => *shape = Some(f),
|
|
2331
|
+
Some(prev) => {
|
|
2332
|
+
if !fields_eq(prev, &f) {
|
|
2333
|
+
*ok = false;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
},
|
|
2337
|
+
None => *ok = false,
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/// Infer the common record shape of candidate array-of-records `name`: unify the shapes of its
|
|
2342
|
+
/// initial object-literal elements and every `name.push({…})` in `tail`. Returns the field list, or
|
|
2343
|
+
/// None if there is no object shape, the literals disagree, or a push arg is not a uniform primitive
|
|
2344
|
+
/// object literal. Loop vars in the literals (`{ x: i }`) are typed by seeding numeric locals first.
|
|
2345
|
+
fn record_array_fields(
|
|
2346
|
+
name: &str,
|
|
2347
|
+
init_elements: &[tishlang_ast::ArrayElement],
|
|
2348
|
+
tail: &[Statement],
|
|
2349
|
+
ctx: &InferCtx,
|
|
2350
|
+
) -> Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>> {
|
|
2351
|
+
// Seed numeric block-locals (incl. enclosing loop vars) so `{ x: i, y: i * 2 }` field values type.
|
|
2352
|
+
let mut sctx = ctx.clone();
|
|
2353
|
+
for _ in 0..4 {
|
|
2354
|
+
for s in tail {
|
|
2355
|
+
seed_numeric_locals(s, &mut sctx);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
let mut shape: Option<Vec<(std::sync::Arc<str>, TypeAnnotation)>> = None;
|
|
2359
|
+
let mut ok = true;
|
|
2360
|
+
for el in init_elements {
|
|
2361
|
+
match el {
|
|
2362
|
+
tishlang_ast::ArrayElement::Expr(Expr::Object { props, .. }) => {
|
|
2363
|
+
record_shape_consider(&mut shape, &mut ok, props, &sctx);
|
|
2364
|
+
}
|
|
2365
|
+
_ => return None,
|
|
2366
|
+
}
|
|
2367
|
+
if !ok {
|
|
2368
|
+
return None;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
for s in tail {
|
|
2372
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2373
|
+
if let Expr::Call { callee, args, .. } = e {
|
|
2374
|
+
if let Expr::Member {
|
|
2375
|
+
object,
|
|
2376
|
+
prop: tishlang_ast::MemberProp::Name { name: m, .. },
|
|
2377
|
+
..
|
|
2378
|
+
} = callee.as_ref()
|
|
2379
|
+
{
|
|
2380
|
+
if m.as_ref() == "push"
|
|
2381
|
+
&& matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
|
|
2382
|
+
{
|
|
2383
|
+
if let [CallArg::Expr(Expr::Object { props, .. })] = args.as_slice() {
|
|
2384
|
+
record_shape_consider(&mut shape, &mut ok, props, &sctx);
|
|
2385
|
+
} else {
|
|
2386
|
+
ok = false;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
if !ok {
|
|
2393
|
+
return None;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
shape
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/// True if every use of array-of-records `name` in `tail` is `name.push({uniform literal})`,
|
|
2400
|
+
/// `name[i].<key>` (field read, `key` in the shape), or `name.length`. Mirrors the #177 S-D safety
|
|
2401
|
+
/// walk; any other use of the binding (bare ref, element/field write, escape into a call / return /
|
|
2402
|
+
/// spread, for-of, method other than push) returns false → the array stays boxed.
|
|
2403
|
+
fn rec_arr_uses_safe(name: &str, keys: &std::collections::HashSet<&str>, tail: &[Statement]) -> bool {
|
|
2404
|
+
tail.iter().all(|s| ra_stmt(s, name, keys))
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/// #179 Stage B/C: shape-union (megamorphic) inference — a heterogeneous closed-shape-set array
|
|
2408
|
+
/// lowers to a native `Vec<enum>` with direct enum-match field reads. DEFAULT-ON as of Stage C
|
|
2409
|
+
/// (the native-length fix made megamorphic beat node: 485→~16ms vs node ~58ms). Part of the native
|
|
2410
|
+
/// stack, so `TISH_NATIVE_OPT=0` disables it (the boxed baseline); `TISH_SHAPE_UNION=0` is a
|
|
2411
|
+
/// dedicated escape hatch.
|
|
2412
|
+
fn shape_union_enabled() -> bool {
|
|
2413
|
+
crate::native_opts_enabled() && std::env::var("TISH_SHAPE_UNION").as_deref() != Ok("0")
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/// #179 Stage B: like [`record_array_fields`] but for a HETEROGENEOUS array — accumulate the set of
|
|
2417
|
+
/// DISTINCT primitive-record shapes across the initial elements + every `name.push({…})`. Returns
|
|
2418
|
+
/// the deduped shapes when there are 2..=8 of them (a genuine union; 1 shape is the existing
|
|
2419
|
+
/// single-struct path), or None if a push arg isn't a uniform primitive object literal, a non-object
|
|
2420
|
+
/// element appears, or the set exceeds 8 shapes.
|
|
2421
|
+
fn record_array_shape_set(
|
|
2422
|
+
name: &str,
|
|
2423
|
+
init_elements: &[tishlang_ast::ArrayElement],
|
|
2424
|
+
tail: &[Statement],
|
|
2425
|
+
ctx: &InferCtx,
|
|
2426
|
+
) -> Option<Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>>> {
|
|
2427
|
+
let mut sctx = ctx.clone();
|
|
2428
|
+
for _ in 0..4 {
|
|
2429
|
+
for s in tail {
|
|
2430
|
+
seed_numeric_locals(s, &mut sctx);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
let mut shapes: Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>> = Vec::new();
|
|
2434
|
+
let mut ok = true;
|
|
2435
|
+
for el in init_elements {
|
|
2436
|
+
match el {
|
|
2437
|
+
tishlang_ast::ArrayElement::Expr(Expr::Object { props, .. }) => {
|
|
2438
|
+
shape_set_consider(&mut shapes, &mut ok, props, &sctx);
|
|
2439
|
+
}
|
|
2440
|
+
_ => return None,
|
|
2441
|
+
}
|
|
2442
|
+
if !ok {
|
|
2443
|
+
return None;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
for s in tail {
|
|
2447
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2448
|
+
if let Expr::Call { callee, args, .. } = e {
|
|
2449
|
+
if let Expr::Member {
|
|
2450
|
+
object,
|
|
2451
|
+
prop: tishlang_ast::MemberProp::Name { name: m, .. },
|
|
2452
|
+
..
|
|
2453
|
+
} = callee.as_ref()
|
|
2454
|
+
{
|
|
2455
|
+
if m.as_ref() == "push"
|
|
2456
|
+
&& matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name)
|
|
2457
|
+
{
|
|
2458
|
+
if let [CallArg::Expr(Expr::Object { props, .. })] = args.as_slice() {
|
|
2459
|
+
shape_set_consider(&mut shapes, &mut ok, props, &sctx);
|
|
2460
|
+
} else {
|
|
2461
|
+
ok = false;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
if !ok {
|
|
2468
|
+
return None;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if !(2..=8).contains(&shapes.len()) {
|
|
2472
|
+
return None;
|
|
2473
|
+
}
|
|
2474
|
+
// Every variant must have a DISTINCT key set — codegen selects the variant for an object literal
|
|
2475
|
+
// by its key set, so a same-keys/different-types collision would be ambiguous → stay boxed.
|
|
2476
|
+
let mut keysets: Vec<Vec<&str>> = shapes
|
|
2477
|
+
.iter()
|
|
2478
|
+
.map(|s| {
|
|
2479
|
+
let mut k: Vec<&str> = s.iter().map(|(k, _)| k.as_ref()).collect();
|
|
2480
|
+
k.sort_unstable();
|
|
2481
|
+
k
|
|
2482
|
+
})
|
|
2483
|
+
.collect();
|
|
2484
|
+
let n = keysets.len();
|
|
2485
|
+
keysets.sort();
|
|
2486
|
+
keysets.dedup();
|
|
2487
|
+
if keysets.len() != n {
|
|
2488
|
+
return None;
|
|
2489
|
+
}
|
|
2490
|
+
Some(shapes)
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/// Accumulate a distinct primitive-record shape into the set (dedup by `fields_eq`). Sets `ok=false`
|
|
2494
|
+
/// if the literal is not a primitive record or the set would exceed 8 shapes.
|
|
2495
|
+
fn shape_set_consider(
|
|
2496
|
+
shapes: &mut Vec<Vec<(std::sync::Arc<str>, TypeAnnotation)>>,
|
|
2497
|
+
ok: &mut bool,
|
|
2498
|
+
props: &[tishlang_ast::ObjectProp],
|
|
2499
|
+
ctx: &InferCtx,
|
|
2500
|
+
) {
|
|
2501
|
+
match infer_object_shape(props, ctx) {
|
|
2502
|
+
Some(f) => {
|
|
2503
|
+
if !shapes.iter().any(|s| fields_eq(s, &f)) {
|
|
2504
|
+
if shapes.len() >= 8 {
|
|
2505
|
+
*ok = false;
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
shapes.push(f);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
None => *ok = false,
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
/// #179 Stage B: the fields present with an IDENTICAL type in EVERY variant of a shape set — the only
|
|
2516
|
+
/// fields a `union[i].field` read may soundly lower to a native `match` (a field missing from, or
|
|
2517
|
+
/// type-divergent across, some variant must stay boxed → undefined/null semantics preserved).
|
|
2518
|
+
fn shape_set_common_keys(
|
|
2519
|
+
shapes: &[Vec<(std::sync::Arc<str>, TypeAnnotation)>],
|
|
2520
|
+
) -> std::collections::HashSet<std::sync::Arc<str>> {
|
|
2521
|
+
let mut common = std::collections::HashSet::new();
|
|
2522
|
+
let Some(first) = shapes.first() else {
|
|
2523
|
+
return common;
|
|
2524
|
+
};
|
|
2525
|
+
'field: for (k, ty) in first {
|
|
2526
|
+
for other in &shapes[1..] {
|
|
2527
|
+
match other.iter().find(|(ok, _)| ok.as_ref() == k.as_ref()) {
|
|
2528
|
+
Some((_, oty)) if type_canon(oty) == type_canon(ty) => {}
|
|
2529
|
+
_ => continue 'field, // absent or type-divergent in some variant
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
common.insert(k.clone());
|
|
2533
|
+
}
|
|
2534
|
+
common
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
fn ra_stmt(s: &Statement, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
|
|
2538
|
+
use Statement::*;
|
|
2539
|
+
match s {
|
|
2540
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
2541
|
+
statements.iter().all(|x| ra_stmt(x, name, keys))
|
|
2542
|
+
}
|
|
2543
|
+
VarDecl { name: n, init, .. } => {
|
|
2544
|
+
n.as_ref() != name && init.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2545
|
+
}
|
|
2546
|
+
ExprStmt { expr, .. } => ra_expr(expr, name, keys),
|
|
2547
|
+
Return { value, .. } => value.as_ref().is_none_or(|e| ra_expr(e, name, keys)),
|
|
2548
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
2549
|
+
ra_expr(cond, name, keys)
|
|
2550
|
+
&& ra_stmt(then_branch, name, keys)
|
|
2551
|
+
&& else_branch.as_ref().is_none_or(|e| ra_stmt(e, name, keys))
|
|
2552
|
+
}
|
|
2553
|
+
While { cond, body, .. } => ra_expr(cond, name, keys) && ra_stmt(body, name, keys),
|
|
2554
|
+
DoWhile { cond, body, .. } => ra_expr(cond, name, keys) && ra_stmt(body, name, keys),
|
|
2555
|
+
For { init, cond, update, body, .. } => {
|
|
2556
|
+
init.as_ref().is_none_or(|x| ra_stmt(x, name, keys))
|
|
2557
|
+
&& cond.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2558
|
+
&& update.as_ref().is_none_or(|e| ra_expr(e, name, keys))
|
|
2559
|
+
&& ra_stmt(body, name, keys)
|
|
2560
|
+
}
|
|
2561
|
+
Break { .. } | Continue { .. } => true,
|
|
2562
|
+
_ => false,
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
fn ra_expr(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>) -> bool {
|
|
2567
|
+
use Expr::*;
|
|
2568
|
+
match e {
|
|
2569
|
+
Literal { .. } => true,
|
|
2570
|
+
Ident { name: n, .. } => n.as_ref() != name,
|
|
2571
|
+
Member { object, prop, optional: false, .. } => {
|
|
2572
|
+
let mname = match prop {
|
|
2573
|
+
tishlang_ast::MemberProp::Name { name: m, .. } => m.as_ref(),
|
|
2574
|
+
tishlang_ast::MemberProp::Expr(_) => return false,
|
|
2575
|
+
};
|
|
2576
|
+
match object.as_ref() {
|
|
2577
|
+
Ident { name: n, .. } if n.as_ref() == name => mname == "length",
|
|
2578
|
+
// `name[i].<key>` field read of THIS array. Guard on `io == name` so a DIFFERENT
|
|
2579
|
+
// record array's `other[i].field` falls through to the safe `_` recursion (bug: the
|
|
2580
|
+
// old unguarded arm returned false for it, failing any 2+-record-array program).
|
|
2581
|
+
Index { object: io, index, .. }
|
|
2582
|
+
if matches!(io.as_ref(), Ident { name: n, .. } if n.as_ref() == name) =>
|
|
2583
|
+
{
|
|
2584
|
+
keys.contains(mname) && ra_expr(index, name, keys)
|
|
2585
|
+
}
|
|
2586
|
+
_ => ra_expr(object, name, keys),
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
Member { .. } => false,
|
|
2590
|
+
Index { object, index, .. } => {
|
|
2591
|
+
if matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name) {
|
|
2592
|
+
return false;
|
|
2593
|
+
}
|
|
2594
|
+
ra_expr(object, name, keys) && ra_expr(index, name, keys)
|
|
2595
|
+
}
|
|
2596
|
+
Binary { left, right, .. } => ra_expr(left, name, keys) && ra_expr(right, name, keys),
|
|
2597
|
+
Unary { operand, .. } => ra_expr(operand, name, keys),
|
|
2598
|
+
Conditional { cond, then_branch, else_branch, .. } => {
|
|
2599
|
+
ra_expr(cond, name, keys)
|
|
2600
|
+
&& ra_expr(then_branch, name, keys)
|
|
2601
|
+
&& ra_expr(else_branch, name, keys)
|
|
2602
|
+
}
|
|
2603
|
+
Assign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2604
|
+
CompoundAssign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2605
|
+
LogicalAssign { name: n, value, .. } => n.as_ref() != name && ra_expr(value, name, keys),
|
|
2606
|
+
MemberAssign { object, value, .. } => {
|
|
2607
|
+
ra_expr(object, name, keys) && ra_expr(value, name, keys)
|
|
2608
|
+
}
|
|
2609
|
+
IndexAssign { object, index, value, .. } => {
|
|
2610
|
+
!matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name)
|
|
2611
|
+
&& ra_expr(object, name, keys)
|
|
2612
|
+
&& ra_expr(index, name, keys)
|
|
2613
|
+
&& ra_expr(value, name, keys)
|
|
2614
|
+
}
|
|
2615
|
+
Call { callee, args, .. } => {
|
|
2616
|
+
if let Member { object, prop, optional: false, .. } = callee.as_ref() {
|
|
2617
|
+
if matches!(object.as_ref(), Ident { name: n, .. } if n.as_ref() == name) {
|
|
2618
|
+
if let tishlang_ast::MemberProp::Name { name: m, .. } = prop {
|
|
2619
|
+
if m.as_ref() == "push" {
|
|
2620
|
+
return matches!(
|
|
2621
|
+
args.as_slice(),
|
|
2622
|
+
[CallArg::Expr(Object { props, .. })]
|
|
2623
|
+
if props.iter().all(|pr| matches!(pr,
|
|
2624
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) if ra_expr(v, name, keys)))
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
// `.sort(cmp)` is an in-place PERMUTATION, not an escape (shape + element
|
|
2628
|
+
// values unchanged). Safe iff the comparator does not escape `name`.
|
|
2629
|
+
if m.as_ref() == "sort" {
|
|
2630
|
+
return args
|
|
2631
|
+
.iter()
|
|
2632
|
+
.all(|a| matches!(a, CallArg::Expr(x) if ra_expr(x, name, keys)));
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
ra_expr(callee, name, keys)
|
|
2639
|
+
&& args
|
|
2640
|
+
.iter()
|
|
2641
|
+
.all(|a| matches!(a, CallArg::Expr(x) if ra_expr(x, name, keys)))
|
|
2642
|
+
}
|
|
2643
|
+
TemplateLiteral { exprs, .. } => exprs.iter().all(|x| ra_expr(x, name, keys)),
|
|
2644
|
+
PostfixInc { name: n, .. }
|
|
2645
|
+
| PostfixDec { name: n, .. }
|
|
2646
|
+
| PrefixInc { name: n, .. }
|
|
2647
|
+
| PrefixDec { name: n, .. } => n.as_ref() != name,
|
|
2648
|
+
Array { elements, .. } => elements
|
|
2649
|
+
.iter()
|
|
2650
|
+
.all(|el| matches!(el, tishlang_ast::ArrayElement::Expr(x) if ra_expr(x, name, keys))),
|
|
2651
|
+
Object { props, .. } => props
|
|
2652
|
+
.iter()
|
|
2653
|
+
.all(|pr| matches!(pr, tishlang_ast::ObjectProp::KeyValue(_, v, _) if ra_expr(v, name, keys))),
|
|
2654
|
+
// HOF callbacks (e.g. the `.sort` comparator): recurse into the body precisely — a comparator
|
|
2655
|
+
// over `a.key`/`b.key` never touches `name` (pi_mentions is too conservative on closures).
|
|
2656
|
+
ArrowFunction { body, .. } => match body {
|
|
2657
|
+
tishlang_ast::ArrowBody::Expr(e) => ra_expr(e, name, keys),
|
|
2658
|
+
tishlang_ast::ArrowBody::Block(st) => ra_stmt(st, name, keys),
|
|
2659
|
+
},
|
|
2660
|
+
_ => false,
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
1230
2664
|
/// Define every provably-numeric block-local into `hyp` (flat across nested scopes): annotated
|
|
1231
2665
|
/// `: number`, a number-literal init, or any init `infer_expr_type` proves `number` *under the
|
|
1232
2666
|
/// current hypothesis* — so `let temp = perm[i]` becomes `number` once `perm` is hypothesized
|
|
@@ -1292,7 +2726,12 @@ fn block_native_arrays(
|
|
|
1292
2726
|
// Empty `[]` is a candidate for either element type; literal elements must match `elem`.
|
|
1293
2727
|
let lit_ok = elements.is_empty()
|
|
1294
2728
|
|| matches!(infer_array_elem(elements, outer_ctx), Some(t) if &t == elem);
|
|
1295
|
-
|
|
2729
|
+
// Soundness: a non-empty literal array accessed at a constant index past its initial
|
|
2730
|
+
// length is sparse (`let arr=[1,2,3]; arr[5]=100`) — the native Vec pads holes with NaN
|
|
2731
|
+
// and OOB reads yield NaN, diverging from boxed `null`. Keep it boxed.
|
|
2732
|
+
let sparse = !elements.is_empty()
|
|
2733
|
+
&& array_oob_const_access(&stmts[i + 1..], name.as_ref(), elements.len());
|
|
2734
|
+
if lit_ok && !sparse {
|
|
1296
2735
|
cands.push((i, name.to_string()));
|
|
1297
2736
|
}
|
|
1298
2737
|
}
|
|
@@ -1330,6 +2769,40 @@ fn block_native_arrays(
|
|
|
1330
2769
|
accepted
|
|
1331
2770
|
}
|
|
1332
2771
|
|
|
2772
|
+
/// True if `stmts` access `name` at a **constant** non-negative integer index `>= init_len`
|
|
2773
|
+
/// (`name[K]` read or `name[K] = v`). For a non-empty literal-initialized array, such an index is
|
|
2774
|
+
/// out of the initial bounds: the native `Vec<elem>` representation pads sparse writes with
|
|
2775
|
+
/// `NaN`/`0`/`false` and yields those on OOB reads, whereas the boxed array grows with `null` holes
|
|
2776
|
+
/// and reads `null`. So an array with such an access must stay boxed to preserve semantics. Only
|
|
2777
|
+
/// meaningful for non-empty literal inits — an empty `[]` grows dynamically (length unknown here),
|
|
2778
|
+
/// and its holes are typically only truth-tested (`NaN`/`null` are both falsy), so it is left alone.
|
|
2779
|
+
fn array_oob_const_access(stmts: &[Statement], name: &str, init_len: usize) -> bool {
|
|
2780
|
+
let mut found = false;
|
|
2781
|
+
for s in stmts {
|
|
2782
|
+
walk_exprs_stmt(s, &mut |e| {
|
|
2783
|
+
let idx = match e {
|
|
2784
|
+
Expr::Index { object, index, .. } | Expr::IndexAssign { object, index, .. } => {
|
|
2785
|
+
if matches!(object.as_ref(), Expr::Ident { name: n, .. } if n.as_ref() == name) {
|
|
2786
|
+
index
|
|
2787
|
+
} else {
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
_ => return,
|
|
2792
|
+
};
|
|
2793
|
+
if let Expr::Literal { value: Literal::Number(k), .. } = idx.as_ref() {
|
|
2794
|
+
if *k >= 0.0 && k.fract() == 0.0 && (*k as usize) >= init_len {
|
|
2795
|
+
found = true;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
if found {
|
|
2800
|
+
return true;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
found
|
|
2804
|
+
}
|
|
2805
|
+
|
|
1333
2806
|
/// A value written into / pushed onto `name` must have the array's element type `elem`: a `name[_]`
|
|
1334
2807
|
/// self-read (already `elem` by hypothesis) or any expression `infer_expr_type` proves is `elem`.
|
|
1335
2808
|
fn mut_arr_value_ok(v: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx) -> bool {
|
|
@@ -1379,7 +2852,8 @@ fn mut_arr_stmt_ok(s: &Statement, name: &str, elem: &TypeAnnotation, hyp: &Infer
|
|
|
1379
2852
|
Return { value, .. } => value.as_ref().is_none_or(|e| mut_arr_expr_ok(e, name, elem, hyp)),
|
|
1380
2853
|
Throw { value, .. } => mut_arr_expr_ok(value, name, elem, hyp),
|
|
1381
2854
|
Break { .. } | Continue { .. } | TypeAlias { .. } => true,
|
|
1382
|
-
|
|
2855
|
+
FunDecl { body, .. } => mut_arr_stmt_ok(body, name, elem, hyp),
|
|
2856
|
+
_ => false, // switch / try / etc: bail
|
|
1383
2857
|
}
|
|
1384
2858
|
}
|
|
1385
2859
|
|
|
@@ -1420,6 +2894,20 @@ fn mut_arr_expr_ok(e: &Expr, name: &str, elem: &TypeAnnotation, hyp: &InferCtx)
|
|
|
1420
2894
|
});
|
|
1421
2895
|
}
|
|
1422
2896
|
}
|
|
2897
|
+
// #175: forwarding `name` into a native-vec fn's ARRAY param is a native use, not a
|
|
2898
|
+
// boxing escape — the callee takes it by `&/&mut Vec<elem>`, so the array stays unboxed.
|
|
2899
|
+
if let Ident { name: fname, .. } = callee.as_ref() {
|
|
2900
|
+
if let Some(is_arr) = hyp.native_vec_array_params.get(fname.as_ref()) {
|
|
2901
|
+
if args.len() == is_arr.len() {
|
|
2902
|
+
return args.iter().enumerate().all(|(i, a)| match a {
|
|
2903
|
+
tishlang_ast::CallArg::Expr(v) => {
|
|
2904
|
+
(is_name(v) && is_arr[i]) || mut_arr_expr_ok(v, name, elem, hyp)
|
|
2905
|
+
}
|
|
2906
|
+
tishlang_ast::CallArg::Spread(_) => false,
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
1423
2911
|
mut_arr_expr_ok(callee, name, elem, hyp)
|
|
1424
2912
|
&& args.iter().all(|a| match a {
|
|
1425
2913
|
tishlang_ast::CallArg::Expr(v) => mut_arr_expr_ok(v, name, elem, hyp),
|
|
@@ -1514,6 +3002,7 @@ fn arr_stmt_safe(s: &Statement, name: &str) -> bool {
|
|
|
1514
3002
|
Return { value, .. } => arr_opt_expr_safe(value, name),
|
|
1515
3003
|
Throw { value, .. } => arr_expr_safe(value, name),
|
|
1516
3004
|
Break { .. } | Continue { .. } | TypeAlias { .. } => true,
|
|
3005
|
+
FunDecl { body, .. } => arr_stmt_safe(body, name),
|
|
1517
3006
|
// Nested fn could capture+mutate; switch/try and anything else: be safe, bail.
|
|
1518
3007
|
_ => false,
|
|
1519
3008
|
}
|
|
@@ -1726,10 +3215,33 @@ fn expr_name_safe(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>)
|
|
|
1726
3215
|
expr_name_safe(e, name, keys)
|
|
1727
3216
|
}
|
|
1728
3217
|
}),
|
|
1729
|
-
Object { props, .. } =>
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
3218
|
+
Object { props, .. } => {
|
|
3219
|
+
// A `{ ...name, k: v, … }` immutable-update spreads `name` — a read-only bulk field read,
|
|
3220
|
+
// not an escape — and `inline_struct_spread` will lower it to a plain struct literal
|
|
3221
|
+
// (`{ a: name.a, …, k: v }`). Treat a leading self-spread as SAFE iff it matches that
|
|
3222
|
+
// inlining's eligibility EXACTLY: first prop, the only spread, and every other prop is a
|
|
3223
|
+
// `KeyValue` whose key doesn't collide with `name`'s keys (and whose value is name-safe).
|
|
3224
|
+
// Consistency: `name` then structifies iff the spread is inlined, so there's never a
|
|
3225
|
+
// struct spread into a boxed object (which would miscompile).
|
|
3226
|
+
let leading_self_spread = matches!(
|
|
3227
|
+
props.first(),
|
|
3228
|
+
Some(tishlang_ast::ObjectProp::Spread(Expr::Ident { name: n, .. })) if n.as_ref() == name
|
|
3229
|
+
);
|
|
3230
|
+
if leading_self_spread
|
|
3231
|
+
&& props[1..].iter().all(|p| match p {
|
|
3232
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
3233
|
+
!keys.contains(k.as_ref()) && expr_name_safe(v, name, keys)
|
|
3234
|
+
}
|
|
3235
|
+
tishlang_ast::ObjectProp::Spread(_) => false,
|
|
3236
|
+
})
|
|
3237
|
+
{
|
|
3238
|
+
return true;
|
|
3239
|
+
}
|
|
3240
|
+
props.iter().all(|p| match p {
|
|
3241
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => expr_name_safe(v, name, keys),
|
|
3242
|
+
tishlang_ast::ObjectProp::Spread(e) => expr_name_safe(e, name, keys),
|
|
3243
|
+
})
|
|
3244
|
+
}
|
|
1733
3245
|
TemplateLiteral { exprs, .. } => exprs.iter().all(|e| expr_name_safe(e, name, keys)),
|
|
1734
3246
|
// Reassignment / mutation referencing `name` by identifier → unsafe.
|
|
1735
3247
|
Assign { name: n, value, .. }
|
|
@@ -2153,6 +3665,11 @@ fn collect_return_shapes(
|
|
|
2153
3665
|
pub fn analyze_aggregate(program: &Program) -> AggregateAnalysis {
|
|
2154
3666
|
let mut analysis = AggregateAnalysis::default();
|
|
2155
3667
|
let mut reg = StructRegistry::default();
|
|
3668
|
+
// Soundness: a fn called ANYWHERE with a provably-non-numeric arg must not get S-0 `: number`
|
|
3669
|
+
// params either — `fn makeRecord(id, name){ return {id, name} }` called `makeRecord(1, "Alice")`
|
|
3670
|
+
// would otherwise emit a `match … panic!("expected number")` coercion on the String. Same set the
|
|
3671
|
+
// M4 `param_infer` path uses (infer.rs:273).
|
|
3672
|
+
let nonnumeric = crate::codegen::Codegen::fns_called_with_nonnumeric_arg(&program.statements);
|
|
2156
3673
|
|
|
2157
3674
|
// ---- S-0 + S-A: per top-level function, find numeric params and a return shape.
|
|
2158
3675
|
for s in &program.statements {
|
|
@@ -2204,7 +3721,7 @@ pub fn analyze_aggregate(program: &Program) -> AggregateAnalysis {
|
|
|
2204
3721
|
}
|
|
2205
3722
|
}
|
|
2206
3723
|
}
|
|
2207
|
-
if !numeric_params.is_empty() {
|
|
3724
|
+
if !numeric_params.is_empty() && !nonnumeric.contains(name.as_ref()) {
|
|
2208
3725
|
analysis.fn_numeric_params.insert(name.to_string(), numeric_params);
|
|
2209
3726
|
}
|
|
2210
3727
|
// S-A: a single all-f64 object-literal return shape ⇒ a struct alias.
|