@tishlang/tish-format 1.0.12 → 2.0.1
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 +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,2604 @@
|
|
|
1
|
+
//! AST to bytecode compiler.
|
|
2
|
+
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use tishlang_ast::{
|
|
7
|
+
ArrayElement, ArrowBody, BinOp, CallArg, DestructElement, DestructPattern, ExportDeclaration,
|
|
8
|
+
Expr, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp,
|
|
9
|
+
ObjectProp, Program, Span, Statement,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
use crate::chunk::{Chunk, Constant};
|
|
13
|
+
use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
|
|
14
|
+
use crate::opcode::Opcode;
|
|
15
|
+
|
|
16
|
+
enum SimpleMapResult {
|
|
17
|
+
Identity,
|
|
18
|
+
BinOp(BinOp, Constant, bool), // op, constant, param_on_left
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn literal_to_constant(expr: &Expr) -> Option<Constant> {
|
|
22
|
+
if let Expr::Literal { value, .. } = expr {
|
|
23
|
+
Some(match value {
|
|
24
|
+
Literal::Number(n) => Constant::Number(*n),
|
|
25
|
+
Literal::String(s) => Constant::String(Arc::clone(s)),
|
|
26
|
+
Literal::Bool(b) => Constant::Bool(*b),
|
|
27
|
+
Literal::Null => Constant::Null,
|
|
28
|
+
})
|
|
29
|
+
} else {
|
|
30
|
+
None
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Debug)]
|
|
35
|
+
pub struct CompileError {
|
|
36
|
+
pub message: String,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl std::fmt::Display for CompileError {
|
|
40
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
41
|
+
write!(f, "{}", self.message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
impl std::error::Error for CompileError {}
|
|
46
|
+
|
|
47
|
+
/// Loop boundary for break/continue.
|
|
48
|
+
struct LoopInfo {
|
|
49
|
+
break_patches: Vec<usize>,
|
|
50
|
+
/// Operand positions for `continue`: either `JumpBack` (while / do-while / for-of) or `Jump`
|
|
51
|
+
/// (C-style `for`, where the update clause is emitted after the body).
|
|
52
|
+
continue_patches: Vec<usize>,
|
|
53
|
+
/// When true, [`Opcode::Jump`] placeholders in `continue_patches` are patched forward with
|
|
54
|
+
/// [`Self::patch_jump`]. When false, they are [`Opcode::JumpBack`] patched with
|
|
55
|
+
/// [`Self::patch_jump_back`].
|
|
56
|
+
continue_is_forward_jump: bool,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Switch boundary: break exits the switch.
|
|
60
|
+
struct SwitchInfo {
|
|
61
|
+
break_patches: Vec<usize>,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Innermost break/continue target for unwinding `EnterBlock` before a jump.
|
|
65
|
+
#[derive(Clone, Copy)]
|
|
66
|
+
enum Breakable {
|
|
67
|
+
/// `usize` = `block_depth` before the loop body (same as Continue unwind target).
|
|
68
|
+
Loop { unwind_depth: usize },
|
|
69
|
+
/// `usize` = `block_depth` before the switch statement.
|
|
70
|
+
Switch { unwind_depth: usize },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
struct Compiler<'a> {
|
|
74
|
+
chunk: &'a mut Chunk,
|
|
75
|
+
/// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
|
|
76
|
+
scope: Vec<HashMap<Arc<str>, bool>>,
|
|
77
|
+
/// Stack of loop info for break/continue.
|
|
78
|
+
loop_stack: Vec<LoopInfo>,
|
|
79
|
+
switch_stack: Vec<SwitchInfo>,
|
|
80
|
+
/// Parallel to nested loops/switches: innermost target for break/continue block unwind.
|
|
81
|
+
breakable_stack: Vec<Breakable>,
|
|
82
|
+
/// Nesting depth of emitted `EnterBlock` (lexical blocks) not yet closed on the compile path.
|
|
83
|
+
block_depth: usize,
|
|
84
|
+
/// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
|
|
85
|
+
retain_last_expr: bool,
|
|
86
|
+
/// When `Some`, this chunk is being compiled in the SIMPLE slot mode: identifier references
|
|
87
|
+
/// resolve to frame slots (`LoadLocal`) via this param→slot map instead of name-keyed `LoadVar`.
|
|
88
|
+
/// Set only for self-contained param-only functions (see [`simple_fn_slots`]).
|
|
89
|
+
slot_ctx: Option<HashMap<Arc<str>, u16>>,
|
|
90
|
+
/// GENERAL slot mode (`TISH_VM_SLOTS`, capture-aware): a block-scoped stack of name→slot maps,
|
|
91
|
+
/// innermost last. Allocated DURING compilation so block scoping + shadowing are correct (each
|
|
92
|
+
/// `let` gets a fresh slot; resolution walks innermost-first; a block pops its frame). Empty unless
|
|
93
|
+
/// [`general_slots`] is set. Captured names (in [`slot_captured`]) are never allocated here — they
|
|
94
|
+
/// stay name-based in `local_scope` (which closures capture).
|
|
95
|
+
slot_scopes: Vec<HashMap<Arc<str>, u16>>,
|
|
96
|
+
/// Names referenced by a nested closure (over-approx) → must stay name-based even in slot mode.
|
|
97
|
+
slot_captured: HashSet<Arc<str>>,
|
|
98
|
+
/// Monotonic slot allocator for [`slot_scopes`] (never reclaimed; final value = frame size).
|
|
99
|
+
next_slot: u16,
|
|
100
|
+
/// True while compiling a chunk in general slot mode (see [`slot_scopes`]).
|
|
101
|
+
general_slots: bool,
|
|
102
|
+
/// Active `finally` bodies of enclosing `try`s in the CURRENT function (innermost last). A
|
|
103
|
+
/// `return` that escapes these trys must run each one on the way out (the bytecode VM jumps
|
|
104
|
+
/// straight to the function return otherwise). Reset per function — nested fns get a fresh
|
|
105
|
+
/// `Compiler`. The exception-unwind path is handled separately in the `Try` emitter.
|
|
106
|
+
finally_stack: Vec<Statement>,
|
|
107
|
+
/// When `Some(name)`, this chunk is the body of `fn name(...)` and `name`'s binding is provably
|
|
108
|
+
/// stable (no param shadows it, no reassignment/redeclaration in the body — see [`stmt_rebinds`]).
|
|
109
|
+
/// A direct call `name(args)` then compiles to `SelfCall` (no name lookup / closure dispatch; the
|
|
110
|
+
/// JIT lowers it to a native recursive call). `None` for anonymous fns, top-level, or anywhere the
|
|
111
|
+
/// self-binding can't be proven stable.
|
|
112
|
+
self_fn_name: Option<Arc<str>>,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Does `e` reference only the given params (no free/global vars, no nested
|
|
116
|
+
/// functions, no mutation)? Such a function can run on a bare slot frame.
|
|
117
|
+
fn expr_is_param_only(e: &Expr, params: &HashSet<&str>) -> bool {
|
|
118
|
+
match e {
|
|
119
|
+
Expr::Literal { .. } => true,
|
|
120
|
+
Expr::Ident { name, .. } => params.contains(name.as_ref()),
|
|
121
|
+
Expr::Binary { left, right, .. } => {
|
|
122
|
+
expr_is_param_only(left, params) && expr_is_param_only(right, params)
|
|
123
|
+
}
|
|
124
|
+
Expr::Unary { operand, .. } => expr_is_param_only(operand, params),
|
|
125
|
+
Expr::Call { callee, args, .. } => {
|
|
126
|
+
expr_is_param_only(callee, params)
|
|
127
|
+
&& args.iter().all(|a| match a {
|
|
128
|
+
CallArg::Expr(x) => expr_is_param_only(x, params),
|
|
129
|
+
CallArg::Spread(_) => false,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
Expr::Member { object, prop, .. } => {
|
|
133
|
+
expr_is_param_only(object, params)
|
|
134
|
+
&& match prop {
|
|
135
|
+
MemberProp::Name { .. } => true,
|
|
136
|
+
MemberProp::Expr(x) => expr_is_param_only(x, params),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
Expr::Index { object, index, .. } => {
|
|
140
|
+
expr_is_param_only(object, params) && expr_is_param_only(index, params)
|
|
141
|
+
}
|
|
142
|
+
Expr::Conditional {
|
|
143
|
+
cond,
|
|
144
|
+
then_branch,
|
|
145
|
+
else_branch,
|
|
146
|
+
..
|
|
147
|
+
} => {
|
|
148
|
+
expr_is_param_only(cond, params)
|
|
149
|
+
&& expr_is_param_only(then_branch, params)
|
|
150
|
+
&& expr_is_param_only(else_branch, params)
|
|
151
|
+
}
|
|
152
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
153
|
+
expr_is_param_only(left, params) && expr_is_param_only(right, params)
|
|
154
|
+
}
|
|
155
|
+
Expr::Array { elements, .. } => elements.iter().all(|el| match el {
|
|
156
|
+
ArrayElement::Expr(x) => expr_is_param_only(x, params),
|
|
157
|
+
ArrayElement::Spread(_) => false,
|
|
158
|
+
}),
|
|
159
|
+
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
160
|
+
ObjectProp::KeyValue(_, x) => expr_is_param_only(x, params),
|
|
161
|
+
ObjectProp::Spread(_) => false,
|
|
162
|
+
}),
|
|
163
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
164
|
+
exprs.iter().all(|x| expr_is_param_only(x, params))
|
|
165
|
+
}
|
|
166
|
+
Expr::TypeOf { operand, .. } => expr_is_param_only(operand, params),
|
|
167
|
+
// Mutation, nested fns, async, jsx, native, `new` — not eligible.
|
|
168
|
+
_ => false,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Statement form of [`expr_is_param_only`]. Only the small set of statements a
|
|
173
|
+
/// pure leaf function body uses is allowed; anything that declares a binding or
|
|
174
|
+
/// loops bails (those keep the name-based path).
|
|
175
|
+
fn stmt_is_param_only(s: &Statement, params: &HashSet<&str>) -> bool {
|
|
176
|
+
match s {
|
|
177
|
+
Statement::Block { statements, .. } => {
|
|
178
|
+
statements.iter().all(|st| stmt_is_param_only(st, params))
|
|
179
|
+
}
|
|
180
|
+
Statement::Multi { statements, .. } => {
|
|
181
|
+
statements.iter().all(|st| stmt_is_param_only(st, params))
|
|
182
|
+
}
|
|
183
|
+
Statement::ExprStmt { expr, .. } => expr_is_param_only(expr, params),
|
|
184
|
+
Statement::Return { value, .. } => {
|
|
185
|
+
value.as_ref().is_none_or(|e| expr_is_param_only(e, params))
|
|
186
|
+
}
|
|
187
|
+
Statement::If {
|
|
188
|
+
cond,
|
|
189
|
+
then_branch,
|
|
190
|
+
else_branch,
|
|
191
|
+
..
|
|
192
|
+
} => {
|
|
193
|
+
expr_is_param_only(cond, params)
|
|
194
|
+
&& stmt_is_param_only(then_branch, params)
|
|
195
|
+
&& else_branch
|
|
196
|
+
.as_ref()
|
|
197
|
+
.is_none_or(|b| stmt_is_param_only(b, params))
|
|
198
|
+
}
|
|
199
|
+
_ => false,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// If a function with these `params` (all simple, no rest) has a body that
|
|
204
|
+
/// references only its params, returns the param→slot map for slot-based
|
|
205
|
+
/// compilation. Slots are the parameter positions (0-based). Returns `None`
|
|
206
|
+
/// when the function must use the name-based path (captures outer scope,
|
|
207
|
+
/// declares locals, mutates, or defines nested functions).
|
|
208
|
+
fn simple_fn_slots(
|
|
209
|
+
params: &[FunParam],
|
|
210
|
+
has_rest: bool,
|
|
211
|
+
body_ok: impl FnOnce(&HashSet<&str>) -> bool,
|
|
212
|
+
) -> Option<HashMap<Arc<str>, u16>> {
|
|
213
|
+
if has_rest {
|
|
214
|
+
return None;
|
|
215
|
+
}
|
|
216
|
+
let mut map: HashMap<Arc<str>, u16> = HashMap::with_capacity(params.len());
|
|
217
|
+
for (i, p) in params.iter().enumerate() {
|
|
218
|
+
match p {
|
|
219
|
+
FunParam::Simple(tp) => {
|
|
220
|
+
map.insert(Arc::clone(&tp.name), i as u16);
|
|
221
|
+
}
|
|
222
|
+
FunParam::Destructure { .. } => return None,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let pset: HashSet<&str> = map.keys().map(|k| k.as_ref()).collect();
|
|
226
|
+
if body_ok(&pset) {
|
|
227
|
+
Some(map)
|
|
228
|
+
} else {
|
|
229
|
+
None
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Capture-aware general slot-based locals (params + uncaptured body/top-level `let`s → frame slots).
|
|
234
|
+
/// **Default ON** — validated across the full cross-backend suite + the compute micros (−22..27%) +
|
|
235
|
+
/// the `main.tish` bundle (−22%). Set `TISH_VM_SLOTS=0` to disable (name-based, the old path).
|
|
236
|
+
fn slots_enabled() -> bool {
|
|
237
|
+
std::env::var("TISH_VM_SLOTS").map(|v| v != "0").unwrap_or(true)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Is `name` bound by one of `params` (so it would shadow a function's own name)? Conservative:
|
|
241
|
+
/// any destructuring param returns `true` (it could bind `name` via a nested pattern we don't analyze).
|
|
242
|
+
fn params_bind_name(params: &[FunParam], name: &str) -> bool {
|
|
243
|
+
params.iter().any(|p| match p {
|
|
244
|
+
FunParam::Simple(tp) => tp.name.as_ref() == name,
|
|
245
|
+
FunParam::Destructure { .. } => true,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Conservative scan: does `name` get REBOUND (assigned `=`, `+=`, `??=`, `++`/`--`, or re-declared
|
|
250
|
+
/// via `let`/`for-of`) anywhere in `s`? Returns `true` on a rebind OR on any node it can't fully
|
|
251
|
+
/// analyze. Used to decide whether `fn NAME`'s body may emit `SelfCall` for `NAME(...)`: only when
|
|
252
|
+
/// NAME's binding is PROVABLY stable throughout the body, because a wrong `SelfCall` would call the
|
|
253
|
+
/// original chunk after a reassignment — a silent miscompile. Erring toward `true` only costs the
|
|
254
|
+
/// optimization, never correctness.
|
|
255
|
+
fn stmt_rebinds(s: &Statement, name: &str) -> bool {
|
|
256
|
+
match s {
|
|
257
|
+
Statement::Block { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
|
|
258
|
+
Statement::Multi { statements, .. } => statements.iter().any(|s| stmt_rebinds(s, name)),
|
|
259
|
+
Statement::VarDecl { name: n, init, .. } => {
|
|
260
|
+
n.as_ref() == name || init.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
261
|
+
}
|
|
262
|
+
Statement::ExprStmt { expr, .. } => expr_rebinds(expr, name),
|
|
263
|
+
Statement::Return { value, .. } => value.as_ref().is_some_and(|e| expr_rebinds(e, name)),
|
|
264
|
+
Statement::Throw { value, .. } => expr_rebinds(value, name),
|
|
265
|
+
Statement::If { cond, then_branch, else_branch, .. } => {
|
|
266
|
+
expr_rebinds(cond, name)
|
|
267
|
+
|| stmt_rebinds(then_branch, name)
|
|
268
|
+
|| else_branch.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
269
|
+
}
|
|
270
|
+
Statement::While { cond, body, .. } => expr_rebinds(cond, name) || stmt_rebinds(body, name),
|
|
271
|
+
Statement::DoWhile { body, cond, .. } => stmt_rebinds(body, name) || expr_rebinds(cond, name),
|
|
272
|
+
Statement::For { init, cond, update, body, .. } => {
|
|
273
|
+
init.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
274
|
+
|| cond.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
275
|
+
|| update.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
276
|
+
|| stmt_rebinds(body, name)
|
|
277
|
+
}
|
|
278
|
+
Statement::ForOf { name: n, iterable, body, .. } => {
|
|
279
|
+
n.as_ref() == name || expr_rebinds(iterable, name) || stmt_rebinds(body, name)
|
|
280
|
+
}
|
|
281
|
+
Statement::Switch { expr, cases, default_body, .. } => {
|
|
282
|
+
expr_rebinds(expr, name)
|
|
283
|
+
|| cases.iter().any(|(t, body)| {
|
|
284
|
+
t.as_ref().is_some_and(|e| expr_rebinds(e, name))
|
|
285
|
+
|| body.iter().any(|s| stmt_rebinds(s, name))
|
|
286
|
+
})
|
|
287
|
+
|| default_body
|
|
288
|
+
.as_ref()
|
|
289
|
+
.is_some_and(|b| b.iter().any(|s| stmt_rebinds(s, name)))
|
|
290
|
+
}
|
|
291
|
+
Statement::Try { body, catch_body, finally_body, .. } => {
|
|
292
|
+
stmt_rebinds(body, name)
|
|
293
|
+
|| catch_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
294
|
+
|| finally_body.as_ref().is_some_and(|s| stmt_rebinds(s, name))
|
|
295
|
+
}
|
|
296
|
+
Statement::Break { .. } | Statement::Continue { .. } => false,
|
|
297
|
+
// VarDeclDestructure (could bind `name`), FunDecl (could shadow), and any unknown construct
|
|
298
|
+
// → conservative: assume it may rebind `name`.
|
|
299
|
+
_ => true,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Expression half of [`stmt_rebinds`]. `true` if `name` is an assignment/update target, or unknown.
|
|
304
|
+
fn expr_rebinds(e: &Expr, name: &str) -> bool {
|
|
305
|
+
match e {
|
|
306
|
+
Expr::Assign { name: n, value, .. }
|
|
307
|
+
| Expr::CompoundAssign { name: n, value, .. }
|
|
308
|
+
| Expr::LogicalAssign { name: n, value, .. } => n.as_ref() == name || expr_rebinds(value, name),
|
|
309
|
+
Expr::PostfixInc { name: n, .. }
|
|
310
|
+
| Expr::PostfixDec { name: n, .. }
|
|
311
|
+
| Expr::PrefixInc { name: n, .. }
|
|
312
|
+
| Expr::PrefixDec { name: n, .. } => n.as_ref() == name,
|
|
313
|
+
Expr::Literal { .. } | Expr::Ident { .. } => false,
|
|
314
|
+
Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
|
|
315
|
+
expr_rebinds(left, name) || expr_rebinds(right, name)
|
|
316
|
+
}
|
|
317
|
+
Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
|
|
318
|
+
expr_rebinds(operand, name)
|
|
319
|
+
}
|
|
320
|
+
Expr::Conditional { cond, then_branch, else_branch, .. } => {
|
|
321
|
+
expr_rebinds(cond, name) || expr_rebinds(then_branch, name) || expr_rebinds(else_branch, name)
|
|
322
|
+
}
|
|
323
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
324
|
+
expr_rebinds(callee, name)
|
|
325
|
+
|| args.iter().any(|a| match a {
|
|
326
|
+
CallArg::Expr(e) | CallArg::Spread(e) => expr_rebinds(e, name),
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
Expr::Member { object, .. } => expr_rebinds(object, name),
|
|
330
|
+
Expr::Index { object, index, .. } => expr_rebinds(object, name) || expr_rebinds(index, name),
|
|
331
|
+
Expr::Array { elements, .. } => elements.iter().any(|el| match el {
|
|
332
|
+
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_rebinds(e, name),
|
|
333
|
+
}),
|
|
334
|
+
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
335
|
+
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_rebinds(e, name),
|
|
336
|
+
}),
|
|
337
|
+
Expr::MemberAssign { object, value, .. } => expr_rebinds(object, name) || expr_rebinds(value, name),
|
|
338
|
+
Expr::IndexAssign { object, index, value, .. } => {
|
|
339
|
+
expr_rebinds(object, name) || expr_rebinds(index, name) || expr_rebinds(value, name)
|
|
340
|
+
}
|
|
341
|
+
Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(|e| expr_rebinds(e, name)),
|
|
342
|
+
// A nested closure could reassign the outer `name`; recurse (over-conservative if it shadows,
|
|
343
|
+
// which only costs the optimization).
|
|
344
|
+
Expr::ArrowFunction { body, .. } => match body {
|
|
345
|
+
ArrowBody::Expr(e) => expr_rebinds(e, name),
|
|
346
|
+
ArrowBody::Block(s) => stmt_rebinds(s, name),
|
|
347
|
+
},
|
|
348
|
+
// Jsx, NativeModuleLoad, and anything unknown → conservative.
|
|
349
|
+
_ => true,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// One conservative pass computing the over-approximated CAPTURED set: every identifier that appears
|
|
354
|
+
/// textually inside any nested closure (`ArrowFunction`/`FunDecl`) — its body AND its parameter
|
|
355
|
+
/// defaults (which evaluate in the enclosing scope, e.g. `(a = secret) => a` captures `secret`). A
|
|
356
|
+
/// captured local must stay name-based in `local_scope` (which closures capture); only uncaptured
|
|
357
|
+
/// locals are slotted. Recurses ALL ordinary control flow (so it finds every closure → the capture
|
|
358
|
+
/// set is complete); returns `false` ONLY on ambient/module constructs it cannot traverse, so the
|
|
359
|
+
/// caller leaves the whole chunk name-based (safe default-bail: a missed closure could otherwise let a
|
|
360
|
+
/// captured local be wrongly slotted). Slot ALLOCATION happens during compilation (scope-aware), not
|
|
361
|
+
/// here — so block scoping + shadowing are handled by the slot-scope stack, not a flat map.
|
|
362
|
+
#[derive(Default)]
|
|
363
|
+
struct SlotScan {
|
|
364
|
+
captured: HashSet<Arc<str>>,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
impl SlotScan {
|
|
368
|
+
fn stmt(&mut self, s: &Statement, in_closure: bool) -> bool {
|
|
369
|
+
match s {
|
|
370
|
+
Statement::Block { statements, .. } => statements.iter().all(|s| self.stmt(s, in_closure)),
|
|
371
|
+
Statement::Multi { statements, .. } => statements.iter().all(|s| self.stmt(s, in_closure)),
|
|
372
|
+
Statement::VarDecl { init, .. } => init.as_ref().is_none_or(|e| self.expr(e, in_closure)),
|
|
373
|
+
Statement::VarDeclDestructure { init, .. } => self.expr(init, in_closure),
|
|
374
|
+
Statement::ExprStmt { expr, .. } => self.expr(expr, in_closure),
|
|
375
|
+
Statement::If { cond, then_branch, else_branch, .. } => {
|
|
376
|
+
self.expr(cond, in_closure)
|
|
377
|
+
&& self.stmt(then_branch, in_closure)
|
|
378
|
+
&& else_branch.as_ref().is_none_or(|s| self.stmt(s, in_closure))
|
|
379
|
+
}
|
|
380
|
+
Statement::While { cond, body, .. } => self.expr(cond, in_closure) && self.stmt(body, in_closure),
|
|
381
|
+
Statement::DoWhile { body, cond, .. } => self.stmt(body, in_closure) && self.expr(cond, in_closure),
|
|
382
|
+
Statement::For { init, cond, update, body, .. } => {
|
|
383
|
+
init.as_ref().is_none_or(|i| self.stmt(i, in_closure))
|
|
384
|
+
&& cond.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
385
|
+
&& update.as_ref().is_none_or(|e| self.expr(e, in_closure))
|
|
386
|
+
&& self.stmt(body, in_closure)
|
|
387
|
+
}
|
|
388
|
+
Statement::ForOf { iterable, body, .. } => {
|
|
389
|
+
self.expr(iterable, in_closure) && self.stmt(body, in_closure)
|
|
390
|
+
}
|
|
391
|
+
Statement::Return { value, .. } => value.as_ref().is_none_or(|e| self.expr(e, in_closure)),
|
|
392
|
+
Statement::Throw { value, .. } => self.expr(value, in_closure),
|
|
393
|
+
Statement::Break { .. } | Statement::Continue { .. } => true,
|
|
394
|
+
Statement::Switch { expr, cases, default_body, .. } => {
|
|
395
|
+
if !self.expr(expr, in_closure) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
for (test, body) in cases {
|
|
399
|
+
if let Some(t) = test {
|
|
400
|
+
if !self.expr(t, in_closure) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if !body.iter().all(|s| self.stmt(s, in_closure)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
default_body
|
|
409
|
+
.as_ref()
|
|
410
|
+
.is_none_or(|b| b.iter().all(|s| self.stmt(s, in_closure)))
|
|
411
|
+
}
|
|
412
|
+
Statement::Try { body, catch_body, finally_body, .. } => {
|
|
413
|
+
self.stmt(body, in_closure)
|
|
414
|
+
&& catch_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
|
|
415
|
+
&& finally_body.as_ref().is_none_or(|s| self.stmt(s, in_closure))
|
|
416
|
+
}
|
|
417
|
+
// A nested named function: its param defaults (enclosing-scope) + whole body capture.
|
|
418
|
+
Statement::FunDecl { params, body, .. } => {
|
|
419
|
+
self.scan_closure_param_defaults(params) && self.stmt(body, true)
|
|
420
|
+
}
|
|
421
|
+
// Ambient/module constructs (Import/Export/TypeAlias/DeclareVar/DeclareFun) → bail.
|
|
422
|
+
_ => false,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn expr(&mut self, e: &Expr, in_closure: bool) -> bool {
|
|
427
|
+
match e {
|
|
428
|
+
Expr::Literal { .. } => true,
|
|
429
|
+
Expr::Ident { name, .. } => {
|
|
430
|
+
if in_closure {
|
|
431
|
+
self.captured.insert(Arc::clone(name));
|
|
432
|
+
}
|
|
433
|
+
true
|
|
434
|
+
}
|
|
435
|
+
Expr::Binary { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
|
|
436
|
+
Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
|
|
437
|
+
self.expr(operand, in_closure)
|
|
438
|
+
}
|
|
439
|
+
Expr::Conditional { cond, then_branch, else_branch, .. } => {
|
|
440
|
+
self.expr(cond, in_closure) && self.expr(then_branch, in_closure) && self.expr(else_branch, in_closure)
|
|
441
|
+
}
|
|
442
|
+
Expr::NullishCoalesce { left, right, .. } => self.expr(left, in_closure) && self.expr(right, in_closure),
|
|
443
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
444
|
+
self.expr(callee, in_closure)
|
|
445
|
+
&& args.iter().all(|a| match a {
|
|
446
|
+
CallArg::Expr(e) | CallArg::Spread(e) => self.expr(e, in_closure),
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
Expr::Member { object, .. } => self.expr(object, in_closure),
|
|
450
|
+
Expr::Index { object, index, .. } => self.expr(object, in_closure) && self.expr(index, in_closure),
|
|
451
|
+
Expr::Array { elements, .. } => elements.iter().all(|el| match el {
|
|
452
|
+
ArrayElement::Expr(e) | ArrayElement::Spread(e) => self.expr(e, in_closure),
|
|
453
|
+
}),
|
|
454
|
+
Expr::Object { props, .. } => props.iter().all(|p| match p {
|
|
455
|
+
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => self.expr(e, in_closure),
|
|
456
|
+
}),
|
|
457
|
+
Expr::Assign { name, value, .. }
|
|
458
|
+
| Expr::CompoundAssign { name, value, .. }
|
|
459
|
+
| Expr::LogicalAssign { name, value, .. } => {
|
|
460
|
+
if in_closure {
|
|
461
|
+
self.captured.insert(Arc::clone(name));
|
|
462
|
+
}
|
|
463
|
+
self.expr(value, in_closure)
|
|
464
|
+
}
|
|
465
|
+
Expr::MemberAssign { object, value, .. } => self.expr(object, in_closure) && self.expr(value, in_closure),
|
|
466
|
+
Expr::IndexAssign { object, index, value, .. } => {
|
|
467
|
+
self.expr(object, in_closure) && self.expr(index, in_closure) && self.expr(value, in_closure)
|
|
468
|
+
}
|
|
469
|
+
Expr::PostfixInc { name, .. }
|
|
470
|
+
| Expr::PostfixDec { name, .. }
|
|
471
|
+
| Expr::PrefixInc { name, .. }
|
|
472
|
+
| Expr::PrefixDec { name, .. } => {
|
|
473
|
+
if in_closure {
|
|
474
|
+
self.captured.insert(Arc::clone(name));
|
|
475
|
+
}
|
|
476
|
+
true
|
|
477
|
+
}
|
|
478
|
+
Expr::TemplateLiteral { exprs, .. } => exprs.iter().all(|e| self.expr(e, in_closure)),
|
|
479
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
480
|
+
if !self.scan_closure_param_defaults(params) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
match body {
|
|
484
|
+
ArrowBody::Expr(e) => self.expr(e, true),
|
|
485
|
+
ArrowBody::Block(s) => self.stmt(s, true),
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Jsx, NativeModuleLoad → bail.
|
|
489
|
+
_ => false,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/// A nested closure's parameter default expressions evaluate in the ENCLOSING scope → captured.
|
|
494
|
+
fn scan_closure_param_defaults(&mut self, params: &[FunParam]) -> bool {
|
|
495
|
+
for p in params {
|
|
496
|
+
let default = match p {
|
|
497
|
+
FunParam::Simple(tp) => &tp.default,
|
|
498
|
+
FunParam::Destructure { default, .. } => default,
|
|
499
|
+
};
|
|
500
|
+
if let Some(d) = default {
|
|
501
|
+
if !self.expr(d, true) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
true
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// Capture-aware eligibility for general slot-based locals in a FUNCTION. Returns the captured-name set
|
|
511
|
+
/// (names that must stay name-based) when eligible, else `None` (compile name-based). Eligible iff the
|
|
512
|
+
/// flag is on, no rest param, all params simple, the body fully analysable, and no PARAM is captured
|
|
513
|
+
/// (the VM binds params into slots 0..n, but a closure reads captures by name from `local_scope`).
|
|
514
|
+
fn slot_analyze(params: &[FunParam], has_rest: bool, body: &Statement) -> Option<HashSet<Arc<str>>> {
|
|
515
|
+
if !slots_enabled() || has_rest {
|
|
516
|
+
return None;
|
|
517
|
+
}
|
|
518
|
+
for p in params {
|
|
519
|
+
if let FunParam::Destructure { .. } = p {
|
|
520
|
+
return None;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
let mut scan = SlotScan::default();
|
|
524
|
+
if !scan.stmt(body, false) {
|
|
525
|
+
return None;
|
|
526
|
+
}
|
|
527
|
+
for p in params {
|
|
528
|
+
if let FunParam::Simple(tp) = p {
|
|
529
|
+
if scan.captured.contains(&tp.name) {
|
|
530
|
+
return None;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
Some(scan.captured)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/// Same, for the TOP-LEVEL program (no params). Caller must additionally ensure non-REPL mode.
|
|
538
|
+
fn slot_analyze_toplevel(statements: &[Statement]) -> Option<HashSet<Arc<str>>> {
|
|
539
|
+
if !slots_enabled() {
|
|
540
|
+
return None;
|
|
541
|
+
}
|
|
542
|
+
let mut scan = SlotScan::default();
|
|
543
|
+
for s in statements {
|
|
544
|
+
if !scan.stmt(s, false) {
|
|
545
|
+
return None;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
Some(scan.captured)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
impl<'a> Compiler<'a> {
|
|
552
|
+
/// Resolve a name to its frame slot. `None` ⇒ name-based (a captured local, a global, or a
|
|
553
|
+
/// builtin) — the single source of truth for slot-vs-name. Checks the simple param-only map first
|
|
554
|
+
/// (a chunk is in exactly one mode), then the general scope stack innermost-first (shadowing).
|
|
555
|
+
#[inline]
|
|
556
|
+
fn resolve_slot(&self, name: &str) -> Option<u16> {
|
|
557
|
+
if let Some(m) = self.slot_ctx.as_ref() {
|
|
558
|
+
if let Some(s) = m.get(name) {
|
|
559
|
+
return Some(*s);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
self.slot_scopes.iter().rev().find_map(|m| m.get(name).copied())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// Emit a variable READ: `LoadLocal` if slotted, else name-based `LoadVar`.
|
|
566
|
+
fn emit_var_load(&mut self, name: &Arc<str>) {
|
|
567
|
+
if let Some(slot) = self.resolve_slot(name) {
|
|
568
|
+
self.emit_u16(Opcode::LoadLocal, slot);
|
|
569
|
+
} else {
|
|
570
|
+
let idx = self.name_idx(name);
|
|
571
|
+
self.emit_u16(Opcode::LoadVar, idx);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/// Emit a variable WRITE (value already on stack): `StoreLocal` if slotted, else `StoreVar`.
|
|
576
|
+
fn emit_var_store(&mut self, name: &Arc<str>) {
|
|
577
|
+
if let Some(slot) = self.resolve_slot(name) {
|
|
578
|
+
self.emit_u16(Opcode::StoreLocal, slot);
|
|
579
|
+
} else {
|
|
580
|
+
let idx = self.name_idx(name);
|
|
581
|
+
self.emit_u16(Opcode::StoreVar, idx);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
|
|
586
|
+
Self {
|
|
587
|
+
chunk,
|
|
588
|
+
scope: vec![HashMap::new()],
|
|
589
|
+
loop_stack: Vec::new(),
|
|
590
|
+
switch_stack: Vec::new(),
|
|
591
|
+
breakable_stack: Vec::new(),
|
|
592
|
+
block_depth: 0,
|
|
593
|
+
retain_last_expr,
|
|
594
|
+
slot_ctx: None,
|
|
595
|
+
slot_scopes: Vec::new(),
|
|
596
|
+
slot_captured: HashSet::new(),
|
|
597
|
+
next_slot: 0,
|
|
598
|
+
general_slots: false,
|
|
599
|
+
finally_stack: Vec::new(),
|
|
600
|
+
self_fn_name: None,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Begin a lexical block: push a name-scope frame and (in general slot mode) a slot-scope frame,
|
|
605
|
+
/// so block-local `let`s shadow correctly and are reclaimed at block end. Pair with [`exit_block_scope`].
|
|
606
|
+
fn enter_block_scope(&mut self) {
|
|
607
|
+
self.scope.push(HashMap::default());
|
|
608
|
+
if self.general_slots {
|
|
609
|
+
self.slot_scopes.push(HashMap::default());
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
fn exit_block_scope(&mut self) {
|
|
614
|
+
let _popped = self.scope.pop();
|
|
615
|
+
if self.general_slots {
|
|
616
|
+
self.slot_scopes.pop();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/// Allocate a fresh frame slot for `name` in the innermost slot scope (general mode).
|
|
621
|
+
fn declare_slot(&mut self, name: &Arc<str>) -> u16 {
|
|
622
|
+
let slot = self.next_slot;
|
|
623
|
+
self.next_slot += 1;
|
|
624
|
+
if let Some(frame) = self.slot_scopes.last_mut() {
|
|
625
|
+
frame.insert(Arc::clone(name), slot);
|
|
626
|
+
}
|
|
627
|
+
slot
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// Emit the pending `finally` bodies (innermost first) before a `return` escapes them. While
|
|
631
|
+
/// emitting, the stack is cleared so a `return` *inside* one of these finallys doesn't recurse.
|
|
632
|
+
fn emit_pending_finallys(&mut self) -> Result<(), CompileError> {
|
|
633
|
+
if self.finally_stack.is_empty() {
|
|
634
|
+
return Ok(());
|
|
635
|
+
}
|
|
636
|
+
let saved = std::mem::take(&mut self.finally_stack);
|
|
637
|
+
for finally in saved.iter().rev() {
|
|
638
|
+
self.compile_statement(finally)?;
|
|
639
|
+
}
|
|
640
|
+
self.finally_stack = saved;
|
|
641
|
+
Ok(())
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
|
|
645
|
+
let n = self.block_depth.saturating_sub(target_depth);
|
|
646
|
+
for _ in 0..n {
|
|
647
|
+
self.emit(Opcode::ExitBlock);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
|
|
652
|
+
/// Formal parameters as VM slot names plus optional destructure patterns (one per formal).
|
|
653
|
+
#[allow(clippy::type_complexity)] // (slot names, optional destructure patterns) — single-use return
|
|
654
|
+
fn plan_function_params(
|
|
655
|
+
params: &[FunParam],
|
|
656
|
+
) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
|
|
657
|
+
let mut names = Vec::with_capacity(params.len());
|
|
658
|
+
let mut slots: Vec<Option<DestructPattern>> = Vec::with_capacity(params.len());
|
|
659
|
+
let mut syn_counter = 0u32;
|
|
660
|
+
for p in params {
|
|
661
|
+
match p {
|
|
662
|
+
FunParam::Simple(tp) => {
|
|
663
|
+
names.push(Arc::clone(&tp.name));
|
|
664
|
+
slots.push(None);
|
|
665
|
+
}
|
|
666
|
+
FunParam::Destructure {
|
|
667
|
+
pattern, default, ..
|
|
668
|
+
} => {
|
|
669
|
+
if default.is_some() {
|
|
670
|
+
return Err(CompileError {
|
|
671
|
+
message: "Default values on destructuring parameters are not supported in bytecode"
|
|
672
|
+
.to_string(),
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
names.push(Arc::from(format!("__param_{}", syn_counter)));
|
|
676
|
+
syn_counter += 1;
|
|
677
|
+
slots.push(Some(pattern.clone()));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
Ok((names, slots))
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/// After VM binds positional args to `param_names`, load each destructure slot and bind pattern locals.
|
|
685
|
+
fn emit_param_destructure_prologue(
|
|
686
|
+
&mut self,
|
|
687
|
+
param_names: &[Arc<str>],
|
|
688
|
+
slots: &[Option<DestructPattern>],
|
|
689
|
+
) -> Result<(), CompileError> {
|
|
690
|
+
debug_assert_eq!(param_names.len(), slots.len());
|
|
691
|
+
for (name, slot) in param_names.iter().zip(slots.iter()) {
|
|
692
|
+
if let Some(pattern) = slot {
|
|
693
|
+
let idx = self.name_idx(name);
|
|
694
|
+
self.emit_u16(Opcode::LoadVar, idx);
|
|
695
|
+
self.compile_destructure(pattern, false, false)?;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
Ok(())
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/// Emit the default-parameter prologue: for each simple param `p_i` with a default,
|
|
702
|
+
/// `if (arg i was not supplied) p_i = <default>`. Runs at the top of the function body so
|
|
703
|
+
/// later defaults can reference earlier (already-bound) params, e.g. `(a, b = a + 1)`.
|
|
704
|
+
///
|
|
705
|
+
/// Uses `ArgMissing(i)` (true iff `i >= argc`) + `JumpIfFalse` so the default applies only
|
|
706
|
+
/// to *missing* positional args — matching the interpreter, where an explicit `null` keeps
|
|
707
|
+
/// the `null` (tish has no `undefined`). The store mirrors variable resolution: a slot-based
|
|
708
|
+
/// chunk writes the slot directly (`StoreLocal`); a name-based chunk binds the name
|
|
709
|
+
/// (`DeclareVarPlain`, since a missing param is absent from the frame scope).
|
|
710
|
+
fn emit_param_defaults_prologue(&mut self, params: &[FunParam]) -> Result<(), CompileError> {
|
|
711
|
+
for (i, p) in params.iter().enumerate() {
|
|
712
|
+
let FunParam::Simple(tp) = p else { continue };
|
|
713
|
+
let Some(default_expr) = &tp.default else {
|
|
714
|
+
continue;
|
|
715
|
+
};
|
|
716
|
+
self.emit_u16(Opcode::ArgMissing, i as u16);
|
|
717
|
+
let skip = self.emit_jump(Opcode::JumpIfFalse);
|
|
718
|
+
self.compile_expr(default_expr)?;
|
|
719
|
+
let slot = self
|
|
720
|
+
.slot_ctx
|
|
721
|
+
.as_ref()
|
|
722
|
+
.and_then(|m| m.get(tp.name.as_ref()))
|
|
723
|
+
.copied();
|
|
724
|
+
match slot {
|
|
725
|
+
Some(slot) => self.emit_u16(Opcode::StoreLocal, slot),
|
|
726
|
+
None => {
|
|
727
|
+
let idx = self.name_idx(&tp.name);
|
|
728
|
+
self.emit_u16(Opcode::DeclareVarPlain, idx);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
self.patch_jump(skip, self.chunk.code.len());
|
|
732
|
+
}
|
|
733
|
+
Ok(())
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// Names `let`/`const`-declared DIRECTLY in a loop body block (not nested blocks). Each is a
|
|
737
|
+
/// fresh per-iteration binding (ES `let`), so closures created in the body must capture this
|
|
738
|
+
/// iteration's value — registered via `LoopVarsBegin`.
|
|
739
|
+
fn loop_body_block_lets(body: &Statement) -> Vec<Arc<str>> {
|
|
740
|
+
let mut out = Vec::new();
|
|
741
|
+
if let Statement::Block { statements, .. } = body {
|
|
742
|
+
for s in statements {
|
|
743
|
+
if let Statement::VarDecl { name, .. } = s {
|
|
744
|
+
out.push(Arc::clone(name));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
out
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
752
|
+
match stmt {
|
|
753
|
+
Statement::VarDecl {
|
|
754
|
+
name,
|
|
755
|
+
init,
|
|
756
|
+
mutable: _,
|
|
757
|
+
..
|
|
758
|
+
} => {
|
|
759
|
+
if let Some(expr) = init {
|
|
760
|
+
self.compile_expr(expr)?;
|
|
761
|
+
} else {
|
|
762
|
+
let idx = self.constant_idx(Constant::Null);
|
|
763
|
+
self.emit(Opcode::LoadConst);
|
|
764
|
+
self.chunk.write_u16(idx);
|
|
765
|
+
}
|
|
766
|
+
if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
|
|
767
|
+
let slot = self.declare_slot(name);
|
|
768
|
+
self.emit_u16(Opcode::StoreLocal, slot);
|
|
769
|
+
} else {
|
|
770
|
+
let idx = self.name_idx(name);
|
|
771
|
+
self.emit_u16(Opcode::DeclareVarPlain, idx);
|
|
772
|
+
self.scope
|
|
773
|
+
.last_mut()
|
|
774
|
+
.unwrap()
|
|
775
|
+
.insert(Arc::clone(name), false);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
Statement::VarDeclDestructure { pattern, init, .. } => {
|
|
779
|
+
self.compile_expr(init)?;
|
|
780
|
+
self.compile_destructure(pattern, false, true)?;
|
|
781
|
+
}
|
|
782
|
+
_ => self.compile_statement(stmt)?,
|
|
783
|
+
}
|
|
784
|
+
Ok(())
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fn name_idx(&mut self, name: &Arc<str>) -> u16 {
|
|
788
|
+
self.chunk.add_name(Arc::clone(name))
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
fn constant_idx(&mut self, c: Constant) -> u16 {
|
|
792
|
+
self.chunk.add_constant(c)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
fn emit(&mut self, op: Opcode) {
|
|
796
|
+
self.chunk.write_u8(op as u8);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/// Record the source line of the code about to be emitted, for runtime error locations
|
|
800
|
+
/// (issue #74). Cheap and deduped: only a line *change* adds a table entry.
|
|
801
|
+
fn mark_line(&mut self, span: tishlang_ast::Span) {
|
|
802
|
+
let offset = self.chunk.code.len();
|
|
803
|
+
self.chunk.mark_line(offset, span.start.0 as u32);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fn emit_u8(&mut self, op: Opcode, v: u8) {
|
|
807
|
+
self.chunk.write_u8(op as u8);
|
|
808
|
+
self.chunk.write_u16(v as u16);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
fn emit_u16(&mut self, op: Opcode, v: u16) {
|
|
812
|
+
self.chunk.write_u8(op as u8);
|
|
813
|
+
self.chunk.write_u16(v);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
fn emit_jump(&mut self, op: Opcode) -> usize {
|
|
817
|
+
let pos = self.chunk.code.len();
|
|
818
|
+
self.chunk.write_u8(op as u8);
|
|
819
|
+
self.chunk.write_u16(0); // placeholder
|
|
820
|
+
pos + 1
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/// Emit JumpBack with placeholder distance; patch later with patch_jump_back.
|
|
824
|
+
fn emit_jump_back(&mut self) -> usize {
|
|
825
|
+
let pos = self.chunk.code.len();
|
|
826
|
+
self.chunk.write_u8(Opcode::JumpBack as u8);
|
|
827
|
+
self.chunk.write_u16(0);
|
|
828
|
+
pos + 1
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
fn patch_jump(&mut self, patch_pos: usize, target: usize) {
|
|
832
|
+
let base = patch_pos + 2;
|
|
833
|
+
let jump_offset = (target as i32).wrapping_sub(base as i32);
|
|
834
|
+
let bytes = (jump_offset as i16).to_be_bytes();
|
|
835
|
+
self.chunk.code[patch_pos] = bytes[0];
|
|
836
|
+
self.chunk.code[patch_pos + 1] = bytes[1];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
|
|
840
|
+
/// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
|
|
841
|
+
fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
|
|
842
|
+
let after_insn = patch_pos + 2;
|
|
843
|
+
let dist = after_insn.saturating_sub(target);
|
|
844
|
+
let bytes = (dist as u16).to_be_bytes();
|
|
845
|
+
self.chunk.code[patch_pos] = bytes[0];
|
|
846
|
+
self.chunk.code[patch_pos + 1] = bytes[1];
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/// Detect property-based numeric sort: (a, b) => a.prop - b.prop or (a, b) => b.prop - a.prop.
|
|
850
|
+
/// Returns Some((prop_name, asc)) or None.
|
|
851
|
+
fn detect_property_sort_comparator(expr: &Expr) -> Option<(Arc<str>, bool)> {
|
|
852
|
+
if let Expr::ArrowFunction { params, body, .. } = expr {
|
|
853
|
+
if params.len() != 2 {
|
|
854
|
+
return None;
|
|
855
|
+
}
|
|
856
|
+
let (param_a, param_b) = match (¶ms[0], ¶ms[1]) {
|
|
857
|
+
(FunParam::Simple(a), FunParam::Simple(b))
|
|
858
|
+
if a.default.is_none() && b.default.is_none() =>
|
|
859
|
+
{
|
|
860
|
+
(a.name.as_ref(), b.name.as_ref())
|
|
861
|
+
}
|
|
862
|
+
_ => return None,
|
|
863
|
+
};
|
|
864
|
+
let body_expr = match body {
|
|
865
|
+
ArrowBody::Expr(e) => e.as_ref(),
|
|
866
|
+
ArrowBody::Block(stmt) => {
|
|
867
|
+
if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
|
|
868
|
+
e
|
|
869
|
+
} else {
|
|
870
|
+
return None;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
if let Expr::Binary {
|
|
875
|
+
left,
|
|
876
|
+
op: BinOp::Sub,
|
|
877
|
+
right,
|
|
878
|
+
..
|
|
879
|
+
} = body_expr
|
|
880
|
+
{
|
|
881
|
+
if let (
|
|
882
|
+
Expr::Member {
|
|
883
|
+
object: lo,
|
|
884
|
+
prop: MemberProp::Name { name: p, .. },
|
|
885
|
+
..
|
|
886
|
+
},
|
|
887
|
+
Expr::Member {
|
|
888
|
+
object: ro,
|
|
889
|
+
prop: MemberProp::Name { name: pr, .. },
|
|
890
|
+
..
|
|
891
|
+
},
|
|
892
|
+
) = (left.as_ref(), right.as_ref())
|
|
893
|
+
{
|
|
894
|
+
if p != pr {
|
|
895
|
+
return None;
|
|
896
|
+
}
|
|
897
|
+
if let (Expr::Ident { name: ln, .. }, Expr::Ident { name: rn, .. }) =
|
|
898
|
+
(lo.as_ref(), ro.as_ref())
|
|
899
|
+
{
|
|
900
|
+
if ln.as_ref() == param_a && rn.as_ref() == param_b {
|
|
901
|
+
return Some((Arc::clone(p), true));
|
|
902
|
+
}
|
|
903
|
+
if ln.as_ref() == param_b && rn.as_ref() == param_a {
|
|
904
|
+
return Some((Arc::clone(p), false));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
None
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/// Detect numeric sort comparator: (a, b) => a - b (asc) or (a, b) => b - a (desc).
|
|
914
|
+
fn detect_numeric_sort_comparator(expr: &Expr) -> Option<bool> {
|
|
915
|
+
if let Expr::ArrowFunction { params, body, .. } = expr {
|
|
916
|
+
if params.len() != 2 {
|
|
917
|
+
return None;
|
|
918
|
+
}
|
|
919
|
+
let (param_a, param_b) = match (¶ms[0], ¶ms[1]) {
|
|
920
|
+
(FunParam::Simple(a), FunParam::Simple(b))
|
|
921
|
+
if a.default.is_none() && b.default.is_none() =>
|
|
922
|
+
{
|
|
923
|
+
(a.name.as_ref(), b.name.as_ref())
|
|
924
|
+
}
|
|
925
|
+
_ => return None,
|
|
926
|
+
};
|
|
927
|
+
let body_expr = match body {
|
|
928
|
+
ArrowBody::Expr(e) => e.as_ref(),
|
|
929
|
+
ArrowBody::Block(stmt) => {
|
|
930
|
+
if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
|
|
931
|
+
e
|
|
932
|
+
} else {
|
|
933
|
+
return None;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
if let Expr::Binary {
|
|
938
|
+
left,
|
|
939
|
+
op: BinOp::Sub,
|
|
940
|
+
right,
|
|
941
|
+
..
|
|
942
|
+
} = body_expr
|
|
943
|
+
{
|
|
944
|
+
if let (
|
|
945
|
+
Expr::Ident {
|
|
946
|
+
name: left_name, ..
|
|
947
|
+
},
|
|
948
|
+
Expr::Ident {
|
|
949
|
+
name: right_name, ..
|
|
950
|
+
},
|
|
951
|
+
) = (left.as_ref(), right.as_ref())
|
|
952
|
+
{
|
|
953
|
+
if left_name.as_ref() == param_a && right_name.as_ref() == param_b {
|
|
954
|
+
return Some(true);
|
|
955
|
+
}
|
|
956
|
+
if left_name.as_ref() == param_b && right_name.as_ref() == param_a {
|
|
957
|
+
return Some(false);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
None
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/// Detect simple map callback: x => x (identity) or x => x op const / x => const op x.
|
|
966
|
+
/// Returns SimpleMapResult for map optimization.
|
|
967
|
+
fn detect_simple_map_callback(expr: &Expr) -> Option<SimpleMapResult> {
|
|
968
|
+
let (params, body) = match expr {
|
|
969
|
+
Expr::ArrowFunction { params, body, .. } => (params, body),
|
|
970
|
+
_ => return None,
|
|
971
|
+
};
|
|
972
|
+
if params.len() != 1 {
|
|
973
|
+
return None;
|
|
974
|
+
}
|
|
975
|
+
let param_name = match ¶ms[0] {
|
|
976
|
+
FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
|
|
977
|
+
_ => return None,
|
|
978
|
+
};
|
|
979
|
+
let expr_ref: &Expr = match body {
|
|
980
|
+
ArrowBody::Expr(e) => e.as_ref(),
|
|
981
|
+
ArrowBody::Block(stmt) => {
|
|
982
|
+
let s = stmt.as_ref();
|
|
983
|
+
if let Statement::Return {
|
|
984
|
+
value: Some(ref e), ..
|
|
985
|
+
} = s
|
|
986
|
+
{
|
|
987
|
+
e
|
|
988
|
+
} else if let Statement::ExprStmt { expr: ref e, .. } = s {
|
|
989
|
+
e
|
|
990
|
+
} else {
|
|
991
|
+
return None;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
// Identity: x => x
|
|
996
|
+
if let Expr::Ident { name, .. } = expr_ref {
|
|
997
|
+
if name.as_ref() == param_name {
|
|
998
|
+
return Some(SimpleMapResult::Identity);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Binary: x op const or const op x
|
|
1002
|
+
if let Expr::Binary {
|
|
1003
|
+
left, op, right, ..
|
|
1004
|
+
} = expr_ref
|
|
1005
|
+
{
|
|
1006
|
+
let left_is_param =
|
|
1007
|
+
matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
|
|
1008
|
+
let right_is_param =
|
|
1009
|
+
matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
|
|
1010
|
+
let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
|
|
1011
|
+
let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
|
|
1012
|
+
if left_is_param && right_is_literal {
|
|
1013
|
+
if let Some(c) = literal_to_constant(right.as_ref()) {
|
|
1014
|
+
return Some(SimpleMapResult::BinOp(*op, c, true));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if left_is_literal && right_is_param {
|
|
1018
|
+
if let Some(c) = literal_to_constant(left.as_ref()) {
|
|
1019
|
+
return Some(SimpleMapResult::BinOp(*op, c, false));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
None
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/// Detect simple filter callback: x => x op const or x => const op x (comparison that returns bool).
|
|
1027
|
+
fn detect_simple_filter_callback(expr: &Expr) -> Option<(BinOp, Constant, bool)> {
|
|
1028
|
+
let (params, body) = match expr {
|
|
1029
|
+
Expr::ArrowFunction { params, body, .. } => (params, body),
|
|
1030
|
+
_ => return None,
|
|
1031
|
+
};
|
|
1032
|
+
if params.len() != 1 {
|
|
1033
|
+
return None;
|
|
1034
|
+
}
|
|
1035
|
+
let param_name = match ¶ms[0] {
|
|
1036
|
+
FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
|
|
1037
|
+
_ => return None,
|
|
1038
|
+
};
|
|
1039
|
+
let expr_ref: &Expr = match body {
|
|
1040
|
+
ArrowBody::Expr(e) => e.as_ref(),
|
|
1041
|
+
ArrowBody::Block(stmt) => {
|
|
1042
|
+
let s = stmt.as_ref();
|
|
1043
|
+
if let Statement::Return {
|
|
1044
|
+
value: Some(ref e), ..
|
|
1045
|
+
} = s
|
|
1046
|
+
{
|
|
1047
|
+
e
|
|
1048
|
+
} else if let Statement::ExprStmt { expr: ref e, .. } = s {
|
|
1049
|
+
e
|
|
1050
|
+
} else {
|
|
1051
|
+
return None;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
if let Expr::Binary {
|
|
1056
|
+
left, op, right, ..
|
|
1057
|
+
} = expr_ref
|
|
1058
|
+
{
|
|
1059
|
+
if !matches!(
|
|
1060
|
+
op,
|
|
1061
|
+
BinOp::Eq
|
|
1062
|
+
| BinOp::Ne
|
|
1063
|
+
| BinOp::StrictEq
|
|
1064
|
+
| BinOp::StrictNe
|
|
1065
|
+
| BinOp::Lt
|
|
1066
|
+
| BinOp::Le
|
|
1067
|
+
| BinOp::Gt
|
|
1068
|
+
| BinOp::Ge
|
|
1069
|
+
| BinOp::And
|
|
1070
|
+
| BinOp::Or
|
|
1071
|
+
) {
|
|
1072
|
+
return None;
|
|
1073
|
+
}
|
|
1074
|
+
let left_is_param =
|
|
1075
|
+
matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
|
|
1076
|
+
let right_is_param =
|
|
1077
|
+
matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
|
|
1078
|
+
let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
|
|
1079
|
+
let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
|
|
1080
|
+
if left_is_param && right_is_literal {
|
|
1081
|
+
if let Some(c) = literal_to_constant(right.as_ref()) {
|
|
1082
|
+
return Some((*op, c, true));
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if left_is_literal && right_is_param {
|
|
1086
|
+
if let Some(c) = literal_to_constant(left.as_ref()) {
|
|
1087
|
+
return Some((*op, c, false));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
None
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
1095
|
+
let stmts = &program.statements;
|
|
1096
|
+
// Top-level general slot-based locals — NON-REPL only (REPL persists top-level `let`s to
|
|
1097
|
+
// globals across lines, which slots can't do). Set up before compiling; the frame size is the
|
|
1098
|
+
// monotonic `next_slot` high-water, applied to the chunk after compilation.
|
|
1099
|
+
if !self.retain_last_expr {
|
|
1100
|
+
if let Some(cap) = slot_analyze_toplevel(stmts) {
|
|
1101
|
+
self.general_slots = true;
|
|
1102
|
+
self.slot_captured = cap;
|
|
1103
|
+
self.slot_scopes.push(HashMap::default());
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
let last_is_expr = self.retain_last_expr
|
|
1107
|
+
&& stmts
|
|
1108
|
+
.last()
|
|
1109
|
+
.map(|s| matches!(s, Statement::ExprStmt { .. }))
|
|
1110
|
+
.unwrap_or(false);
|
|
1111
|
+
|
|
1112
|
+
if last_is_expr {
|
|
1113
|
+
let (rest, last) = stmts.split_at(stmts.len().saturating_sub(1));
|
|
1114
|
+
for stmt in rest {
|
|
1115
|
+
self.compile_statement(stmt)?;
|
|
1116
|
+
}
|
|
1117
|
+
if let Some(Statement::ExprStmt { expr, .. }) = last.first() {
|
|
1118
|
+
self.compile_expr(expr)?;
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
for stmt in stmts {
|
|
1122
|
+
self.compile_statement(stmt)?;
|
|
1123
|
+
}
|
|
1124
|
+
let idx = self.constant_idx(Constant::Null);
|
|
1125
|
+
self.emit(Opcode::LoadConst);
|
|
1126
|
+
self.chunk.write_u16(idx);
|
|
1127
|
+
}
|
|
1128
|
+
// Apply the top-level slot frame size (only if any local was actually slotted).
|
|
1129
|
+
if self.general_slots && self.next_slot > 0 {
|
|
1130
|
+
self.chunk.slot_based = true;
|
|
1131
|
+
self.chunk.num_slots = self.next_slot;
|
|
1132
|
+
}
|
|
1133
|
+
Ok(())
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
1137
|
+
self.mark_line(stmt.span());
|
|
1138
|
+
match stmt {
|
|
1139
|
+
Statement::Block { statements, .. } => {
|
|
1140
|
+
self.emit(Opcode::EnterBlock);
|
|
1141
|
+
self.block_depth += 1;
|
|
1142
|
+
self.enter_block_scope();
|
|
1143
|
+
for s in statements {
|
|
1144
|
+
self.compile_statement(s)?;
|
|
1145
|
+
}
|
|
1146
|
+
self.exit_block_scope();
|
|
1147
|
+
self.emit(Opcode::ExitBlock);
|
|
1148
|
+
self.block_depth -= 1;
|
|
1149
|
+
}
|
|
1150
|
+
// Comma-declarators: a transparent group — compile each declarator in
|
|
1151
|
+
// the *current* block scope (no EnterBlock/ExitBlock).
|
|
1152
|
+
Statement::Multi { statements, .. } => {
|
|
1153
|
+
for s in statements {
|
|
1154
|
+
self.compile_statement(s)?;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
Statement::VarDecl {
|
|
1158
|
+
name,
|
|
1159
|
+
init,
|
|
1160
|
+
mutable: _,
|
|
1161
|
+
..
|
|
1162
|
+
} => {
|
|
1163
|
+
if let Some(expr) = init {
|
|
1164
|
+
self.compile_expr(expr)?;
|
|
1165
|
+
} else {
|
|
1166
|
+
let idx = self.constant_idx(Constant::Null);
|
|
1167
|
+
self.emit(Opcode::LoadConst);
|
|
1168
|
+
self.chunk.write_u16(idx);
|
|
1169
|
+
}
|
|
1170
|
+
if self.general_slots && !self.slot_captured.contains(name.as_ref()) {
|
|
1171
|
+
// Uncaptured local → allocate a fresh frame slot + write it directly.
|
|
1172
|
+
let slot = self.declare_slot(name);
|
|
1173
|
+
self.emit_u16(Opcode::StoreLocal, slot);
|
|
1174
|
+
} else {
|
|
1175
|
+
let idx = self.name_idx(name);
|
|
1176
|
+
self.emit_u16(Opcode::DeclareVar, idx);
|
|
1177
|
+
self.scope
|
|
1178
|
+
.last_mut()
|
|
1179
|
+
.unwrap()
|
|
1180
|
+
.insert(Arc::clone(name), false);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
Statement::VarDeclDestructure { pattern, init, .. } => {
|
|
1184
|
+
self.compile_expr(init)?;
|
|
1185
|
+
self.compile_destructure(pattern, false, false)?;
|
|
1186
|
+
}
|
|
1187
|
+
Statement::ExprStmt { expr, .. } => {
|
|
1188
|
+
self.compile_expr(expr)?;
|
|
1189
|
+
self.emit(Opcode::Pop);
|
|
1190
|
+
}
|
|
1191
|
+
Statement::If {
|
|
1192
|
+
cond,
|
|
1193
|
+
then_branch,
|
|
1194
|
+
else_branch,
|
|
1195
|
+
..
|
|
1196
|
+
} => {
|
|
1197
|
+
self.compile_expr(cond)?;
|
|
1198
|
+
let jump_else = self.emit_jump(Opcode::JumpIfFalse);
|
|
1199
|
+
self.compile_statement(then_branch)?;
|
|
1200
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1201
|
+
self.patch_jump(jump_else, self.chunk.code.len());
|
|
1202
|
+
if let Some(else_s) = else_branch {
|
|
1203
|
+
self.compile_statement(else_s)?;
|
|
1204
|
+
}
|
|
1205
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
1206
|
+
}
|
|
1207
|
+
Statement::While { cond, body, .. } => {
|
|
1208
|
+
// Per-iteration `let`: a `let` declared directly in the loop body is a fresh binding
|
|
1209
|
+
// each iteration, so a closure created in the body captures THIS iteration's value.
|
|
1210
|
+
// Register those names (same overlay mechanism as for/for-of loop vars).
|
|
1211
|
+
let body_lets = Self::loop_body_block_lets(body);
|
|
1212
|
+
for n in &body_lets {
|
|
1213
|
+
let idx = self.name_idx(n);
|
|
1214
|
+
self.emit_u16(Opcode::LoopVarsBegin, idx);
|
|
1215
|
+
}
|
|
1216
|
+
let start = self.chunk.code.len();
|
|
1217
|
+
self.loop_stack.push(LoopInfo {
|
|
1218
|
+
break_patches: Vec::new(),
|
|
1219
|
+
continue_patches: Vec::new(),
|
|
1220
|
+
continue_is_forward_jump: false,
|
|
1221
|
+
});
|
|
1222
|
+
self.breakable_stack.push(Breakable::Loop {
|
|
1223
|
+
unwind_depth: self.block_depth,
|
|
1224
|
+
});
|
|
1225
|
+
self.compile_expr(cond)?;
|
|
1226
|
+
let jump_out = self.emit_jump(Opcode::JumpIfFalse);
|
|
1227
|
+
// JumpIfFalse already pops condition when taking body
|
|
1228
|
+
self.compile_statement(body)?;
|
|
1229
|
+
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
|
|
1230
|
+
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1231
|
+
let end = self.chunk.code.len();
|
|
1232
|
+
self.patch_jump(jump_out, end);
|
|
1233
|
+
let info = self.loop_stack.pop().unwrap();
|
|
1234
|
+
self.breakable_stack.pop();
|
|
1235
|
+
for p in info.continue_patches {
|
|
1236
|
+
self.patch_jump_back(p, start);
|
|
1237
|
+
}
|
|
1238
|
+
for p in info.break_patches {
|
|
1239
|
+
self.patch_jump(p, end);
|
|
1240
|
+
}
|
|
1241
|
+
for _ in &body_lets {
|
|
1242
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
Statement::For {
|
|
1246
|
+
init,
|
|
1247
|
+
cond,
|
|
1248
|
+
update,
|
|
1249
|
+
body,
|
|
1250
|
+
..
|
|
1251
|
+
} => {
|
|
1252
|
+
self.enter_block_scope();
|
|
1253
|
+
if let Some(i) = init {
|
|
1254
|
+
self.compile_for_init_statement(i.as_ref())?;
|
|
1255
|
+
}
|
|
1256
|
+
// ES per-iteration `let`: register the loop var so a closure created in the body
|
|
1257
|
+
// captures THIS iteration's value (not the final one). One push per loop entry; the
|
|
1258
|
+
// per-iteration snapshot only happens when a closure is actually created, so
|
|
1259
|
+
// closure-free loops are unaffected.
|
|
1260
|
+
let loop_var: Option<Arc<str>> = match init.as_deref() {
|
|
1261
|
+
Some(Statement::VarDecl { name, .. }) => Some(Arc::clone(name)),
|
|
1262
|
+
_ => None,
|
|
1263
|
+
};
|
|
1264
|
+
if let Some(ref n) = loop_var {
|
|
1265
|
+
let idx = self.name_idx(n);
|
|
1266
|
+
self.emit_u16(Opcode::LoopVarsBegin, idx);
|
|
1267
|
+
}
|
|
1268
|
+
let cond_start = self.chunk.code.len();
|
|
1269
|
+
if let Some(c) = cond {
|
|
1270
|
+
self.compile_expr(c)?;
|
|
1271
|
+
} else {
|
|
1272
|
+
let idx = self.constant_idx(Constant::Bool(true));
|
|
1273
|
+
self.emit(Opcode::LoadConst);
|
|
1274
|
+
self.chunk.write_u16(idx);
|
|
1275
|
+
}
|
|
1276
|
+
let jump_out = self.emit_jump(Opcode::JumpIfFalse);
|
|
1277
|
+
self.loop_stack.push(LoopInfo {
|
|
1278
|
+
break_patches: Vec::new(),
|
|
1279
|
+
continue_patches: Vec::new(),
|
|
1280
|
+
continue_is_forward_jump: true,
|
|
1281
|
+
});
|
|
1282
|
+
self.breakable_stack.push(Breakable::Loop {
|
|
1283
|
+
unwind_depth: self.block_depth,
|
|
1284
|
+
});
|
|
1285
|
+
self.compile_statement(body)?;
|
|
1286
|
+
let update_start = self.chunk.code.len();
|
|
1287
|
+
if let Some(u) = update {
|
|
1288
|
+
self.compile_expr(u)?;
|
|
1289
|
+
self.emit(Opcode::Pop);
|
|
1290
|
+
}
|
|
1291
|
+
let info = self.loop_stack.pop().unwrap();
|
|
1292
|
+
for p in info.continue_patches {
|
|
1293
|
+
self.patch_jump(p, update_start);
|
|
1294
|
+
}
|
|
1295
|
+
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
|
|
1296
|
+
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1297
|
+
let end = self.chunk.code.len();
|
|
1298
|
+
self.patch_jump(jump_out, end);
|
|
1299
|
+
for p in info.break_patches {
|
|
1300
|
+
self.patch_jump(p, end);
|
|
1301
|
+
}
|
|
1302
|
+
// After the loop fully exits (normal or break, both land at `end`): close the
|
|
1303
|
+
// per-iteration region.
|
|
1304
|
+
if loop_var.is_some() {
|
|
1305
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1306
|
+
}
|
|
1307
|
+
self.breakable_stack.pop();
|
|
1308
|
+
self.exit_block_scope();
|
|
1309
|
+
}
|
|
1310
|
+
Statement::ForOf {
|
|
1311
|
+
name,
|
|
1312
|
+
iterable,
|
|
1313
|
+
body,
|
|
1314
|
+
..
|
|
1315
|
+
} => {
|
|
1316
|
+
self.compile_expr(iterable)?;
|
|
1317
|
+
// Normalize a JS iterator object (Map/Set `.values()` etc.) to an array so the
|
|
1318
|
+
// index-based loop below can iterate it; arrays/strings pass through untouched.
|
|
1319
|
+
self.emit(Opcode::IterNormalize);
|
|
1320
|
+
self.enter_block_scope();
|
|
1321
|
+
let arr_name = Arc::from("__forof_arr__");
|
|
1322
|
+
let i_name = Arc::from("__forof_i__");
|
|
1323
|
+
let len_name = Arc::from("__forof_len__");
|
|
1324
|
+
let arr_idx = self.name_idx(&arr_name);
|
|
1325
|
+
let i_idx = self.name_idx(&i_name);
|
|
1326
|
+
let len_idx = self.name_idx(&len_name);
|
|
1327
|
+
let name_idx = self.name_idx(name);
|
|
1328
|
+
self.emit_u16(Opcode::DeclareVar, arr_idx);
|
|
1329
|
+
self.scope
|
|
1330
|
+
.last_mut()
|
|
1331
|
+
.unwrap()
|
|
1332
|
+
.insert(arr_name.clone(), false);
|
|
1333
|
+
self.emit_u16(Opcode::LoadVar, arr_idx);
|
|
1334
|
+
let len_name_idx = self.name_idx(&Arc::from("length"));
|
|
1335
|
+
self.emit_u16(Opcode::GetMember, len_name_idx);
|
|
1336
|
+
self.emit_u16(Opcode::DeclareVar, len_idx);
|
|
1337
|
+
self.scope
|
|
1338
|
+
.last_mut()
|
|
1339
|
+
.unwrap()
|
|
1340
|
+
.insert(len_name.clone(), false);
|
|
1341
|
+
let zero_idx = self.constant_idx(Constant::Number(0.0));
|
|
1342
|
+
self.emit(Opcode::LoadConst);
|
|
1343
|
+
self.chunk.write_u16(zero_idx);
|
|
1344
|
+
self.emit_u16(Opcode::DeclareVar, i_idx);
|
|
1345
|
+
self.scope.last_mut().unwrap().insert(i_name.clone(), false);
|
|
1346
|
+
// ES per-iteration `let` for `for (let v of …)`: register the loop var so a closure
|
|
1347
|
+
// in the body captures this iteration's element (emitted once, before loop_start).
|
|
1348
|
+
self.emit_u16(Opcode::LoopVarsBegin, name_idx);
|
|
1349
|
+
// Pre-tested loop, like the C-style `for` above: test `i < len` at the TOP, before
|
|
1350
|
+
// reading `arr[i]`. A bottom-tested loop ran the body once on an empty array (reading
|
|
1351
|
+
// `arr[0]` → null) and spun forever on `continue` (which skipped the increment).
|
|
1352
|
+
let cond_start = self.chunk.code.len();
|
|
1353
|
+
self.emit_u16(Opcode::LoadVar, i_idx);
|
|
1354
|
+
self.emit_u16(Opcode::LoadVar, len_idx);
|
|
1355
|
+
self.emit_u8(Opcode::BinOp, 10);
|
|
1356
|
+
let jump_out = self.emit_jump(Opcode::JumpIfFalse);
|
|
1357
|
+
self.loop_stack.push(LoopInfo {
|
|
1358
|
+
break_patches: Vec::new(),
|
|
1359
|
+
continue_patches: Vec::new(),
|
|
1360
|
+
continue_is_forward_jump: true,
|
|
1361
|
+
});
|
|
1362
|
+
self.breakable_stack.push(Breakable::Loop {
|
|
1363
|
+
unwind_depth: self.block_depth,
|
|
1364
|
+
});
|
|
1365
|
+
self.emit_u16(Opcode::LoadVar, arr_idx);
|
|
1366
|
+
self.emit_u16(Opcode::LoadVar, i_idx);
|
|
1367
|
+
self.emit(Opcode::GetIndex);
|
|
1368
|
+
self.emit_u16(Opcode::DeclareVar, name_idx);
|
|
1369
|
+
self.scope
|
|
1370
|
+
.last_mut()
|
|
1371
|
+
.unwrap()
|
|
1372
|
+
.insert(Arc::clone(name), false);
|
|
1373
|
+
self.compile_statement(body)?;
|
|
1374
|
+
// `continue` lands here: increment `i`, then fall through to the JumpBack → re-test.
|
|
1375
|
+
let update_start = self.chunk.code.len();
|
|
1376
|
+
self.emit_u16(Opcode::LoadVar, i_idx);
|
|
1377
|
+
let one_idx = self.constant_idx(Constant::Number(1.0));
|
|
1378
|
+
self.emit(Opcode::LoadConst);
|
|
1379
|
+
self.chunk.write_u16(one_idx);
|
|
1380
|
+
self.emit_u8(Opcode::BinOp, 0);
|
|
1381
|
+
self.emit_u16(Opcode::StoreVar, i_idx);
|
|
1382
|
+
let info = self.loop_stack.pop().unwrap();
|
|
1383
|
+
self.breakable_stack.pop();
|
|
1384
|
+
for p in info.continue_patches {
|
|
1385
|
+
self.patch_jump(p, update_start);
|
|
1386
|
+
}
|
|
1387
|
+
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
|
|
1388
|
+
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1389
|
+
let end = self.chunk.code.len();
|
|
1390
|
+
self.patch_jump(jump_out, end);
|
|
1391
|
+
for p in info.break_patches {
|
|
1392
|
+
self.patch_jump(p, end);
|
|
1393
|
+
}
|
|
1394
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1395
|
+
self.exit_block_scope();
|
|
1396
|
+
}
|
|
1397
|
+
Statement::Return { value, .. } => {
|
|
1398
|
+
// Evaluate the return value first (JS order), then run any enclosing `finally`
|
|
1399
|
+
// blocks (they're stack-neutral, so the value stays on top), then return.
|
|
1400
|
+
if let Some(v) = value {
|
|
1401
|
+
self.compile_expr(v)?;
|
|
1402
|
+
} else {
|
|
1403
|
+
let idx = self.constant_idx(Constant::Null);
|
|
1404
|
+
self.emit(Opcode::LoadConst);
|
|
1405
|
+
self.chunk.write_u16(idx);
|
|
1406
|
+
}
|
|
1407
|
+
self.emit_pending_finallys()?;
|
|
1408
|
+
self.emit(Opcode::Return);
|
|
1409
|
+
}
|
|
1410
|
+
Statement::Break { .. } => {
|
|
1411
|
+
let unwind_depth = match self.breakable_stack.last() {
|
|
1412
|
+
Some(Breakable::Loop { unwind_depth })
|
|
1413
|
+
| Some(Breakable::Switch { unwind_depth }) => *unwind_depth,
|
|
1414
|
+
None => {
|
|
1415
|
+
return Err(CompileError {
|
|
1416
|
+
message: "break not inside a loop or switch".to_string(),
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
self.emit_exit_blocks_until_depth(unwind_depth);
|
|
1421
|
+
let pos = self.emit_jump(Opcode::Jump);
|
|
1422
|
+
match self.breakable_stack.last() {
|
|
1423
|
+
Some(Breakable::Loop { .. }) => {
|
|
1424
|
+
self.loop_stack.last_mut().unwrap().break_patches.push(pos);
|
|
1425
|
+
}
|
|
1426
|
+
Some(Breakable::Switch { .. }) => {
|
|
1427
|
+
self.switch_stack
|
|
1428
|
+
.last_mut()
|
|
1429
|
+
.unwrap()
|
|
1430
|
+
.break_patches
|
|
1431
|
+
.push(pos);
|
|
1432
|
+
}
|
|
1433
|
+
None => {}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
Statement::Continue { .. } => {
|
|
1437
|
+
let unwind_depth = self
|
|
1438
|
+
.breakable_stack
|
|
1439
|
+
.iter()
|
|
1440
|
+
.rev()
|
|
1441
|
+
.find_map(|b| match b {
|
|
1442
|
+
Breakable::Loop { unwind_depth } => Some(*unwind_depth),
|
|
1443
|
+
Breakable::Switch { .. } => None,
|
|
1444
|
+
})
|
|
1445
|
+
.ok_or_else(|| CompileError {
|
|
1446
|
+
message: "continue not inside a loop".to_string(),
|
|
1447
|
+
})?;
|
|
1448
|
+
self.emit_exit_blocks_until_depth(unwind_depth);
|
|
1449
|
+
let forward = self
|
|
1450
|
+
.loop_stack
|
|
1451
|
+
.last()
|
|
1452
|
+
.expect("continue not inside a loop")
|
|
1453
|
+
.continue_is_forward_jump;
|
|
1454
|
+
let pos = if forward {
|
|
1455
|
+
self.emit_jump(Opcode::Jump)
|
|
1456
|
+
} else {
|
|
1457
|
+
self.emit_jump_back()
|
|
1458
|
+
};
|
|
1459
|
+
self.loop_stack
|
|
1460
|
+
.last_mut()
|
|
1461
|
+
.expect("continue not inside a loop")
|
|
1462
|
+
.continue_patches
|
|
1463
|
+
.push(pos);
|
|
1464
|
+
}
|
|
1465
|
+
Statement::FunDecl {
|
|
1466
|
+
name,
|
|
1467
|
+
params,
|
|
1468
|
+
body,
|
|
1469
|
+
rest_param,
|
|
1470
|
+
async_: _,
|
|
1471
|
+
..
|
|
1472
|
+
} => {
|
|
1473
|
+
let formal_len = params.len();
|
|
1474
|
+
let (mut param_names, slots) = Self::plan_function_params(params)?;
|
|
1475
|
+
let simple_slots = simple_fn_slots(params, rest_param.is_some(), |pset| {
|
|
1476
|
+
stmt_is_param_only(body, pset)
|
|
1477
|
+
});
|
|
1478
|
+
// Capture-aware general slot-based locals when the simple param-only fast path doesn't
|
|
1479
|
+
// apply. Gated by `TISH_VM_SLOTS` (off ⇒ None ⇒ byte-identical). Frame size is known only
|
|
1480
|
+
// AFTER compilation (slots are allocated as the body declares locals) → set on the chunk below.
|
|
1481
|
+
let captured = if simple_slots.is_none() {
|
|
1482
|
+
slot_analyze(params, rest_param.is_some(), body)
|
|
1483
|
+
} else {
|
|
1484
|
+
None
|
|
1485
|
+
};
|
|
1486
|
+
let mut inner = Chunk::new();
|
|
1487
|
+
inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
|
|
1488
|
+
if let Some(rp) = rest_param {
|
|
1489
|
+
param_names.push(Arc::clone(&rp.name));
|
|
1490
|
+
inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
|
|
1491
|
+
}
|
|
1492
|
+
for p in ¶m_names {
|
|
1493
|
+
inner.add_name(Arc::clone(p));
|
|
1494
|
+
}
|
|
1495
|
+
inner.param_count = param_names.len() as u16;
|
|
1496
|
+
if simple_slots.is_some() {
|
|
1497
|
+
inner.slot_based = true;
|
|
1498
|
+
inner.num_slots = param_names.len() as u16;
|
|
1499
|
+
}
|
|
1500
|
+
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
1501
|
+
// Recursion-JIT enabler: if `name`'s binding is provably stable in the body (no
|
|
1502
|
+
// param shadows it, no reassignment/redeclaration), direct `name(args)` calls inside
|
|
1503
|
+
// compile to `SelfCall` — no name lookup, and the numeric JIT lowers it to a native
|
|
1504
|
+
// recursive call. Conservative `stmt_rebinds` errs toward NOT enabling (safe).
|
|
1505
|
+
if !params_bind_name(params, name.as_ref()) && !stmt_rebinds(body, name.as_ref()) {
|
|
1506
|
+
inner_comp.self_fn_name = Some(Arc::clone(name));
|
|
1507
|
+
}
|
|
1508
|
+
let mut general_frame_slots: Option<u16> = None;
|
|
1509
|
+
if let Some(map) = simple_slots {
|
|
1510
|
+
inner_comp.slot_ctx = Some(map);
|
|
1511
|
+
inner_comp.emit_param_defaults_prologue(params)?;
|
|
1512
|
+
inner_comp.compile_statement(body)?;
|
|
1513
|
+
} else if let Some(cap) = captured {
|
|
1514
|
+
// Params (all uncaptured — gated) → slots 0..n (matching the VM's param binding);
|
|
1515
|
+
// uncaptured body `let`s get fresh slots via the scope-aware allocator; captured
|
|
1516
|
+
// locals stay name-based in `local_scope` (which closures capture).
|
|
1517
|
+
inner_comp.general_slots = true;
|
|
1518
|
+
inner_comp.slot_captured = cap;
|
|
1519
|
+
inner_comp.slot_scopes.push(HashMap::new());
|
|
1520
|
+
for p in ¶m_names {
|
|
1521
|
+
inner_comp.declare_slot(p);
|
|
1522
|
+
}
|
|
1523
|
+
inner_comp.emit_param_defaults_prologue(params)?;
|
|
1524
|
+
inner_comp.compile_statement(body)?;
|
|
1525
|
+
general_frame_slots = Some(inner_comp.next_slot);
|
|
1526
|
+
} else {
|
|
1527
|
+
inner_comp.scope = vec![param_names
|
|
1528
|
+
.iter()
|
|
1529
|
+
.map(|n| (Arc::clone(n), false))
|
|
1530
|
+
.collect::<HashMap<_, _>>()];
|
|
1531
|
+
inner_comp.emit_param_destructure_prologue(¶m_names[..formal_len], &slots)?;
|
|
1532
|
+
inner_comp.emit_param_defaults_prologue(params)?;
|
|
1533
|
+
inner_comp.compile_statement(body)?;
|
|
1534
|
+
}
|
|
1535
|
+
inner_comp.emit(Opcode::LoadConst);
|
|
1536
|
+
let idx = inner_comp.constant_idx(Constant::Null);
|
|
1537
|
+
inner_comp.chunk.write_u16(idx);
|
|
1538
|
+
inner_comp.emit(Opcode::Return);
|
|
1539
|
+
if let Some(n) = general_frame_slots {
|
|
1540
|
+
inner_comp.chunk.slot_based = true;
|
|
1541
|
+
inner_comp.chunk.num_slots = n;
|
|
1542
|
+
}
|
|
1543
|
+
let nested_idx = self.chunk.add_nested(inner);
|
|
1544
|
+
self.emit(Opcode::LoadConst);
|
|
1545
|
+
let idx = self.constant_idx(Constant::Closure(nested_idx));
|
|
1546
|
+
self.chunk.write_u16(idx);
|
|
1547
|
+
let idx = self.name_idx(name);
|
|
1548
|
+
self.emit_u16(Opcode::DeclareVar, idx);
|
|
1549
|
+
self.scope
|
|
1550
|
+
.last_mut()
|
|
1551
|
+
.unwrap()
|
|
1552
|
+
.insert(Arc::clone(name), false);
|
|
1553
|
+
}
|
|
1554
|
+
Statement::DoWhile { body, cond, .. } => {
|
|
1555
|
+
let body_lets = Self::loop_body_block_lets(body);
|
|
1556
|
+
for n in &body_lets {
|
|
1557
|
+
let idx = self.name_idx(n);
|
|
1558
|
+
self.emit_u16(Opcode::LoopVarsBegin, idx);
|
|
1559
|
+
}
|
|
1560
|
+
let start = self.chunk.code.len();
|
|
1561
|
+
self.loop_stack.push(LoopInfo {
|
|
1562
|
+
break_patches: Vec::new(),
|
|
1563
|
+
continue_patches: Vec::new(),
|
|
1564
|
+
continue_is_forward_jump: false,
|
|
1565
|
+
});
|
|
1566
|
+
self.breakable_stack.push(Breakable::Loop {
|
|
1567
|
+
unwind_depth: self.block_depth,
|
|
1568
|
+
});
|
|
1569
|
+
self.compile_statement(body)?;
|
|
1570
|
+
let cond_start = self.chunk.code.len();
|
|
1571
|
+
self.compile_expr(cond)?;
|
|
1572
|
+
let jump_back = self.emit_jump(Opcode::JumpIfFalse);
|
|
1573
|
+
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
|
|
1574
|
+
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
1575
|
+
let end = self.chunk.code.len();
|
|
1576
|
+
self.patch_jump(jump_back, end);
|
|
1577
|
+
let info = self.loop_stack.pop().unwrap();
|
|
1578
|
+
self.breakable_stack.pop();
|
|
1579
|
+
for p in info.continue_patches {
|
|
1580
|
+
self.patch_jump_back(p, cond_start);
|
|
1581
|
+
}
|
|
1582
|
+
for p in info.break_patches {
|
|
1583
|
+
self.patch_jump(p, end);
|
|
1584
|
+
}
|
|
1585
|
+
for _ in &body_lets {
|
|
1586
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
Statement::Switch {
|
|
1590
|
+
expr,
|
|
1591
|
+
cases,
|
|
1592
|
+
default_body,
|
|
1593
|
+
..
|
|
1594
|
+
} => {
|
|
1595
|
+
let switch_unwind_depth = self.block_depth;
|
|
1596
|
+
self.switch_stack.push(SwitchInfo {
|
|
1597
|
+
break_patches: Vec::new(),
|
|
1598
|
+
});
|
|
1599
|
+
self.breakable_stack.push(Breakable::Switch {
|
|
1600
|
+
unwind_depth: switch_unwind_depth,
|
|
1601
|
+
});
|
|
1602
|
+
self.compile_expr(expr)?;
|
|
1603
|
+
self.emit(Opcode::Dup);
|
|
1604
|
+
let mut end_patches = Vec::new();
|
|
1605
|
+
for (case_expr, case_body) in cases {
|
|
1606
|
+
self.emit(Opcode::Dup);
|
|
1607
|
+
if let Some(ce) = case_expr {
|
|
1608
|
+
self.compile_expr(ce)?;
|
|
1609
|
+
self.emit_u8(Opcode::BinOp, 8);
|
|
1610
|
+
let jump_next = self.emit_jump(Opcode::JumpIfFalse);
|
|
1611
|
+
// JumpIfFalse already pops the match result when taking this case
|
|
1612
|
+
self.compile_statement(&Statement::Block {
|
|
1613
|
+
statements: case_body.clone(),
|
|
1614
|
+
span: Span {
|
|
1615
|
+
start: (0, 0),
|
|
1616
|
+
end: (0, 0),
|
|
1617
|
+
},
|
|
1618
|
+
})?;
|
|
1619
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1620
|
+
end_patches.push(jump_end);
|
|
1621
|
+
self.patch_jump(jump_next, self.chunk.code.len());
|
|
1622
|
+
} else {
|
|
1623
|
+
self.emit(Opcode::Pop);
|
|
1624
|
+
self.compile_statement(&Statement::Block {
|
|
1625
|
+
statements: case_body.clone(),
|
|
1626
|
+
span: Span {
|
|
1627
|
+
start: (0, 0),
|
|
1628
|
+
end: (0, 0),
|
|
1629
|
+
},
|
|
1630
|
+
})?;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if let Some(body) = default_body {
|
|
1634
|
+
self.emit(Opcode::Pop);
|
|
1635
|
+
self.compile_statement(&Statement::Block {
|
|
1636
|
+
statements: body.clone(),
|
|
1637
|
+
span: Span {
|
|
1638
|
+
start: (0, 0),
|
|
1639
|
+
end: (0, 0),
|
|
1640
|
+
},
|
|
1641
|
+
})?;
|
|
1642
|
+
} else {
|
|
1643
|
+
self.emit(Opcode::Pop);
|
|
1644
|
+
}
|
|
1645
|
+
for p in end_patches {
|
|
1646
|
+
self.patch_jump(p, self.chunk.code.len());
|
|
1647
|
+
}
|
|
1648
|
+
let sw = self.switch_stack.pop().unwrap();
|
|
1649
|
+
self.breakable_stack.pop();
|
|
1650
|
+
for p in sw.break_patches {
|
|
1651
|
+
self.patch_jump(p, self.chunk.code.len());
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
Statement::Throw { value, .. } => {
|
|
1655
|
+
self.compile_expr(value)?;
|
|
1656
|
+
self.emit(Opcode::Throw);
|
|
1657
|
+
}
|
|
1658
|
+
Statement::Try {
|
|
1659
|
+
body,
|
|
1660
|
+
catch_param,
|
|
1661
|
+
catch_body,
|
|
1662
|
+
finally_body,
|
|
1663
|
+
..
|
|
1664
|
+
} => {
|
|
1665
|
+
let catch_offset_pos = self.chunk.code.len();
|
|
1666
|
+
self.emit(Opcode::EnterTry);
|
|
1667
|
+
self.chunk.write_u16(0);
|
|
1668
|
+
// A `return` inside the body/catch must run this finally on the way out.
|
|
1669
|
+
if let Some(f) = finally_body {
|
|
1670
|
+
self.finally_stack.push((**f).clone());
|
|
1671
|
+
}
|
|
1672
|
+
self.compile_statement(body)?;
|
|
1673
|
+
self.emit(Opcode::ExitTry);
|
|
1674
|
+
let jump_over_catch = self.emit_jump(Opcode::Jump);
|
|
1675
|
+
let catch_start = self.chunk.code.len();
|
|
1676
|
+
if let Some(catch_stmt) = catch_body {
|
|
1677
|
+
if let Some(param) = catch_param {
|
|
1678
|
+
self.emit(Opcode::EnterBlock);
|
|
1679
|
+
self.block_depth += 1;
|
|
1680
|
+
self.enter_block_scope();
|
|
1681
|
+
let param_idx = self.name_idx(param);
|
|
1682
|
+
self.emit_u16(Opcode::DeclareVar, param_idx);
|
|
1683
|
+
self.scope
|
|
1684
|
+
.last_mut()
|
|
1685
|
+
.unwrap()
|
|
1686
|
+
.insert(Arc::clone(param), false);
|
|
1687
|
+
self.compile_statement(catch_stmt)?;
|
|
1688
|
+
self.exit_block_scope();
|
|
1689
|
+
self.emit(Opcode::ExitBlock);
|
|
1690
|
+
self.block_depth -= 1;
|
|
1691
|
+
} else {
|
|
1692
|
+
self.emit(Opcode::Pop);
|
|
1693
|
+
self.compile_statement(catch_stmt)?;
|
|
1694
|
+
}
|
|
1695
|
+
} else {
|
|
1696
|
+
// No catch: run `finally` on the exception path, then re-raise (propagate).
|
|
1697
|
+
if let Some(f) = finally_body {
|
|
1698
|
+
self.compile_statement(f)?;
|
|
1699
|
+
}
|
|
1700
|
+
self.emit(Opcode::Throw);
|
|
1701
|
+
}
|
|
1702
|
+
let after_catch = self.chunk.code.len();
|
|
1703
|
+
self.patch_jump(jump_over_catch, after_catch);
|
|
1704
|
+
// The finally is no longer pending for enclosing returns once we emit its inline
|
|
1705
|
+
// (normal-path) copy below.
|
|
1706
|
+
if finally_body.is_some() {
|
|
1707
|
+
self.finally_stack.pop();
|
|
1708
|
+
}
|
|
1709
|
+
if let Some(finally) = finally_body {
|
|
1710
|
+
self.compile_statement(finally)?;
|
|
1711
|
+
}
|
|
1712
|
+
let catch_offset =
|
|
1713
|
+
catch_start.wrapping_sub(catch_offset_pos).wrapping_sub(3) as u16;
|
|
1714
|
+
self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
|
|
1715
|
+
self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
|
|
1716
|
+
}
|
|
1717
|
+
Statement::Import { .. } => {
|
|
1718
|
+
return Err(CompileError {
|
|
1719
|
+
message: "Import not supported in bytecode".to_string(),
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
1723
|
+
ExportDeclaration::Named(inner_stmt) => {
|
|
1724
|
+
self.compile_statement(inner_stmt.as_ref())?;
|
|
1725
|
+
}
|
|
1726
|
+
ExportDeclaration::Default(_) => {
|
|
1727
|
+
return Err(CompileError {
|
|
1728
|
+
message: "export default is not supported in bytecode".to_string(),
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
Statement::TypeAlias { .. }
|
|
1733
|
+
| Statement::DeclareVar { .. }
|
|
1734
|
+
| Statement::DeclareFun { .. } => {}
|
|
1735
|
+
}
|
|
1736
|
+
Ok(())
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
fn compile_destructure(
|
|
1740
|
+
&mut self,
|
|
1741
|
+
pattern: &DestructPattern,
|
|
1742
|
+
mutable: bool,
|
|
1743
|
+
for_header_binding: bool,
|
|
1744
|
+
) -> Result<(), CompileError> {
|
|
1745
|
+
let decl_op = if for_header_binding {
|
|
1746
|
+
Opcode::DeclareVarPlain
|
|
1747
|
+
} else {
|
|
1748
|
+
Opcode::DeclareVar
|
|
1749
|
+
};
|
|
1750
|
+
match pattern {
|
|
1751
|
+
DestructPattern::Array(elements) => {
|
|
1752
|
+
for (i, elem) in elements.iter().enumerate() {
|
|
1753
|
+
match elem {
|
|
1754
|
+
Some(DestructElement::Ident(name, _)) => {
|
|
1755
|
+
self.emit(Opcode::Dup);
|
|
1756
|
+
let idx = self.constant_idx(Constant::Number(i as f64));
|
|
1757
|
+
self.emit(Opcode::LoadConst);
|
|
1758
|
+
self.chunk.write_u16(idx);
|
|
1759
|
+
self.emit(Opcode::GetIndex);
|
|
1760
|
+
let idx = self.name_idx(name);
|
|
1761
|
+
self.emit_u16(decl_op, idx);
|
|
1762
|
+
self.scope
|
|
1763
|
+
.last_mut()
|
|
1764
|
+
.unwrap()
|
|
1765
|
+
.insert(Arc::clone(name), false);
|
|
1766
|
+
}
|
|
1767
|
+
// Array hole `[a, , c]`: position is skipped, no binding emitted.
|
|
1768
|
+
None => {}
|
|
1769
|
+
// Nested pattern `[[a, b], c]` or `[{x}, y]`: push source[i] and recurse.
|
|
1770
|
+
// compile_destructure is stack-balanced (consumes exactly the value it
|
|
1771
|
+
// destructures), so the source array beneath stays intact.
|
|
1772
|
+
Some(DestructElement::Pattern(sub)) => {
|
|
1773
|
+
self.emit(Opcode::Dup);
|
|
1774
|
+
let idx = self.constant_idx(Constant::Number(i as f64));
|
|
1775
|
+
self.emit(Opcode::LoadConst);
|
|
1776
|
+
self.chunk.write_u16(idx);
|
|
1777
|
+
self.emit(Opcode::GetIndex);
|
|
1778
|
+
self.compile_destructure(sub, mutable, for_header_binding)?;
|
|
1779
|
+
}
|
|
1780
|
+
// Rest `[a, ...rest]`: rest = source.slice(i). Use GetMember (not GetIndex)
|
|
1781
|
+
// so the array's `slice` method resolves via get_member; GetIndex rejects
|
|
1782
|
+
// string keys on arrays.
|
|
1783
|
+
Some(DestructElement::Rest(name, _)) => {
|
|
1784
|
+
self.emit(Opcode::Dup);
|
|
1785
|
+
let slice_idx = self.name_idx(&Arc::from("slice"));
|
|
1786
|
+
self.emit_u16(Opcode::GetMember, slice_idx);
|
|
1787
|
+
let idx = self.constant_idx(Constant::Number(i as f64));
|
|
1788
|
+
self.emit(Opcode::LoadConst);
|
|
1789
|
+
self.chunk.write_u16(idx);
|
|
1790
|
+
self.emit_u16(Opcode::Call, 1);
|
|
1791
|
+
let nidx = self.name_idx(name);
|
|
1792
|
+
self.emit_u16(decl_op, nidx);
|
|
1793
|
+
self.scope
|
|
1794
|
+
.last_mut()
|
|
1795
|
+
.unwrap()
|
|
1796
|
+
.insert(Arc::clone(name), false);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
self.emit(Opcode::Pop);
|
|
1801
|
+
}
|
|
1802
|
+
DestructPattern::Object(props) => {
|
|
1803
|
+
for prop in props {
|
|
1804
|
+
self.emit(Opcode::Dup);
|
|
1805
|
+
let key_idx = self.constant_idx(Constant::String(Arc::clone(&prop.key)));
|
|
1806
|
+
self.emit(Opcode::LoadConst);
|
|
1807
|
+
self.chunk.write_u16(key_idx);
|
|
1808
|
+
self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
|
|
1809
|
+
match &prop.value {
|
|
1810
|
+
DestructElement::Ident(name, _) => {
|
|
1811
|
+
let idx = self.name_idx(name);
|
|
1812
|
+
self.emit_u16(decl_op, idx);
|
|
1813
|
+
if mutable {
|
|
1814
|
+
self.scope
|
|
1815
|
+
.last_mut()
|
|
1816
|
+
.unwrap()
|
|
1817
|
+
.insert(Arc::clone(name), false);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
// Nested value `{ outer: { inner } }` or `{ arr: [a, b] }`: obj[key] is
|
|
1821
|
+
// already on the stack (GetIndex above); recurse to destructure it.
|
|
1822
|
+
DestructElement::Pattern(sub) => {
|
|
1823
|
+
self.compile_destructure(sub, mutable, for_header_binding)?;
|
|
1824
|
+
}
|
|
1825
|
+
// `{ ...rest }` needs the set of *remaining* keys; not yet supported.
|
|
1826
|
+
DestructElement::Rest(_, _) => {
|
|
1827
|
+
return Err(CompileError {
|
|
1828
|
+
message: "Object rest destructuring not yet supported".to_string(),
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
self.emit(Opcode::Pop);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
Ok(())
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
|
|
1840
|
+
self.mark_line(expr.span());
|
|
1841
|
+
match expr {
|
|
1842
|
+
Expr::Literal { value, .. } => {
|
|
1843
|
+
let c = match value {
|
|
1844
|
+
Literal::Number(n) => Constant::Number(*n),
|
|
1845
|
+
Literal::String(s) => Constant::String(Arc::clone(s)),
|
|
1846
|
+
Literal::Bool(b) => Constant::Bool(*b),
|
|
1847
|
+
Literal::Null => Constant::Null,
|
|
1848
|
+
};
|
|
1849
|
+
let idx = self.constant_idx(c);
|
|
1850
|
+
self.emit(Opcode::LoadConst);
|
|
1851
|
+
self.chunk.write_u16(idx);
|
|
1852
|
+
}
|
|
1853
|
+
Expr::Ident { name, .. } => {
|
|
1854
|
+
// `resolve_slot` checks BOTH the simple param-only map and the general scope stack.
|
|
1855
|
+
self.emit_var_load(name);
|
|
1856
|
+
}
|
|
1857
|
+
Expr::Binary {
|
|
1858
|
+
left, op, right, ..
|
|
1859
|
+
} => {
|
|
1860
|
+
match op {
|
|
1861
|
+
BinOp::And => {
|
|
1862
|
+
// Short-circuit: a && b => if !a then a else b
|
|
1863
|
+
self.compile_expr(left)?;
|
|
1864
|
+
self.emit(Opcode::Dup);
|
|
1865
|
+
let jump_shortcut = self.emit_jump(Opcode::JumpIfFalse);
|
|
1866
|
+
self.compile_expr(right)?; // left still on stack from Dup
|
|
1867
|
+
self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::And));
|
|
1868
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1869
|
+
self.patch_jump(jump_shortcut, self.chunk.code.len());
|
|
1870
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
1871
|
+
}
|
|
1872
|
+
BinOp::Or => {
|
|
1873
|
+
// Short-circuit: a || b => if a then a else b
|
|
1874
|
+
self.compile_expr(left)?;
|
|
1875
|
+
self.emit(Opcode::Dup);
|
|
1876
|
+
let jump_eval_right = self.emit_jump(Opcode::JumpIfFalse);
|
|
1877
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
1878
|
+
self.patch_jump(jump_eval_right, self.chunk.code.len());
|
|
1879
|
+
self.emit(Opcode::Pop); // discard falsy left
|
|
1880
|
+
self.compile_expr(right)?;
|
|
1881
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
1882
|
+
}
|
|
1883
|
+
_ => {
|
|
1884
|
+
self.compile_expr(left)?;
|
|
1885
|
+
self.compile_expr(right)?;
|
|
1886
|
+
self.emit_u8(Opcode::BinOp, binop_to_u8(*op));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
Expr::Unary { op, operand, .. } => {
|
|
1891
|
+
self.compile_expr(operand)?;
|
|
1892
|
+
self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
|
|
1893
|
+
}
|
|
1894
|
+
Expr::Call { callee, args, .. } => {
|
|
1895
|
+
// Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
|
|
1896
|
+
if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
|
|
1897
|
+
&& args.len() == 1
|
|
1898
|
+
&& matches!(args[0], CallArg::Expr(_))
|
|
1899
|
+
{
|
|
1900
|
+
if let (
|
|
1901
|
+
Expr::Member {
|
|
1902
|
+
object,
|
|
1903
|
+
prop: MemberProp::Name { name: key, .. },
|
|
1904
|
+
optional: false,
|
|
1905
|
+
..
|
|
1906
|
+
},
|
|
1907
|
+
CallArg::Expr(cmp_expr),
|
|
1908
|
+
) = (callee.as_ref(), &args[0])
|
|
1909
|
+
{
|
|
1910
|
+
if key.as_ref() == "sort" {
|
|
1911
|
+
if let Some(ascending) = Self::detect_numeric_sort_comparator(cmp_expr)
|
|
1912
|
+
{
|
|
1913
|
+
self.compile_expr(object)?;
|
|
1914
|
+
self.emit_u8(
|
|
1915
|
+
Opcode::ArraySortNumeric,
|
|
1916
|
+
if ascending { 0 } else { 1 },
|
|
1917
|
+
);
|
|
1918
|
+
return Ok(());
|
|
1919
|
+
}
|
|
1920
|
+
if let Some((prop, ascending)) =
|
|
1921
|
+
Self::detect_property_sort_comparator(cmp_expr)
|
|
1922
|
+
{
|
|
1923
|
+
self.compile_expr(object)?;
|
|
1924
|
+
let prop_idx = self.constant_idx(Constant::String(prop));
|
|
1925
|
+
self.emit(Opcode::ArraySortByProperty);
|
|
1926
|
+
self.chunk.write_u16(prop_idx);
|
|
1927
|
+
self.chunk.write_u16(if ascending { 0 } else { 1 });
|
|
1928
|
+
return Ok(());
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if key.as_ref() == "map" {
|
|
1932
|
+
if let Some(simple) = Self::detect_simple_map_callback(cmp_expr) {
|
|
1933
|
+
self.compile_expr(object)?;
|
|
1934
|
+
match simple {
|
|
1935
|
+
SimpleMapResult::Identity => {
|
|
1936
|
+
self.emit(Opcode::ArrayMapIdentity);
|
|
1937
|
+
}
|
|
1938
|
+
SimpleMapResult::BinOp(op, c, param_left) => {
|
|
1939
|
+
let const_idx = self.constant_idx(c);
|
|
1940
|
+
self.emit(Opcode::ArrayMapBinOp);
|
|
1941
|
+
self.chunk.write_u8(binop_to_u8(op));
|
|
1942
|
+
self.chunk.write_u16(const_idx);
|
|
1943
|
+
self.chunk.write_u8(if param_left { 0 } else { 1 });
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return Ok(());
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if key.as_ref() == "filter" {
|
|
1950
|
+
if let Some((op, const_val, param_left)) =
|
|
1951
|
+
Self::detect_simple_filter_callback(cmp_expr)
|
|
1952
|
+
{
|
|
1953
|
+
self.compile_expr(object)?;
|
|
1954
|
+
let const_idx = self.constant_idx(const_val);
|
|
1955
|
+
self.emit(Opcode::ArrayFilterBinOp);
|
|
1956
|
+
self.chunk.write_u8(binop_to_u8(op));
|
|
1957
|
+
self.chunk.write_u16(const_idx);
|
|
1958
|
+
self.chunk.write_u8(if param_left { 0 } else { 1 });
|
|
1959
|
+
return Ok(());
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
|
|
1965
|
+
if has_spread {
|
|
1966
|
+
// Build args array [a, ...b, c], then callee, then CallSpread
|
|
1967
|
+
self.emit_u16(Opcode::NewArray, 0);
|
|
1968
|
+
for arg in args {
|
|
1969
|
+
match arg {
|
|
1970
|
+
CallArg::Expr(e) => {
|
|
1971
|
+
self.compile_expr(e)?;
|
|
1972
|
+
self.emit_u16(Opcode::NewArray, 1);
|
|
1973
|
+
self.emit(Opcode::ConcatArray);
|
|
1974
|
+
}
|
|
1975
|
+
CallArg::Spread(expr) => {
|
|
1976
|
+
self.compile_expr(expr)?;
|
|
1977
|
+
self.emit(Opcode::ConcatArray);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
self.compile_expr(callee)?;
|
|
1982
|
+
self.emit(Opcode::CallSpread);
|
|
1983
|
+
} else {
|
|
1984
|
+
// Self-recursion fast path: `name(args)` where `name` is this function's own
|
|
1985
|
+
// provably-stable binding → `SelfCall` (no callee LoadVar, no closure dispatch;
|
|
1986
|
+
// the JIT lowers it to a native recursive call). `self_fn_name` is only `Some`
|
|
1987
|
+
// when the compiler proved `name` isn't shadowed or rebound (see FunDecl).
|
|
1988
|
+
let is_self_call = matches!(
|
|
1989
|
+
callee.as_ref(),
|
|
1990
|
+
Expr::Ident { name, .. } if self.self_fn_name.as_deref() == Some(name.as_ref())
|
|
1991
|
+
);
|
|
1992
|
+
if is_self_call {
|
|
1993
|
+
for arg in args {
|
|
1994
|
+
if let CallArg::Expr(e) = arg {
|
|
1995
|
+
self.compile_expr(e)?;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
self.emit_u16(Opcode::SelfCall, args.len() as u16);
|
|
1999
|
+
} else {
|
|
2000
|
+
self.compile_expr(callee)?;
|
|
2001
|
+
for arg in args {
|
|
2002
|
+
if let CallArg::Expr(e) = arg {
|
|
2003
|
+
self.compile_expr(e)?;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
self.emit_u16(Opcode::Call, args.len() as u16);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
Expr::Member {
|
|
2011
|
+
object,
|
|
2012
|
+
prop,
|
|
2013
|
+
optional,
|
|
2014
|
+
..
|
|
2015
|
+
} => {
|
|
2016
|
+
self.compile_expr(object)?;
|
|
2017
|
+
if *optional {
|
|
2018
|
+
self.emit(Opcode::Dup);
|
|
2019
|
+
let null_idx = self.constant_idx(Constant::Null);
|
|
2020
|
+
self.emit(Opcode::LoadConst);
|
|
2021
|
+
self.chunk.write_u16(null_idx);
|
|
2022
|
+
self.emit_u8(Opcode::BinOp, 8);
|
|
2023
|
+
let jump_to_null = self.emit_jump(Opcode::JumpIfFalse);
|
|
2024
|
+
let jump_to_get_instr = self.chunk.code.len();
|
|
2025
|
+
let jump_to_get = self.emit_jump(Opcode::Jump);
|
|
2026
|
+
self.patch_jump(jump_to_null, jump_to_get_instr);
|
|
2027
|
+
self.emit(Opcode::Pop);
|
|
2028
|
+
self.emit(Opcode::LoadConst);
|
|
2029
|
+
self.chunk.write_u16(null_idx);
|
|
2030
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
2031
|
+
self.patch_jump(jump_to_get, self.chunk.code.len());
|
|
2032
|
+
match prop {
|
|
2033
|
+
MemberProp::Name { name: key, .. } => {
|
|
2034
|
+
let idx = self.name_idx(key);
|
|
2035
|
+
self.emit_u16(Opcode::GetMemberOptional, idx);
|
|
2036
|
+
}
|
|
2037
|
+
MemberProp::Expr(e) => {
|
|
2038
|
+
self.compile_expr(e)?;
|
|
2039
|
+
self.emit(Opcode::GetIndex);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
2043
|
+
} else {
|
|
2044
|
+
match prop {
|
|
2045
|
+
MemberProp::Name { name: key, .. } => {
|
|
2046
|
+
let idx = self.name_idx(key);
|
|
2047
|
+
self.emit_u16(Opcode::GetMember, idx);
|
|
2048
|
+
}
|
|
2049
|
+
MemberProp::Expr(e) => {
|
|
2050
|
+
self.compile_expr(e)?;
|
|
2051
|
+
self.emit(Opcode::GetIndex);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
Expr::Index { object, index, .. } => {
|
|
2057
|
+
self.compile_expr(object)?;
|
|
2058
|
+
self.compile_expr(index)?;
|
|
2059
|
+
self.emit(Opcode::GetIndex);
|
|
2060
|
+
}
|
|
2061
|
+
Expr::Conditional {
|
|
2062
|
+
cond,
|
|
2063
|
+
then_branch,
|
|
2064
|
+
else_branch,
|
|
2065
|
+
..
|
|
2066
|
+
} => {
|
|
2067
|
+
self.compile_expr(cond)?;
|
|
2068
|
+
let jump_else = self.emit_jump(Opcode::JumpIfFalse);
|
|
2069
|
+
// JumpIfFalse pops condition when taking then; when taking else it also pops
|
|
2070
|
+
self.compile_expr(then_branch)?;
|
|
2071
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
2072
|
+
self.patch_jump(jump_else, self.chunk.code.len());
|
|
2073
|
+
// no Pop: condition was already popped by JumpIfFalse
|
|
2074
|
+
self.compile_expr(else_branch)?;
|
|
2075
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
2076
|
+
}
|
|
2077
|
+
Expr::NullishCoalesce { left, right, .. } => {
|
|
2078
|
+
self.compile_expr(left)?;
|
|
2079
|
+
self.emit(Opcode::Dup);
|
|
2080
|
+
let idx = self.constant_idx(Constant::Null);
|
|
2081
|
+
self.emit(Opcode::LoadConst);
|
|
2082
|
+
self.chunk.write_u16(idx);
|
|
2083
|
+
self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictNe));
|
|
2084
|
+
let jump_to_right = self.emit_jump(Opcode::JumpIfFalse);
|
|
2085
|
+
let jump_end = self.emit_jump(Opcode::Jump);
|
|
2086
|
+
self.patch_jump(jump_to_right, self.chunk.code.len());
|
|
2087
|
+
self.emit(Opcode::Pop);
|
|
2088
|
+
self.compile_expr(right)?;
|
|
2089
|
+
self.patch_jump(jump_end, self.chunk.code.len());
|
|
2090
|
+
}
|
|
2091
|
+
Expr::Array { elements, .. } => {
|
|
2092
|
+
let has_spread = elements
|
|
2093
|
+
.iter()
|
|
2094
|
+
.any(|e| matches!(e, ArrayElement::Spread(_)));
|
|
2095
|
+
if has_spread {
|
|
2096
|
+
// Build array incrementally: start with [], concat each element
|
|
2097
|
+
self.emit_u16(Opcode::NewArray, 0);
|
|
2098
|
+
for elem in elements {
|
|
2099
|
+
match elem {
|
|
2100
|
+
ArrayElement::Expr(e) => {
|
|
2101
|
+
self.compile_expr(e)?;
|
|
2102
|
+
self.emit_u16(Opcode::NewArray, 1);
|
|
2103
|
+
self.emit(Opcode::ConcatArray);
|
|
2104
|
+
}
|
|
2105
|
+
ArrayElement::Spread(expr) => {
|
|
2106
|
+
self.compile_expr(expr)?;
|
|
2107
|
+
self.emit(Opcode::ConcatArray);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
for elem in elements {
|
|
2113
|
+
if let ArrayElement::Expr(e) = elem {
|
|
2114
|
+
self.compile_expr(e)?;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
self.emit_u16(Opcode::NewArray, elements.len() as u16);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
Expr::Object { props, .. } => {
|
|
2121
|
+
let has_spread = props.iter().any(|p| matches!(p, ObjectProp::Spread(_)));
|
|
2122
|
+
if has_spread {
|
|
2123
|
+
self.emit_u16(Opcode::NewObject, 0); // start with {}
|
|
2124
|
+
for prop in props {
|
|
2125
|
+
match prop {
|
|
2126
|
+
ObjectProp::KeyValue(k, v) => {
|
|
2127
|
+
let idx = self.constant_idx(Constant::String(Arc::clone(k)));
|
|
2128
|
+
self.emit(Opcode::LoadConst);
|
|
2129
|
+
self.chunk.write_u16(idx);
|
|
2130
|
+
self.compile_expr(v)?;
|
|
2131
|
+
self.emit_u16(Opcode::NewObject, 1);
|
|
2132
|
+
self.emit(Opcode::MergeObject);
|
|
2133
|
+
}
|
|
2134
|
+
ObjectProp::Spread(expr) => {
|
|
2135
|
+
self.compile_expr(expr)?;
|
|
2136
|
+
self.emit(Opcode::MergeObject);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
} else {
|
|
2141
|
+
for prop in props {
|
|
2142
|
+
if let ObjectProp::KeyValue(k, v) = prop {
|
|
2143
|
+
let idx = self.constant_idx(Constant::String(Arc::clone(k)));
|
|
2144
|
+
self.emit(Opcode::LoadConst);
|
|
2145
|
+
self.chunk.write_u16(idx);
|
|
2146
|
+
self.compile_expr(v)?;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
self.emit_u16(Opcode::NewObject, props.len() as u16);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
Expr::Assign { name, value, .. } => {
|
|
2153
|
+
self.compile_expr(value)?;
|
|
2154
|
+
self.emit_var_store(name);
|
|
2155
|
+
self.emit_var_load(name); // assign yields value
|
|
2156
|
+
}
|
|
2157
|
+
Expr::TypeOf { operand, .. } => {
|
|
2158
|
+
let typeof_idx = self.name_idx(&Arc::from("typeof"));
|
|
2159
|
+
self.emit_u16(Opcode::LoadGlobal, typeof_idx);
|
|
2160
|
+
self.compile_expr(operand)?;
|
|
2161
|
+
self.emit_u16(Opcode::Call, 1);
|
|
2162
|
+
}
|
|
2163
|
+
Expr::ArrowFunction { params, body, .. } => {
|
|
2164
|
+
let formal_len = params.len();
|
|
2165
|
+
let (param_names, slots) = Self::plan_function_params(params)?;
|
|
2166
|
+
let simple_slots = simple_fn_slots(params, false, |pset| match body {
|
|
2167
|
+
ArrowBody::Expr(e) => expr_is_param_only(e, pset),
|
|
2168
|
+
ArrowBody::Block(s) => stmt_is_param_only(s, pset),
|
|
2169
|
+
});
|
|
2170
|
+
let mut inner = Chunk::new();
|
|
2171
|
+
inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
|
|
2172
|
+
for p in ¶m_names {
|
|
2173
|
+
inner.add_name(Arc::clone(p));
|
|
2174
|
+
}
|
|
2175
|
+
inner.param_count = param_names.len() as u16;
|
|
2176
|
+
if simple_slots.is_some() {
|
|
2177
|
+
inner.slot_based = true;
|
|
2178
|
+
inner.num_slots = param_names.len() as u16;
|
|
2179
|
+
}
|
|
2180
|
+
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
2181
|
+
if let Some(map) = simple_slots {
|
|
2182
|
+
inner_comp.slot_ctx = Some(map);
|
|
2183
|
+
} else {
|
|
2184
|
+
inner_comp.scope = vec![param_names
|
|
2185
|
+
.iter()
|
|
2186
|
+
.map(|n| (Arc::clone(n), false))
|
|
2187
|
+
.collect::<HashMap<_, _>>()];
|
|
2188
|
+
inner_comp.emit_param_destructure_prologue(¶m_names[..formal_len], &slots)?;
|
|
2189
|
+
}
|
|
2190
|
+
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2191
|
+
match body {
|
|
2192
|
+
ArrowBody::Expr(e) => {
|
|
2193
|
+
inner_comp.compile_expr(e)?;
|
|
2194
|
+
inner_comp.emit(Opcode::Return);
|
|
2195
|
+
}
|
|
2196
|
+
ArrowBody::Block(s) => {
|
|
2197
|
+
inner_comp.compile_statement(s)?;
|
|
2198
|
+
let idx = inner_comp.constant_idx(Constant::Null);
|
|
2199
|
+
inner_comp.emit(Opcode::LoadConst);
|
|
2200
|
+
inner_comp.chunk.write_u16(idx);
|
|
2201
|
+
inner_comp.emit(Opcode::Return);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
let nested_idx = self.chunk.add_nested(inner);
|
|
2205
|
+
let idx = self.constant_idx(Constant::Closure(nested_idx));
|
|
2206
|
+
self.emit(Opcode::LoadConst);
|
|
2207
|
+
self.chunk.write_u16(idx);
|
|
2208
|
+
}
|
|
2209
|
+
Expr::TemplateLiteral { quasis, exprs, .. } => {
|
|
2210
|
+
if exprs.is_empty() {
|
|
2211
|
+
let s = quasis[0].to_string();
|
|
2212
|
+
let idx = self.constant_idx(Constant::String(Arc::from(s)));
|
|
2213
|
+
self.emit(Opcode::LoadConst);
|
|
2214
|
+
self.chunk.write_u16(idx);
|
|
2215
|
+
} else {
|
|
2216
|
+
// Interleave quasis and exprs: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + quasi[n]
|
|
2217
|
+
let first = quasis[0].to_string();
|
|
2218
|
+
let idx = self.constant_idx(Constant::String(Arc::from(first)));
|
|
2219
|
+
self.emit(Opcode::LoadConst);
|
|
2220
|
+
self.chunk.write_u16(idx);
|
|
2221
|
+
for (i, expr) in exprs.iter().enumerate() {
|
|
2222
|
+
self.compile_expr(expr)?;
|
|
2223
|
+
self.emit_u8(Opcode::BinOp, 0); // Add (string concat)
|
|
2224
|
+
let quasi_s = quasis[i + 1].to_string();
|
|
2225
|
+
let qidx = self.constant_idx(Constant::String(Arc::from(quasi_s)));
|
|
2226
|
+
self.emit(Opcode::LoadConst);
|
|
2227
|
+
self.chunk.write_u16(qidx);
|
|
2228
|
+
self.emit_u8(Opcode::BinOp, 0); // Add
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
Expr::PostfixInc { name, .. } => {
|
|
2233
|
+
let one = self.constant_idx(Constant::Number(1.0));
|
|
2234
|
+
self.emit_var_load(name);
|
|
2235
|
+
self.emit(Opcode::Dup);
|
|
2236
|
+
self.emit(Opcode::LoadConst);
|
|
2237
|
+
self.chunk.write_u16(one);
|
|
2238
|
+
self.emit_u8(Opcode::BinOp, 0);
|
|
2239
|
+
self.emit_var_store(name);
|
|
2240
|
+
}
|
|
2241
|
+
Expr::PostfixDec { name, .. } => {
|
|
2242
|
+
let one = self.constant_idx(Constant::Number(1.0));
|
|
2243
|
+
self.emit_var_load(name);
|
|
2244
|
+
self.emit(Opcode::Dup);
|
|
2245
|
+
self.emit(Opcode::LoadConst);
|
|
2246
|
+
self.chunk.write_u16(one);
|
|
2247
|
+
self.emit_u8(Opcode::BinOp, 1);
|
|
2248
|
+
self.emit_var_store(name);
|
|
2249
|
+
}
|
|
2250
|
+
Expr::PrefixInc { name, .. } => {
|
|
2251
|
+
let one = self.constant_idx(Constant::Number(1.0));
|
|
2252
|
+
self.emit_var_load(name);
|
|
2253
|
+
self.emit(Opcode::LoadConst);
|
|
2254
|
+
self.chunk.write_u16(one);
|
|
2255
|
+
self.emit_u8(Opcode::BinOp, 0);
|
|
2256
|
+
self.emit(Opcode::Dup);
|
|
2257
|
+
self.emit_var_store(name);
|
|
2258
|
+
}
|
|
2259
|
+
Expr::PrefixDec { name, .. } => {
|
|
2260
|
+
let one = self.constant_idx(Constant::Number(1.0));
|
|
2261
|
+
self.emit_var_load(name);
|
|
2262
|
+
self.emit(Opcode::LoadConst);
|
|
2263
|
+
self.chunk.write_u16(one);
|
|
2264
|
+
self.emit_u8(Opcode::BinOp, 1);
|
|
2265
|
+
self.emit(Opcode::Dup);
|
|
2266
|
+
self.emit_var_store(name);
|
|
2267
|
+
}
|
|
2268
|
+
Expr::CompoundAssign {
|
|
2269
|
+
name, op, value, ..
|
|
2270
|
+
} => {
|
|
2271
|
+
self.emit_var_load(name);
|
|
2272
|
+
self.compile_expr(value)?;
|
|
2273
|
+
self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
|
|
2274
|
+
self.emit(Opcode::Dup);
|
|
2275
|
+
self.emit_var_store(name);
|
|
2276
|
+
}
|
|
2277
|
+
Expr::MemberAssign {
|
|
2278
|
+
object,
|
|
2279
|
+
prop,
|
|
2280
|
+
value,
|
|
2281
|
+
..
|
|
2282
|
+
} => {
|
|
2283
|
+
self.compile_expr(object)?;
|
|
2284
|
+
self.compile_expr(value)?;
|
|
2285
|
+
let idx = self.name_idx(prop);
|
|
2286
|
+
self.emit_u16(Opcode::SetMember, idx); // SetMember pops obj, val and pushes val back
|
|
2287
|
+
}
|
|
2288
|
+
Expr::IndexAssign {
|
|
2289
|
+
object,
|
|
2290
|
+
index,
|
|
2291
|
+
value,
|
|
2292
|
+
..
|
|
2293
|
+
} => {
|
|
2294
|
+
self.compile_expr(object)?;
|
|
2295
|
+
self.compile_expr(index)?;
|
|
2296
|
+
self.compile_expr(value)?;
|
|
2297
|
+
self.emit(Opcode::Dup); // leave copy for assignment expression result
|
|
2298
|
+
self.emit(Opcode::SetIndex);
|
|
2299
|
+
}
|
|
2300
|
+
Expr::NativeModuleLoad {
|
|
2301
|
+
spec, export_name, ..
|
|
2302
|
+
} => {
|
|
2303
|
+
let spec_idx = self.constant_idx(Constant::String(Arc::clone(spec)));
|
|
2304
|
+
let export_idx = self.constant_idx(Constant::String(Arc::clone(export_name)));
|
|
2305
|
+
self.emit(Opcode::LoadNativeExport);
|
|
2306
|
+
self.chunk.write_u16(spec_idx);
|
|
2307
|
+
self.chunk.write_u16(export_idx);
|
|
2308
|
+
}
|
|
2309
|
+
Expr::JsxElement {
|
|
2310
|
+
tag,
|
|
2311
|
+
props,
|
|
2312
|
+
children,
|
|
2313
|
+
..
|
|
2314
|
+
} => {
|
|
2315
|
+
self.compile_jsx_element(tag, props, children)?;
|
|
2316
|
+
}
|
|
2317
|
+
Expr::JsxFragment { children, .. } => {
|
|
2318
|
+
self.compile_jsx_fragment(children)?;
|
|
2319
|
+
}
|
|
2320
|
+
Expr::Await { operand, .. } => {
|
|
2321
|
+
// await expr => evaluate operand, then VM Opcode::AwaitPromise (throw on reject).
|
|
2322
|
+
self.compile_expr(operand)?;
|
|
2323
|
+
self.emit(Opcode::AwaitPromise);
|
|
2324
|
+
}
|
|
2325
|
+
Expr::Delete { target, .. } => {
|
|
2326
|
+
// `delete obj.prop` / `delete obj[key]` → push [obj, key], then DeleteIndex
|
|
2327
|
+
// pops both, removes the property, and pushes `true`. Deleting anything that
|
|
2328
|
+
// isn't a property reference is a no-op that still yields `true` (JS).
|
|
2329
|
+
match target.as_ref() {
|
|
2330
|
+
Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
|
|
2331
|
+
self.compile_expr(object)?;
|
|
2332
|
+
let idx = self.constant_idx(Constant::String(Arc::clone(name)));
|
|
2333
|
+
self.emit(Opcode::LoadConst);
|
|
2334
|
+
self.chunk.write_u16(idx);
|
|
2335
|
+
self.emit(Opcode::DeleteIndex);
|
|
2336
|
+
}
|
|
2337
|
+
Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
|
|
2338
|
+
self.compile_expr(object)?;
|
|
2339
|
+
self.compile_expr(key)?;
|
|
2340
|
+
self.emit(Opcode::DeleteIndex);
|
|
2341
|
+
}
|
|
2342
|
+
Expr::Index { object, index, .. } => {
|
|
2343
|
+
self.compile_expr(object)?;
|
|
2344
|
+
self.compile_expr(index)?;
|
|
2345
|
+
self.emit(Opcode::DeleteIndex);
|
|
2346
|
+
}
|
|
2347
|
+
_ => {
|
|
2348
|
+
let idx = self.constant_idx(Constant::Bool(true));
|
|
2349
|
+
self.emit(Opcode::LoadConst);
|
|
2350
|
+
self.chunk.write_u16(idx);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
Expr::LogicalAssign {
|
|
2355
|
+
name, op, value, ..
|
|
2356
|
+
} => {
|
|
2357
|
+
match op {
|
|
2358
|
+
LogicalAssignOp::OrOr => {
|
|
2359
|
+
// ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
|
|
2360
|
+
self.emit_var_load(name);
|
|
2361
|
+
self.emit(Opcode::Dup);
|
|
2362
|
+
let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
|
|
2363
|
+
let j_end = self.emit_jump(Opcode::Jump);
|
|
2364
|
+
self.patch_jump(j_rhs, self.chunk.code.len());
|
|
2365
|
+
self.emit(Opcode::Pop);
|
|
2366
|
+
self.compile_expr(value)?;
|
|
2367
|
+
self.emit_var_store(name);
|
|
2368
|
+
self.emit_var_load(name);
|
|
2369
|
+
let end = self.chunk.code.len();
|
|
2370
|
+
self.patch_jump(j_end, end);
|
|
2371
|
+
}
|
|
2372
|
+
LogicalAssignOp::AndAnd => {
|
|
2373
|
+
// &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
|
|
2374
|
+
self.emit_var_load(name);
|
|
2375
|
+
self.emit(Opcode::Dup);
|
|
2376
|
+
let j_short = self.emit_jump(Opcode::JumpIfFalse);
|
|
2377
|
+
self.emit(Opcode::Pop);
|
|
2378
|
+
self.compile_expr(value)?;
|
|
2379
|
+
self.emit_var_store(name);
|
|
2380
|
+
self.emit_var_load(name);
|
|
2381
|
+
let j_end = self.emit_jump(Opcode::Jump);
|
|
2382
|
+
let end = self.chunk.code.len();
|
|
2383
|
+
self.patch_jump(j_short, end);
|
|
2384
|
+
self.patch_jump(j_end, end);
|
|
2385
|
+
}
|
|
2386
|
+
LogicalAssignOp::Nullish => {
|
|
2387
|
+
// ??= : assign only when current === null (matches interpreter)
|
|
2388
|
+
let null_c = self.constant_idx(Constant::Null);
|
|
2389
|
+
self.emit_var_load(name);
|
|
2390
|
+
self.emit(Opcode::Dup);
|
|
2391
|
+
self.emit(Opcode::LoadConst);
|
|
2392
|
+
self.chunk.write_u16(null_c);
|
|
2393
|
+
self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictEq));
|
|
2394
|
+
let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
|
|
2395
|
+
self.emit(Opcode::Pop);
|
|
2396
|
+
self.compile_expr(value)?;
|
|
2397
|
+
self.emit_var_store(name);
|
|
2398
|
+
self.emit_var_load(name);
|
|
2399
|
+
let j_end = self.emit_jump(Opcode::Jump);
|
|
2400
|
+
let end = self.chunk.code.len();
|
|
2401
|
+
self.patch_jump(j_not_null, end);
|
|
2402
|
+
self.patch_jump(j_end, end);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
Expr::New { callee, args, .. } => {
|
|
2407
|
+
let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
|
|
2408
|
+
if has_spread {
|
|
2409
|
+
self.emit_u16(Opcode::NewArray, 0);
|
|
2410
|
+
for arg in args {
|
|
2411
|
+
match arg {
|
|
2412
|
+
CallArg::Expr(e) => {
|
|
2413
|
+
self.compile_expr(e)?;
|
|
2414
|
+
self.emit_u16(Opcode::NewArray, 1);
|
|
2415
|
+
self.emit(Opcode::ConcatArray);
|
|
2416
|
+
}
|
|
2417
|
+
CallArg::Spread(expr) => {
|
|
2418
|
+
self.compile_expr(expr)?;
|
|
2419
|
+
self.emit(Opcode::ConcatArray);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
self.compile_expr(callee)?;
|
|
2424
|
+
self.emit(Opcode::ConstructSpread);
|
|
2425
|
+
} else {
|
|
2426
|
+
self.compile_expr(callee)?;
|
|
2427
|
+
for arg in args {
|
|
2428
|
+
if let CallArg::Expr(e) = arg {
|
|
2429
|
+
self.compile_expr(e)?;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
self.emit_u16(Opcode::Construct, args.len() as u16);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
Ok(())
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
fn compile_jsx_element(
|
|
2440
|
+
&mut self,
|
|
2441
|
+
tag: &Arc<str>,
|
|
2442
|
+
props: &[JsxProp],
|
|
2443
|
+
children: &[JsxChild],
|
|
2444
|
+
) -> Result<(), CompileError> {
|
|
2445
|
+
let h_idx = self.name_idx(&Arc::from("h"));
|
|
2446
|
+
self.emit_u16(Opcode::LoadGlobal, h_idx);
|
|
2447
|
+
let tag_str = tag.as_ref();
|
|
2448
|
+
let is_component = tag_str
|
|
2449
|
+
.chars()
|
|
2450
|
+
.next()
|
|
2451
|
+
.map(|c| c.is_uppercase())
|
|
2452
|
+
.unwrap_or(false);
|
|
2453
|
+
if is_component {
|
|
2454
|
+
let tag_idx = self.name_idx(tag);
|
|
2455
|
+
self.emit_u16(Opcode::LoadGlobal, tag_idx);
|
|
2456
|
+
} else {
|
|
2457
|
+
let tag_const = self.constant_idx(Constant::String(Arc::from(tag_str)));
|
|
2458
|
+
self.emit(Opcode::LoadConst);
|
|
2459
|
+
self.chunk.write_u16(tag_const);
|
|
2460
|
+
}
|
|
2461
|
+
self.compile_jsx_props(props)?;
|
|
2462
|
+
self.compile_jsx_children(children)?;
|
|
2463
|
+
self.emit_u16(Opcode::Call, 3);
|
|
2464
|
+
Ok(())
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
fn compile_jsx_fragment(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
|
|
2468
|
+
let h_idx = self.name_idx(&Arc::from("h"));
|
|
2469
|
+
self.emit_u16(Opcode::LoadGlobal, h_idx);
|
|
2470
|
+
let fragment_idx = self.name_idx(&Arc::from("Fragment"));
|
|
2471
|
+
self.emit_u16(Opcode::LoadGlobal, fragment_idx);
|
|
2472
|
+
let null_idx = self.constant_idx(Constant::Null);
|
|
2473
|
+
self.emit(Opcode::LoadConst);
|
|
2474
|
+
self.chunk.write_u16(null_idx);
|
|
2475
|
+
self.compile_jsx_children(children)?;
|
|
2476
|
+
self.emit_u16(Opcode::Call, 3);
|
|
2477
|
+
Ok(())
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
fn compile_jsx_props(&mut self, props: &[JsxProp]) -> Result<(), CompileError> {
|
|
2481
|
+
if props.is_empty() {
|
|
2482
|
+
let null_idx = self.constant_idx(Constant::Null);
|
|
2483
|
+
self.emit(Opcode::LoadConst);
|
|
2484
|
+
self.chunk.write_u16(null_idx);
|
|
2485
|
+
return Ok(());
|
|
2486
|
+
}
|
|
2487
|
+
let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
|
|
2488
|
+
if has_spread {
|
|
2489
|
+
self.emit_u16(Opcode::NewObject, 0);
|
|
2490
|
+
for prop in props {
|
|
2491
|
+
match prop {
|
|
2492
|
+
JsxProp::Attr { name, value } => {
|
|
2493
|
+
let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
|
|
2494
|
+
self.emit(Opcode::LoadConst);
|
|
2495
|
+
self.chunk.write_u16(key_idx);
|
|
2496
|
+
match value {
|
|
2497
|
+
JsxAttrValue::String(s) => {
|
|
2498
|
+
let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
|
|
2499
|
+
self.emit(Opcode::LoadConst);
|
|
2500
|
+
self.chunk.write_u16(val_idx);
|
|
2501
|
+
}
|
|
2502
|
+
JsxAttrValue::Expr(e) => self.compile_expr(e)?,
|
|
2503
|
+
JsxAttrValue::ImplicitTrue => {
|
|
2504
|
+
let true_idx = self.constant_idx(Constant::Bool(true));
|
|
2505
|
+
self.emit(Opcode::LoadConst);
|
|
2506
|
+
self.chunk.write_u16(true_idx);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
self.emit_u16(Opcode::NewObject, 1);
|
|
2510
|
+
self.emit(Opcode::MergeObject);
|
|
2511
|
+
}
|
|
2512
|
+
JsxProp::Spread(expr) => {
|
|
2513
|
+
self.compile_expr(expr)?;
|
|
2514
|
+
self.emit(Opcode::MergeObject);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
} else {
|
|
2519
|
+
for prop in props {
|
|
2520
|
+
if let JsxProp::Attr { name, value } = prop {
|
|
2521
|
+
let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
|
|
2522
|
+
self.emit(Opcode::LoadConst);
|
|
2523
|
+
self.chunk.write_u16(key_idx);
|
|
2524
|
+
match value {
|
|
2525
|
+
JsxAttrValue::String(s) => {
|
|
2526
|
+
let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
|
|
2527
|
+
self.emit(Opcode::LoadConst);
|
|
2528
|
+
self.chunk.write_u16(val_idx);
|
|
2529
|
+
}
|
|
2530
|
+
JsxAttrValue::Expr(e) => self.compile_expr(e)?,
|
|
2531
|
+
JsxAttrValue::ImplicitTrue => {
|
|
2532
|
+
let true_idx = self.constant_idx(Constant::Bool(true));
|
|
2533
|
+
self.emit(Opcode::LoadConst);
|
|
2534
|
+
self.chunk.write_u16(true_idx);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
self.emit_u16(Opcode::NewObject, props.len() as u16);
|
|
2540
|
+
}
|
|
2541
|
+
Ok(())
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
fn compile_jsx_children(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
|
|
2545
|
+
for child in children {
|
|
2546
|
+
match child {
|
|
2547
|
+
JsxChild::Text(s) => {
|
|
2548
|
+
let idx = self.constant_idx(Constant::String(Arc::clone(s)));
|
|
2549
|
+
self.emit(Opcode::LoadConst);
|
|
2550
|
+
self.chunk.write_u16(idx);
|
|
2551
|
+
}
|
|
2552
|
+
JsxChild::Expr(e) => self.compile_expr(e)?,
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
self.emit_u16(Opcode::NewArray, children.len() as u16);
|
|
2556
|
+
Ok(())
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/// Compile a Tish program to bytecode (with peephole optimizations).
|
|
2561
|
+
pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
|
|
2562
|
+
compile_internal(program, true, false, None)
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/// Compile, tagging the chunk with a source file path so runtime errors can report
|
|
2566
|
+
/// `file:line` (issue #74). The line table is built during compilation and survives the
|
|
2567
|
+
/// in-place peephole pass; it is not serialized.
|
|
2568
|
+
pub fn compile_with_source(
|
|
2569
|
+
program: &Program,
|
|
2570
|
+
source: Option<std::sync::Arc<str>>,
|
|
2571
|
+
) -> Result<Chunk, CompileError> {
|
|
2572
|
+
compile_internal(program, true, false, source)
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/// Compile without peephole optimizations (for --no-optimize).
|
|
2576
|
+
pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
|
|
2577
|
+
compile_internal(program, false, false, None)
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
|
|
2581
|
+
pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
|
|
2582
|
+
compile_internal(program, true, true, None)
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/// Compile for REPL without peephole optimizations.
|
|
2586
|
+
pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
|
|
2587
|
+
compile_internal(program, false, true, None)
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
fn compile_internal(
|
|
2591
|
+
program: &Program,
|
|
2592
|
+
peephole: bool,
|
|
2593
|
+
retain_last_expr: bool,
|
|
2594
|
+
source: Option<std::sync::Arc<str>>,
|
|
2595
|
+
) -> Result<Chunk, CompileError> {
|
|
2596
|
+
let mut chunk = Chunk::new();
|
|
2597
|
+
chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
|
|
2598
|
+
let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
|
|
2599
|
+
compiler.compile_program(program)?;
|
|
2600
|
+
if peephole {
|
|
2601
|
+
crate::peephole::optimize(&mut chunk);
|
|
2602
|
+
}
|
|
2603
|
+
Ok(chunk)
|
|
2604
|
+
}
|