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