@tishlang/tish-lsp 2.12.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.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,50 @@ 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
|
-
|
|
335
|
+
Statement::ForOf {
|
|
336
|
+
name: n,
|
|
337
|
+
iterable: e,
|
|
338
|
+
body,
|
|
339
|
+
..
|
|
280
340
|
}
|
|
281
|
-
Statement::
|
|
341
|
+
| Statement::ForIn {
|
|
342
|
+
name: n,
|
|
343
|
+
object: e,
|
|
344
|
+
body,
|
|
345
|
+
..
|
|
346
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || stmt_rebinds(body, name),
|
|
347
|
+
Statement::Switch {
|
|
348
|
+
expr,
|
|
349
|
+
cases,
|
|
350
|
+
default_body,
|
|
351
|
+
..
|
|
352
|
+
} => {
|
|
282
353
|
expr_rebinds(expr, name)
|
|
283
354
|
|| cases.iter().any(|(t, body)| {
|
|
284
355
|
t.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
@@ -288,7 +359,12 @@ fn stmt_rebinds(s: &Statement, name: &str) -> bool {
|
|
|
288
359
|
.as_ref()
|
|
289
360
|
.is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
|
|
290
361
|
}
|
|
291
|
-
Statement::Try {
|
|
362
|
+
Statement::Try {
|
|
363
|
+
body,
|
|
364
|
+
catch_body,
|
|
365
|
+
finally_body,
|
|
366
|
+
..
|
|
367
|
+
} => {
|
|
292
368
|
stmt_rebinds(body, name)
|
|
293
369
|
|| catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
294
370
|
|| finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
@@ -305,7 +381,9 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
305
381
|
match e {
|
|
306
382
|
Expr::Assign { name: n, value, .. }
|
|
307
383
|
| Expr::CompoundAssign { name: n, value, .. }
|
|
308
|
-
| Expr::LogicalAssign { name: n, value, .. } =>
|
|
384
|
+
| Expr::LogicalAssign { name: n, value, .. } => {
|
|
385
|
+
n.as_ref() == name || expr_rebinds(value, name)
|
|
386
|
+
}
|
|
309
387
|
Expr::PostfixInc { name: n, .. }
|
|
310
388
|
| Expr::PostfixDec { name: n, .. }
|
|
311
389
|
| Expr::PrefixInc { name: n, .. }
|
|
@@ -314,11 +392,18 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
314
392
|
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
315
393
|
expr_rebinds(left, name) || expr_rebinds(right, name)
|
|
316
394
|
}
|
|
317
|
-
Expr::Unary { operand, .. }
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
Expr::Conditional {
|
|
321
|
-
|
|
395
|
+
Expr::Unary { operand, .. }
|
|
396
|
+
| Expr::TypeOf { operand, .. }
|
|
397
|
+
| Expr::Await { operand, .. } => expr_rebinds(operand, name),
|
|
398
|
+
Expr::Conditional {
|
|
399
|
+
cond,
|
|
400
|
+
then_branch,
|
|
401
|
+
else_branch,
|
|
402
|
+
..
|
|
403
|
+
} => {
|
|
404
|
+
expr_rebinds(cond, name)
|
|
405
|
+
|| expr_rebinds(then_branch, name)
|
|
406
|
+
|| expr_rebinds(else_branch, name)
|
|
322
407
|
}
|
|
323
408
|
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
324
409
|
expr_rebinds(callee, name)
|
|
@@ -327,29 +412,203 @@ fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
|
327
412
|
})
|
|
328
413
|
}
|
|
329
414
|
Expr::Member { object, .. } => expr_rebinds(object, name),
|
|
330
|
-
Expr::Index { object, index, .. } =>
|
|
415
|
+
Expr::Index { object, index, .. } => {
|
|
416
|
+
expr_rebinds(object, name) || expr_rebinds(index, name)
|
|
417
|
+
}
|
|
331
418
|
Expr::Array { elements, .. } => elements.iter().any(|el| match el {
|
|
332
419
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
|
|
333
420
|
}),
|
|
334
421
|
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
335
422
|
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_rebinds(e, name),
|
|
336
423
|
}),
|
|
337
|
-
Expr::MemberAssign { object, value, .. } =>
|
|
338
|
-
|
|
339
|
-
expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name)
|
|
424
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
425
|
+
expr_rebinds(object, name) || expr_rebinds(value, name)
|
|
340
426
|
}
|
|
427
|
+
Expr::IndexAssign {
|
|
428
|
+
object,
|
|
429
|
+
index,
|
|
430
|
+
value,
|
|
431
|
+
..
|
|
432
|
+
} => expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name),
|
|
341
433
|
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
|
-
|
|
434
|
+
// A nested closure could reassign the outer `name` — in its body OR a param default (which
|
|
435
|
+
// evaluates in the enclosing scope). Recurse into both (over-conservative if it shadows, which
|
|
436
|
+
// only costs the optimization). #187: the param-default scan closes the `(a = (foo = x)) => …` hole.
|
|
437
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
438
|
+
params_default_rebinds(params, name)
|
|
439
|
+
|| match body {
|
|
440
|
+
ArrowBody::Expr(e) => expr_rebinds(e, name),
|
|
441
|
+
ArrowBody::Block(s) => stmt_rebinds(s, name),
|
|
442
|
+
}
|
|
443
|
+
}
|
|
348
444
|
// Jsx, NativeModuleLoad, and anything unknown → conservative.
|
|
349
445
|
_ => true,
|
|
350
446
|
}
|
|
351
447
|
}
|
|
352
448
|
|
|
449
|
+
/// #187: whole-program scan — does `name` get REBOUND (assigned/updated, redeclared via `let`/`const`/
|
|
450
|
+
/// `for-of`/`catch`, shadowed by a param, or (re)declared via a `function name`) anywhere in `s`,
|
|
451
|
+
/// INCLUDING inside every function body? Unlike [`stmt_rebinds`] (which returns `true` on any `FunDecl`
|
|
452
|
+
/// so it can't run program-wide), this has an explicit `FunDecl` arm: a `function name` is itself a
|
|
453
|
+
/// binding, and every function body is recursed into. `true` (or any node it can't analyze) ⇒ NOT
|
|
454
|
+
/// stable — which only forgoes the cross-function-call optimization, never risks a miscompile.
|
|
455
|
+
fn name_rebinds_in_stmt(s: &Statement, name: &str) -> bool {
|
|
456
|
+
match s {
|
|
457
|
+
Statement::FunDecl {
|
|
458
|
+
name: n,
|
|
459
|
+
params,
|
|
460
|
+
rest_param,
|
|
461
|
+
body,
|
|
462
|
+
..
|
|
463
|
+
} => {
|
|
464
|
+
n.as_ref() == name
|
|
465
|
+
|| params_bind_name(params, name)
|
|
466
|
+
|| params_default_rebinds(params, name)
|
|
467
|
+
|| rest_param
|
|
468
|
+
.as_ref()
|
|
469
|
+
.is_some_and(|rp| rp.name.as_ref() == name)
|
|
470
|
+
|| name_rebinds_in_stmt(body, name)
|
|
471
|
+
}
|
|
472
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
473
|
+
statements.iter().any(|s| name_rebinds_in_stmt(s, name))
|
|
474
|
+
}
|
|
475
|
+
Statement::VarDecl { name: n, init, .. } => {
|
|
476
|
+
n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
477
|
+
}
|
|
478
|
+
Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
|
|
479
|
+
Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
|
|
480
|
+
Statement::Throw { value, .. } => expr_rebinds(value, name),
|
|
481
|
+
Statement::If {
|
|
482
|
+
cond,
|
|
483
|
+
then_branch,
|
|
484
|
+
else_branch,
|
|
485
|
+
..
|
|
486
|
+
} => {
|
|
487
|
+
expr_rebinds(cond, name)
|
|
488
|
+
|| name_rebinds_in_stmt(then_branch, name)
|
|
489
|
+
|| else_branch
|
|
490
|
+
.as_ref()
|
|
491
|
+
.is_some_and(|s| name_rebinds_in_stmt(s, name))
|
|
492
|
+
}
|
|
493
|
+
Statement::While { cond, body, .. } => {
|
|
494
|
+
expr_rebinds(cond, name) || name_rebinds_in_stmt(body, name)
|
|
495
|
+
}
|
|
496
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
497
|
+
name_rebinds_in_stmt(body, name) || expr_rebinds(cond, name)
|
|
498
|
+
}
|
|
499
|
+
Statement::For {
|
|
500
|
+
init,
|
|
501
|
+
cond,
|
|
502
|
+
update,
|
|
503
|
+
body,
|
|
504
|
+
..
|
|
505
|
+
} => {
|
|
506
|
+
init.as_ref().is_some_and(|s| name_rebinds_in_stmt(s, name))
|
|
507
|
+
|| cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
508
|
+
|| update.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
509
|
+
|| name_rebinds_in_stmt(body, name)
|
|
510
|
+
}
|
|
511
|
+
Statement::ForOf {
|
|
512
|
+
name: n,
|
|
513
|
+
iterable: e,
|
|
514
|
+
body,
|
|
515
|
+
..
|
|
516
|
+
}
|
|
517
|
+
| Statement::ForIn {
|
|
518
|
+
name: n,
|
|
519
|
+
object: e,
|
|
520
|
+
body,
|
|
521
|
+
..
|
|
522
|
+
} => n.as_ref() == name || expr_rebinds(e, name) || name_rebinds_in_stmt(body, name),
|
|
523
|
+
Statement::Switch {
|
|
524
|
+
expr,
|
|
525
|
+
cases,
|
|
526
|
+
default_body,
|
|
527
|
+
..
|
|
528
|
+
} => {
|
|
529
|
+
expr_rebinds(expr, name)
|
|
530
|
+
|| cases.iter().any(|(t, body)| {
|
|
531
|
+
t.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
532
|
+
|| body.iter().any(|s| name_rebinds_in_stmt(s, name))
|
|
533
|
+
})
|
|
534
|
+
|| default_body
|
|
535
|
+
.as_ref()
|
|
536
|
+
.is_some_and(|b| b.iter().any(|s| name_rebinds_in_stmt(s, name)))
|
|
537
|
+
}
|
|
538
|
+
Statement::Try {
|
|
539
|
+
body,
|
|
540
|
+
catch_body,
|
|
541
|
+
finally_body,
|
|
542
|
+
..
|
|
543
|
+
} => {
|
|
544
|
+
name_rebinds_in_stmt(body, name)
|
|
545
|
+
|| catch_body
|
|
546
|
+
.as_ref()
|
|
547
|
+
.is_some_and(|s| name_rebinds_in_stmt(s, name))
|
|
548
|
+
|| finally_body
|
|
549
|
+
.as_ref()
|
|
550
|
+
.is_some_and(|s| name_rebinds_in_stmt(s, name))
|
|
551
|
+
}
|
|
552
|
+
Statement::Break { .. } | Statement::Continue { .. } => false,
|
|
553
|
+
// VarDeclDestructure (could bind `name`) + any unknown construct → conservative.
|
|
554
|
+
_ => true,
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// #187: the set of top-level function names that are PROVABLY stable across the whole program — each
|
|
559
|
+
/// declared exactly once as a top-level `function N`, never also bound by a top-level `let`/`var`/
|
|
560
|
+
/// `const`, and never reassigned/redeclared/shadowed anywhere (via [`name_rebinds_in_stmt`], skipping
|
|
561
|
+
/// the single defining declaration but still recursing into its body). A direct native call to such a
|
|
562
|
+
/// callee can never dispatch a stale function, because the binding can never change.
|
|
563
|
+
fn compute_stable_globals(program: &Program) -> std::collections::HashSet<Arc<str>> {
|
|
564
|
+
let mut fn_count: HashMap<Arc<str>, usize> = HashMap::new();
|
|
565
|
+
let mut nonfn_toplevel: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
|
|
566
|
+
for s in &program.statements {
|
|
567
|
+
match s {
|
|
568
|
+
Statement::FunDecl { name, .. } => *fn_count.entry(Arc::clone(name)).or_insert(0) += 1,
|
|
569
|
+
Statement::VarDecl { name, .. } => {
|
|
570
|
+
nonfn_toplevel.insert(Arc::clone(name));
|
|
571
|
+
}
|
|
572
|
+
_ => {}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
let mut stable: std::collections::HashSet<Arc<str>> = std::collections::HashSet::new();
|
|
576
|
+
'cand: for (name, &count) in &fn_count {
|
|
577
|
+
if count != 1 || nonfn_toplevel.contains(name) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
for s in &program.statements {
|
|
581
|
+
match s {
|
|
582
|
+
// The single defining `function name`: not a rebind, but its body/params still count.
|
|
583
|
+
Statement::FunDecl {
|
|
584
|
+
name: n,
|
|
585
|
+
params,
|
|
586
|
+
rest_param,
|
|
587
|
+
body,
|
|
588
|
+
..
|
|
589
|
+
} if n == name => {
|
|
590
|
+
if params_bind_name(params, name)
|
|
591
|
+
|| params_default_rebinds(params, name)
|
|
592
|
+
|| rest_param
|
|
593
|
+
.as_ref()
|
|
594
|
+
.is_some_and(|rp| rp.name.as_ref() == name.as_ref())
|
|
595
|
+
|| name_rebinds_in_stmt(body, name)
|
|
596
|
+
{
|
|
597
|
+
continue 'cand;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
other => {
|
|
601
|
+
if name_rebinds_in_stmt(other, name) {
|
|
602
|
+
continue 'cand;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
stable.insert(Arc::clone(name));
|
|
608
|
+
}
|
|
609
|
+
stable
|
|
610
|
+
}
|
|
611
|
+
|
|
353
612
|
/// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
|
|
354
613
|
/// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
|
|
355
614
|
/// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
|
|
@@ -367,19 +626,42 @@ struct SlotScan {
|
|
|
367
626
|
impl SlotScan {
|
|
368
627
|
fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
|
|
369
628
|
match s {
|
|
370
|
-
Statement::Block { statements, .. } =>
|
|
371
|
-
|
|
372
|
-
|
|
629
|
+
Statement::Block { statements, .. } => {
|
|
630
|
+
statements.iter().all(|s| self.stmt(s, in_closure))
|
|
631
|
+
}
|
|
632
|
+
Statement::Multi { statements, .. } => {
|
|
633
|
+
statements.iter().all(|s| self.stmt(s, in_closure))
|
|
634
|
+
}
|
|
635
|
+
Statement::VarDecl { init, .. } => {
|
|
636
|
+
init.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
637
|
+
}
|
|
373
638
|
Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
|
|
374
639
|
Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
|
|
375
|
-
Statement::If {
|
|
640
|
+
Statement::If {
|
|
641
|
+
cond,
|
|
642
|
+
then_branch,
|
|
643
|
+
else_branch,
|
|
644
|
+
..
|
|
645
|
+
} => {
|
|
376
646
|
self.expr(cond, in_closure)
|
|
377
647
|
&& self.stmt(then_branch, in_closure)
|
|
378
|
-
&& else_branch
|
|
648
|
+
&& else_branch
|
|
649
|
+
.as_ref()
|
|
650
|
+
.is_none_or(|s| self.stmt(s, in_closure))
|
|
651
|
+
}
|
|
652
|
+
Statement::While { cond, body, .. } => {
|
|
653
|
+
self.expr(cond, in_closure) && self.stmt(body, in_closure)
|
|
379
654
|
}
|
|
380
|
-
Statement::
|
|
381
|
-
|
|
382
|
-
|
|
655
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
656
|
+
self.stmt(body, in_closure) && self.expr(cond, in_closure)
|
|
657
|
+
}
|
|
658
|
+
Statement::For {
|
|
659
|
+
init,
|
|
660
|
+
cond,
|
|
661
|
+
update,
|
|
662
|
+
body,
|
|
663
|
+
..
|
|
664
|
+
} => {
|
|
383
665
|
init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
|
|
384
666
|
&& cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
385
667
|
&& update.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
@@ -388,10 +670,17 @@ impl SlotScan {
|
|
|
388
670
|
Statement::ForOf { iterable, body, .. } => {
|
|
389
671
|
self.expr(iterable, in_closure) && self.stmt(body, in_closure)
|
|
390
672
|
}
|
|
391
|
-
Statement::Return { value, .. } =>
|
|
673
|
+
Statement::Return { value, .. } => {
|
|
674
|
+
value.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
675
|
+
}
|
|
392
676
|
Statement::Throw { value, .. } => self.expr(value, in_closure),
|
|
393
677
|
Statement::Break { .. } | Statement::Continue { .. } => true,
|
|
394
|
-
Statement::Switch {
|
|
678
|
+
Statement::Switch {
|
|
679
|
+
expr,
|
|
680
|
+
cases,
|
|
681
|
+
default_body,
|
|
682
|
+
..
|
|
683
|
+
} => {
|
|
395
684
|
if !self.expr(expr, in_closure) {
|
|
396
685
|
return false;
|
|
397
686
|
}
|
|
@@ -409,10 +698,17 @@ impl SlotScan {
|
|
|
409
698
|
.as_ref()
|
|
410
699
|
.is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
|
|
411
700
|
}
|
|
412
|
-
Statement::Try {
|
|
701
|
+
Statement::Try {
|
|
702
|
+
body,
|
|
703
|
+
catch_body,
|
|
704
|
+
finally_body,
|
|
705
|
+
..
|
|
706
|
+
} => {
|
|
413
707
|
self.stmt(body, in_closure)
|
|
414
708
|
&& catch_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
|
|
415
|
-
&& finally_body
|
|
709
|
+
&& finally_body
|
|
710
|
+
.as_ref()
|
|
711
|
+
.is_none_or(|s| self.stmt(s, in_closure))
|
|
416
712
|
}
|
|
417
713
|
// A nested named function: its param defaults (enclosing-scope) + whole body capture.
|
|
418
714
|
Statement::FunDecl { params, body, .. } => {
|
|
@@ -432,14 +728,25 @@ impl SlotScan {
|
|
|
432
728
|
}
|
|
433
729
|
true
|
|
434
730
|
}
|
|
435
|
-
Expr::Binary { left, right, .. } =>
|
|
436
|
-
|
|
437
|
-
self.expr(operand, in_closure)
|
|
731
|
+
Expr::Binary { left, right, .. } => {
|
|
732
|
+
self.expr(left, in_closure) && self.expr(right, in_closure)
|
|
438
733
|
}
|
|
439
|
-
Expr::
|
|
440
|
-
|
|
734
|
+
Expr::Unary { operand, .. }
|
|
735
|
+
| Expr::TypeOf { operand, .. }
|
|
736
|
+
| Expr::Await { operand, .. } => self.expr(operand, in_closure),
|
|
737
|
+
Expr::Conditional {
|
|
738
|
+
cond,
|
|
739
|
+
then_branch,
|
|
740
|
+
else_branch,
|
|
741
|
+
..
|
|
742
|
+
} => {
|
|
743
|
+
self.expr(cond, in_closure)
|
|
744
|
+
&& self.expr(then_branch, in_closure)
|
|
745
|
+
&& self.expr(else_branch, in_closure)
|
|
746
|
+
}
|
|
747
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
748
|
+
self.expr(left, in_closure) && self.expr(right, in_closure)
|
|
441
749
|
}
|
|
442
|
-
Expr::NullishCoalesce { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
|
|
443
750
|
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
444
751
|
self.expr(callee, in_closure)
|
|
445
752
|
&& args.iter().all(|a| match a {
|
|
@@ -447,7 +754,9 @@ impl SlotScan {
|
|
|
447
754
|
})
|
|
448
755
|
}
|
|
449
756
|
Expr::Member { object, .. } => self.expr(object, in_closure),
|
|
450
|
-
Expr::Index { object, index, .. } =>
|
|
757
|
+
Expr::Index { object, index, .. } => {
|
|
758
|
+
self.expr(object, in_closure) && self.expr(index, in_closure)
|
|
759
|
+
}
|
|
451
760
|
Expr::Array { elements, .. } => elements.iter().all(|el| match el {
|
|
452
761
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
|
|
453
762
|
}),
|
|
@@ -462,9 +771,18 @@ impl SlotScan {
|
|
|
462
771
|
}
|
|
463
772
|
self.expr(value, in_closure)
|
|
464
773
|
}
|
|
465
|
-
Expr::MemberAssign { object, value, .. } =>
|
|
466
|
-
|
|
467
|
-
|
|
774
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
775
|
+
self.expr(object, in_closure) && self.expr(value, in_closure)
|
|
776
|
+
}
|
|
777
|
+
Expr::IndexAssign {
|
|
778
|
+
object,
|
|
779
|
+
index,
|
|
780
|
+
value,
|
|
781
|
+
..
|
|
782
|
+
} => {
|
|
783
|
+
self.expr(object, in_closure)
|
|
784
|
+
&& self.expr(index, in_closure)
|
|
785
|
+
&& self.expr(value, in_closure)
|
|
468
786
|
}
|
|
469
787
|
Expr::PostfixInc { name, .. }
|
|
470
788
|
| Expr::PostfixDec { name, .. }
|
|
@@ -511,7 +829,11 @@ impl SlotScan {
|
|
|
511
829
|
/// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
|
|
512
830
|
/// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
|
|
513
831
|
/// (the VM binds params into slots 0..n, but a closure reads captures by name from `local_scope`).
|
|
514
|
-
fn slot_analyze(
|
|
832
|
+
fn slot_analyze(
|
|
833
|
+
params: &[FunParam],
|
|
834
|
+
has_rest: bool,
|
|
835
|
+
body: &Statement,
|
|
836
|
+
) -> Option<HashSet<Arc<str>>> {
|
|
515
837
|
if !slots_enabled() || has_rest {
|
|
516
838
|
return None;
|
|
517
839
|
}
|
|
@@ -559,7 +881,10 @@ impl<'a> Compiler<'a> {
|
|
|
559
881
|
return Some(*s);
|
|
560
882
|
}
|
|
561
883
|
}
|
|
562
|
-
self.slot_scopes
|
|
884
|
+
self.slot_scopes
|
|
885
|
+
.iter()
|
|
886
|
+
.rev()
|
|
887
|
+
.find_map(|m| m.get(name).copied())
|
|
563
888
|
}
|
|
564
889
|
|
|
565
890
|
/// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
|
|
@@ -598,6 +923,8 @@ impl<'a> Compiler<'a> {
|
|
|
598
923
|
general_slots: false,
|
|
599
924
|
finally_stack: Vec::new(),
|
|
600
925
|
self_fn_name: None,
|
|
926
|
+
math_is_global: false,
|
|
927
|
+
stable_globals: Arc::new(std::collections::HashSet::new()),
|
|
601
928
|
}
|
|
602
929
|
}
|
|
603
930
|
|
|
@@ -842,10 +1169,7 @@ impl<'a> Compiler<'a> {
|
|
|
842
1169
|
/// condition shape stays eligible for the cranelift JIT (#167). Returns the `JumpIfFalse` patch
|
|
843
1170
|
/// sites the caller must point at its "condition is false" target. The condition value itself is
|
|
844
1171
|
/// 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> {
|
|
1172
|
+
fn compile_condition_jump_if_false(&mut self, cond: &Expr) -> Result<Vec<usize>, CompileError> {
|
|
849
1173
|
match cond {
|
|
850
1174
|
// `a && b` is false if EITHER operand is false — test each in order; both exit to the
|
|
851
1175
|
// same false-target, so concatenate their patch sites.
|
|
@@ -1232,6 +1556,48 @@ impl<'a> Compiler<'a> {
|
|
|
1232
1556
|
self.compile_destructure(pattern, false, false)?;
|
|
1233
1557
|
}
|
|
1234
1558
|
Statement::ExprStmt { expr, .. } => {
|
|
1559
|
+
// String-builder fast path: statement-position `acc += rhs` on a frame-slot local
|
|
1560
|
+
// compiles to `<rhs>; AppendLocal slot` (no LoadLocal/Dup/StoreLocal/Pop), letting
|
|
1561
|
+
// the VM append in amortized O(1) without materializing the discarded result. Only
|
|
1562
|
+
// for a simple slot-resolved identifier with the `+` compound op; everything else
|
|
1563
|
+
// (name-based vars, other ops) keeps the generic path.
|
|
1564
|
+
if let Expr::CompoundAssign {
|
|
1565
|
+
name,
|
|
1566
|
+
op: CompoundOp::Add,
|
|
1567
|
+
value,
|
|
1568
|
+
..
|
|
1569
|
+
} = expr
|
|
1570
|
+
{
|
|
1571
|
+
if let Some(slot) = self.resolve_slot(name) {
|
|
1572
|
+
self.compile_expr(value)?;
|
|
1573
|
+
self.emit_u16(Opcode::AppendLocal, slot);
|
|
1574
|
+
return Ok(());
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
// Same builder fast path for the plain-assign spelling `s = s + <str>` — but ONLY when
|
|
1578
|
+
// the appended operand is PROVABLY a string. A numeric accumulator (`i = i + 1`) must
|
|
1579
|
+
// NOT builder-ize: the VM keeps a single string builder slot, so a second builder-ized
|
|
1580
|
+
// slot flushes the first every iteration → O(n²) (#186). Restricting to string-typed
|
|
1581
|
+
// RHS keeps `s = s + "x"` fast (string_concat) while `i = i + 1` stays a plain store.
|
|
1582
|
+
if let Expr::Assign { name, value, .. } = expr {
|
|
1583
|
+
if let Expr::Binary {
|
|
1584
|
+
left,
|
|
1585
|
+
op: BinOp::Add,
|
|
1586
|
+
right,
|
|
1587
|
+
..
|
|
1588
|
+
} = value.as_ref()
|
|
1589
|
+
{
|
|
1590
|
+
if matches!(left.as_ref(), Expr::Ident { name: ln, .. } if ln == name)
|
|
1591
|
+
&& is_string_typed(right)
|
|
1592
|
+
{
|
|
1593
|
+
if let Some(slot) = self.resolve_slot(name) {
|
|
1594
|
+
self.compile_expr(right)?;
|
|
1595
|
+
self.emit_u16(Opcode::AppendLocal, slot);
|
|
1596
|
+
return Ok(());
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1235
1601
|
self.compile_expr(expr)?;
|
|
1236
1602
|
self.emit(Opcode::Pop);
|
|
1237
1603
|
}
|
|
@@ -1449,6 +1815,46 @@ impl<'a> Compiler<'a> {
|
|
|
1449
1815
|
self.emit(Opcode::LoopVarsEnd);
|
|
1450
1816
|
self.exit_block_scope();
|
|
1451
1817
|
}
|
|
1818
|
+
Statement::ForIn {
|
|
1819
|
+
name,
|
|
1820
|
+
name_span,
|
|
1821
|
+
object,
|
|
1822
|
+
body,
|
|
1823
|
+
span,
|
|
1824
|
+
} => {
|
|
1825
|
+
// Lower `for (let k in obj)` to `for (let k of Object.keys(obj))` (#413): Object.keys
|
|
1826
|
+
// yields exactly the own enumerable keys the interpreter enumerates — insertion order
|
|
1827
|
+
// for objects, index strings for arrays, and `[]` for a non-object (so `for (k in
|
|
1828
|
+
// null)` no-ops). Reuses the whole for-of iteration path.
|
|
1829
|
+
let dummy = Span {
|
|
1830
|
+
start: (0, 0),
|
|
1831
|
+
end: (0, 0),
|
|
1832
|
+
};
|
|
1833
|
+
let keys_call = Expr::Call {
|
|
1834
|
+
callee: Box::new(Expr::Member {
|
|
1835
|
+
object: Box::new(Expr::Ident {
|
|
1836
|
+
name: Arc::from("Object"),
|
|
1837
|
+
span: dummy,
|
|
1838
|
+
}),
|
|
1839
|
+
prop: MemberProp::Name {
|
|
1840
|
+
name: Arc::from("keys"),
|
|
1841
|
+
span: dummy,
|
|
1842
|
+
},
|
|
1843
|
+
optional: false,
|
|
1844
|
+
span: dummy,
|
|
1845
|
+
}),
|
|
1846
|
+
args: vec![CallArg::Expr(object.clone())],
|
|
1847
|
+
span: dummy,
|
|
1848
|
+
};
|
|
1849
|
+
let lowered = Statement::ForOf {
|
|
1850
|
+
name: Arc::clone(name),
|
|
1851
|
+
name_span: *name_span,
|
|
1852
|
+
iterable: keys_call,
|
|
1853
|
+
body: body.clone(),
|
|
1854
|
+
span: *span,
|
|
1855
|
+
};
|
|
1856
|
+
self.compile_statement(&lowered)?;
|
|
1857
|
+
}
|
|
1452
1858
|
Statement::Return { value, .. } => {
|
|
1453
1859
|
// Evaluate the return value first (JS order), then run any enclosing `finally`
|
|
1454
1860
|
// blocks (they're stack-neutral, so the value stays on top), then return.
|
|
@@ -1552,7 +1958,16 @@ impl<'a> Compiler<'a> {
|
|
|
1552
1958
|
inner.slot_based = true;
|
|
1553
1959
|
inner.num_slots = param_names.len() as u16;
|
|
1554
1960
|
}
|
|
1961
|
+
// #187: stamp the chunk with its global name so the JIT can register it as a directly-
|
|
1962
|
+
// callable callee — but ONLY when it is a provably-stable top-level function. `self`
|
|
1963
|
+
// being the ROOT compiler (top-level) is indicated by `self.self_fn_name.is_none()` and
|
|
1964
|
+
// an empty `slot_scopes`; simplest sound check: the name is in `stable_globals` (which
|
|
1965
|
+
// by construction contains only the unique top-level `function name`).
|
|
1966
|
+
if self.stable_globals.contains(name) {
|
|
1967
|
+
inner.global_name = Some(Arc::clone(name));
|
|
1968
|
+
}
|
|
1555
1969
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
1970
|
+
inner_comp.stable_globals = Arc::clone(&self.stable_globals);
|
|
1556
1971
|
// Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
|
|
1557
1972
|
// param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
|
|
1558
1973
|
// compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
|
|
@@ -1583,7 +1998,8 @@ impl<'a> Compiler<'a> {
|
|
|
1583
1998
|
.iter()
|
|
1584
1999
|
.map(|n| (Arc::clone(n), false))
|
|
1585
2000
|
.collect::<HashMap<_, _>>()];
|
|
1586
|
-
inner_comp
|
|
2001
|
+
inner_comp
|
|
2002
|
+
.emit_param_destructure_prologue(¶m_names[..formal_len], &slots)?;
|
|
1587
2003
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
1588
2004
|
inner_comp.compile_statement(body)?;
|
|
1589
2005
|
}
|
|
@@ -1792,6 +2208,7 @@ impl<'a> Compiler<'a> {
|
|
|
1792
2208
|
message: "export default is not supported in bytecode".to_string(),
|
|
1793
2209
|
});
|
|
1794
2210
|
}
|
|
2211
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
1795
2212
|
},
|
|
1796
2213
|
Statement::TypeAlias { .. }
|
|
1797
2214
|
| Statement::DeclareVar { .. }
|
|
@@ -1958,6 +2375,30 @@ impl<'a> Compiler<'a> {
|
|
|
1958
2375
|
self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
|
|
1959
2376
|
}
|
|
1960
2377
|
Expr::Call { callee, args, .. } => {
|
|
2378
|
+
// #186: `Math.<unaryfn>(arg)` → `<arg>; MathUnary(id)` when `Math` is provably the
|
|
2379
|
+
// global builtin (unshadowed program-wide) and there is exactly one argument. Lets the
|
|
2380
|
+
// numeric JIT lower the intrinsic (math_trig; a Math.* prereq for other kernels).
|
|
2381
|
+
if self.math_is_global && args.len() == 1 {
|
|
2382
|
+
if let Expr::Member {
|
|
2383
|
+
object,
|
|
2384
|
+
prop: MemberProp::Name { name: fname, .. },
|
|
2385
|
+
optional: false,
|
|
2386
|
+
..
|
|
2387
|
+
} = callee.as_ref()
|
|
2388
|
+
{
|
|
2389
|
+
if let (Expr::Ident { name: obj, .. }, CallArg::Expr(arg)) =
|
|
2390
|
+
(object.as_ref(), &args[0])
|
|
2391
|
+
{
|
|
2392
|
+
if obj.as_ref() == "Math" && self.resolve_slot("Math").is_none() {
|
|
2393
|
+
if let Some(mfn) = MathUnaryFn::from_name(fname.as_ref()) {
|
|
2394
|
+
self.compile_expr(arg)?;
|
|
2395
|
+
self.emit_u16(Opcode::MathUnary, mfn as u16);
|
|
2396
|
+
return Ok(());
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
1961
2402
|
// Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
|
|
1962
2403
|
if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
|
|
1963
2404
|
&& args.len() == 1
|
|
@@ -2244,6 +2685,7 @@ impl<'a> Compiler<'a> {
|
|
|
2244
2685
|
inner.num_slots = param_names.len() as u16;
|
|
2245
2686
|
}
|
|
2246
2687
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
2688
|
+
inner_comp.stable_globals = Arc::clone(&self.stable_globals); // #187
|
|
2247
2689
|
if let Some(map) = simple_slots {
|
|
2248
2690
|
inner_comp.slot_ctx = Some(map);
|
|
2249
2691
|
} else {
|
|
@@ -2251,7 +2693,8 @@ impl<'a> Compiler<'a> {
|
|
|
2251
2693
|
.iter()
|
|
2252
2694
|
.map(|n| (Arc::clone(n), false))
|
|
2253
2695
|
.collect::<HashMap<_, _>>()];
|
|
2254
|
-
inner_comp
|
|
2696
|
+
inner_comp
|
|
2697
|
+
.emit_param_destructure_prologue(¶m_names[..formal_len], &slots)?;
|
|
2255
2698
|
}
|
|
2256
2699
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2257
2700
|
match body {
|
|
@@ -2393,14 +2836,22 @@ impl<'a> Compiler<'a> {
|
|
|
2393
2836
|
// pops both, removes the property, and pushes `true`. Deleting anything that
|
|
2394
2837
|
// isn't a property reference is a no-op that still yields `true` (JS).
|
|
2395
2838
|
match target.as_ref() {
|
|
2396
|
-
Expr::Member {
|
|
2839
|
+
Expr::Member {
|
|
2840
|
+
object,
|
|
2841
|
+
prop: MemberProp::Name { name, .. },
|
|
2842
|
+
..
|
|
2843
|
+
} => {
|
|
2397
2844
|
self.compile_expr(object)?;
|
|
2398
2845
|
let idx = self.constant_idx(Constant::String(Arc::clone(name)));
|
|
2399
2846
|
self.emit(Opcode::LoadConst);
|
|
2400
2847
|
self.chunk.write_u16(idx);
|
|
2401
2848
|
self.emit(Opcode::DeleteIndex);
|
|
2402
2849
|
}
|
|
2403
|
-
Expr::Member {
|
|
2850
|
+
Expr::Member {
|
|
2851
|
+
object,
|
|
2852
|
+
prop: MemberProp::Expr(key),
|
|
2853
|
+
..
|
|
2854
|
+
} => {
|
|
2404
2855
|
self.compile_expr(object)?;
|
|
2405
2856
|
self.compile_expr(key)?;
|
|
2406
2857
|
self.emit(Opcode::DeleteIndex);
|
|
@@ -2662,9 +3113,106 @@ fn compile_internal(
|
|
|
2662
3113
|
let mut chunk = Chunk::new();
|
|
2663
3114
|
chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
|
|
2664
3115
|
let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
|
|
3116
|
+
// #186 — `Math` intrinsics are sound only if `Math` is never rebound anywhere in the program.
|
|
3117
|
+
// `stmt_rebinds` is conservative (any rebind, destructure, FunDecl, or unknown node → true), so a
|
|
3118
|
+
// `false` here only forgoes the optimization, never risks a miscompile.
|
|
3119
|
+
compiler.math_is_global = !program.statements.iter().any(|s| stmt_rebinds(s, "Math"));
|
|
3120
|
+
// #187 — the set of top-level functions safe to call directly from JIT'd code (never reassigned/
|
|
3121
|
+
// shadowed/redeclared anywhere). Conservative: a name absent here only forgoes the optimization.
|
|
3122
|
+
compiler.stable_globals = Arc::new(compute_stable_globals(program));
|
|
2665
3123
|
compiler.compile_program(program)?;
|
|
2666
3124
|
if peephole {
|
|
2667
3125
|
crate::peephole::optimize(&mut chunk);
|
|
2668
3126
|
}
|
|
2669
3127
|
Ok(chunk)
|
|
2670
3128
|
}
|
|
3129
|
+
|
|
3130
|
+
#[cfg(test)]
|
|
3131
|
+
mod stable_globals_tests {
|
|
3132
|
+
use super::compute_stable_globals;
|
|
3133
|
+
|
|
3134
|
+
fn stable(src: &str) -> std::collections::HashSet<String> {
|
|
3135
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3136
|
+
compute_stable_globals(&prog)
|
|
3137
|
+
.into_iter()
|
|
3138
|
+
.map(|n| n.to_string())
|
|
3139
|
+
.collect()
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
/// #187 gate: the exact spectral_norm shape — five top-level functions, none reassigned — must ALL
|
|
3143
|
+
/// be provably stable (this is what lets `multiplyAv` directly call `evalA`). The naive
|
|
3144
|
+
/// `stmt_rebinds`-based scan would return the empty set here (it treats every `FunDecl` as a
|
|
3145
|
+
/// rebind), so this is the regression tripwire for the purpose-built analysis.
|
|
3146
|
+
#[test]
|
|
3147
|
+
fn all_unreassigned_top_level_fns_are_stable() {
|
|
3148
|
+
let s = stable(
|
|
3149
|
+
"function evalA(i, j) { return 1.0 / (i + j) }\n\
|
|
3150
|
+
function multiplyAv(n, v, av) { let i = 0; while (i < n) { av[i] = evalA(i, i) * v[i]; i = i + 1 } }\n\
|
|
3151
|
+
function spectralNorm(n) { multiplyAv(n, n, n); return n }\n",
|
|
3152
|
+
);
|
|
3153
|
+
assert!(
|
|
3154
|
+
s.contains("evalA"),
|
|
3155
|
+
"evalA (called by multiplyAv) must be stable"
|
|
3156
|
+
);
|
|
3157
|
+
assert!(s.contains("multiplyAv"));
|
|
3158
|
+
assert!(s.contains("spectralNorm"));
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
/// A reassigned function is NOT stable (a direct call could hit a stale binding) — but a sibling
|
|
3162
|
+
/// that is never reassigned still is.
|
|
3163
|
+
#[test]
|
|
3164
|
+
fn reassigned_function_is_excluded() {
|
|
3165
|
+
let s = stable(
|
|
3166
|
+
"function f(x) { return x + 1 }\n\
|
|
3167
|
+
function g(x) { return f(x) * 2 }\n\
|
|
3168
|
+
f = (x) => x + 100\n",
|
|
3169
|
+
);
|
|
3170
|
+
assert!(!s.contains("f"), "f is reassigned → must NOT be stable");
|
|
3171
|
+
assert!(s.contains("g"), "g is never reassigned → stable");
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/// Reassignment/redeclaration/shadowing INSIDE another function body must disqualify the name —
|
|
3175
|
+
/// the whole-program walk has to recurse into every body (the whole point vs `stmt_rebinds`).
|
|
3176
|
+
#[test]
|
|
3177
|
+
fn cross_body_rebind_and_shadow_and_redecl_excluded() {
|
|
3178
|
+
// reassigned inside another function's body
|
|
3179
|
+
assert!(!stable(
|
|
3180
|
+
"function h(x) { return x }\n\
|
|
3181
|
+
function k() { h = 5; return h }\n"
|
|
3182
|
+
)
|
|
3183
|
+
.contains("h"));
|
|
3184
|
+
// shadowed by a param in another function
|
|
3185
|
+
assert!(!stable(
|
|
3186
|
+
"function p(x) { return x }\n\
|
|
3187
|
+
function q(p) { return p }\n"
|
|
3188
|
+
)
|
|
3189
|
+
.contains("p"));
|
|
3190
|
+
// declared twice at top level
|
|
3191
|
+
assert!(!stable("function d(x) { return x }\nfunction d(y) { return y }\n").contains("d"));
|
|
3192
|
+
// also bound by a top-level let
|
|
3193
|
+
assert!(!stable("function e(x) { return x }\nlet e = 3\n").contains("e"));
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
/// A global reassigned inside a PARAMETER DEFAULT (of a function or arrow) must disqualify it — the
|
|
3197
|
+
/// default runs in the enclosing scope when the function is called, so a direct call would be stale.
|
|
3198
|
+
#[test]
|
|
3199
|
+
fn param_default_rebind_excluded() {
|
|
3200
|
+
// `foo` reassigned in another function's param default
|
|
3201
|
+
assert!(!stable(
|
|
3202
|
+
"function evil(x) { return x + 1000 }\n\
|
|
3203
|
+
function foo(x) { return x + 1 }\n\
|
|
3204
|
+
function resetFoo(y = (foo = evil)) { return y }\n"
|
|
3205
|
+
)
|
|
3206
|
+
.contains("foo"));
|
|
3207
|
+
// `bar` reassigned in an arrow's param default
|
|
3208
|
+
assert!(!stable(
|
|
3209
|
+
"function bar(x) { return x }\n\
|
|
3210
|
+
let f = (z = (bar = 5)) => z\n"
|
|
3211
|
+
)
|
|
3212
|
+
.contains("bar"));
|
|
3213
|
+
// a param default that does NOT touch the name leaves it stable
|
|
3214
|
+
assert!(
|
|
3215
|
+
stable("function ok(x) { return x }\nfunction u(a = 1) { return a }\n").contains("ok")
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
}
|