@tishlang/tish 1.13.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -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 +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- 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/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//! AST to bytecode compiler.
|
|
2
2
|
|
|
3
|
-
use std::collections::HashMap;
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
4
|
use std::sync::Arc;
|
|
5
5
|
|
|
6
6
|
use tishlang_ast::{
|
|
@@ -83,9 +83,505 @@ struct Compiler<'a> {
|
|
|
83
83
|
block_depth: usize,
|
|
84
84
|
/// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
|
|
85
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)
|
|
86
549
|
}
|
|
87
550
|
|
|
88
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
|
+
|
|
89
585
|
fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
|
|
90
586
|
Self {
|
|
91
587
|
chunk,
|
|
@@ -95,9 +591,56 @@ impl<'a> Compiler<'a> {
|
|
|
95
591
|
breakable_stack: Vec::new(),
|
|
96
592
|
block_depth: 0,
|
|
97
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,
|
|
98
601
|
}
|
|
99
602
|
}
|
|
100
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
|
+
|
|
101
644
|
fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
|
|
102
645
|
let n = self.block_depth.saturating_sub(target_depth);
|
|
103
646
|
for _ in 0..n {
|
|
@@ -107,6 +650,7 @@ impl<'a> Compiler<'a> {
|
|
|
107
650
|
|
|
108
651
|
/// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
|
|
109
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
|
|
110
654
|
fn plan_function_params(
|
|
111
655
|
params: &[FunParam],
|
|
112
656
|
) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
|
|
@@ -154,6 +698,56 @@ impl<'a> Compiler<'a> {
|
|
|
154
698
|
Ok(())
|
|
155
699
|
}
|
|
156
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
|
+
|
|
157
751
|
fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
158
752
|
match stmt {
|
|
159
753
|
Statement::VarDecl {
|
|
@@ -169,12 +763,17 @@ impl<'a> Compiler<'a> {
|
|
|
169
763
|
self.emit(Opcode::LoadConst);
|
|
170
764
|
self.chunk.write_u16(idx);
|
|
171
765
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
.
|
|
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
|
+
}
|
|
178
777
|
}
|
|
179
778
|
Statement::VarDeclDestructure { pattern, init, .. } => {
|
|
180
779
|
self.compile_expr(init)?;
|
|
@@ -197,6 +796,13 @@ impl<'a> Compiler<'a> {
|
|
|
197
796
|
self.chunk.write_u8(op as u8);
|
|
198
797
|
}
|
|
199
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
|
+
|
|
200
806
|
fn emit_u8(&mut self, op: Opcode, v: u8) {
|
|
201
807
|
self.chunk.write_u8(op as u8);
|
|
202
808
|
self.chunk.write_u16(v as u16);
|
|
@@ -487,6 +1093,16 @@ impl<'a> Compiler<'a> {
|
|
|
487
1093
|
|
|
488
1094
|
fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
|
|
489
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
|
+
}
|
|
490
1106
|
let last_is_expr = self.retain_last_expr
|
|
491
1107
|
&& stmts
|
|
492
1108
|
.last()
|
|
@@ -509,22 +1125,35 @@ impl<'a> Compiler<'a> {
|
|
|
509
1125
|
self.emit(Opcode::LoadConst);
|
|
510
1126
|
self.chunk.write_u16(idx);
|
|
511
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
|
+
}
|
|
512
1133
|
Ok(())
|
|
513
1134
|
}
|
|
514
1135
|
|
|
515
1136
|
fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
1137
|
+
self.mark_line(stmt.span());
|
|
516
1138
|
match stmt {
|
|
517
1139
|
Statement::Block { statements, .. } => {
|
|
518
1140
|
self.emit(Opcode::EnterBlock);
|
|
519
1141
|
self.block_depth += 1;
|
|
520
|
-
self.
|
|
1142
|
+
self.enter_block_scope();
|
|
521
1143
|
for s in statements {
|
|
522
1144
|
self.compile_statement(s)?;
|
|
523
1145
|
}
|
|
524
|
-
self.
|
|
1146
|
+
self.exit_block_scope();
|
|
525
1147
|
self.emit(Opcode::ExitBlock);
|
|
526
1148
|
self.block_depth -= 1;
|
|
527
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
|
+
}
|
|
528
1157
|
Statement::VarDecl {
|
|
529
1158
|
name,
|
|
530
1159
|
init,
|
|
@@ -538,12 +1167,18 @@ impl<'a> Compiler<'a> {
|
|
|
538
1167
|
self.emit(Opcode::LoadConst);
|
|
539
1168
|
self.chunk.write_u16(idx);
|
|
540
1169
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
.
|
|
545
|
-
|
|
546
|
-
.
|
|
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
|
+
}
|
|
547
1182
|
}
|
|
548
1183
|
Statement::VarDeclDestructure { pattern, init, .. } => {
|
|
549
1184
|
self.compile_expr(init)?;
|
|
@@ -570,6 +1205,14 @@ impl<'a> Compiler<'a> {
|
|
|
570
1205
|
self.patch_jump(jump_end, self.chunk.code.len());
|
|
571
1206
|
}
|
|
572
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
|
+
}
|
|
573
1216
|
let start = self.chunk.code.len();
|
|
574
1217
|
self.loop_stack.push(LoopInfo {
|
|
575
1218
|
break_patches: Vec::new(),
|
|
@@ -595,6 +1238,9 @@ impl<'a> Compiler<'a> {
|
|
|
595
1238
|
for p in info.break_patches {
|
|
596
1239
|
self.patch_jump(p, end);
|
|
597
1240
|
}
|
|
1241
|
+
for _ in &body_lets {
|
|
1242
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1243
|
+
}
|
|
598
1244
|
}
|
|
599
1245
|
Statement::For {
|
|
600
1246
|
init,
|
|
@@ -603,10 +1249,22 @@ impl<'a> Compiler<'a> {
|
|
|
603
1249
|
body,
|
|
604
1250
|
..
|
|
605
1251
|
} => {
|
|
606
|
-
self.
|
|
1252
|
+
self.enter_block_scope();
|
|
607
1253
|
if let Some(i) = init {
|
|
608
1254
|
self.compile_for_init_statement(i.as_ref())?;
|
|
609
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
|
+
}
|
|
610
1268
|
let cond_start = self.chunk.code.len();
|
|
611
1269
|
if let Some(c) = cond {
|
|
612
1270
|
self.compile_expr(c)?;
|
|
@@ -641,8 +1299,13 @@ impl<'a> Compiler<'a> {
|
|
|
641
1299
|
for p in info.break_patches {
|
|
642
1300
|
self.patch_jump(p, end);
|
|
643
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
|
+
}
|
|
644
1307
|
self.breakable_stack.pop();
|
|
645
|
-
self.
|
|
1308
|
+
self.exit_block_scope();
|
|
646
1309
|
}
|
|
647
1310
|
Statement::ForOf {
|
|
648
1311
|
name,
|
|
@@ -651,7 +1314,10 @@ impl<'a> Compiler<'a> {
|
|
|
651
1314
|
..
|
|
652
1315
|
} => {
|
|
653
1316
|
self.compile_expr(iterable)?;
|
|
654
|
-
|
|
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();
|
|
655
1321
|
let arr_name = Arc::from("__forof_arr__");
|
|
656
1322
|
let i_name = Arc::from("__forof_i__");
|
|
657
1323
|
let len_name = Arc::from("__forof_len__");
|
|
@@ -677,11 +1343,21 @@ impl<'a> Compiler<'a> {
|
|
|
677
1343
|
self.chunk.write_u16(zero_idx);
|
|
678
1344
|
self.emit_u16(Opcode::DeclareVar, i_idx);
|
|
679
1345
|
self.scope.last_mut().unwrap().insert(i_name.clone(), false);
|
|
680
|
-
let
|
|
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);
|
|
681
1357
|
self.loop_stack.push(LoopInfo {
|
|
682
1358
|
break_patches: Vec::new(),
|
|
683
1359
|
continue_patches: Vec::new(),
|
|
684
|
-
continue_is_forward_jump:
|
|
1360
|
+
continue_is_forward_jump: true,
|
|
685
1361
|
});
|
|
686
1362
|
self.breakable_stack.push(Breakable::Loop {
|
|
687
1363
|
unwind_depth: self.block_depth,
|
|
@@ -695,31 +1371,32 @@ impl<'a> Compiler<'a> {
|
|
|
695
1371
|
.unwrap()
|
|
696
1372
|
.insert(Arc::clone(name), false);
|
|
697
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();
|
|
698
1376
|
self.emit_u16(Opcode::LoadVar, i_idx);
|
|
699
1377
|
let one_idx = self.constant_idx(Constant::Number(1.0));
|
|
700
1378
|
self.emit(Opcode::LoadConst);
|
|
701
1379
|
self.chunk.write_u16(one_idx);
|
|
702
1380
|
self.emit_u8(Opcode::BinOp, 0);
|
|
703
1381
|
self.emit_u16(Opcode::StoreVar, i_idx);
|
|
704
|
-
self.emit_u16(Opcode::LoadVar, i_idx);
|
|
705
|
-
self.emit_u16(Opcode::LoadVar, len_idx);
|
|
706
|
-
self.emit_u8(Opcode::BinOp, 10);
|
|
707
|
-
let jump_out = self.emit_jump(Opcode::JumpIfFalse);
|
|
708
|
-
let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(loop_start);
|
|
709
|
-
self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
|
|
710
|
-
let end = self.chunk.code.len();
|
|
711
|
-
self.patch_jump(jump_out, end);
|
|
712
1382
|
let info = self.loop_stack.pop().unwrap();
|
|
713
1383
|
self.breakable_stack.pop();
|
|
714
1384
|
for p in info.continue_patches {
|
|
715
|
-
self.
|
|
1385
|
+
self.patch_jump(p, update_start);
|
|
716
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);
|
|
717
1391
|
for p in info.break_patches {
|
|
718
1392
|
self.patch_jump(p, end);
|
|
719
1393
|
}
|
|
720
|
-
self.
|
|
1394
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1395
|
+
self.exit_block_scope();
|
|
721
1396
|
}
|
|
722
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.
|
|
723
1400
|
if let Some(v) = value {
|
|
724
1401
|
self.compile_expr(v)?;
|
|
725
1402
|
} else {
|
|
@@ -727,6 +1404,7 @@ impl<'a> Compiler<'a> {
|
|
|
727
1404
|
self.emit(Opcode::LoadConst);
|
|
728
1405
|
self.chunk.write_u16(idx);
|
|
729
1406
|
}
|
|
1407
|
+
self.emit_pending_finallys()?;
|
|
730
1408
|
self.emit(Opcode::Return);
|
|
731
1409
|
}
|
|
732
1410
|
Statement::Break { .. } => {
|
|
@@ -794,7 +1472,19 @@ impl<'a> Compiler<'a> {
|
|
|
794
1472
|
} => {
|
|
795
1473
|
let formal_len = params.len();
|
|
796
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
|
+
};
|
|
797
1486
|
let mut inner = Chunk::new();
|
|
1487
|
+
inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
|
|
798
1488
|
if let Some(rp) = rest_param {
|
|
799
1489
|
param_names.push(Arc::clone(&rp.name));
|
|
800
1490
|
inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
|
|
@@ -803,17 +1493,53 @@ impl<'a> Compiler<'a> {
|
|
|
803
1493
|
inner.add_name(Arc::clone(p));
|
|
804
1494
|
}
|
|
805
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
|
+
}
|
|
806
1500
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
}
|
|
813
1535
|
inner_comp.emit(Opcode::LoadConst);
|
|
814
1536
|
let idx = inner_comp.constant_idx(Constant::Null);
|
|
815
1537
|
inner_comp.chunk.write_u16(idx);
|
|
816
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
|
+
}
|
|
817
1543
|
let nested_idx = self.chunk.add_nested(inner);
|
|
818
1544
|
self.emit(Opcode::LoadConst);
|
|
819
1545
|
let idx = self.constant_idx(Constant::Closure(nested_idx));
|
|
@@ -826,6 +1552,11 @@ impl<'a> Compiler<'a> {
|
|
|
826
1552
|
.insert(Arc::clone(name), false);
|
|
827
1553
|
}
|
|
828
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
|
+
}
|
|
829
1560
|
let start = self.chunk.code.len();
|
|
830
1561
|
self.loop_stack.push(LoopInfo {
|
|
831
1562
|
break_patches: Vec::new(),
|
|
@@ -851,6 +1582,9 @@ impl<'a> Compiler<'a> {
|
|
|
851
1582
|
for p in info.break_patches {
|
|
852
1583
|
self.patch_jump(p, end);
|
|
853
1584
|
}
|
|
1585
|
+
for _ in &body_lets {
|
|
1586
|
+
self.emit(Opcode::LoopVarsEnd);
|
|
1587
|
+
}
|
|
854
1588
|
}
|
|
855
1589
|
Statement::Switch {
|
|
856
1590
|
expr,
|
|
@@ -931,6 +1665,10 @@ impl<'a> Compiler<'a> {
|
|
|
931
1665
|
let catch_offset_pos = self.chunk.code.len();
|
|
932
1666
|
self.emit(Opcode::EnterTry);
|
|
933
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
|
+
}
|
|
934
1672
|
self.compile_statement(body)?;
|
|
935
1673
|
self.emit(Opcode::ExitTry);
|
|
936
1674
|
let jump_over_catch = self.emit_jump(Opcode::Jump);
|
|
@@ -939,7 +1677,7 @@ impl<'a> Compiler<'a> {
|
|
|
939
1677
|
if let Some(param) = catch_param {
|
|
940
1678
|
self.emit(Opcode::EnterBlock);
|
|
941
1679
|
self.block_depth += 1;
|
|
942
|
-
self.
|
|
1680
|
+
self.enter_block_scope();
|
|
943
1681
|
let param_idx = self.name_idx(param);
|
|
944
1682
|
self.emit_u16(Opcode::DeclareVar, param_idx);
|
|
945
1683
|
self.scope
|
|
@@ -947,7 +1685,7 @@ impl<'a> Compiler<'a> {
|
|
|
947
1685
|
.unwrap()
|
|
948
1686
|
.insert(Arc::clone(param), false);
|
|
949
1687
|
self.compile_statement(catch_stmt)?;
|
|
950
|
-
self.
|
|
1688
|
+
self.exit_block_scope();
|
|
951
1689
|
self.emit(Opcode::ExitBlock);
|
|
952
1690
|
self.block_depth -= 1;
|
|
953
1691
|
} else {
|
|
@@ -955,10 +1693,19 @@ impl<'a> Compiler<'a> {
|
|
|
955
1693
|
self.compile_statement(catch_stmt)?;
|
|
956
1694
|
}
|
|
957
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
|
+
}
|
|
958
1700
|
self.emit(Opcode::Throw);
|
|
959
1701
|
}
|
|
960
1702
|
let after_catch = self.chunk.code.len();
|
|
961
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
|
+
}
|
|
962
1709
|
if let Some(finally) = finally_body {
|
|
963
1710
|
self.compile_statement(finally)?;
|
|
964
1711
|
}
|
|
@@ -1017,10 +1764,36 @@ impl<'a> Compiler<'a> {
|
|
|
1017
1764
|
.unwrap()
|
|
1018
1765
|
.insert(Arc::clone(name), false);
|
|
1019
1766
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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);
|
|
1024
1797
|
}
|
|
1025
1798
|
}
|
|
1026
1799
|
}
|
|
@@ -1044,10 +1817,15 @@ impl<'a> Compiler<'a> {
|
|
|
1044
1817
|
.insert(Arc::clone(name), false);
|
|
1045
1818
|
}
|
|
1046
1819
|
}
|
|
1047
|
-
|
|
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(_, _) => {
|
|
1048
1827
|
return Err(CompileError {
|
|
1049
|
-
message: "
|
|
1050
|
-
.to_string(),
|
|
1828
|
+
message: "Object rest destructuring not yet supported".to_string(),
|
|
1051
1829
|
});
|
|
1052
1830
|
}
|
|
1053
1831
|
}
|
|
@@ -1059,6 +1837,7 @@ impl<'a> Compiler<'a> {
|
|
|
1059
1837
|
}
|
|
1060
1838
|
|
|
1061
1839
|
fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
|
|
1840
|
+
self.mark_line(expr.span());
|
|
1062
1841
|
match expr {
|
|
1063
1842
|
Expr::Literal { value, .. } => {
|
|
1064
1843
|
let c = match value {
|
|
@@ -1072,8 +1851,8 @@ impl<'a> Compiler<'a> {
|
|
|
1072
1851
|
self.chunk.write_u16(idx);
|
|
1073
1852
|
}
|
|
1074
1853
|
Expr::Ident { name, .. } => {
|
|
1075
|
-
|
|
1076
|
-
self.
|
|
1854
|
+
// `resolve_slot` checks BOTH the simple param-only map and the general scope stack.
|
|
1855
|
+
self.emit_var_load(name);
|
|
1077
1856
|
}
|
|
1078
1857
|
Expr::Binary {
|
|
1079
1858
|
left, op, right, ..
|
|
@@ -1202,13 +1981,30 @@ impl<'a> Compiler<'a> {
|
|
|
1202
1981
|
self.compile_expr(callee)?;
|
|
1203
1982
|
self.emit(Opcode::CallSpread);
|
|
1204
1983
|
} else {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
+
}
|
|
1209
2005
|
}
|
|
2006
|
+
self.emit_u16(Opcode::Call, args.len() as u16);
|
|
1210
2007
|
}
|
|
1211
|
-
self.emit_u16(Opcode::Call, args.len() as u16);
|
|
1212
2008
|
}
|
|
1213
2009
|
}
|
|
1214
2010
|
Expr::Member {
|
|
@@ -1355,9 +2151,8 @@ impl<'a> Compiler<'a> {
|
|
|
1355
2151
|
}
|
|
1356
2152
|
Expr::Assign { name, value, .. } => {
|
|
1357
2153
|
self.compile_expr(value)?;
|
|
1358
|
-
|
|
1359
|
-
self.
|
|
1360
|
-
self.emit_u16(Opcode::LoadVar, idx); // assign yields value
|
|
2154
|
+
self.emit_var_store(name);
|
|
2155
|
+
self.emit_var_load(name); // assign yields value
|
|
1361
2156
|
}
|
|
1362
2157
|
Expr::TypeOf { operand, .. } => {
|
|
1363
2158
|
let typeof_idx = self.name_idx(&Arc::from("typeof"));
|
|
@@ -1368,17 +2163,31 @@ impl<'a> Compiler<'a> {
|
|
|
1368
2163
|
Expr::ArrowFunction { params, body, .. } => {
|
|
1369
2164
|
let formal_len = params.len();
|
|
1370
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
|
+
});
|
|
1371
2170
|
let mut inner = Chunk::new();
|
|
2171
|
+
inner.source = self.chunk.source.clone(); // propagate file for error locations (#74)
|
|
1372
2172
|
for p in ¶m_names {
|
|
1373
2173
|
inner.add_name(Arc::clone(p));
|
|
1374
2174
|
}
|
|
1375
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
|
+
}
|
|
1376
2180
|
let mut inner_comp = Compiler::new(&mut inner, false);
|
|
1377
|
-
|
|
1378
|
-
.
|
|
1379
|
-
|
|
1380
|
-
.
|
|
1381
|
-
|
|
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)?;
|
|
1382
2191
|
match body {
|
|
1383
2192
|
ArrowBody::Expr(e) => {
|
|
1384
2193
|
inner_comp.compile_expr(e)?;
|
|
@@ -1421,54 +2230,49 @@ impl<'a> Compiler<'a> {
|
|
|
1421
2230
|
}
|
|
1422
2231
|
}
|
|
1423
2232
|
Expr::PostfixInc { name, .. } => {
|
|
1424
|
-
let idx = self.name_idx(name);
|
|
1425
2233
|
let one = self.constant_idx(Constant::Number(1.0));
|
|
1426
|
-
self.
|
|
2234
|
+
self.emit_var_load(name);
|
|
1427
2235
|
self.emit(Opcode::Dup);
|
|
1428
2236
|
self.emit(Opcode::LoadConst);
|
|
1429
2237
|
self.chunk.write_u16(one);
|
|
1430
2238
|
self.emit_u8(Opcode::BinOp, 0);
|
|
1431
|
-
self.
|
|
2239
|
+
self.emit_var_store(name);
|
|
1432
2240
|
}
|
|
1433
2241
|
Expr::PostfixDec { name, .. } => {
|
|
1434
|
-
let idx = self.name_idx(name);
|
|
1435
2242
|
let one = self.constant_idx(Constant::Number(1.0));
|
|
1436
|
-
self.
|
|
2243
|
+
self.emit_var_load(name);
|
|
1437
2244
|
self.emit(Opcode::Dup);
|
|
1438
2245
|
self.emit(Opcode::LoadConst);
|
|
1439
2246
|
self.chunk.write_u16(one);
|
|
1440
2247
|
self.emit_u8(Opcode::BinOp, 1);
|
|
1441
|
-
self.
|
|
2248
|
+
self.emit_var_store(name);
|
|
1442
2249
|
}
|
|
1443
2250
|
Expr::PrefixInc { name, .. } => {
|
|
1444
|
-
let idx = self.name_idx(name);
|
|
1445
2251
|
let one = self.constant_idx(Constant::Number(1.0));
|
|
1446
|
-
self.
|
|
2252
|
+
self.emit_var_load(name);
|
|
1447
2253
|
self.emit(Opcode::LoadConst);
|
|
1448
2254
|
self.chunk.write_u16(one);
|
|
1449
2255
|
self.emit_u8(Opcode::BinOp, 0);
|
|
1450
2256
|
self.emit(Opcode::Dup);
|
|
1451
|
-
self.
|
|
2257
|
+
self.emit_var_store(name);
|
|
1452
2258
|
}
|
|
1453
2259
|
Expr::PrefixDec { name, .. } => {
|
|
1454
|
-
let idx = self.name_idx(name);
|
|
1455
2260
|
let one = self.constant_idx(Constant::Number(1.0));
|
|
1456
|
-
self.
|
|
2261
|
+
self.emit_var_load(name);
|
|
1457
2262
|
self.emit(Opcode::LoadConst);
|
|
1458
2263
|
self.chunk.write_u16(one);
|
|
1459
2264
|
self.emit_u8(Opcode::BinOp, 1);
|
|
1460
2265
|
self.emit(Opcode::Dup);
|
|
1461
|
-
self.
|
|
2266
|
+
self.emit_var_store(name);
|
|
1462
2267
|
}
|
|
1463
2268
|
Expr::CompoundAssign {
|
|
1464
2269
|
name, op, value, ..
|
|
1465
2270
|
} => {
|
|
1466
|
-
|
|
1467
|
-
self.emit_u16(Opcode::LoadVar, idx);
|
|
2271
|
+
self.emit_var_load(name);
|
|
1468
2272
|
self.compile_expr(value)?;
|
|
1469
2273
|
self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
|
|
1470
2274
|
self.emit(Opcode::Dup);
|
|
1471
|
-
self.
|
|
2275
|
+
self.emit_var_store(name);
|
|
1472
2276
|
}
|
|
1473
2277
|
Expr::MemberAssign {
|
|
1474
2278
|
object,
|
|
@@ -1518,34 +2322,62 @@ impl<'a> Compiler<'a> {
|
|
|
1518
2322
|
self.compile_expr(operand)?;
|
|
1519
2323
|
self.emit(Opcode::AwaitPromise);
|
|
1520
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
|
+
}
|
|
1521
2354
|
Expr::LogicalAssign {
|
|
1522
2355
|
name, op, value, ..
|
|
1523
2356
|
} => {
|
|
1524
|
-
let idx = self.name_idx(name);
|
|
1525
2357
|
match op {
|
|
1526
2358
|
LogicalAssignOp::OrOr => {
|
|
1527
2359
|
// ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
|
|
1528
|
-
self.
|
|
2360
|
+
self.emit_var_load(name);
|
|
1529
2361
|
self.emit(Opcode::Dup);
|
|
1530
2362
|
let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
|
|
1531
2363
|
let j_end = self.emit_jump(Opcode::Jump);
|
|
1532
2364
|
self.patch_jump(j_rhs, self.chunk.code.len());
|
|
1533
2365
|
self.emit(Opcode::Pop);
|
|
1534
2366
|
self.compile_expr(value)?;
|
|
1535
|
-
self.
|
|
1536
|
-
self.
|
|
2367
|
+
self.emit_var_store(name);
|
|
2368
|
+
self.emit_var_load(name);
|
|
1537
2369
|
let end = self.chunk.code.len();
|
|
1538
2370
|
self.patch_jump(j_end, end);
|
|
1539
2371
|
}
|
|
1540
2372
|
LogicalAssignOp::AndAnd => {
|
|
1541
2373
|
// &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
|
|
1542
|
-
self.
|
|
2374
|
+
self.emit_var_load(name);
|
|
1543
2375
|
self.emit(Opcode::Dup);
|
|
1544
2376
|
let j_short = self.emit_jump(Opcode::JumpIfFalse);
|
|
1545
2377
|
self.emit(Opcode::Pop);
|
|
1546
2378
|
self.compile_expr(value)?;
|
|
1547
|
-
self.
|
|
1548
|
-
self.
|
|
2379
|
+
self.emit_var_store(name);
|
|
2380
|
+
self.emit_var_load(name);
|
|
1549
2381
|
let j_end = self.emit_jump(Opcode::Jump);
|
|
1550
2382
|
let end = self.chunk.code.len();
|
|
1551
2383
|
self.patch_jump(j_short, end);
|
|
@@ -1554,7 +2386,7 @@ impl<'a> Compiler<'a> {
|
|
|
1554
2386
|
LogicalAssignOp::Nullish => {
|
|
1555
2387
|
// ??= : assign only when current === null (matches interpreter)
|
|
1556
2388
|
let null_c = self.constant_idx(Constant::Null);
|
|
1557
|
-
self.
|
|
2389
|
+
self.emit_var_load(name);
|
|
1558
2390
|
self.emit(Opcode::Dup);
|
|
1559
2391
|
self.emit(Opcode::LoadConst);
|
|
1560
2392
|
self.chunk.write_u16(null_c);
|
|
@@ -1562,8 +2394,8 @@ impl<'a> Compiler<'a> {
|
|
|
1562
2394
|
let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
|
|
1563
2395
|
self.emit(Opcode::Pop);
|
|
1564
2396
|
self.compile_expr(value)?;
|
|
1565
|
-
self.
|
|
1566
|
-
self.
|
|
2397
|
+
self.emit_var_store(name);
|
|
2398
|
+
self.emit_var_load(name);
|
|
1567
2399
|
let j_end = self.emit_jump(Opcode::Jump);
|
|
1568
2400
|
let end = self.chunk.code.len();
|
|
1569
2401
|
self.patch_jump(j_not_null, end);
|
|
@@ -1727,30 +2559,42 @@ impl<'a> Compiler<'a> {
|
|
|
1727
2559
|
|
|
1728
2560
|
/// Compile a Tish program to bytecode (with peephole optimizations).
|
|
1729
2561
|
pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
|
|
1730
|
-
compile_internal(program, true, false)
|
|
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)
|
|
1731
2573
|
}
|
|
1732
2574
|
|
|
1733
2575
|
/// Compile without peephole optimizations (for --no-optimize).
|
|
1734
2576
|
pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
|
|
1735
|
-
compile_internal(program, false, false)
|
|
2577
|
+
compile_internal(program, false, false, None)
|
|
1736
2578
|
}
|
|
1737
2579
|
|
|
1738
2580
|
/// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
|
|
1739
2581
|
pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
|
|
1740
|
-
compile_internal(program, true, true)
|
|
2582
|
+
compile_internal(program, true, true, None)
|
|
1741
2583
|
}
|
|
1742
2584
|
|
|
1743
2585
|
/// Compile for REPL without peephole optimizations.
|
|
1744
2586
|
pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
|
|
1745
|
-
compile_internal(program, false, true)
|
|
2587
|
+
compile_internal(program, false, true, None)
|
|
1746
2588
|
}
|
|
1747
2589
|
|
|
1748
2590
|
fn compile_internal(
|
|
1749
2591
|
program: &Program,
|
|
1750
2592
|
peephole: bool,
|
|
1751
2593
|
retain_last_expr: bool,
|
|
2594
|
+
source: Option<std::sync::Arc<str>>,
|
|
1752
2595
|
) -> Result<Chunk, CompileError> {
|
|
1753
2596
|
let mut chunk = Chunk::new();
|
|
2597
|
+
chunk.source = source; // tag before compiling so nested chunks inherit it (#74)
|
|
1754
2598
|
let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
|
|
1755
2599
|
compiler.compile_program(program)?;
|
|
1756
2600
|
if peephole {
|