@tishlang/tish 1.13.2 → 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 +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 +61 -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
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
//! Numeric JIT — native codegen for `slot_based` numeric functions.
|
|
2
|
+
//!
|
|
3
|
+
//! Compiles numeric `f64`-in/`f64`-out functions to native machine code via Cranelift; the VM calls
|
|
4
|
+
//! them directly from the `Call` path when every argument is a number. Two builders:
|
|
5
|
+
//! * [`build_body`] — straight-line + the ternary-`select` shape (leaf callbacks `x => x * 2`,
|
|
6
|
+
//! `(a, b) => a - b`, `x => x === 500`, …).
|
|
7
|
+
//! * [`build_body_cfg`] — **functions with LOOPS and branches** (the big win: a numeric loop
|
|
8
|
+
//! function went from ~89× Node interpreted to ≈1× Node native). Uses a cranelift `Variable` per
|
|
9
|
+
//! frame slot and a block per bytecode jump target; handles for/while/nested loops, if/else,
|
|
10
|
+
//! early return, break, continue. Enabled by slot-based locals making such functions `slot_based`.
|
|
11
|
+
//!
|
|
12
|
+
//! Anything unsupported (member/index, calls, arrays/objects, non-number constants, pow/shift,
|
|
13
|
+
//! booleans in slots, a ternary inside a loop) makes compilation return `None`, and any non-number
|
|
14
|
+
//! argument at call time falls back to the interpreter — so this is purely ADDITIVE and can never
|
|
15
|
+
//! change behaviour (a miss runs the VM). Only a logic bug here could, hence the differential
|
|
16
|
+
//! validation (vm-JIT ≡ interp ≡ node) + `tests/core/jit_loops.tish`.
|
|
17
|
+
//!
|
|
18
|
+
//! Not compiled for wasm targets (cranelift-jit emits host code).
|
|
19
|
+
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
use std::sync::{Mutex, OnceLock};
|
|
22
|
+
|
|
23
|
+
use cranelift::codegen::settings::{self, Configurable};
|
|
24
|
+
use cranelift::prelude::types;
|
|
25
|
+
use cranelift::prelude::{
|
|
26
|
+
AbiParam, Block, FloatCC, FunctionBuilder, FunctionBuilderContext, InstBuilder, IntCC, MemFlags,
|
|
27
|
+
Value as ClifValue, Variable,
|
|
28
|
+
};
|
|
29
|
+
use cranelift_jit::{JITBuilder, JITModule};
|
|
30
|
+
use cranelift_module::{Linkage, Module};
|
|
31
|
+
|
|
32
|
+
use tishlang_ast::{BinOp, UnaryOp};
|
|
33
|
+
use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
|
|
34
|
+
|
|
35
|
+
/// A JIT-compiled numeric function: a pointer to native code plus its arity.
|
|
36
|
+
/// The pointer is into a leaked, never-freed `JITModule`, so it is valid for the
|
|
37
|
+
/// life of the process and safe to call from any thread.
|
|
38
|
+
#[derive(Clone, Copy)]
|
|
39
|
+
pub struct NumericFn {
|
|
40
|
+
ptr: usize,
|
|
41
|
+
arity: u8,
|
|
42
|
+
/// True when the function's result is a comparison (boolean), so the caller
|
|
43
|
+
/// boxes the returned f64 as `Value::Bool` (1.0→true) instead of
|
|
44
|
+
/// `Value::Number`. Needed for callbacks like `x => x === c` used by `map`.
|
|
45
|
+
result_bool: bool,
|
|
46
|
+
/// Array-param bitmask (bit k set ⇒ param k is an ARRAY, read as `arr[i]`). `0` ⇒ the ordinary
|
|
47
|
+
/// pure-numeric register-`f64` ABI (the [`NumericFn::call`] path, unchanged). Nonzero ⇒ the
|
|
48
|
+
/// array-mode uniform 3-pointer ABI (the [`NumericFn::call_arrays`] path). Kept as a `u8` (arity
|
|
49
|
+
/// ≤ 8) so `NumericFn` stays `Copy`. `TISH_JIT_ARRAYS`-gated; `0` in every default build.
|
|
50
|
+
array_param_mask: u8,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A flat numeric array handed to an array-mode JIT function: a raw `f64` slice (`ptr`, `len`).
|
|
54
|
+
/// Built by the VM wrapper from a `NumberArray` (zero-copy) or by extracting an all-numeric
|
|
55
|
+
/// `Array` into a scratch `Vec<f64>` (the wrapper only builds one when every element is a
|
|
56
|
+
/// `Value::Number` — non-numeric arrays never reach the JIT, so the slice is always valid `f64`).
|
|
57
|
+
#[repr(C)]
|
|
58
|
+
#[derive(Clone, Copy)]
|
|
59
|
+
pub struct ArrayHandle {
|
|
60
|
+
pub ptr: *const f64,
|
|
61
|
+
pub len: usize,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Array-element reads inside JIT'd loops (`arr[i]`/`arr[const]`). **Default ON**; `TISH_JIT_ARRAYS=0`
|
|
65
|
+
/// disables it (escape hatch). Cached in a `OnceLock` — NEVER read the env var on a hot path (see the
|
|
66
|
+
/// frame-VM regression note in docs/perf.md). Purely ADDITIVE: only numeric-array-reduction functions
|
|
67
|
+
/// are array-compiled, and the VM wrapper bails to the interpreter for any non-numeric element,
|
|
68
|
+
/// `NumberArray`, or out-of-bounds deopt — so a non-fast case is always correct, just interpreted.
|
|
69
|
+
/// Validated: full cross-backend suite 17/0 both ON and OFF; vm(JIT) ≡ interp ≡ node on
|
|
70
|
+
/// sum/dot/max/const-index/OOB/non-numeric/float fixtures; 38× on `sumArr`-style reductions.
|
|
71
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
72
|
+
pub fn jit_arrays_enabled() -> bool {
|
|
73
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
74
|
+
*ENABLED.get_or_init(|| std::env::var("TISH_JIT_ARRAYS").map(|v| v != "0").unwrap_or(true))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SAFETY: `ptr` references immutable executable code in a module that is never
|
|
78
|
+
// dropped (it lives in the process-global `JIT` below). All mutation of the
|
|
79
|
+
// module happens under the `Mutex`; only the raw code pointer escapes.
|
|
80
|
+
unsafe impl Send for NumericFn {}
|
|
81
|
+
unsafe impl Sync for NumericFn {}
|
|
82
|
+
|
|
83
|
+
impl NumericFn {
|
|
84
|
+
#[inline]
|
|
85
|
+
pub fn arity(&self) -> usize {
|
|
86
|
+
self.arity as usize
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Whether the result should be boxed as `Value::Bool` rather than `Number`.
|
|
90
|
+
#[inline]
|
|
91
|
+
pub fn result_is_bool(&self) -> bool {
|
|
92
|
+
self.result_bool
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Call the native function. `args.len()` must equal `arity`.
|
|
96
|
+
#[inline]
|
|
97
|
+
pub fn call(&self, args: &[f64]) -> f64 {
|
|
98
|
+
// The module's default call conv matches the C ABI for `f64` scalars on
|
|
99
|
+
// x86-64 SysV and AArch64, so transmuting to `extern "C" fn` is sound here.
|
|
100
|
+
unsafe {
|
|
101
|
+
match self.arity {
|
|
102
|
+
1 => {
|
|
103
|
+
let f: extern "C" fn(f64) -> f64 = std::mem::transmute(self.ptr);
|
|
104
|
+
f(args[0])
|
|
105
|
+
}
|
|
106
|
+
2 => {
|
|
107
|
+
let f: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(self.ptr);
|
|
108
|
+
f(args[0], args[1])
|
|
109
|
+
}
|
|
110
|
+
3 => {
|
|
111
|
+
let f: extern "C" fn(f64, f64, f64) -> f64 = std::mem::transmute(self.ptr);
|
|
112
|
+
f(args[0], args[1], args[2])
|
|
113
|
+
}
|
|
114
|
+
// Arities 4..=8: still fully register-passed (x86-64 SysV → XMM0-7,
|
|
115
|
+
// AArch64 AAPCS → V0-7), so the `extern "C"` transmute stays sound.
|
|
116
|
+
4 => {
|
|
117
|
+
let f: extern "C" fn(f64, f64, f64, f64) -> f64 = std::mem::transmute(self.ptr);
|
|
118
|
+
f(args[0], args[1], args[2], args[3])
|
|
119
|
+
}
|
|
120
|
+
5 => {
|
|
121
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64) -> f64 =
|
|
122
|
+
std::mem::transmute(self.ptr);
|
|
123
|
+
f(args[0], args[1], args[2], args[3], args[4])
|
|
124
|
+
}
|
|
125
|
+
6 => {
|
|
126
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64, f64) -> f64 =
|
|
127
|
+
std::mem::transmute(self.ptr);
|
|
128
|
+
f(args[0], args[1], args[2], args[3], args[4], args[5])
|
|
129
|
+
}
|
|
130
|
+
7 => {
|
|
131
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64) -> f64 =
|
|
132
|
+
std::mem::transmute(self.ptr);
|
|
133
|
+
f(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
|
|
134
|
+
}
|
|
135
|
+
8 => {
|
|
136
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64 =
|
|
137
|
+
std::mem::transmute(self.ptr);
|
|
138
|
+
f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7])
|
|
139
|
+
}
|
|
140
|
+
_ => f64::NAN,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Bit k set ⇒ param k is an array param (read via `arr[i]`). `0` ⇒ pure-numeric (use [`call`]).
|
|
146
|
+
#[inline]
|
|
147
|
+
pub fn array_param_mask(&self) -> u8 {
|
|
148
|
+
self.array_param_mask
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Call an array-mode function (`array_param_mask != 0`). `numeric` holds the f64 values for the
|
|
152
|
+
/// numeric params in numeric-param order; `arrays` the [`ArrayHandle`]s for the array params in
|
|
153
|
+
/// array-param order. Returns `(result, deopt)` — when `deopt` is true an out-of-bounds access
|
|
154
|
+
/// was hit and the JIT bailed, so the caller MUST discard `result` and re-run the interpreter
|
|
155
|
+
/// (OOB reads return `Value::Null` in the VM, whose per-operator coercion the JIT can't replicate).
|
|
156
|
+
#[inline]
|
|
157
|
+
pub fn call_arrays(&self, numeric: &[f64], arrays: &[ArrayHandle]) -> (f64, bool) {
|
|
158
|
+
let mut deopt: u8 = 0;
|
|
159
|
+
// ONE uniform signature for every array-mode fn: (numeric*, handles*, deopt*) -> f64. Empty
|
|
160
|
+
// slices pass a dangling-but-aligned non-null ptr (the body only loads indices it uses).
|
|
161
|
+
let num_ptr = if numeric.is_empty() {
|
|
162
|
+
std::ptr::NonNull::<f64>::dangling().as_ptr() as *const f64
|
|
163
|
+
} else {
|
|
164
|
+
numeric.as_ptr()
|
|
165
|
+
};
|
|
166
|
+
let arr_ptr = if arrays.is_empty() {
|
|
167
|
+
std::ptr::NonNull::<ArrayHandle>::dangling().as_ptr() as *const ArrayHandle
|
|
168
|
+
} else {
|
|
169
|
+
arrays.as_ptr()
|
|
170
|
+
};
|
|
171
|
+
let res = unsafe {
|
|
172
|
+
let f: extern "C" fn(*const f64, *const ArrayHandle, *mut u8) -> f64 =
|
|
173
|
+
std::mem::transmute(self.ptr);
|
|
174
|
+
f(num_ptr, arr_ptr, &mut deopt as *mut u8)
|
|
175
|
+
};
|
|
176
|
+
(res, deopt != 0)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
struct JitGlobal {
|
|
181
|
+
module: JITModule,
|
|
182
|
+
/// Keyed by the address of the (stable, un-cloned) nested `Chunk`. `None`
|
|
183
|
+
/// caches "this chunk is not JIT-eligible" so we don't re-analyze it.
|
|
184
|
+
cache: HashMap<usize, Option<NumericFn>>,
|
|
185
|
+
counter: usize,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// SAFETY: `JITModule` is `!Send`, but the single instance lives behind the
|
|
189
|
+
// `Mutex` in the process-global `JIT` and is never moved out or dropped; all
|
|
190
|
+
// access is serialized by the mutex.
|
|
191
|
+
unsafe impl Send for JitGlobal {}
|
|
192
|
+
|
|
193
|
+
static JIT: OnceLock<Option<Mutex<JitGlobal>>> = OnceLock::new();
|
|
194
|
+
|
|
195
|
+
fn new_module() -> Option<JITModule> {
|
|
196
|
+
let mut flag_builder = settings::builder();
|
|
197
|
+
// JIT code is loaded at a fixed address; no PIC / colocated libcalls needed.
|
|
198
|
+
flag_builder.set("use_colocated_libcalls", "false").ok()?;
|
|
199
|
+
flag_builder.set("is_pic", "false").ok()?;
|
|
200
|
+
flag_builder.set("opt_level", "speed").ok()?;
|
|
201
|
+
let isa_builder = cranelift_native::builder().ok()?;
|
|
202
|
+
let isa = isa_builder
|
|
203
|
+
.finish(settings::Flags::new(flag_builder))
|
|
204
|
+
.ok()?;
|
|
205
|
+
let builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
|
|
206
|
+
Some(JITModule::new(builder))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn jit() -> Option<&'static Mutex<JitGlobal>> {
|
|
210
|
+
JIT.get_or_init(|| {
|
|
211
|
+
new_module().map(|module| {
|
|
212
|
+
Mutex::new(JitGlobal {
|
|
213
|
+
module,
|
|
214
|
+
cache: HashMap::new(),
|
|
215
|
+
counter: 0,
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
.as_ref()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Read a big-endian u16 operand (matches the VM/compiler encoding).
|
|
223
|
+
#[inline]
|
|
224
|
+
fn read_u16(code: &[u8], ip: &mut usize) -> Option<u16> {
|
|
225
|
+
let a = *code.get(*ip)? as u16;
|
|
226
|
+
let b = *code.get(*ip + 1)? as u16;
|
|
227
|
+
*ip += 2;
|
|
228
|
+
Some((a << 8) | b)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Get (or compile, then cache) the native numeric function for `chunk`.
|
|
232
|
+
/// Returns `None` if the chunk isn't a straight-line numeric function.
|
|
233
|
+
pub fn try_compile_numeric(chunk: &Chunk) -> Option<NumericFn> {
|
|
234
|
+
if !chunk.slot_based
|
|
235
|
+
|| chunk.rest_param_index != NO_REST_PARAM
|
|
236
|
+
|| chunk.param_count == 0
|
|
237
|
+
|| chunk.param_count > 8
|
|
238
|
+
{
|
|
239
|
+
return None;
|
|
240
|
+
}
|
|
241
|
+
let key = chunk as *const Chunk as usize;
|
|
242
|
+
let lock = jit()?;
|
|
243
|
+
let mut g = lock.lock().ok()?;
|
|
244
|
+
if let Some(cached) = g.cache.get(&key).copied() {
|
|
245
|
+
return cached;
|
|
246
|
+
}
|
|
247
|
+
let result = compile_chunk(&mut g, chunk);
|
|
248
|
+
g.cache.insert(key, result);
|
|
249
|
+
result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Lower `f64` comparison to `1.0`/`0.0` (JS boolean-in-number form).
|
|
253
|
+
fn fcmp_f64(bcx: &mut FunctionBuilder, cc: FloatCC, a: ClifValue, b: ClifValue) -> ClifValue {
|
|
254
|
+
let cond = bcx.ins().fcmp(cc, a, b);
|
|
255
|
+
let one = bcx.ins().f64const(1.0);
|
|
256
|
+
let zero = bcx.ins().f64const(0.0);
|
|
257
|
+
bcx.ins().select(cond, one, zero)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// f64 → JS `ToInt32` as an `I32` clif value, matching `tishlang_core::to_int32` (so a JIT-compiled
|
|
261
|
+
/// `& | ^ ~` agrees with the VM fallback). Saturating-cast→`ireduce` is the modulo-2³² for finite
|
|
262
|
+
/// values and already gives 0 for NaN / `-∞`; the branchless `select` on `|x| < ∞` maps `+∞` (which
|
|
263
|
+
/// saturates to `i64::MAX` → `-1`, the one wrong case) to 0. Branchless, so the hot path stays fast.
|
|
264
|
+
fn js_to_int32(bcx: &mut FunctionBuilder, x: ClifValue) -> ClifValue {
|
|
265
|
+
let sat = bcx.ins().fcvt_to_sint_sat(types::I64, x);
|
|
266
|
+
let red = bcx.ins().ireduce(types::I32, sat);
|
|
267
|
+
let absx = bcx.ins().fabs(x);
|
|
268
|
+
let inf = bcx.ins().f64const(f64::INFINITY);
|
|
269
|
+
let finite = bcx.ins().fcmp(FloatCC::LessThan, absx, inf);
|
|
270
|
+
let zero = bcx.ins().iconst(types::I32, 0);
|
|
271
|
+
bcx.ins().select(finite, red, zero)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn compile_chunk(g: &mut JitGlobal, chunk: &Chunk) -> Option<NumericFn> {
|
|
275
|
+
let arity = chunk.param_count as usize;
|
|
276
|
+
|
|
277
|
+
// Array-mode (`TISH_JIT_ARRAYS`): if a param is used purely as `arr[i]`/`arr[const]`, compile the
|
|
278
|
+
// 3-pointer array ABI instead of the register-`f64` ABI. mask 0 ⇒ ordinary numeric path below.
|
|
279
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
280
|
+
{
|
|
281
|
+
let array_mask = if jit_arrays_enabled() {
|
|
282
|
+
classify_params(chunk, arity)
|
|
283
|
+
} else {
|
|
284
|
+
0
|
|
285
|
+
};
|
|
286
|
+
if array_mask != 0 {
|
|
287
|
+
return compile_chunk_arrays(g, chunk, arity, array_mask);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let mut sig = g.module.make_signature();
|
|
292
|
+
for _ in 0..arity {
|
|
293
|
+
sig.params.push(AbiParam::new(types::F64));
|
|
294
|
+
}
|
|
295
|
+
sig.returns.push(AbiParam::new(types::F64));
|
|
296
|
+
|
|
297
|
+
// Declare the function FIRST so its own `FuncRef` is available while building the body — that is
|
|
298
|
+
// what lets `SelfCall` lower to a native recursive call (the recursion-JIT win). Cranelift
|
|
299
|
+
// resolves the forward self-reference at `finalize_definitions`.
|
|
300
|
+
let name = format!("tish_num_{}", g.counter);
|
|
301
|
+
g.counter += 1;
|
|
302
|
+
let id = match g.module.declare_function(&name, Linkage::Export, &sig) {
|
|
303
|
+
Ok(id) => id,
|
|
304
|
+
Err(_) => return None,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Try the loop-capable CFG builder first; if it bails, retry the straight-line/ternary builder.
|
|
308
|
+
// Each attempt needs a fresh function (a partial build leaves the context dirty).
|
|
309
|
+
let mut ctx = g.module.make_context();
|
|
310
|
+
ctx.func.signature = sig.clone();
|
|
311
|
+
let self_ref = g.module.declare_func_in_func(id, &mut ctx.func);
|
|
312
|
+
let mut fbctx = FunctionBuilderContext::new();
|
|
313
|
+
let result_bool = match build_body_cfg(&mut ctx.func, &mut fbctx, chunk, arity, Some(self_ref), 0)
|
|
314
|
+
{
|
|
315
|
+
Some(b) => b,
|
|
316
|
+
None => {
|
|
317
|
+
g.module.clear_context(&mut ctx);
|
|
318
|
+
ctx = g.module.make_context();
|
|
319
|
+
ctx.func.signature = sig.clone();
|
|
320
|
+
fbctx = FunctionBuilderContext::new();
|
|
321
|
+
// build_body (straight-line/ternary) has no self-call path; it bails on SelfCall → VM.
|
|
322
|
+
match build_body(&mut ctx.func, &mut fbctx, chunk, arity) {
|
|
323
|
+
Some(b) => b,
|
|
324
|
+
None => {
|
|
325
|
+
g.module.clear_context(&mut ctx);
|
|
326
|
+
return None;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if g.module.define_function(id, &mut ctx).is_err() {
|
|
333
|
+
g.module.clear_context(&mut ctx);
|
|
334
|
+
return None;
|
|
335
|
+
}
|
|
336
|
+
g.module.clear_context(&mut ctx);
|
|
337
|
+
if g.module.finalize_definitions().is_err() {
|
|
338
|
+
return None;
|
|
339
|
+
}
|
|
340
|
+
let ptr = g.module.get_finalized_function(id);
|
|
341
|
+
Some(NumericFn {
|
|
342
|
+
ptr: ptr as usize,
|
|
343
|
+
arity: arity as u8,
|
|
344
|
+
result_bool,
|
|
345
|
+
array_param_mask: 0,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// Classify each param slot of an array-mode candidate. Returns a bitmask: **bit k set ⇒ param k is
|
|
350
|
+
/// an ARRAY** used only as `arr[i]` / `arr[const]`. Returns `0` when there are no array params OR the
|
|
351
|
+
/// function is ineligible for array mode (a param used both as an array and as a number, a `GetIndex`
|
|
352
|
+
/// the peephole can't consume, etc.) — the caller then takes the ordinary numeric path, which itself
|
|
353
|
+
/// bails on `GetIndex`, so a `0` here is always safe (never a miscompile, just no array-JIT).
|
|
354
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
355
|
+
fn classify_params(chunk: &Chunk, arity: usize) -> u8 {
|
|
356
|
+
if arity == 0 || arity > 8 || (chunk.num_slots as usize) == 0 {
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
let code = &chunk.code;
|
|
360
|
+
let mut used_numeric = [false; 8];
|
|
361
|
+
let mut used_array = [false; 8];
|
|
362
|
+
let mut ip = 0usize;
|
|
363
|
+
while ip < code.len() {
|
|
364
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
365
|
+
Some(o) => o,
|
|
366
|
+
None => return 0,
|
|
367
|
+
};
|
|
368
|
+
let size = match op_size(op) {
|
|
369
|
+
Some(s) => s,
|
|
370
|
+
None => return 0, // an opcode the array CFG can't handle ⇒ ineligible
|
|
371
|
+
};
|
|
372
|
+
match op {
|
|
373
|
+
Opcode::LoadLocal => {
|
|
374
|
+
let slot = match peek_u16(code, ip + 1) {
|
|
375
|
+
Some(s) => s as usize,
|
|
376
|
+
None => return 0,
|
|
377
|
+
};
|
|
378
|
+
// Peephole: `LoadLocal(arr) ; (LoadLocal|LoadConst) ; GetIndex` ⇒ array access of arr.
|
|
379
|
+
let idx_then_getindex = matches!(
|
|
380
|
+
(
|
|
381
|
+
code.get(ip + 3).copied().and_then(Opcode::from_u8),
|
|
382
|
+
code.get(ip + 6).copied().and_then(Opcode::from_u8),
|
|
383
|
+
),
|
|
384
|
+
(Some(Opcode::LoadLocal), Some(Opcode::GetIndex))
|
|
385
|
+
| (Some(Opcode::LoadConst), Some(Opcode::GetIndex))
|
|
386
|
+
);
|
|
387
|
+
if idx_then_getindex && slot < arity {
|
|
388
|
+
used_array[slot] = true;
|
|
389
|
+
ip += 7; // consume LoadLocal(arr) + index op + GetIndex
|
|
390
|
+
continue;
|
|
391
|
+
} else if slot < arity {
|
|
392
|
+
used_numeric[slot] = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
Opcode::StoreLocal => {
|
|
396
|
+
let slot = match peek_u16(code, ip + 1) {
|
|
397
|
+
Some(s) => s as usize,
|
|
398
|
+
None => return 0,
|
|
399
|
+
};
|
|
400
|
+
if slot < arity {
|
|
401
|
+
used_numeric[slot] = true; // a written param is numeric-shaped (can't be our array)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Any `GetIndex` not already consumed by the peephole above ⇒ an index shape we don't
|
|
405
|
+
// handle (e.g. `arr[i+1]`, `arr[brr[i]]`) ⇒ ineligible.
|
|
406
|
+
Opcode::GetIndex => return 0,
|
|
407
|
+
_ => {}
|
|
408
|
+
}
|
|
409
|
+
ip += size;
|
|
410
|
+
}
|
|
411
|
+
let mut mask = 0u8;
|
|
412
|
+
for k in 0..arity {
|
|
413
|
+
if used_array[k] {
|
|
414
|
+
if used_numeric[k] {
|
|
415
|
+
return 0; // used as BOTH array and number ⇒ ambiguous ⇒ bail
|
|
416
|
+
}
|
|
417
|
+
mask |= 1u8 << k;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
mask
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Compile an array-mode function: numeric params + array params (read as `arr[i]`). Uses ONE uniform
|
|
424
|
+
/// ABI for every such function — `extern "C" fn(numeric: *const f64, handles: *const ArrayHandle,
|
|
425
|
+
/// deopt: *mut u8) -> f64` — so there is a single transmute (no per-arity explosion). Out-of-bounds
|
|
426
|
+
/// reads set `*deopt` and bail (the caller re-runs the interpreter); non-numeric arrays never reach
|
|
427
|
+
/// here (the VM wrapper only calls this when every element is a `Value::Number`).
|
|
428
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
429
|
+
fn compile_chunk_arrays(g: &mut JitGlobal, chunk: &Chunk, arity: usize, mask: u8) -> Option<NumericFn> {
|
|
430
|
+
let ptr_ty = g.module.target_config().pointer_type();
|
|
431
|
+
let mut sig = g.module.make_signature();
|
|
432
|
+
sig.params.push(AbiParam::new(ptr_ty)); // numeric_ptr
|
|
433
|
+
sig.params.push(AbiParam::new(ptr_ty)); // handles_ptr
|
|
434
|
+
sig.params.push(AbiParam::new(ptr_ty)); // deopt_ptr
|
|
435
|
+
sig.returns.push(AbiParam::new(types::F64));
|
|
436
|
+
|
|
437
|
+
let name = format!("tish_arr_{}", g.counter);
|
|
438
|
+
g.counter += 1;
|
|
439
|
+
let id = g.module.declare_function(&name, Linkage::Export, &sig).ok()?;
|
|
440
|
+
|
|
441
|
+
let mut ctx = g.module.make_context();
|
|
442
|
+
ctx.func.signature = sig.clone();
|
|
443
|
+
let mut fbctx = FunctionBuilderContext::new();
|
|
444
|
+
// No self-call in array mode (recursive call would need the array signature) → pass None.
|
|
445
|
+
if build_body_cfg(&mut ctx.func, &mut fbctx, chunk, arity, None, mask).is_none() {
|
|
446
|
+
g.module.clear_context(&mut ctx);
|
|
447
|
+
return None;
|
|
448
|
+
}
|
|
449
|
+
if g.module.define_function(id, &mut ctx).is_err() {
|
|
450
|
+
g.module.clear_context(&mut ctx);
|
|
451
|
+
return None;
|
|
452
|
+
}
|
|
453
|
+
g.module.clear_context(&mut ctx);
|
|
454
|
+
if g.module.finalize_definitions().is_err() {
|
|
455
|
+
return None;
|
|
456
|
+
}
|
|
457
|
+
let ptr = g.module.get_finalized_function(id);
|
|
458
|
+
Some(NumericFn {
|
|
459
|
+
ptr: ptr as usize,
|
|
460
|
+
arity: arity as u8,
|
|
461
|
+
result_bool: false,
|
|
462
|
+
array_param_mask: mask,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// Outcome of trying to emit one *straight-line* numeric opcode.
|
|
467
|
+
enum SimpleOp {
|
|
468
|
+
/// Handled: bytecode consumed, IR emitted, `bool` flags a comparison/`!` result.
|
|
469
|
+
#[allow(dead_code)] // reserved: the flag will carry a comparison/`!` result; currently always false
|
|
470
|
+
Handled(bool),
|
|
471
|
+
/// The opcode is control flow / `Return` / non-numeric — NOT consumed; caller decides.
|
|
472
|
+
NotSimple,
|
|
473
|
+
/// A simple-op *type* but an unsupported variant (Pow / shift / non-number const) —
|
|
474
|
+
/// the whole function is ineligible. State may be partially consumed; caller bails.
|
|
475
|
+
Unsupported,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Emit one straight-line numeric opcode at `*ip` (no control flow, no `Return`). On
|
|
479
|
+
/// `NotSimple` neither `ip` nor `stack` is touched, so the caller can re-dispatch.
|
|
480
|
+
fn emit_simple_op(
|
|
481
|
+
bcx: &mut FunctionBuilder,
|
|
482
|
+
chunk: &Chunk,
|
|
483
|
+
code: &[u8],
|
|
484
|
+
ip: &mut usize,
|
|
485
|
+
stack: &mut Vec<(ClifValue, bool)>,
|
|
486
|
+
params: &[ClifValue],
|
|
487
|
+
arity: usize,
|
|
488
|
+
) -> SimpleOp {
|
|
489
|
+
let op = match code.get(*ip).copied().and_then(Opcode::from_u8) {
|
|
490
|
+
Some(o) => o,
|
|
491
|
+
None => return SimpleOp::NotSimple,
|
|
492
|
+
};
|
|
493
|
+
match op {
|
|
494
|
+
Opcode::Nop | Opcode::LoadLocal | Opcode::LoadConst | Opcode::BinOp | Opcode::UnaryOp => {}
|
|
495
|
+
// Control flow / member / index / call / array / object / Return → caller handles.
|
|
496
|
+
_ => return SimpleOp::NotSimple,
|
|
497
|
+
}
|
|
498
|
+
*ip += 1;
|
|
499
|
+
match op {
|
|
500
|
+
Opcode::Nop => {}
|
|
501
|
+
Opcode::LoadLocal => {
|
|
502
|
+
let slot = match read_u16(code, ip) {
|
|
503
|
+
Some(s) => s as usize,
|
|
504
|
+
None => return SimpleOp::Unsupported,
|
|
505
|
+
};
|
|
506
|
+
// Straight-line simple fns declare no locals; only params (numbers).
|
|
507
|
+
if slot >= arity {
|
|
508
|
+
return SimpleOp::Unsupported;
|
|
509
|
+
}
|
|
510
|
+
stack.push((params[slot], false));
|
|
511
|
+
}
|
|
512
|
+
Opcode::LoadConst => {
|
|
513
|
+
let idx = match read_u16(code, ip) {
|
|
514
|
+
Some(i) => i as usize,
|
|
515
|
+
None => return SimpleOp::Unsupported,
|
|
516
|
+
};
|
|
517
|
+
match chunk.constants.get(idx) {
|
|
518
|
+
Some(Constant::Number(n)) => {
|
|
519
|
+
let v = bcx.ins().f64const(*n);
|
|
520
|
+
stack.push((v, false));
|
|
521
|
+
}
|
|
522
|
+
Some(Constant::Bool(b)) => {
|
|
523
|
+
let v = bcx.ins().f64const(if *b { 1.0 } else { 0.0 });
|
|
524
|
+
stack.push((v, true));
|
|
525
|
+
}
|
|
526
|
+
_ => return SimpleOp::Unsupported,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
Opcode::BinOp => {
|
|
530
|
+
let bop = match read_u16(code, ip).map(|r| r as u8).and_then(u8_to_binop) {
|
|
531
|
+
Some(b) => b,
|
|
532
|
+
None => return SimpleOp::Unsupported,
|
|
533
|
+
};
|
|
534
|
+
if stack.len() < 2 {
|
|
535
|
+
return SimpleOp::Unsupported;
|
|
536
|
+
}
|
|
537
|
+
let (r, _) = stack.pop().unwrap();
|
|
538
|
+
let (l, _) = stack.pop().unwrap();
|
|
539
|
+
let is_cmp = matches!(
|
|
540
|
+
bop,
|
|
541
|
+
BinOp::Eq | BinOp::Ne | BinOp::StrictEq | BinOp::StrictNe
|
|
542
|
+
| BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge
|
|
543
|
+
);
|
|
544
|
+
let v = match bop {
|
|
545
|
+
BinOp::Add => bcx.ins().fadd(l, r),
|
|
546
|
+
BinOp::Sub => bcx.ins().fsub(l, r),
|
|
547
|
+
BinOp::Mul => bcx.ins().fmul(l, r),
|
|
548
|
+
BinOp::Div => bcx.ins().fdiv(l, r),
|
|
549
|
+
BinOp::Eq | BinOp::StrictEq => fcmp_f64(bcx, FloatCC::Equal, l, r),
|
|
550
|
+
BinOp::Ne | BinOp::StrictNe => fcmp_f64(bcx, FloatCC::NotEqual, l, r),
|
|
551
|
+
BinOp::Lt => fcmp_f64(bcx, FloatCC::LessThan, l, r),
|
|
552
|
+
BinOp::Le => fcmp_f64(bcx, FloatCC::LessThanOrEqual, l, r),
|
|
553
|
+
BinOp::Gt => fcmp_f64(bcx, FloatCC::GreaterThan, l, r),
|
|
554
|
+
BinOp::Ge => fcmp_f64(bcx, FloatCC::GreaterThanOrEqual, l, r),
|
|
555
|
+
BinOp::Mod => {
|
|
556
|
+
// f64 remainder a - trunc(a/b)*b — exactly Rust's `%`, which the
|
|
557
|
+
// VM's eval_binop uses, so JIT and VM-fallback agree bit-for-bit.
|
|
558
|
+
let q = bcx.ins().fdiv(l, r);
|
|
559
|
+
let t = bcx.ins().trunc(q);
|
|
560
|
+
let p = bcx.ins().fmul(t, r);
|
|
561
|
+
bcx.ins().fsub(l, p)
|
|
562
|
+
}
|
|
563
|
+
// Bitwise AND/OR/XOR via JS ToInt32 (modulo 2³², NaN/±∞ → 0) — see [`js_to_int32`].
|
|
564
|
+
// Shifts / `>>>` stay on the VM (shift-amount edge cases).
|
|
565
|
+
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => {
|
|
566
|
+
let li = js_to_int32(bcx, l);
|
|
567
|
+
let ri = js_to_int32(bcx, r);
|
|
568
|
+
let res = match bop {
|
|
569
|
+
BinOp::BitAnd => bcx.ins().band(li, ri),
|
|
570
|
+
BinOp::BitOr => bcx.ins().bor(li, ri),
|
|
571
|
+
BinOp::BitXor => bcx.ins().bxor(li, ri),
|
|
572
|
+
_ => unreachable!(),
|
|
573
|
+
};
|
|
574
|
+
bcx.ins().fcvt_from_sint(types::F64, res)
|
|
575
|
+
}
|
|
576
|
+
// Pow/shifts/`>>>`/In/And/Or: fall back to the VM.
|
|
577
|
+
_ => return SimpleOp::Unsupported,
|
|
578
|
+
};
|
|
579
|
+
stack.push((v, is_cmp));
|
|
580
|
+
}
|
|
581
|
+
Opcode::UnaryOp => {
|
|
582
|
+
let uop = match read_u16(code, ip).map(|r| r as u8).and_then(u8_to_unaryop) {
|
|
583
|
+
Some(u) => u,
|
|
584
|
+
None => return SimpleOp::Unsupported,
|
|
585
|
+
};
|
|
586
|
+
let (o, _) = match stack.pop() {
|
|
587
|
+
Some(x) => x,
|
|
588
|
+
None => return SimpleOp::Unsupported,
|
|
589
|
+
};
|
|
590
|
+
let (v, is_bool) = match uop {
|
|
591
|
+
UnaryOp::Neg => (bcx.ins().fneg(o), false),
|
|
592
|
+
UnaryOp::Pos => (o, false),
|
|
593
|
+
UnaryOp::Not => {
|
|
594
|
+
let zero = bcx.ins().f64const(0.0);
|
|
595
|
+
(fcmp_f64(bcx, FloatCC::Equal, o, zero), true)
|
|
596
|
+
}
|
|
597
|
+
// `~x` = `!ToInt32(x) as f64` — JS ToInt32 (modulo, NaN/±∞ → 0) via [`js_to_int32`],
|
|
598
|
+
// matching the VM so a JIT-compiled `~` can't diverge on large/non-finite values.
|
|
599
|
+
UnaryOp::BitNot => {
|
|
600
|
+
let oi = js_to_int32(bcx, o);
|
|
601
|
+
let res = bcx.ins().bnot(oi);
|
|
602
|
+
(bcx.ins().fcvt_from_sint(types::F64, res), false)
|
|
603
|
+
}
|
|
604
|
+
_ => return SimpleOp::Unsupported,
|
|
605
|
+
};
|
|
606
|
+
stack.push((v, is_bool));
|
|
607
|
+
}
|
|
608
|
+
_ => unreachable!("guarded above"),
|
|
609
|
+
}
|
|
610
|
+
SimpleOp::Handled(false)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// `is_truthy(cond)` as a Cranelift bool, matching the VM: a number is truthy iff it is
|
|
614
|
+
/// nonzero AND not NaN. Returns the **falsy** flag (so callers can `select(falsy, else, then)`
|
|
615
|
+
/// without a logical-not, which `bnot` can't express on a 0/1 value).
|
|
616
|
+
fn falsy_flag(bcx: &mut FunctionBuilder, cond: ClifValue) -> ClifValue {
|
|
617
|
+
let zero = bcx.ins().f64const(0.0);
|
|
618
|
+
let eq_zero = bcx.ins().fcmp(FloatCC::Equal, cond, zero); // ordered: false for NaN
|
|
619
|
+
let is_nan = bcx.ins().fcmp(FloatCC::NotEqual, cond, cond); // UNE self-compare: true iff NaN
|
|
620
|
+
bcx.ins().bor(eq_zero, is_nan)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Translate the chunk's numeric bytecode into the function body. Straight-line ops plus the
|
|
624
|
+
/// **ternary `cond ? A : B`** pattern (forward `JumpIfFalse`/`Jump` with branch-free, net-+1,
|
|
625
|
+
/// agreeing-`is_bool` arms) → a Cranelift `select`. Loops (`JumpBack`), early returns inside a
|
|
626
|
+
/// branch, nested branches, calls, member/index, or mismatched `is_bool` all return `None` so the
|
|
627
|
+
/// VM runs the chunk instead — purely additive. Returns `Some(result_is_bool)`.
|
|
628
|
+
/// Byte size of an opcode the loop-JIT understands; `None` ⇒ unsupported (bail → VM).
|
|
629
|
+
fn op_size(op: Opcode) -> Option<usize> {
|
|
630
|
+
use Opcode::*;
|
|
631
|
+
Some(match op {
|
|
632
|
+
Nop | Pop | Dup | Return | LoopVarsEnd | EnterBlock | ExitBlock | GetIndex => 1,
|
|
633
|
+
LoadLocal | StoreLocal | LoadConst | BinOp | UnaryOp | Jump | JumpIfFalse | JumpBack
|
|
634
|
+
| LoopVarsBegin | SelfCall => 3,
|
|
635
|
+
_ => return None,
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// Read a big-endian u16 operand at `off` without advancing (matches [`read_u16`]).
|
|
640
|
+
#[inline]
|
|
641
|
+
fn peek_u16(code: &[u8], off: usize) -> Option<u16> {
|
|
642
|
+
let a = *code.get(off)? as u16;
|
|
643
|
+
let b = *code.get(off + 1)? as u16;
|
|
644
|
+
Some((a << 8) | b)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/// Control-flow JIT: lower a slot-based numeric function WITH loops/branches to native code. Uses a
|
|
648
|
+
/// cranelift `Variable` per slot (loop-carried locals are mutable across blocks; cranelift inserts the
|
|
649
|
+
/// SSA phis at `seal_all_blocks`), and one block per bytecode jump target. Handles LoadLocal/StoreLocal,
|
|
650
|
+
/// LoadConst, numeric BinOp/UnaryOp, Pop/Dup/Nop, LoopVarsBegin/End (skipped — a slotted loop var needs
|
|
651
|
+
/// no per-iteration overlay), Jump/JumpIfFalse/JumpBack/Return, AND direct calls to JIT-compiled callees
|
|
652
|
+
/// (a `LoadVar name_idx` + `Call arity` where the callee has a `NumericFn` in `callees` → emit a
|
|
653
|
+
/// direct cranelift `call` to the native code pointer, skipping all Value boxing). CONSERVATIVE: bails
|
|
654
|
+
/// (→ caller → VM) on any other opcode, a non-empty operand stack at a block boundary, boolean slots,
|
|
655
|
+
/// or a `Call` whose callee is not in `callees`. ADDITIVE: a bail just runs the VM.
|
|
656
|
+
/// Returns `Some(false)` (Number result) on success.
|
|
657
|
+
fn build_body_cfg(
|
|
658
|
+
func: &mut cranelift::codegen::ir::Function,
|
|
659
|
+
fbctx: &mut FunctionBuilderContext,
|
|
660
|
+
chunk: &Chunk,
|
|
661
|
+
arity: usize,
|
|
662
|
+
self_ref: Option<cranelift::codegen::ir::FuncRef>,
|
|
663
|
+
// Array-mode bitmask (bit k ⇒ param k is an array). `0` ⇒ ordinary register-`f64` ABI (every
|
|
664
|
+
// existing caller passes 0, so that path is byte-identical). Nonzero ⇒ the 3-pointer array ABI:
|
|
665
|
+
// params are `[numeric_ptr, handles_ptr, deopt_ptr]` and `arr[i]` lowers to a bounds-checked load.
|
|
666
|
+
array_mask: u8,
|
|
667
|
+
) -> Option<bool> {
|
|
668
|
+
let code = &chunk.code;
|
|
669
|
+
let num_slots = chunk.num_slots as usize;
|
|
670
|
+
if num_slots == 0 || num_slots > 256 {
|
|
671
|
+
return None;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 1. Validate every opcode is supported + collect block leaders (jump targets, the fall-through
|
|
675
|
+
// after each branch, entry). Bail on any unsupported opcode (so we never mis-size the scan).
|
|
676
|
+
let mut leaders: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
|
|
677
|
+
leaders.insert(0);
|
|
678
|
+
let mut has_loop = false;
|
|
679
|
+
let mut has_self_call = false;
|
|
680
|
+
let mut ip = 0;
|
|
681
|
+
while ip < code.len() {
|
|
682
|
+
let op = Opcode::from_u8(code[ip])?;
|
|
683
|
+
let size = op_size(op)?;
|
|
684
|
+
// A SelfCall whose arity != this function's arity can't be a plain f64-ABI native call → bail.
|
|
685
|
+
if op == Opcode::SelfCall {
|
|
686
|
+
if self_ref.is_none() || peek_u16(code, ip + 1)? as usize != arity {
|
|
687
|
+
return None;
|
|
688
|
+
}
|
|
689
|
+
has_self_call = true;
|
|
690
|
+
}
|
|
691
|
+
match op {
|
|
692
|
+
// A conditional branch: BOTH the target and the fall-through are reachable.
|
|
693
|
+
Opcode::JumpIfFalse => {
|
|
694
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
695
|
+
leaders.insert(((ip + 3) as isize + off).max(0) as usize);
|
|
696
|
+
leaders.insert(ip + 3);
|
|
697
|
+
}
|
|
698
|
+
// Unconditional jumps: only the TARGET is a leader. The byte after the jump is reachable
|
|
699
|
+
// iff something else jumps to it — in which case that jump adds it. Code after an
|
|
700
|
+
// unconditional terminator (Jump/JumpBack/Return) that nothing targets is UNREACHABLE
|
|
701
|
+
// (e.g. the compiler's trailing implicit `LoadConst Null; Return`) and must be skipped,
|
|
702
|
+
// not translated — else we'd bail on `LoadConst Null`.
|
|
703
|
+
Opcode::Jump => {
|
|
704
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
705
|
+
leaders.insert(((ip + 3) as isize + off).max(0) as usize);
|
|
706
|
+
}
|
|
707
|
+
Opcode::JumpBack => {
|
|
708
|
+
let dist = peek_u16(code, ip + 1)? as usize;
|
|
709
|
+
leaders.insert((ip + 3).checked_sub(dist)?);
|
|
710
|
+
has_loop = true;
|
|
711
|
+
}
|
|
712
|
+
_ => {}
|
|
713
|
+
}
|
|
714
|
+
ip += size;
|
|
715
|
+
}
|
|
716
|
+
// Worth the CFG path when there's a loop OR a self-recursive call (e.g. `fib`: branches + early
|
|
717
|
+
// return + recursion, no loop). Pure straight-line/ternary stays on `build_body`.
|
|
718
|
+
if !has_loop && !has_self_call {
|
|
719
|
+
return None;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let mut bcx = FunctionBuilder::new(func, fbctx);
|
|
723
|
+
let blocks: std::collections::BTreeMap<usize, Block> =
|
|
724
|
+
leaders.iter().map(|&o| (o, bcx.create_block())).collect();
|
|
725
|
+
let entry = *blocks.get(&0)?;
|
|
726
|
+
bcx.append_block_params_for_function_params(entry);
|
|
727
|
+
bcx.switch_to_block(entry);
|
|
728
|
+
let params: Vec<ClifValue> = bcx.block_params(entry).to_vec();
|
|
729
|
+
|
|
730
|
+
// 2. A Variable per slot, all defined at entry so every path defines them.
|
|
731
|
+
let vars: Vec<Variable> = (0..num_slots).map(|_| bcx.declare_var(types::F64)).collect();
|
|
732
|
+
// Array-mode state: per-array-param `(ptr,len)` loaded from the handles array + the deopt pad.
|
|
733
|
+
let mut array_slots: HashMap<usize, (ClifValue, ClifValue)> = HashMap::new();
|
|
734
|
+
let mut deopt_block: Option<Block> = None;
|
|
735
|
+
let mut deopt_ptr: Option<ClifValue> = None;
|
|
736
|
+
if array_mask != 0 {
|
|
737
|
+
// ABI params: [numeric_ptr, handles_ptr, deopt_ptr]. Numeric params load from numeric_ptr in
|
|
738
|
+
// numeric-param order; array params load (ptr,len) from handles_ptr in array-param order.
|
|
739
|
+
let numeric_ptr = *params.first()?;
|
|
740
|
+
let handles_ptr = *params.get(1)?;
|
|
741
|
+
deopt_ptr = Some(*params.get(2)?);
|
|
742
|
+
let mut numeric_i = 0i32;
|
|
743
|
+
let mut array_i = 0i64;
|
|
744
|
+
#[allow(clippy::needless_range_loop)] // `slot` drives bit-mask math (`array_mask >> slot`) + map keys, not just indexing
|
|
745
|
+
for slot in 0..num_slots {
|
|
746
|
+
let init = if slot < arity && (array_mask >> slot) & 1 == 1 {
|
|
747
|
+
let base = bcx.ins().iadd_imm(handles_ptr, array_i * 16);
|
|
748
|
+
let p = bcx.ins().load(types::I64, MemFlags::new(), base, 0);
|
|
749
|
+
let l = bcx.ins().load(types::I64, MemFlags::new(), base, 8);
|
|
750
|
+
array_slots.insert(slot, (p, l));
|
|
751
|
+
array_i += 1;
|
|
752
|
+
bcx.ins().f64const(0.0) // an array slot's f64 Variable is never read
|
|
753
|
+
} else if slot < arity {
|
|
754
|
+
let v = bcx
|
|
755
|
+
.ins()
|
|
756
|
+
.load(types::F64, MemFlags::new(), numeric_ptr, numeric_i * 8);
|
|
757
|
+
numeric_i += 1;
|
|
758
|
+
v
|
|
759
|
+
} else {
|
|
760
|
+
bcx.ins().f64const(0.0)
|
|
761
|
+
};
|
|
762
|
+
bcx.def_var(vars[slot], init);
|
|
763
|
+
}
|
|
764
|
+
deopt_block = Some(bcx.create_block());
|
|
765
|
+
} else {
|
|
766
|
+
for (i, &v) in vars.iter().enumerate() {
|
|
767
|
+
let init = if i < arity {
|
|
768
|
+
params[i]
|
|
769
|
+
} else {
|
|
770
|
+
bcx.ins().f64const(0.0)
|
|
771
|
+
};
|
|
772
|
+
bcx.def_var(v, init);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// 3. Translate. The operand stack is empty at every block boundary (statement-level control flow).
|
|
777
|
+
let mut stack: Vec<(ClifValue, bool)> = Vec::new();
|
|
778
|
+
let mut cur = entry;
|
|
779
|
+
let mut terminated = false;
|
|
780
|
+
let mut ip = 0usize;
|
|
781
|
+
while ip < code.len() {
|
|
782
|
+
if let Some(&blk) = blocks.get(&ip) {
|
|
783
|
+
if blk != cur {
|
|
784
|
+
if !terminated {
|
|
785
|
+
if !stack.is_empty() {
|
|
786
|
+
return None;
|
|
787
|
+
}
|
|
788
|
+
bcx.ins().jump(blk, &[]);
|
|
789
|
+
}
|
|
790
|
+
bcx.switch_to_block(blk);
|
|
791
|
+
cur = blk;
|
|
792
|
+
terminated = false;
|
|
793
|
+
stack.clear();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if terminated {
|
|
797
|
+
ip += op_size(Opcode::from_u8(code[ip])?)?; // skip unreachable tail before next leader
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
let op = Opcode::from_u8(code[ip])?;
|
|
801
|
+
match op {
|
|
802
|
+
Opcode::LoadLocal => {
|
|
803
|
+
let slot = peek_u16(code, ip + 1)? as usize;
|
|
804
|
+
// Array-mode peephole: `LoadLocal(arrayparam) ; (LoadLocal|LoadConst) ; GetIndex` →
|
|
805
|
+
// a bounds-checked native f64 load; out-of-bounds branches to the deopt pad.
|
|
806
|
+
if array_mask != 0 && slot < arity && (array_mask >> slot) & 1 == 1 {
|
|
807
|
+
let (aptr, alen) = *array_slots.get(&slot)?;
|
|
808
|
+
let idx_op = Opcode::from_u8(*code.get(ip + 3)?)?;
|
|
809
|
+
let idx_f64 = match idx_op {
|
|
810
|
+
Opcode::LoadLocal => {
|
|
811
|
+
let islot = peek_u16(code, ip + 4)? as usize;
|
|
812
|
+
bcx.use_var(*vars.get(islot)?)
|
|
813
|
+
}
|
|
814
|
+
Opcode::LoadConst => {
|
|
815
|
+
let ci = peek_u16(code, ip + 4)? as usize;
|
|
816
|
+
match chunk.constants.get(ci) {
|
|
817
|
+
Some(Constant::Number(n)) => bcx.ins().f64const(*n),
|
|
818
|
+
_ => return None,
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
_ => return None,
|
|
822
|
+
};
|
|
823
|
+
if Opcode::from_u8(*code.get(ip + 6)?)? != Opcode::GetIndex {
|
|
824
|
+
return None;
|
|
825
|
+
}
|
|
826
|
+
// i = idx as usize (saturating: NaN→0, neg→0 — matches the VM's `n as usize`).
|
|
827
|
+
let i = bcx.ins().fcvt_to_uint_sat(types::I64, idx_f64);
|
|
828
|
+
let inb = bcx.ins().icmp(IntCC::UnsignedLessThan, i, alen);
|
|
829
|
+
let cont = bcx.create_block();
|
|
830
|
+
let db = deopt_block?;
|
|
831
|
+
bcx.ins().brif(inb, cont, &[], db, &[]);
|
|
832
|
+
bcx.switch_to_block(cont);
|
|
833
|
+
cur = cont; // keep block-boundary tracking accurate after the mid-stream split
|
|
834
|
+
let off = bcx.ins().imul_imm(i, 8);
|
|
835
|
+
let addr = bcx.ins().iadd(aptr, off);
|
|
836
|
+
let val = bcx.ins().load(types::F64, MemFlags::new(), addr, 0);
|
|
837
|
+
stack.push((val, false));
|
|
838
|
+
ip += 7; // LoadLocal(arr) + index op + GetIndex
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
let v = *vars.get(slot)?;
|
|
842
|
+
stack.push((bcx.use_var(v), false));
|
|
843
|
+
ip += 3;
|
|
844
|
+
}
|
|
845
|
+
Opcode::StoreLocal => {
|
|
846
|
+
let slot = peek_u16(code, ip + 1)? as usize;
|
|
847
|
+
let (val, is_bool) = stack.pop()?;
|
|
848
|
+
if is_bool {
|
|
849
|
+
return None; // no boolean slots — keeps result boxing simple
|
|
850
|
+
}
|
|
851
|
+
let v = *vars.get(slot)?;
|
|
852
|
+
bcx.def_var(v, val);
|
|
853
|
+
ip += 3;
|
|
854
|
+
}
|
|
855
|
+
Opcode::Pop => {
|
|
856
|
+
stack.pop()?;
|
|
857
|
+
ip += 1;
|
|
858
|
+
}
|
|
859
|
+
Opcode::Dup => {
|
|
860
|
+
let top = *stack.last()?;
|
|
861
|
+
stack.push(top);
|
|
862
|
+
ip += 1;
|
|
863
|
+
}
|
|
864
|
+
// Scope markers (EnterBlock/ExitBlock) + loop-var registration only affect the VM's
|
|
865
|
+
// name-based block scope / per-iteration overlay — irrelevant to flat frame slots.
|
|
866
|
+
Opcode::Nop | Opcode::EnterBlock | Opcode::ExitBlock | Opcode::LoopVarsEnd => ip += 1,
|
|
867
|
+
Opcode::LoopVarsBegin => ip += 3,
|
|
868
|
+
Opcode::Return => {
|
|
869
|
+
let (v, is_bool) = stack.pop()?;
|
|
870
|
+
if is_bool {
|
|
871
|
+
return None;
|
|
872
|
+
}
|
|
873
|
+
bcx.ins().return_(&[v]);
|
|
874
|
+
terminated = true;
|
|
875
|
+
ip += 1;
|
|
876
|
+
}
|
|
877
|
+
Opcode::Jump => {
|
|
878
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
879
|
+
let blk = *blocks.get(&(((ip + 3) as isize + off).max(0) as usize))?;
|
|
880
|
+
if !stack.is_empty() {
|
|
881
|
+
return None;
|
|
882
|
+
}
|
|
883
|
+
bcx.ins().jump(blk, &[]);
|
|
884
|
+
terminated = true;
|
|
885
|
+
ip += 3;
|
|
886
|
+
}
|
|
887
|
+
Opcode::JumpBack => {
|
|
888
|
+
let dist = peek_u16(code, ip + 1)? as usize;
|
|
889
|
+
let blk = *blocks.get(&((ip + 3).checked_sub(dist)?))?;
|
|
890
|
+
if !stack.is_empty() {
|
|
891
|
+
return None;
|
|
892
|
+
}
|
|
893
|
+
bcx.ins().jump(blk, &[]);
|
|
894
|
+
terminated = true;
|
|
895
|
+
ip += 3;
|
|
896
|
+
}
|
|
897
|
+
Opcode::JumpIfFalse => {
|
|
898
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
899
|
+
let (cond, _) = stack.pop()?;
|
|
900
|
+
if !stack.is_empty() {
|
|
901
|
+
return None; // non-empty stack ⇒ ternary shape ⇒ leave to build_body / VM
|
|
902
|
+
}
|
|
903
|
+
let falsy = falsy_flag(&mut bcx, cond);
|
|
904
|
+
let target = *blocks.get(&(((ip + 3) as isize + off).max(0) as usize))?;
|
|
905
|
+
let fallthrough = *blocks.get(&(ip + 3))?;
|
|
906
|
+
bcx.ins().brif(falsy, target, &[], fallthrough, &[]);
|
|
907
|
+
terminated = true;
|
|
908
|
+
ip += 3;
|
|
909
|
+
}
|
|
910
|
+
Opcode::SelfCall => {
|
|
911
|
+
// Recursive self-call → a native cranelift call to this very function. Args are the
|
|
912
|
+
// top `arity` f64 stack values; result is pushed. Validated above (self_ref present,
|
|
913
|
+
// arity matches). This is what makes `fib` etc. run at native speed.
|
|
914
|
+
let sref = self_ref?; // guaranteed Some by the validation scan
|
|
915
|
+
if stack.len() < arity {
|
|
916
|
+
return None;
|
|
917
|
+
}
|
|
918
|
+
let arg_start = stack.len() - arity;
|
|
919
|
+
let mut call_args = Vec::with_capacity(arity);
|
|
920
|
+
for (v, is_bool) in stack.drain(arg_start..) {
|
|
921
|
+
if is_bool {
|
|
922
|
+
return None; // boolean args don't match the f64 ABI
|
|
923
|
+
}
|
|
924
|
+
call_args.push(v);
|
|
925
|
+
}
|
|
926
|
+
let call = bcx.ins().call(sref, &call_args);
|
|
927
|
+
let result = bcx.inst_results(call)[0];
|
|
928
|
+
stack.push((result, false));
|
|
929
|
+
ip += 3;
|
|
930
|
+
}
|
|
931
|
+
_ => match emit_simple_op(&mut bcx, chunk, code, &mut ip, &mut stack, ¶ms, arity) {
|
|
932
|
+
SimpleOp::Handled(_) => {}
|
|
933
|
+
_ => return None, // LoadConst/BinOp/UnaryOp handled; anything else → VM
|
|
934
|
+
},
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if !terminated {
|
|
939
|
+
return None;
|
|
940
|
+
}
|
|
941
|
+
// Array-mode deopt landing pad: `*deopt = 1; return 0.0`. Reached from every OOB bounds-check; the
|
|
942
|
+
// VM wrapper sees the flag and re-runs the interpreter (so OOB → `Value::Null` stays correct).
|
|
943
|
+
if let (Some(db), Some(dp)) = (deopt_block, deopt_ptr) {
|
|
944
|
+
bcx.switch_to_block(db);
|
|
945
|
+
let one = bcx.ins().iconst(types::I8, 1);
|
|
946
|
+
bcx.ins().store(MemFlags::new(), one, dp, 0);
|
|
947
|
+
let zero = bcx.ins().f64const(0.0);
|
|
948
|
+
bcx.ins().return_(&[zero]);
|
|
949
|
+
}
|
|
950
|
+
bcx.seal_all_blocks();
|
|
951
|
+
bcx.finalize();
|
|
952
|
+
Some(false)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
fn build_body(
|
|
956
|
+
func: &mut cranelift::codegen::ir::Function,
|
|
957
|
+
fbctx: &mut FunctionBuilderContext,
|
|
958
|
+
chunk: &Chunk,
|
|
959
|
+
arity: usize,
|
|
960
|
+
) -> Option<bool> {
|
|
961
|
+
let mut bcx = FunctionBuilder::new(func, fbctx);
|
|
962
|
+
let entry = bcx.create_block();
|
|
963
|
+
bcx.append_block_params_for_function_params(entry);
|
|
964
|
+
bcx.switch_to_block(entry);
|
|
965
|
+
bcx.seal_block(entry);
|
|
966
|
+
let params: Vec<ClifValue> = bcx.block_params(entry).to_vec();
|
|
967
|
+
|
|
968
|
+
let code = &chunk.code;
|
|
969
|
+
// Each entry is (clif f64 value, is_bool). `is_bool` marks comparison/`!`
|
|
970
|
+
// results (logical 0.0/1.0) so the final value boxes as Bool, not Number.
|
|
971
|
+
let mut stack: Vec<(ClifValue, bool)> = Vec::new();
|
|
972
|
+
let mut ip = 0usize;
|
|
973
|
+
let mut result: Option<bool> = None;
|
|
974
|
+
|
|
975
|
+
while ip < code.len() {
|
|
976
|
+
match emit_simple_op(&mut bcx, chunk, code, &mut ip, &mut stack, ¶ms, arity) {
|
|
977
|
+
SimpleOp::Handled(_) => continue,
|
|
978
|
+
SimpleOp::Unsupported => return None,
|
|
979
|
+
SimpleOp::NotSimple => {}
|
|
980
|
+
}
|
|
981
|
+
let op = Opcode::from_u8(code[ip])?;
|
|
982
|
+
match op {
|
|
983
|
+
Opcode::Return => {
|
|
984
|
+
let (v, is_bool) = stack.pop()?;
|
|
985
|
+
bcx.ins().return_(&[v]);
|
|
986
|
+
result = Some(is_bool); // first Return ends a (sub)path
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
// Ternary `cond ? A : B` → `select`. Both arms must be branch-free numeric
|
|
990
|
+
// sub-sequences, each pushing exactly one value, with matching is_bool.
|
|
991
|
+
Opcode::JumpIfFalse => {
|
|
992
|
+
let (cond, _) = stack.pop()?;
|
|
993
|
+
let mut p = ip + 1;
|
|
994
|
+
let off = read_u16(code, &mut p)? as i16 as isize; // p now past the operand
|
|
995
|
+
let else_target = (p as isize + off).max(0) as usize;
|
|
996
|
+
let base = stack.len();
|
|
997
|
+
|
|
998
|
+
// THEN arm: straight-line ops until the trailing `Jump`.
|
|
999
|
+
let mut tip = p;
|
|
1000
|
+
loop {
|
|
1001
|
+
match emit_simple_op(&mut bcx, chunk, code, &mut tip, &mut stack, ¶ms, arity)
|
|
1002
|
+
{
|
|
1003
|
+
SimpleOp::Handled(_) => continue,
|
|
1004
|
+
SimpleOp::Unsupported => return None,
|
|
1005
|
+
SimpleOp::NotSimple => break,
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if Opcode::from_u8(*code.get(tip)?)? != Opcode::Jump {
|
|
1009
|
+
return None; // not the ternary shape (e.g. early return) → VM
|
|
1010
|
+
}
|
|
1011
|
+
let mut jp = tip + 1;
|
|
1012
|
+
let joff = read_u16(code, &mut jp)? as i16 as isize;
|
|
1013
|
+
let merge_target = (jp as isize + joff).max(0) as usize;
|
|
1014
|
+
// The else arm must begin exactly where the then-arm's `Jump` left off.
|
|
1015
|
+
if else_target != jp || stack.len() != base + 1 {
|
|
1016
|
+
return None;
|
|
1017
|
+
}
|
|
1018
|
+
let (then_v, then_b) = stack.pop()?;
|
|
1019
|
+
|
|
1020
|
+
// ELSE arm: straight-line ops from `jp` up to the merge point.
|
|
1021
|
+
let mut eip = jp;
|
|
1022
|
+
while eip < merge_target {
|
|
1023
|
+
match emit_simple_op(&mut bcx, chunk, code, &mut eip, &mut stack, ¶ms, arity)
|
|
1024
|
+
{
|
|
1025
|
+
SimpleOp::Handled(_) => continue,
|
|
1026
|
+
_ => return None, // nested control flow / unsupported → VM
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if eip != merge_target || stack.len() != base + 1 {
|
|
1030
|
+
return None;
|
|
1031
|
+
}
|
|
1032
|
+
let (else_v, else_b) = stack.pop()?;
|
|
1033
|
+
// One result_bool per function: arms must agree on Bool-vs-Number.
|
|
1034
|
+
if then_b != else_b {
|
|
1035
|
+
return None;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
let falsy = falsy_flag(&mut bcx, cond);
|
|
1039
|
+
let sel = bcx.ins().select(falsy, else_v, then_v);
|
|
1040
|
+
stack.push((sel, then_b));
|
|
1041
|
+
ip = merge_target;
|
|
1042
|
+
}
|
|
1043
|
+
// Loops / member / index / call / array / object → VM.
|
|
1044
|
+
_ => return None,
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
bcx.finalize();
|
|
1049
|
+
result
|
|
1050
|
+
}
|