@tishlang/tish 2.10.1 → 2.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +16 -0
- package/crates/tish/src/main.rs +24 -4
- package/crates/tish/tests/integration_test.rs +149 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +91 -4
- package/crates/tish_compile_js/src/lib.rs +5 -2
- package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +253 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -20,19 +20,64 @@
|
|
|
20
20
|
//! Not compiled for wasm targets (cranelift-jit emits host code).
|
|
21
21
|
|
|
22
22
|
use std::collections::HashMap;
|
|
23
|
+
use std::sync::Arc;
|
|
23
24
|
use std::sync::{Mutex, OnceLock};
|
|
24
25
|
|
|
25
26
|
use cranelift::codegen::settings::{self, Configurable};
|
|
26
27
|
use cranelift::prelude::types;
|
|
27
28
|
use cranelift::prelude::{
|
|
28
|
-
AbiParam, Block, FloatCC, FunctionBuilder, FunctionBuilderContext, InstBuilder, IntCC,
|
|
29
|
-
Value as ClifValue, Variable,
|
|
29
|
+
AbiParam, Block, FloatCC, FunctionBuilder, FunctionBuilderContext, InstBuilder, IntCC,
|
|
30
|
+
MemFlags, Value as ClifValue, Variable,
|
|
30
31
|
};
|
|
31
32
|
use cranelift_jit::{JITBuilder, JITModule};
|
|
32
33
|
use cranelift_module::{Linkage, Module};
|
|
33
34
|
|
|
34
35
|
use tishlang_ast::{BinOp, UnaryOp};
|
|
35
|
-
use tishlang_bytecode::{
|
|
36
|
+
use tishlang_bytecode::{
|
|
37
|
+
u8_to_binop, u8_to_unaryop, Chunk, Constant, MathUnaryFn, Opcode, NO_REST_PARAM,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/// A JIT-compiled hot LOOP REGION for on-stack replacement (#190). Unlike [`NumericFn`] (a whole
|
|
41
|
+
/// function over register-`f64` params), this is native code over a chunk's **numeric slot frame**:
|
|
42
|
+
/// the VM copies the region's live-in slots into an `f64` buffer, calls the region, then copies the
|
|
43
|
+
/// live-outs back. ABI: `extern "C" fn(slots: *mut f64, deopt: *mut u8) -> i32`, returning the EXIT
|
|
44
|
+
/// id (index into [`exits`]). `deopt` is reserved (a `*mut u8` flag): the v1 whitelist is pure numeric
|
|
45
|
+
/// slot math, which cannot deopt mid-run, so it is never set — it keeps the ABI stable for the future
|
|
46
|
+
/// array/property regions that will need a bail path. The pointer is into the never-freed `JITModule`.
|
|
47
|
+
#[derive(Clone)]
|
|
48
|
+
pub struct LoopFn {
|
|
49
|
+
ptr: usize,
|
|
50
|
+
/// Slots read or written inside the region — the live set. The `f64` buffer is indexed by
|
|
51
|
+
/// POSITION here (buffer slot `p` ↔ chunk slot `used_slots[p]`); the emitted code loads/stores at
|
|
52
|
+
/// that same position, so the VM and the native code agree without threading slot numbers.
|
|
53
|
+
pub used_slots: Vec<u16>,
|
|
54
|
+
/// Exit id → the chunk `ip` to resume interpreting at (a loop-exit / `break` target outside the
|
|
55
|
+
/// region). The region always has ≥1 exit (an exit-less region would be an uninterruptible native
|
|
56
|
+
/// loop, so compilation bails).
|
|
57
|
+
pub exits: Vec<usize>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// SAFETY: identical to `NumericFn` — `ptr` is immutable executable code in the process-global,
|
|
61
|
+
// never-dropped `JITModule`; the slot buffer is caller-owned. Send/Sync so the frame VM (which may
|
|
62
|
+
// run on any thread) can hold a cached `LoopFn`.
|
|
63
|
+
unsafe impl Send for LoopFn {}
|
|
64
|
+
unsafe impl Sync for LoopFn {}
|
|
65
|
+
|
|
66
|
+
impl LoopFn {
|
|
67
|
+
/// Run the region. `buf` holds the live-ins (`used_slots.len()` `f64`s), updated in place with the
|
|
68
|
+
/// live-outs on return. `deopt` is a 1-byte flag (unused in v1). Returns the exit id. Safe wrapper
|
|
69
|
+
/// — the raw-pointer transmute (same soundness as [`NumericFn::call`]: immutable native code with a
|
|
70
|
+
/// fixed C ABI) is confined here, so call sites need no `unsafe`.
|
|
71
|
+
#[inline]
|
|
72
|
+
pub fn call(&self, buf: &mut [f64], deopt: &mut u8) -> i32 {
|
|
73
|
+
// SAFETY: `ptr` is immutable executable code compiled for exactly this `(*mut f64, *mut u8)`
|
|
74
|
+
// ABI; `buf`/`deopt` are valid for the call and the region only touches `buf[0..used_slots]`.
|
|
75
|
+
unsafe {
|
|
76
|
+
let f: extern "C" fn(*mut f64, *mut u8) -> i32 = std::mem::transmute(self.ptr);
|
|
77
|
+
f(buf.as_mut_ptr(), deopt as *mut u8)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
36
81
|
|
|
37
82
|
/// A JIT-compiled numeric function: a pointer to native code plus its arity.
|
|
38
83
|
/// The pointer is into a leaked, never-freed `JITModule`, so it is valid for the
|
|
@@ -50,19 +95,61 @@ pub struct NumericFn {
|
|
|
50
95
|
/// array-mode uniform 3-pointer ABI (the [`NumericFn::call_arrays`] path). Kept as a `u8` (arity
|
|
51
96
|
/// ≤ 8) so `NumericFn` stays `Copy`. `TISH_JIT_ARRAYS`-gated; `0` in every default build.
|
|
52
97
|
array_param_mask: u8,
|
|
98
|
+
/// #187: subset of `array_param_mask` whose arrays are WRITTEN (`arr[i] = v`). After a non-deopt
|
|
99
|
+
/// `call_arrays`, [`try_call_array_jit`] copies each such scratch buffer back into the caller's
|
|
100
|
+
/// `Value::Array`. `0` ⇒ every array param is read-only (no writeback).
|
|
101
|
+
array_writable_mask: u8,
|
|
102
|
+
/// #187: subset of `array_writable_mask` whose elements are written with a BOOL const (`arr[i]=true`)
|
|
103
|
+
/// rather than a number. The JIT flattens elements to `f64`, so the writeback re-boxes by the array's
|
|
104
|
+
/// ORIGINAL element type — sound only when the WRITTEN kind matches that entry type. `try_call_array_jit`
|
|
105
|
+
/// bails to the interpreter when a bool-written array is passed a number array (or vice versa); a mix of
|
|
106
|
+
/// bool + non-bool writes to one array is rejected at compile time (`classify_params` returns `(0,0,0)`).
|
|
107
|
+
array_bool_write_mask: u8,
|
|
108
|
+
/// True when this is a self-recursive function compiled with a trailing `*mut RecurGuard` param
|
|
109
|
+
/// (the recursion-depth bail, #381). Such a function must be invoked via [`NumericFn::call_guarded`];
|
|
110
|
+
/// non-recursive functions keep the plain ABI and [`NumericFn::call`] (zero overhead).
|
|
111
|
+
recur_guarded: bool,
|
|
112
|
+
/// True when this function has JIT'd local `f64` arrays (#189). It uses the plain register-`f64`
|
|
113
|
+
/// [`NumericFn::call`] ABI; an out-of-bounds array access (or a non-numeric return) sets a
|
|
114
|
+
/// per-thread deopt flag ([`jv_take_deopt`]) and the caller discards the result + re-interprets.
|
|
115
|
+
jv: bool,
|
|
116
|
+
/// #187: true when this function embeds a native call to a registered callee. Such a function is
|
|
117
|
+
/// NOT cached by [`try_compile_numeric`] (its embedded callee address could go stale if a
|
|
118
|
+
/// long-lived process reuses chunk addresses across programs) — it is recompiled per closure
|
|
119
|
+
/// creation, which resolves against the live callee registry. `false` (cacheable) for all others.
|
|
120
|
+
uses_xcall: bool,
|
|
121
|
+
/// #187: true when this is a VOID array-mode function (only returns the implicit `null`). Its
|
|
122
|
+
/// `f64` result is a dummy, so [`try_call_array_jit`] returns `Value::Null` instead of a number.
|
|
123
|
+
void_result: bool,
|
|
53
124
|
}
|
|
54
125
|
|
|
55
126
|
/// A flat numeric array handed to an array-mode JIT function: a raw `f64` slice (`ptr`, `len`).
|
|
56
|
-
/// Built by the VM wrapper
|
|
57
|
-
///
|
|
58
|
-
/// `
|
|
127
|
+
/// Built by the VM wrapper by extracting an all-numeric `Array` into a scratch `Vec<f64>` (the
|
|
128
|
+
/// wrapper only builds one when every element is a `Value::Number`, so the slice is always valid
|
|
129
|
+
/// `f64`). `ptr` is `*mut` because #187 array-param WRITES store through it into the scratch buffer;
|
|
130
|
+
/// read-only params only load, so the mut provenance is harmless there.
|
|
59
131
|
#[repr(C)]
|
|
60
132
|
#[derive(Clone, Copy)]
|
|
61
133
|
pub struct ArrayHandle {
|
|
62
|
-
pub ptr: *
|
|
134
|
+
pub ptr: *mut f64,
|
|
63
135
|
pub len: usize,
|
|
64
136
|
}
|
|
65
137
|
|
|
138
|
+
/// Recursion guard handed to a self-recursive JIT'd numeric function (#381). tish's JIT is an
|
|
139
|
+
/// additive, bail-to-interpreter tier, so rather than throw from inside JIT'd code (V8/JSC's model),
|
|
140
|
+
/// a too-deep recursion simply BAILS — exactly the deopt path array-mode already uses for an
|
|
141
|
+
/// out-of-bounds read: the function compares its stack pointer to `stack_limit` at entry, and if it
|
|
142
|
+
/// has crossed it (recursion approaching stack exhaustion) it stores `tripped = 1` and returns a
|
|
143
|
+
/// sentinel instead of recursing further. `VmClosure::call` then raises the catchable `RangeError`
|
|
144
|
+
/// through the normal pending-throw path. A single stack-pointer compare per call, sized from the
|
|
145
|
+
/// REAL remaining stack (`stacker::remaining_stack`) — never a per-call counter, so the hot recursion
|
|
146
|
+
/// (fib/spectral_norm) is untaxed. `#[repr(C)]`: `stack_limit` at offset 0, `tripped` at offset 8.
|
|
147
|
+
#[repr(C)]
|
|
148
|
+
pub struct RecurGuard {
|
|
149
|
+
pub stack_limit: usize,
|
|
150
|
+
pub tripped: u8,
|
|
151
|
+
}
|
|
152
|
+
|
|
66
153
|
/// Array-element reads inside JIT'd loops (`arr[i]`/`arr[const]`). **Default ON**; `TISH_JIT_ARRAYS=0`
|
|
67
154
|
/// disables it (escape hatch). Cached in a `OnceLock` — NEVER read the env var on a hot path (see the
|
|
68
155
|
/// frame-VM regression note in docs/perf.md). Purely ADDITIVE: only numeric-array-reduction functions
|
|
@@ -73,7 +160,60 @@ pub struct ArrayHandle {
|
|
|
73
160
|
#[cfg(not(target_arch = "wasm32"))]
|
|
74
161
|
pub fn jit_arrays_enabled() -> bool {
|
|
75
162
|
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
76
|
-
*ENABLED.get_or_init(||
|
|
163
|
+
*ENABLED.get_or_init(|| {
|
|
164
|
+
std::env::var("TISH_JIT_ARRAYS")
|
|
165
|
+
.map(|v| v != "0")
|
|
166
|
+
.unwrap_or(true)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// JIT-compiled function-LOCAL `f64` arrays via the `tish_jv_*` runtime (#189). **Default ON**;
|
|
171
|
+
/// `TISH_JIT_JV=0` disables it (escape hatch). Additive: a function whose arrays don't fit the
|
|
172
|
+
/// non-escaping-JV shape just runs interpreted, and any out-of-bounds access deopts to the interpreter.
|
|
173
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
174
|
+
pub fn jit_jv_enabled() -> bool {
|
|
175
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
176
|
+
*ENABLED.get_or_init(|| {
|
|
177
|
+
std::env::var("TISH_JIT_JV")
|
|
178
|
+
.map(|v| v != "0")
|
|
179
|
+
.unwrap_or(true)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Boolean scalar local slots in the numeric CFG JIT (#187). **Default ON**; `TISH_JIT_BOOL_SLOTS=0`
|
|
184
|
+
/// disables it. A `let flag = false` / `flag = true` / `if (flag)` local is represented as an `f64`
|
|
185
|
+
/// `0.0`/`1.0`; a syntactic pre-pass ([`classify_bool_slots`]) tags the slots, and the equality
|
|
186
|
+
/// guard in [`emit_simple_op`] + the return-of-bool bail keep it sound (a bool never reaches a
|
|
187
|
+
/// diverging `bool === number` compare or a boolean function result). Unblocks e.g. fannkuch.
|
|
188
|
+
pub fn jit_bool_slots_enabled() -> bool {
|
|
189
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
190
|
+
*ENABLED.get_or_init(|| {
|
|
191
|
+
std::env::var("TISH_JIT_BOOL_SLOTS")
|
|
192
|
+
.map(|v| v != "0")
|
|
193
|
+
.unwrap_or(true)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// The self-recursion stack guard (#381). **Default ON**; `TISH_JIT_RECUR_GUARD=0` disables it.
|
|
198
|
+
///
|
|
199
|
+
/// A JIT'd self-recursive numeric function recurses on the native stack (SelfCall lowers to a native
|
|
200
|
+
/// call), bypassing the VM's `inc_call_depth` ceiling. Without a guard, unbounded numeric recursion
|
|
201
|
+
/// overflows the native stack — an uncatchable `SIGSEGV`/abort that takes down the whole worker
|
|
202
|
+
/// process (all in-flight requests on it), the DoS hole #381 exists to close. The guard adds a
|
|
203
|
+
/// trailing `*mut RecurGuard` param whose entry compares the stack pointer to a per-thread limit and
|
|
204
|
+
/// bails (→ a catchable `RangeError`, like the interp/VM paths) before overflow. This is like Go's
|
|
205
|
+
/// per-prologue stack check — cheap safety for a server language — but the extra param must stay live
|
|
206
|
+
/// across the recursive calls, so it costs ~12-34% on hot numeric recursion (worst on trivial bodies
|
|
207
|
+
/// like `fib`). Trusted, provably-bounded hot recursion can opt out for raw speed. Cached in a
|
|
208
|
+
/// `OnceLock`; the env read is off the hot path (compile time only), never per call.
|
|
209
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
210
|
+
pub fn jit_recur_guard_enabled() -> bool {
|
|
211
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
212
|
+
*ENABLED.get_or_init(|| {
|
|
213
|
+
std::env::var("TISH_JIT_RECUR_GUARD")
|
|
214
|
+
.map(|v| v != "0")
|
|
215
|
+
.unwrap_or(true)
|
|
216
|
+
})
|
|
77
217
|
}
|
|
78
218
|
|
|
79
219
|
// SAFETY: `ptr` references immutable executable code in a module that is never
|
|
@@ -132,24 +272,140 @@ impl NumericFn {
|
|
|
132
272
|
7 => {
|
|
133
273
|
let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64) -> f64 =
|
|
134
274
|
std::mem::transmute(self.ptr);
|
|
135
|
-
f(
|
|
275
|
+
f(
|
|
276
|
+
args[0], args[1], args[2], args[3], args[4], args[5], args[6],
|
|
277
|
+
)
|
|
136
278
|
}
|
|
137
279
|
8 => {
|
|
138
280
|
let f: extern "C" fn(f64, f64, f64, f64, f64, f64, f64, f64) -> f64 =
|
|
139
281
|
std::mem::transmute(self.ptr);
|
|
140
|
-
f(
|
|
282
|
+
f(
|
|
283
|
+
args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7],
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
_ => f64::NAN,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Whether this function was compiled with a `RecurGuard` param (self-recursive). If so it MUST be
|
|
292
|
+
/// invoked via [`call_guarded`], not [`call`] (the ABI has the trailing pointer param). #381
|
|
293
|
+
#[inline]
|
|
294
|
+
pub fn recur_guarded(&self) -> bool {
|
|
295
|
+
self.recur_guarded
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Call a self-recursive (`recur_guarded`) function, passing the `RecurGuard` as a trailing pointer
|
|
299
|
+
/// param. On return the caller inspects `guard.tripped`: if set, the recursion hit the stack limit
|
|
300
|
+
/// and bailed (the numeric result is a discarded sentinel) → raise a catchable `RangeError`. #381
|
|
301
|
+
#[inline]
|
|
302
|
+
pub fn call_guarded(&self, args: &[f64], guard: *mut RecurGuard) -> f64 {
|
|
303
|
+
// Same `extern "C"` f64-register ABI as `call`, plus a trailing pointer arg for the guard.
|
|
304
|
+
unsafe {
|
|
305
|
+
match self.arity {
|
|
306
|
+
1 => {
|
|
307
|
+
let f: extern "C" fn(f64, *mut RecurGuard) -> f64 =
|
|
308
|
+
std::mem::transmute(self.ptr);
|
|
309
|
+
f(args[0], guard)
|
|
310
|
+
}
|
|
311
|
+
2 => {
|
|
312
|
+
let f: extern "C" fn(f64, f64, *mut RecurGuard) -> f64 =
|
|
313
|
+
std::mem::transmute(self.ptr);
|
|
314
|
+
f(args[0], args[1], guard)
|
|
315
|
+
}
|
|
316
|
+
3 => {
|
|
317
|
+
let f: extern "C" fn(f64, f64, f64, *mut RecurGuard) -> f64 =
|
|
318
|
+
std::mem::transmute(self.ptr);
|
|
319
|
+
f(args[0], args[1], args[2], guard)
|
|
320
|
+
}
|
|
321
|
+
4 => {
|
|
322
|
+
let f: extern "C" fn(f64, f64, f64, f64, *mut RecurGuard) -> f64 =
|
|
323
|
+
std::mem::transmute(self.ptr);
|
|
324
|
+
f(args[0], args[1], args[2], args[3], guard)
|
|
325
|
+
}
|
|
326
|
+
5 => {
|
|
327
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64, *mut RecurGuard) -> f64 =
|
|
328
|
+
std::mem::transmute(self.ptr);
|
|
329
|
+
f(args[0], args[1], args[2], args[3], args[4], guard)
|
|
330
|
+
}
|
|
331
|
+
6 => {
|
|
332
|
+
let f: extern "C" fn(f64, f64, f64, f64, f64, f64, *mut RecurGuard) -> f64 =
|
|
333
|
+
std::mem::transmute(self.ptr);
|
|
334
|
+
f(args[0], args[1], args[2], args[3], args[4], args[5], guard)
|
|
335
|
+
}
|
|
336
|
+
7 => {
|
|
337
|
+
let f: extern "C" fn(
|
|
338
|
+
f64,
|
|
339
|
+
f64,
|
|
340
|
+
f64,
|
|
341
|
+
f64,
|
|
342
|
+
f64,
|
|
343
|
+
f64,
|
|
344
|
+
f64,
|
|
345
|
+
*mut RecurGuard,
|
|
346
|
+
) -> f64 = std::mem::transmute(self.ptr);
|
|
347
|
+
f(
|
|
348
|
+
args[0], args[1], args[2], args[3], args[4], args[5], args[6], guard,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
8 => {
|
|
352
|
+
let f: extern "C" fn(
|
|
353
|
+
f64,
|
|
354
|
+
f64,
|
|
355
|
+
f64,
|
|
356
|
+
f64,
|
|
357
|
+
f64,
|
|
358
|
+
f64,
|
|
359
|
+
f64,
|
|
360
|
+
f64,
|
|
361
|
+
*mut RecurGuard,
|
|
362
|
+
) -> f64 = std::mem::transmute(self.ptr);
|
|
363
|
+
f(
|
|
364
|
+
args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7],
|
|
365
|
+
guard,
|
|
366
|
+
)
|
|
141
367
|
}
|
|
142
368
|
_ => f64::NAN,
|
|
143
369
|
}
|
|
144
370
|
}
|
|
145
371
|
}
|
|
146
372
|
|
|
373
|
+
/// Whether this function has JIT'd local arrays (#189). Such a function uses the ordinary
|
|
374
|
+
/// register-`f64` [`call`] ABI — its OOB/deopt signal is the per-thread flag (see
|
|
375
|
+
/// [`jv_reset_deopt`] / [`jv_take_deopt`]), not a trailing pointer param.
|
|
376
|
+
#[inline]
|
|
377
|
+
pub fn is_jv(&self) -> bool {
|
|
378
|
+
self.jv
|
|
379
|
+
}
|
|
380
|
+
|
|
147
381
|
/// Bit k set ⇒ param k is an array param (read via `arr[i]`). `0` ⇒ pure-numeric (use [`call`]).
|
|
148
382
|
#[inline]
|
|
149
383
|
pub fn array_param_mask(&self) -> u8 {
|
|
150
384
|
self.array_param_mask
|
|
151
385
|
}
|
|
152
386
|
|
|
387
|
+
/// #187: bit k set ⇒ array param k is WRITTEN (`arr[i] = v`) and its scratch buffer must be copied
|
|
388
|
+
/// back into the caller's `Value::Array` after a non-deopt run. Subset of [`array_param_mask`].
|
|
389
|
+
#[inline]
|
|
390
|
+
pub fn array_writable_mask(&self) -> u8 {
|
|
391
|
+
self.array_writable_mask
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// #187: bit k set ⇒ writable array param k is written with BOOL consts (`arr[i]=true`), not numbers.
|
|
395
|
+
/// The writeback re-boxes by the entry array's element type, so [`try_call_array_jit`] must bail when
|
|
396
|
+
/// a bool-written array receives a number array (or a number-written array receives a bool array).
|
|
397
|
+
#[inline]
|
|
398
|
+
pub fn array_bool_write_mask(&self) -> u8 {
|
|
399
|
+
self.array_bool_write_mask
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// #187: true when this array-mode function is VOID (returns the implicit `null`); its `f64` result
|
|
403
|
+
/// is a dummy, so [`try_call_array_jit`] returns `Value::Null` for it.
|
|
404
|
+
#[inline]
|
|
405
|
+
pub fn returns_void(&self) -> bool {
|
|
406
|
+
self.void_result
|
|
407
|
+
}
|
|
408
|
+
|
|
153
409
|
/// Call an array-mode function (`array_param_mask != 0`). `numeric` holds the f64 values for the
|
|
154
410
|
/// numeric params in numeric-param order; `arrays` the [`ArrayHandle`]s for the array params in
|
|
155
411
|
/// array-param order. Returns `(result, deopt)` — when `deopt` is true an out-of-bounds access
|
|
@@ -157,9 +413,22 @@ impl NumericFn {
|
|
|
157
413
|
/// (OOB reads return `Value::Null` in the VM, whose per-operator coercion the JIT can't replicate).
|
|
158
414
|
#[inline]
|
|
159
415
|
pub fn call_arrays(&self, numeric: &[f64], arrays: &[ArrayHandle]) -> (f64, bool) {
|
|
416
|
+
self.call_arrays_guarded(numeric, arrays, std::ptr::null_mut())
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/// #187: array-mode call, optionally passing a trailing `*mut RecurGuard` (non-null iff
|
|
420
|
+
/// `recur_guarded()` — a self-recursive array fn). On return the caller inspects `guard.tripped`:
|
|
421
|
+
/// set ⇒ the native recursion hit the stack limit and bailed (result is a sentinel) → RangeError.
|
|
422
|
+
#[inline]
|
|
423
|
+
pub fn call_arrays_guarded(
|
|
424
|
+
&self,
|
|
425
|
+
numeric: &[f64],
|
|
426
|
+
arrays: &[ArrayHandle],
|
|
427
|
+
guard: *mut RecurGuard,
|
|
428
|
+
) -> (f64, bool) {
|
|
160
429
|
let mut deopt: u8 = 0;
|
|
161
|
-
// ONE uniform signature for every array-mode fn: (numeric*, handles*, deopt*) -> f64.
|
|
162
|
-
// slices pass a dangling-but-aligned non-null ptr (the body only loads indices it uses).
|
|
430
|
+
// ONE uniform signature for every array-mode fn: (numeric*, handles*, deopt*[, guard*]) -> f64.
|
|
431
|
+
// Empty slices pass a dangling-but-aligned non-null ptr (the body only loads indices it uses).
|
|
163
432
|
let num_ptr = if numeric.is_empty() {
|
|
164
433
|
std::ptr::NonNull::<f64>::dangling().as_ptr() as *const f64
|
|
165
434
|
} else {
|
|
@@ -171,9 +440,19 @@ impl NumericFn {
|
|
|
171
440
|
arrays.as_ptr()
|
|
172
441
|
};
|
|
173
442
|
let res = unsafe {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
443
|
+
if guard.is_null() {
|
|
444
|
+
let f: extern "C" fn(*const f64, *const ArrayHandle, *mut u8) -> f64 =
|
|
445
|
+
std::mem::transmute(self.ptr);
|
|
446
|
+
f(num_ptr, arr_ptr, &mut deopt as *mut u8)
|
|
447
|
+
} else {
|
|
448
|
+
let f: extern "C" fn(
|
|
449
|
+
*const f64,
|
|
450
|
+
*const ArrayHandle,
|
|
451
|
+
*mut u8,
|
|
452
|
+
*mut RecurGuard,
|
|
453
|
+
) -> f64 = std::mem::transmute(self.ptr);
|
|
454
|
+
f(num_ptr, arr_ptr, &mut deopt as *mut u8, guard)
|
|
455
|
+
}
|
|
177
456
|
};
|
|
178
457
|
(res, deopt != 0)
|
|
179
458
|
}
|
|
@@ -190,7 +469,32 @@ struct JitGlobal {
|
|
|
190
469
|
/// reused by a different chunk, so we recompile (and overwrite) instead of returning stale native
|
|
191
470
|
/// code. `None` still caches "not JIT-eligible". See [`chunk_fingerprint`].
|
|
192
471
|
cache: HashMap<usize, (u64, Option<NumericFn>)>,
|
|
472
|
+
/// OSR loop-region cache (#190), keyed by `(chunk address, loop header ip)` with the same
|
|
473
|
+
/// fingerprint guard as `cache`. `None` caches "region not compilable" so a loop that fails the
|
|
474
|
+
/// whitelist is scanned once, not on every back-edge past the trigger threshold.
|
|
475
|
+
osr_cache: HashMap<(usize, usize), (u64, Option<LoopFn>)>,
|
|
193
476
|
counter: usize,
|
|
477
|
+
/// `FuncId` of the imported `tish_math_call` host fn (#186), declared once at module init and
|
|
478
|
+
/// re-imported into each compiled function via `declare_func_in_func`.
|
|
479
|
+
math_call_id: cranelift_module::FuncId,
|
|
480
|
+
/// `FuncId`s of the imported `tish_jv_*` vector runtime (#189).
|
|
481
|
+
jv_fns: JvFns,
|
|
482
|
+
/// #187: directly-callable numeric callees, keyed by the stable global name a top-level function is
|
|
483
|
+
/// bound to (`Chunk.global_name`). Populated when a plain register-`f64` function compiles; a
|
|
484
|
+
/// caller's `name(args)` then lowers to a native cranelift call to `id`. Sound because the compiler
|
|
485
|
+
/// only stamps `global_name` on functions it proved are never reassigned/shadowed program-wide, so
|
|
486
|
+
/// the binding can never change under a cached caller. A caller that references a name NOT yet here
|
|
487
|
+
/// (a forward reference) simply bails to the interpreter.
|
|
488
|
+
callees: HashMap<Arc<str>, CalleeEntry>,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// #187: a registered directly-callable numeric callee (register-`f64` ABI). Callers resolve against
|
|
492
|
+
/// the LIVE registry at compile time and are never cached ([`NumericFn::uses_xcall`]), so a name
|
|
493
|
+
/// re-registered by a later program simply overwrites this — a stale callee is never invoked.
|
|
494
|
+
#[derive(Clone, Copy)]
|
|
495
|
+
struct CalleeEntry {
|
|
496
|
+
id: cranelift_module::FuncId,
|
|
497
|
+
arity: u8,
|
|
194
498
|
}
|
|
195
499
|
|
|
196
500
|
// SAFETY: `JITModule` is `!Send`, but the single instance lives behind the
|
|
@@ -200,7 +504,172 @@ unsafe impl Send for JitGlobal {}
|
|
|
200
504
|
|
|
201
505
|
static JIT: OnceLock<Option<Mutex<JitGlobal>>> = OnceLock::new();
|
|
202
506
|
|
|
203
|
-
|
|
507
|
+
// #189 — a minimal C-ABI `f64` vector runtime for JIT-compiled function-LOCAL arrays that never
|
|
508
|
+
// escape the frame. A qualifying local slot (only def = empty `[]`, only uses = push / `[i]` /
|
|
509
|
+
// `[i]=v` / `.length`) is addressed by a `u64` HANDLE into a per-thread arena instead of a boxed
|
|
510
|
+
// `Value::Array`, so its index math is a bounds-checked slab lookup, not a per-op bound-method
|
|
511
|
+
// alloc — and, because the arena is a plain `Vec<Vec<f64>>` behind a `RefCell`, the host fns need
|
|
512
|
+
// NO `unsafe` (no raw-pointer deref). SOUND because such arrays never escape: no `Value` ever
|
|
513
|
+
// references a `Vec`, so an out-of-bounds access can set the deopt flag, let the (about-to-be-
|
|
514
|
+
// discarded) native run finish, and re-run the interpreter with no observable state change. Handle
|
|
515
|
+
// `0` is the null sentinel (a JV slot inits to 0); every exit frees every live slot back to the
|
|
516
|
+
// free list, so the arena is bounded by a function's peak live-array count, not total allocations.
|
|
517
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
518
|
+
struct JvArena {
|
|
519
|
+
vecs: Vec<Vec<f64>>,
|
|
520
|
+
free: Vec<usize>,
|
|
521
|
+
deopt: bool,
|
|
522
|
+
}
|
|
523
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
524
|
+
thread_local! {
|
|
525
|
+
static JV_ARENA: std::cell::RefCell<JvArena> = const {
|
|
526
|
+
std::cell::RefCell::new(JvArena { vecs: Vec::new(), free: Vec::new(), deopt: false })
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/// `handle` (1-based, `0` = null) → arena index, or `None` for the null handle.
|
|
530
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
531
|
+
#[inline]
|
|
532
|
+
fn jv_index(handle: u64) -> Option<usize> {
|
|
533
|
+
handle.checked_sub(1).map(|i| i as usize)
|
|
534
|
+
}
|
|
535
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
536
|
+
extern "C" fn tish_jv_new(cap: u64) -> u64 {
|
|
537
|
+
JV_ARENA.with(|c| {
|
|
538
|
+
let mut a = c.borrow_mut();
|
|
539
|
+
let v = Vec::with_capacity(cap as usize);
|
|
540
|
+
let idx = match a.free.pop() {
|
|
541
|
+
Some(i) => {
|
|
542
|
+
a.vecs[i] = v;
|
|
543
|
+
i
|
|
544
|
+
}
|
|
545
|
+
None => {
|
|
546
|
+
a.vecs.push(v);
|
|
547
|
+
a.vecs.len() - 1
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
idx as u64 + 1 // handle = index + 1 (0 stays reserved for null)
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
554
|
+
extern "C" fn tish_jv_push(handle: u64, x: f64) {
|
|
555
|
+
if let Some(idx) = jv_index(handle) {
|
|
556
|
+
JV_ARENA.with(|c| {
|
|
557
|
+
if let Some(v) = c.borrow_mut().vecs.get_mut(idx) {
|
|
558
|
+
v.push(x);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
564
|
+
extern "C" fn tish_jv_get(handle: u64, i: u64) -> f64 {
|
|
565
|
+
JV_ARENA.with(|c| {
|
|
566
|
+
let mut a = c.borrow_mut();
|
|
567
|
+
match jv_index(handle)
|
|
568
|
+
.and_then(|idx| a.vecs.get(idx))
|
|
569
|
+
.and_then(|v| v.get(i as usize))
|
|
570
|
+
{
|
|
571
|
+
Some(&x) => x,
|
|
572
|
+
None => {
|
|
573
|
+
a.deopt = true; // OOB (or null): caller discards the result + re-interprets
|
|
574
|
+
f64::NAN
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
580
|
+
extern "C" fn tish_jv_set(handle: u64, i: u64, x: f64) {
|
|
581
|
+
JV_ARENA.with(|c| {
|
|
582
|
+
let mut a = c.borrow_mut();
|
|
583
|
+
match jv_index(handle)
|
|
584
|
+
.and_then(|idx| a.vecs.get_mut(idx))
|
|
585
|
+
.and_then(|v| v.get_mut(i as usize))
|
|
586
|
+
{
|
|
587
|
+
Some(p) => *p = x,
|
|
588
|
+
None => a.deopt = true, // OOB (or null) → deopt (no store performed)
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
}
|
|
592
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
593
|
+
extern "C" fn tish_jv_len(handle: u64) -> u64 {
|
|
594
|
+
JV_ARENA.with(|c| {
|
|
595
|
+
jv_index(handle)
|
|
596
|
+
.and_then(|idx| c.borrow().vecs.get(idx).map(|v| v.len() as u64))
|
|
597
|
+
.unwrap_or(0)
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
601
|
+
extern "C" fn tish_jv_free(handle: u64) {
|
|
602
|
+
if let Some(idx) = jv_index(handle) {
|
|
603
|
+
JV_ARENA.with(|c| {
|
|
604
|
+
let mut a = c.borrow_mut();
|
|
605
|
+
if let Some(v) = a.vecs.get_mut(idx) {
|
|
606
|
+
v.clear(); // keep capacity; the slot is returned to the free list for reuse
|
|
607
|
+
a.free.push(idx);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/// Signal a deopt from JIT'd code that can't produce an `f64` result (a non-numeric `return`, e.g. the
|
|
613
|
+
/// dead `return null` epilogue after a `while (true)`); the wrapper then re-interprets. #189
|
|
614
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
615
|
+
extern "C" fn tish_jv_deopt() {
|
|
616
|
+
JV_ARENA.with(|c| c.borrow_mut().deopt = true);
|
|
617
|
+
}
|
|
618
|
+
/// Clear the per-thread deopt flag before entering a JV function. #189
|
|
619
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
620
|
+
pub(crate) fn jv_reset_deopt() {
|
|
621
|
+
JV_ARENA.with(|c| c.borrow_mut().deopt = false);
|
|
622
|
+
}
|
|
623
|
+
/// Read and clear the deopt flag after a JV function returns; `true` ⇒ discard its result + interpret.
|
|
624
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
625
|
+
pub(crate) fn jv_take_deopt() -> bool {
|
|
626
|
+
JV_ARENA.with(|c| {
|
|
627
|
+
let mut a = c.borrow_mut();
|
|
628
|
+
std::mem::replace(&mut a.deopt, false)
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// Imported `FuncId`s of the `tish_jv_*` vector runtime (#189), declared once at module init.
|
|
633
|
+
#[derive(Clone, Copy)]
|
|
634
|
+
struct JvFns {
|
|
635
|
+
new: cranelift_module::FuncId,
|
|
636
|
+
push: cranelift_module::FuncId,
|
|
637
|
+
get: cranelift_module::FuncId,
|
|
638
|
+
set: cranelift_module::FuncId,
|
|
639
|
+
len: cranelift_module::FuncId,
|
|
640
|
+
free: cranelift_module::FuncId,
|
|
641
|
+
deopt: cranelift_module::FuncId,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/// Per-function-build handles for JV local arrays (#189): the `tish_jv_*` `FuncRef`s imported into
|
|
645
|
+
/// *this* function, plus the set of slots classified as JV arrays. Passed to [`build_body_cfg`].
|
|
646
|
+
/// The deopt flag lives in the per-thread arena (set by `get`/`set`/`deopt`), so — unlike array
|
|
647
|
+
/// mode — the JV function ABI carries no trailing deopt pointer.
|
|
648
|
+
struct JvCtx<'a> {
|
|
649
|
+
new: cranelift::codegen::ir::FuncRef,
|
|
650
|
+
push: cranelift::codegen::ir::FuncRef,
|
|
651
|
+
get: cranelift::codegen::ir::FuncRef,
|
|
652
|
+
set: cranelift::codegen::ir::FuncRef,
|
|
653
|
+
len: cranelift::codegen::ir::FuncRef,
|
|
654
|
+
free: cranelift::codegen::ir::FuncRef,
|
|
655
|
+
deopt: cranelift::codegen::ir::FuncRef,
|
|
656
|
+
slots: &'a std::collections::HashSet<usize>,
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// Host entry point the JIT calls for `Math.<fn>` intrinsics it doesn't lower to a native op (#186):
|
|
660
|
+
/// sin/cos/tan/exp/log/round/sign/… The single source of truth is [`MathUnaryFn::apply`], so JIT ≡
|
|
661
|
+
/// VM ≡ interpreter bit-for-bit. `extern "C"` and registered as a JIT symbol; the id is the operand.
|
|
662
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
663
|
+
extern "C" fn tish_math_call(id: i32, x: f64) -> f64 {
|
|
664
|
+
match tishlang_bytecode::MathUnaryFn::from_u16(id as u16) {
|
|
665
|
+
Some(m) => m.apply(x),
|
|
666
|
+
None => f64::NAN,
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/// Build the JIT module and declare the imported host functions (`tish_math_call` #186, the
|
|
671
|
+
/// `tish_jv_*` vector runtime #189), returning the module + their `FuncId`s.
|
|
672
|
+
fn new_module() -> Option<(JITModule, cranelift_module::FuncId, JvFns)> {
|
|
204
673
|
let mut flag_builder = settings::builder();
|
|
205
674
|
// JIT code is loaded at a fixed address; no PIC / colocated libcalls needed.
|
|
206
675
|
flag_builder.set("use_colocated_libcalls", "false").ok()?;
|
|
@@ -210,17 +679,82 @@ fn new_module() -> Option<JITModule> {
|
|
|
210
679
|
let isa = isa_builder
|
|
211
680
|
.finish(settings::Flags::new(flag_builder))
|
|
212
681
|
.ok()?;
|
|
213
|
-
let builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
|
|
214
|
-
|
|
682
|
+
let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
|
|
683
|
+
builder.symbol("tish_math_call", tish_math_call as *const u8);
|
|
684
|
+
builder.symbol("tish_jv_new", tish_jv_new as *const u8);
|
|
685
|
+
builder.symbol("tish_jv_push", tish_jv_push as *const u8);
|
|
686
|
+
builder.symbol("tish_jv_get", tish_jv_get as *const u8);
|
|
687
|
+
builder.symbol("tish_jv_set", tish_jv_set as *const u8);
|
|
688
|
+
builder.symbol("tish_jv_len", tish_jv_len as *const u8);
|
|
689
|
+
builder.symbol("tish_jv_free", tish_jv_free as *const u8);
|
|
690
|
+
builder.symbol("tish_jv_deopt", tish_jv_deopt as *const u8);
|
|
691
|
+
let mut module = JITModule::new(builder);
|
|
692
|
+
|
|
693
|
+
// `tish_math_call(i32 fn-id, f64 x) -> f64`.
|
|
694
|
+
let mut msig = module.make_signature();
|
|
695
|
+
msig.params.push(AbiParam::new(types::I32));
|
|
696
|
+
msig.params.push(AbiParam::new(types::F64));
|
|
697
|
+
msig.returns.push(AbiParam::new(types::F64));
|
|
698
|
+
let math_id = module
|
|
699
|
+
.declare_function("tish_math_call", Linkage::Import, &msig)
|
|
700
|
+
.ok()?;
|
|
701
|
+
|
|
702
|
+
// Helper to declare a `tish_jv_*` import from param/return abi lists.
|
|
703
|
+
let mut declare = |name: &str, params: &[AbiParam], rets: &[AbiParam]| {
|
|
704
|
+
let mut s = module.make_signature();
|
|
705
|
+
s.params.extend_from_slice(params);
|
|
706
|
+
s.returns.extend_from_slice(rets);
|
|
707
|
+
module.declare_function(name, Linkage::Import, &s).ok()
|
|
708
|
+
};
|
|
709
|
+
// A JV array is addressed by an `i64` HANDLE (a per-thread arena index; `0` = null), not a raw
|
|
710
|
+
// pointer. `get`/`set` need no deopt-pointer arg — OOB sets a per-thread flag the wrapper reads.
|
|
711
|
+
let jv = JvFns {
|
|
712
|
+
new: declare(
|
|
713
|
+
"tish_jv_new",
|
|
714
|
+
&[AbiParam::new(types::I64)],
|
|
715
|
+
&[AbiParam::new(types::I64)],
|
|
716
|
+
)?,
|
|
717
|
+
push: declare(
|
|
718
|
+
"tish_jv_push",
|
|
719
|
+
&[AbiParam::new(types::I64), AbiParam::new(types::F64)],
|
|
720
|
+
&[],
|
|
721
|
+
)?,
|
|
722
|
+
get: declare(
|
|
723
|
+
"tish_jv_get",
|
|
724
|
+
&[AbiParam::new(types::I64), AbiParam::new(types::I64)],
|
|
725
|
+
&[AbiParam::new(types::F64)],
|
|
726
|
+
)?,
|
|
727
|
+
set: declare(
|
|
728
|
+
"tish_jv_set",
|
|
729
|
+
&[
|
|
730
|
+
AbiParam::new(types::I64),
|
|
731
|
+
AbiParam::new(types::I64),
|
|
732
|
+
AbiParam::new(types::F64),
|
|
733
|
+
],
|
|
734
|
+
&[],
|
|
735
|
+
)?,
|
|
736
|
+
len: declare(
|
|
737
|
+
"tish_jv_len",
|
|
738
|
+
&[AbiParam::new(types::I64)],
|
|
739
|
+
&[AbiParam::new(types::I64)],
|
|
740
|
+
)?,
|
|
741
|
+
free: declare("tish_jv_free", &[AbiParam::new(types::I64)], &[])?,
|
|
742
|
+
deopt: declare("tish_jv_deopt", &[], &[])?,
|
|
743
|
+
};
|
|
744
|
+
Some((module, math_id, jv))
|
|
215
745
|
}
|
|
216
746
|
|
|
217
747
|
fn jit() -> Option<&'static Mutex<JitGlobal>> {
|
|
218
748
|
JIT.get_or_init(|| {
|
|
219
|
-
new_module().map(|module| {
|
|
749
|
+
new_module().map(|(module, math_call_id, jv_fns)| {
|
|
220
750
|
Mutex::new(JitGlobal {
|
|
221
751
|
module,
|
|
222
752
|
cache: HashMap::new(),
|
|
753
|
+
osr_cache: HashMap::new(),
|
|
223
754
|
counter: 0,
|
|
755
|
+
math_call_id,
|
|
756
|
+
jv_fns,
|
|
757
|
+
callees: HashMap::new(),
|
|
224
758
|
})
|
|
225
759
|
})
|
|
226
760
|
})
|
|
@@ -297,6 +831,19 @@ fn chunk_fingerprint(chunk: &Chunk) -> u64 {
|
|
|
297
831
|
|
|
298
832
|
/// Get (or compile, then cache) the native numeric function for `chunk`.
|
|
299
833
|
/// Returns `None` if the chunk isn't a straight-line numeric function.
|
|
834
|
+
/// #187: clear the directly-callable-callee registry at the start of each top-level program run, so a
|
|
835
|
+
/// long-lived process (REPL / embedder) never resolves a callee registered by a PRIOR program (a name
|
|
836
|
+
/// re-registered non-numerically would otherwise leave a stale native entry). Cross-callers aren't
|
|
837
|
+
/// cached, so they always re-resolve against the freshly-populated registry.
|
|
838
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
839
|
+
pub fn reset_callees() {
|
|
840
|
+
if let Some(lock) = jit() {
|
|
841
|
+
if let Ok(mut g) = lock.lock() {
|
|
842
|
+
g.callees.clear();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
300
847
|
pub fn try_compile_numeric(chunk: &Chunk) -> Option<NumericFn> {
|
|
301
848
|
if !chunk.slot_based
|
|
302
849
|
|| chunk.rest_param_index != NO_REST_PARAM
|
|
@@ -317,10 +864,461 @@ pub fn try_compile_numeric(chunk: &Chunk) -> Option<NumericFn> {
|
|
|
317
864
|
}
|
|
318
865
|
}
|
|
319
866
|
let result = compile_chunk(&mut g, chunk);
|
|
320
|
-
|
|
867
|
+
// #187: a function that embeds a native call to a registered callee is NOT cached — its callee
|
|
868
|
+
// address could go stale across programs in a long-lived process. It recompiles per closure
|
|
869
|
+
// creation (once, in practice), resolving against the live registry. Everything else caches.
|
|
870
|
+
if !result.is_some_and(|nf| nf.uses_xcall) {
|
|
871
|
+
g.cache.insert(key, (fp, result));
|
|
872
|
+
}
|
|
873
|
+
result
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/// Compile the hot loop region `[header_ip, region_end)` of `chunk` to native code (#190 OSR), or
|
|
877
|
+
/// `None` if it is not a pure-numeric slot loop. Cached per `(chunk, header_ip)` with a fingerprint
|
|
878
|
+
/// guard (negative results included, so a non-compilable loop is scanned once). Called from the frame
|
|
879
|
+
/// VM's `JumpBack` handler once a loop's back-edge counter crosses the trigger threshold.
|
|
880
|
+
pub fn try_compile_loop(chunk: &Chunk, header_ip: usize, region_end: usize) -> Option<LoopFn> {
|
|
881
|
+
let key = (chunk as *const Chunk as usize, header_ip);
|
|
882
|
+
let fp = chunk_fingerprint(chunk);
|
|
883
|
+
let lock = jit()?;
|
|
884
|
+
let mut g = lock.lock().ok()?;
|
|
885
|
+
if let Some((cached_fp, cached)) = g.osr_cache.get(&key) {
|
|
886
|
+
if *cached_fp == fp {
|
|
887
|
+
return cached.clone();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
let result = compile_loop_region(&mut g, chunk, header_ip, region_end);
|
|
891
|
+
g.osr_cache.insert(key, (fp, result.clone()));
|
|
321
892
|
result
|
|
322
893
|
}
|
|
323
894
|
|
|
895
|
+
/// Lower one hot loop region to a `LoopFn`. The region must be pure numeric slot math: the same
|
|
896
|
+
/// opcode whitelist as [`build_body_cfg`] minus everything that touches a non-slot value (calls,
|
|
897
|
+
/// member/index, arrays, objects, `LoadVar`, `Return`). Reuses [`emit_simple_op`] for the arithmetic
|
|
898
|
+
/// so the region is bit-for-bit identical to the interpreter's `eval_binop`. Live-ins are loaded from
|
|
899
|
+
/// the `slots` pointer at entry; each loop-exit target stores the live set back and returns its id.
|
|
900
|
+
fn compile_loop_region(
|
|
901
|
+
g: &mut JitGlobal,
|
|
902
|
+
chunk: &Chunk,
|
|
903
|
+
header_ip: usize,
|
|
904
|
+
region_end: usize,
|
|
905
|
+
) -> Option<LoopFn> {
|
|
906
|
+
let code = &chunk.code;
|
|
907
|
+
let num_slots = chunk.num_slots as usize;
|
|
908
|
+
if num_slots == 0 || num_slots > 256 || header_ip >= region_end || region_end > code.len() {
|
|
909
|
+
return None;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 1. Scan the region: validate the whitelist (op_size = None ⇒ bail), collect in-region block
|
|
913
|
+
// leaders, the live slot set, and the EXIT targets (jump targets outside the region). A
|
|
914
|
+
// JumpBack must stay inside the region (its own loop, possibly nested); one leaving the region
|
|
915
|
+
// is not a structured single-region loop → bail.
|
|
916
|
+
let mut leaders: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
|
|
917
|
+
leaders.insert(header_ip);
|
|
918
|
+
let mut used: std::collections::BTreeSet<u16> = std::collections::BTreeSet::new();
|
|
919
|
+
let mut exit_targets: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
|
|
920
|
+
let mut ip = header_ip;
|
|
921
|
+
while ip < region_end {
|
|
922
|
+
let op = Opcode::from_u8(*code.get(ip)?)?;
|
|
923
|
+
match op {
|
|
924
|
+
// Pure slot / stack / arithmetic / structured control flow — the region vocabulary.
|
|
925
|
+
Opcode::Nop
|
|
926
|
+
| Opcode::Pop
|
|
927
|
+
| Opcode::Dup
|
|
928
|
+
| Opcode::EnterBlock
|
|
929
|
+
| Opcode::ExitBlock
|
|
930
|
+
| Opcode::LoopVarsEnd
|
|
931
|
+
| Opcode::LoopVarsBegin
|
|
932
|
+
| Opcode::UnaryOp
|
|
933
|
+
| Opcode::MathUnary => {} // #186 — `Math.<fn>(x)`: 1 f64 in, 1 f64 out (like UnaryOp)
|
|
934
|
+
Opcode::LoadLocal | Opcode::StoreLocal => {
|
|
935
|
+
used.insert(peek_u16(code, ip + 1)?);
|
|
936
|
+
}
|
|
937
|
+
Opcode::LoadConst => match chunk.constants.get(peek_u16(code, ip + 1)? as usize) {
|
|
938
|
+
Some(Constant::Number(_)) | Some(Constant::Bool(_)) => {}
|
|
939
|
+
_ => return None, // a String/Null/Closure const is not numeric slot math
|
|
940
|
+
},
|
|
941
|
+
Opcode::BinOp => {
|
|
942
|
+
// Reject non-numeric binops up front (Pow/In/logical) so the scan and the emit agree.
|
|
943
|
+
match peek_u16(code, ip + 1)
|
|
944
|
+
.map(|r| r as u8)
|
|
945
|
+
.and_then(u8_to_binop)?
|
|
946
|
+
{
|
|
947
|
+
BinOp::And | BinOp::Or | BinOp::Pow | BinOp::In => return None,
|
|
948
|
+
_ => {}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
Opcode::Jump => {
|
|
952
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
953
|
+
let t = ((ip + 3) as isize + off).max(0) as usize;
|
|
954
|
+
if t < header_ip || t >= region_end {
|
|
955
|
+
exit_targets.insert(t);
|
|
956
|
+
} else {
|
|
957
|
+
leaders.insert(t);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
Opcode::JumpIfFalse => {
|
|
961
|
+
let off = peek_u16(code, ip + 1)? as i16 as isize;
|
|
962
|
+
let t = ((ip + 3) as isize + off).max(0) as usize;
|
|
963
|
+
if t < header_ip || t >= region_end {
|
|
964
|
+
exit_targets.insert(t);
|
|
965
|
+
} else {
|
|
966
|
+
leaders.insert(t);
|
|
967
|
+
}
|
|
968
|
+
leaders.insert(ip + 3); // fall-through
|
|
969
|
+
}
|
|
970
|
+
Opcode::JumpBack => {
|
|
971
|
+
let dist = peek_u16(code, ip + 1)? as usize;
|
|
972
|
+
let t = (ip + 3).checked_sub(dist)?;
|
|
973
|
+
if t < header_ip || t >= region_end {
|
|
974
|
+
return None; // back-edge leaving the region — not a single structured region
|
|
975
|
+
}
|
|
976
|
+
leaders.insert(t);
|
|
977
|
+
}
|
|
978
|
+
// Everything else (Call, SelfCall, LoadVar, GetIndex, member/object/array, Return, …)
|
|
979
|
+
// reads or writes a non-slot Value the f64 buffer can't carry → not OSR-eligible.
|
|
980
|
+
_ => return None,
|
|
981
|
+
}
|
|
982
|
+
ip += op_size(op)?;
|
|
983
|
+
}
|
|
984
|
+
// An exit-less region would compile to an uninterruptible native infinite loop — never OSR it.
|
|
985
|
+
if exit_targets.is_empty() {
|
|
986
|
+
return None;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// buffer position of each live slot (both the emitted loads/stores and the VM copy use this).
|
|
990
|
+
let used_slots: Vec<u16> = used.iter().copied().collect();
|
|
991
|
+
let buf_pos: HashMap<u16, usize> = used_slots
|
|
992
|
+
.iter()
|
|
993
|
+
.enumerate()
|
|
994
|
+
.map(|(p, &s)| (s, p))
|
|
995
|
+
.collect();
|
|
996
|
+
let exits: Vec<usize> = exit_targets.iter().copied().collect();
|
|
997
|
+
let exit_id: HashMap<usize, usize> = exits.iter().enumerate().map(|(i, &t)| (t, i)).collect();
|
|
998
|
+
|
|
999
|
+
// 2. Build the region function. Signature: (slots: i64 ptr, deopt: i64 ptr) -> i32 exit id.
|
|
1000
|
+
let ptr_ty = g.module.target_config().pointer_type();
|
|
1001
|
+
let mut sig = g.module.make_signature();
|
|
1002
|
+
sig.params.push(AbiParam::new(ptr_ty)); // slots buffer
|
|
1003
|
+
sig.params.push(AbiParam::new(ptr_ty)); // deopt flag (reserved)
|
|
1004
|
+
sig.returns.push(AbiParam::new(types::I32));
|
|
1005
|
+
|
|
1006
|
+
let name = format!("tish_osr_{}", g.counter);
|
|
1007
|
+
g.counter += 1;
|
|
1008
|
+
let id = g
|
|
1009
|
+
.module
|
|
1010
|
+
.declare_function(&name, Linkage::Export, &sig)
|
|
1011
|
+
.ok()?;
|
|
1012
|
+
|
|
1013
|
+
let mut ctx = g.module.make_context();
|
|
1014
|
+
ctx.func.signature = sig.clone();
|
|
1015
|
+
let math_fref = g.module.declare_func_in_func(g.math_call_id, &mut ctx.func);
|
|
1016
|
+
let mut fbctx = FunctionBuilderContext::new();
|
|
1017
|
+
let built = build_loop_region_body(
|
|
1018
|
+
&mut ctx.func,
|
|
1019
|
+
&mut fbctx,
|
|
1020
|
+
chunk,
|
|
1021
|
+
header_ip,
|
|
1022
|
+
region_end,
|
|
1023
|
+
&leaders,
|
|
1024
|
+
&used_slots,
|
|
1025
|
+
&buf_pos,
|
|
1026
|
+
&exit_id,
|
|
1027
|
+
math_fref,
|
|
1028
|
+
);
|
|
1029
|
+
if !built {
|
|
1030
|
+
g.module.clear_context(&mut ctx);
|
|
1031
|
+
return None;
|
|
1032
|
+
}
|
|
1033
|
+
if g.module.define_function(id, &mut ctx).is_err() {
|
|
1034
|
+
g.module.clear_context(&mut ctx);
|
|
1035
|
+
return None;
|
|
1036
|
+
}
|
|
1037
|
+
g.module.clear_context(&mut ctx);
|
|
1038
|
+
if g.module.finalize_definitions().is_err() {
|
|
1039
|
+
return None;
|
|
1040
|
+
}
|
|
1041
|
+
let fptr = g.module.get_finalized_function(id);
|
|
1042
|
+
Some(LoopFn {
|
|
1043
|
+
ptr: fptr as usize,
|
|
1044
|
+
used_slots,
|
|
1045
|
+
exits,
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/// Emit the CLIF body of a loop region (#190). `true` on success; `false` on any shape the emitter
|
|
1050
|
+
/// can't lower (the caller then negative-caches the region). Structure mirrors [`build_body_cfg`]'s
|
|
1051
|
+
/// translate loop, but: slots load from / store to the `slots` pointer instead of register params,
|
|
1052
|
+
/// and a jump/branch to an EXIT target lands in an exit block that writes the live set back and
|
|
1053
|
+
/// returns the exit id. No self-call, no arrays, no `Return` (all rejected in the scan).
|
|
1054
|
+
#[allow(clippy::too_many_arguments)]
|
|
1055
|
+
fn build_loop_region_body(
|
|
1056
|
+
func: &mut cranelift::codegen::ir::Function,
|
|
1057
|
+
fbctx: &mut FunctionBuilderContext,
|
|
1058
|
+
chunk: &Chunk,
|
|
1059
|
+
header_ip: usize,
|
|
1060
|
+
region_end: usize,
|
|
1061
|
+
leaders: &std::collections::BTreeSet<usize>,
|
|
1062
|
+
used_slots: &[u16],
|
|
1063
|
+
buf_pos: &HashMap<u16, usize>,
|
|
1064
|
+
exit_id: &HashMap<usize, usize>,
|
|
1065
|
+
math_fref: cranelift::codegen::ir::FuncRef,
|
|
1066
|
+
) -> bool {
|
|
1067
|
+
let code = &chunk.code;
|
|
1068
|
+
let num_slots = chunk.num_slots as usize;
|
|
1069
|
+
let mut bcx = FunctionBuilder::new(func, fbctx);
|
|
1070
|
+
|
|
1071
|
+
let blocks: HashMap<usize, Block> = leaders.iter().map(|&o| (o, bcx.create_block())).collect();
|
|
1072
|
+
let exit_blocks: HashMap<usize, Block> =
|
|
1073
|
+
exit_id.keys().map(|&t| (t, bcx.create_block())).collect();
|
|
1074
|
+
|
|
1075
|
+
// A jump target is either an in-region leader or a loop-exit target (the scan proved it is one).
|
|
1076
|
+
let target_block =
|
|
1077
|
+
|t: usize| -> Option<Block> { blocks.get(&t).or_else(|| exit_blocks.get(&t)).copied() };
|
|
1078
|
+
|
|
1079
|
+
// Entry: load the live-in slots from the buffer, then jump into the loop header.
|
|
1080
|
+
let entry = bcx.create_block();
|
|
1081
|
+
bcx.append_block_params_for_function_params(entry);
|
|
1082
|
+
bcx.switch_to_block(entry);
|
|
1083
|
+
let params: Vec<ClifValue> = bcx.block_params(entry).to_vec();
|
|
1084
|
+
let slots_ptr = params[0]; // params[1] = deopt flag, reserved (v1 never writes it)
|
|
1085
|
+
let vars: Vec<Variable> = (0..num_slots)
|
|
1086
|
+
.map(|_| bcx.declare_var(types::F64))
|
|
1087
|
+
.collect();
|
|
1088
|
+
for (slot, &var) in vars.iter().enumerate() {
|
|
1089
|
+
// Live slots load from their buffer position; slots the region never touches init to 0 (dead).
|
|
1090
|
+
let init = if let Some(&p) = buf_pos.get(&(slot as u16)) {
|
|
1091
|
+
bcx.ins()
|
|
1092
|
+
.load(types::F64, MemFlags::new(), slots_ptr, (p * 8) as i32)
|
|
1093
|
+
} else {
|
|
1094
|
+
bcx.ins().f64const(0.0)
|
|
1095
|
+
};
|
|
1096
|
+
bcx.def_var(var, init);
|
|
1097
|
+
}
|
|
1098
|
+
let header_block = match blocks.get(&header_ip) {
|
|
1099
|
+
Some(&b) => b,
|
|
1100
|
+
None => return false,
|
|
1101
|
+
};
|
|
1102
|
+
bcx.ins().jump(header_block, &[]);
|
|
1103
|
+
|
|
1104
|
+
// Translate the region. Operand stack is empty at every block boundary (statement-level flow).
|
|
1105
|
+
let mut stack: Vec<(ClifValue, bool)> = Vec::new();
|
|
1106
|
+
bcx.switch_to_block(header_block);
|
|
1107
|
+
let mut cur = header_block;
|
|
1108
|
+
let mut terminated = false;
|
|
1109
|
+
let mut ip = header_ip;
|
|
1110
|
+
while ip < region_end {
|
|
1111
|
+
if let Some(&blk) = blocks.get(&ip) {
|
|
1112
|
+
if blk != cur {
|
|
1113
|
+
if !terminated {
|
|
1114
|
+
if !stack.is_empty() {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
bcx.ins().jump(blk, &[]);
|
|
1118
|
+
}
|
|
1119
|
+
bcx.switch_to_block(blk);
|
|
1120
|
+
cur = blk;
|
|
1121
|
+
terminated = false;
|
|
1122
|
+
stack.clear();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
let op = match Opcode::from_u8(code[ip]).zip(op_size_at(code, ip)) {
|
|
1126
|
+
Some((o, _)) => o,
|
|
1127
|
+
None => return false,
|
|
1128
|
+
};
|
|
1129
|
+
if terminated {
|
|
1130
|
+
ip += match op_size(op) {
|
|
1131
|
+
Some(s) => s,
|
|
1132
|
+
None => return false,
|
|
1133
|
+
};
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
match op {
|
|
1137
|
+
Opcode::LoadLocal => {
|
|
1138
|
+
let slot = match peek_u16(code, ip + 1) {
|
|
1139
|
+
Some(s) => s as usize,
|
|
1140
|
+
None => return false,
|
|
1141
|
+
};
|
|
1142
|
+
let v = match vars.get(slot) {
|
|
1143
|
+
Some(v) => *v,
|
|
1144
|
+
None => return false,
|
|
1145
|
+
};
|
|
1146
|
+
stack.push((bcx.use_var(v), false));
|
|
1147
|
+
ip += 3;
|
|
1148
|
+
}
|
|
1149
|
+
Opcode::StoreLocal => {
|
|
1150
|
+
let slot = match peek_u16(code, ip + 1) {
|
|
1151
|
+
Some(s) => s as usize,
|
|
1152
|
+
None => return false,
|
|
1153
|
+
};
|
|
1154
|
+
let (val, is_bool) = match stack.pop() {
|
|
1155
|
+
Some(x) => x,
|
|
1156
|
+
None => return false,
|
|
1157
|
+
};
|
|
1158
|
+
if is_bool {
|
|
1159
|
+
return false; // no boolean slots (keeps the number/bool distinction clean)
|
|
1160
|
+
}
|
|
1161
|
+
let v = match vars.get(slot) {
|
|
1162
|
+
Some(v) => *v,
|
|
1163
|
+
None => return false,
|
|
1164
|
+
};
|
|
1165
|
+
bcx.def_var(v, val);
|
|
1166
|
+
ip += 3;
|
|
1167
|
+
}
|
|
1168
|
+
Opcode::Pop => {
|
|
1169
|
+
if stack.pop().is_none() {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
ip += 1;
|
|
1173
|
+
}
|
|
1174
|
+
Opcode::Dup => {
|
|
1175
|
+
let top = match stack.last() {
|
|
1176
|
+
Some(x) => *x,
|
|
1177
|
+
None => return false,
|
|
1178
|
+
};
|
|
1179
|
+
stack.push(top);
|
|
1180
|
+
ip += 1;
|
|
1181
|
+
}
|
|
1182
|
+
Opcode::Nop | Opcode::EnterBlock | Opcode::ExitBlock | Opcode::LoopVarsEnd => ip += 1,
|
|
1183
|
+
Opcode::LoopVarsBegin => ip += 3,
|
|
1184
|
+
Opcode::Jump => {
|
|
1185
|
+
let off = match peek_u16(code, ip + 1) {
|
|
1186
|
+
Some(o) => o as i16 as isize,
|
|
1187
|
+
None => return false,
|
|
1188
|
+
};
|
|
1189
|
+
let t = ((ip + 3) as isize + off).max(0) as usize;
|
|
1190
|
+
let blk = match target_block(t) {
|
|
1191
|
+
Some(b) => b,
|
|
1192
|
+
None => return false,
|
|
1193
|
+
};
|
|
1194
|
+
if !stack.is_empty() {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
bcx.ins().jump(blk, &[]);
|
|
1198
|
+
terminated = true;
|
|
1199
|
+
ip += 3;
|
|
1200
|
+
}
|
|
1201
|
+
Opcode::JumpBack => {
|
|
1202
|
+
let dist = match peek_u16(code, ip + 1) {
|
|
1203
|
+
Some(d) => d as usize,
|
|
1204
|
+
None => return false,
|
|
1205
|
+
};
|
|
1206
|
+
let t = match (ip + 3).checked_sub(dist) {
|
|
1207
|
+
Some(t) => t,
|
|
1208
|
+
None => return false,
|
|
1209
|
+
};
|
|
1210
|
+
let blk = match blocks.get(&t) {
|
|
1211
|
+
Some(&b) => b,
|
|
1212
|
+
None => return false,
|
|
1213
|
+
};
|
|
1214
|
+
if !stack.is_empty() {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
bcx.ins().jump(blk, &[]);
|
|
1218
|
+
terminated = true;
|
|
1219
|
+
ip += 3;
|
|
1220
|
+
}
|
|
1221
|
+
Opcode::JumpIfFalse => {
|
|
1222
|
+
let off = match peek_u16(code, ip + 1) {
|
|
1223
|
+
Some(o) => o as i16 as isize,
|
|
1224
|
+
None => return false,
|
|
1225
|
+
};
|
|
1226
|
+
let (cond, _) = match stack.pop() {
|
|
1227
|
+
Some(x) => x,
|
|
1228
|
+
None => return false,
|
|
1229
|
+
};
|
|
1230
|
+
if !stack.is_empty() {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
let falsy = falsy_flag(&mut bcx, cond);
|
|
1234
|
+
let t = ((ip + 3) as isize + off).max(0) as usize;
|
|
1235
|
+
let target = match target_block(t) {
|
|
1236
|
+
Some(b) => b,
|
|
1237
|
+
None => return false,
|
|
1238
|
+
};
|
|
1239
|
+
let fallthrough = match blocks.get(&(ip + 3)) {
|
|
1240
|
+
Some(&b) => b,
|
|
1241
|
+
None => return false,
|
|
1242
|
+
};
|
|
1243
|
+
bcx.ins().brif(falsy, target, &[], fallthrough, &[]);
|
|
1244
|
+
terminated = true;
|
|
1245
|
+
ip += 3;
|
|
1246
|
+
}
|
|
1247
|
+
Opcode::MathUnary => {
|
|
1248
|
+
// #186 — `Math.<fn>(x)`: pop the arg, emit the native op / host call, push the result.
|
|
1249
|
+
let id = match peek_u16(code, ip + 1) {
|
|
1250
|
+
Some(v) => v,
|
|
1251
|
+
None => return false,
|
|
1252
|
+
};
|
|
1253
|
+
let mfn = match MathUnaryFn::from_u16(id) {
|
|
1254
|
+
Some(m) => m,
|
|
1255
|
+
None => return false,
|
|
1256
|
+
};
|
|
1257
|
+
let (x, _) = match stack.pop() {
|
|
1258
|
+
Some(v) => v,
|
|
1259
|
+
None => return false,
|
|
1260
|
+
};
|
|
1261
|
+
let r = emit_math_unary(&mut bcx, math_fref, mfn, x);
|
|
1262
|
+
stack.push((r, false));
|
|
1263
|
+
ip += 3;
|
|
1264
|
+
}
|
|
1265
|
+
_ => match emit_simple_op(&mut bcx, chunk, code, &mut ip, &mut stack, &[], 0) {
|
|
1266
|
+
SimpleOp::Handled(_) => {}
|
|
1267
|
+
_ => return false, // LoadConst/BinOp/UnaryOp handled; anything else → keep interpreting
|
|
1268
|
+
},
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if !terminated {
|
|
1272
|
+
return false; // the region must end on its back-edge (a terminator)
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Exit blocks: flush the live set back through the slots pointer and return the exit id.
|
|
1276
|
+
for (&t, &blk) in &exit_blocks {
|
|
1277
|
+
bcx.switch_to_block(blk);
|
|
1278
|
+
for (p, &slot) in used_slots.iter().enumerate() {
|
|
1279
|
+
let v = bcx.use_var(vars[slot as usize]);
|
|
1280
|
+
bcx.ins()
|
|
1281
|
+
.store(MemFlags::new(), v, slots_ptr, (p * 8) as i32);
|
|
1282
|
+
}
|
|
1283
|
+
let id = bcx.ins().iconst(types::I32, exit_id[&t] as i64);
|
|
1284
|
+
bcx.ins().return_(&[id]);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
bcx.seal_all_blocks();
|
|
1288
|
+
bcx.finalize();
|
|
1289
|
+
true
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/// `op_size` of the opcode at `off`, or `None` if the byte is not a known opcode.
|
|
1293
|
+
fn op_size_at(code: &[u8], off: usize) -> Option<usize> {
|
|
1294
|
+
op_size(Opcode::from_u8(*code.get(off)?)?)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/// Lower `Math.<mfn>(x)` (#186): a single native op for the ones bit-identical to Rust's `f64`
|
|
1298
|
+
/// methods (== the `tishlang_builtins::math` fns), else a call to the imported `tish_math_call` host
|
|
1299
|
+
/// fn (`math_fref`) which routes through [`MathUnaryFn::apply`] — so JIT ≡ VM ≡ interp exactly.
|
|
1300
|
+
fn emit_math_unary(
|
|
1301
|
+
bcx: &mut FunctionBuilder,
|
|
1302
|
+
math_fref: cranelift::codegen::ir::FuncRef,
|
|
1303
|
+
mfn: MathUnaryFn,
|
|
1304
|
+
x: ClifValue,
|
|
1305
|
+
) -> ClifValue {
|
|
1306
|
+
match mfn {
|
|
1307
|
+
MathUnaryFn::Sqrt => bcx.ins().sqrt(x),
|
|
1308
|
+
MathUnaryFn::Floor => bcx.ins().floor(x),
|
|
1309
|
+
MathUnaryFn::Ceil => bcx.ins().ceil(x),
|
|
1310
|
+
MathUnaryFn::Trunc => bcx.ins().trunc(x),
|
|
1311
|
+
MathUnaryFn::Abs => bcx.ins().fabs(x),
|
|
1312
|
+
// `Round` (JS `-0` tie edge), `Sign`, and every transcendental go through the host call so
|
|
1313
|
+
// the result is byte-identical to the interpreter (no native-op divergence).
|
|
1314
|
+
_ => {
|
|
1315
|
+
let id = bcx.ins().iconst(types::I32, mfn as i64);
|
|
1316
|
+
let call = bcx.ins().call(math_fref, &[id, x]);
|
|
1317
|
+
bcx.inst_results(call)[0]
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
324
1322
|
/// Lower `f64` comparison to `1.0`/`0.0` (JS boolean-in-number form).
|
|
325
1323
|
fn fcmp_f64(bcx: &mut FunctionBuilder, cc: FloatCC, a: ClifValue, b: ClifValue) -> ClifValue {
|
|
326
1324
|
let cond = bcx.ins().fcmp(cc, a, b);
|
|
@@ -343,6 +1341,50 @@ fn js_to_int32(bcx: &mut FunctionBuilder, x: ClifValue) -> ClifValue {
|
|
|
343
1341
|
bcx.ins().select(finite, red, zero)
|
|
344
1342
|
}
|
|
345
1343
|
|
|
1344
|
+
/// #187: import every registered directly-callable callee this chunk references (via `LoadVar name`)
|
|
1345
|
+
/// into `func`, returning `name → (FuncRef, arity)` for the Call-lowering peephole. Empty when the
|
|
1346
|
+
/// chunk calls no registered callee (the common case) — then `LoadVar`/`Call` still bail to interp.
|
|
1347
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1348
|
+
fn build_resolved_callees(
|
|
1349
|
+
g: &mut JitGlobal,
|
|
1350
|
+
chunk: &Chunk,
|
|
1351
|
+
func: &mut cranelift::codegen::ir::Function,
|
|
1352
|
+
) -> HashMap<Arc<str>, (cranelift::codegen::ir::FuncRef, u8)> {
|
|
1353
|
+
let mut resolved = HashMap::new();
|
|
1354
|
+
if g.callees.is_empty() {
|
|
1355
|
+
return resolved;
|
|
1356
|
+
}
|
|
1357
|
+
// Collect (name, id, arity) for referenced registered callees first, so the `g.callees` borrow is
|
|
1358
|
+
// released before the `g.module` mutable borrow in `declare_func_in_func`.
|
|
1359
|
+
let mut refs: Vec<(Arc<str>, cranelift_module::FuncId, u8)> = Vec::new();
|
|
1360
|
+
let code = &chunk.code;
|
|
1361
|
+
let mut ip = 0usize;
|
|
1362
|
+
while ip < code.len() {
|
|
1363
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
1364
|
+
Some(o) => o,
|
|
1365
|
+
None => break,
|
|
1366
|
+
};
|
|
1367
|
+
if op == Opcode::LoadVar {
|
|
1368
|
+
if let Some(name) = peek_u16(code, ip + 1).and_then(|ni| chunk.names.get(ni as usize)) {
|
|
1369
|
+
if let Some(entry) = g.callees.get(name) {
|
|
1370
|
+
if !refs.iter().any(|(n, _, _)| n == name) {
|
|
1371
|
+
refs.push((Arc::clone(name), entry.id, entry.arity));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
ip += match op.instruction_size(code, ip) {
|
|
1377
|
+
Some(s) => s,
|
|
1378
|
+
None => break,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
for (name, id, arity) in refs {
|
|
1382
|
+
let fref = g.module.declare_func_in_func(id, func);
|
|
1383
|
+
resolved.insert(name, (fref, arity));
|
|
1384
|
+
}
|
|
1385
|
+
resolved
|
|
1386
|
+
}
|
|
1387
|
+
|
|
346
1388
|
fn compile_chunk(g: &mut JitGlobal, chunk: &Chunk) -> Option<NumericFn> {
|
|
347
1389
|
let arity = chunk.param_count as usize;
|
|
348
1390
|
|
|
@@ -350,20 +1392,53 @@ fn compile_chunk(g: &mut JitGlobal, chunk: &Chunk) -> Option<NumericFn> {
|
|
|
350
1392
|
// 3-pointer array ABI instead of the register-`f64` ABI. mask 0 ⇒ ordinary numeric path below.
|
|
351
1393
|
#[cfg(not(target_arch = "wasm32"))]
|
|
352
1394
|
{
|
|
353
|
-
let array_mask = if jit_arrays_enabled() {
|
|
1395
|
+
let (array_mask, writable_mask, bool_write_mask) = if jit_arrays_enabled() {
|
|
354
1396
|
classify_params(chunk, arity)
|
|
355
1397
|
} else {
|
|
356
|
-
0
|
|
1398
|
+
(0, 0, 0)
|
|
357
1399
|
};
|
|
358
1400
|
if array_mask != 0 {
|
|
359
|
-
return compile_chunk_arrays(
|
|
1401
|
+
return compile_chunk_arrays(
|
|
1402
|
+
g,
|
|
1403
|
+
chunk,
|
|
1404
|
+
arity,
|
|
1405
|
+
array_mask,
|
|
1406
|
+
writable_mask,
|
|
1407
|
+
bool_write_mask,
|
|
1408
|
+
);
|
|
360
1409
|
}
|
|
361
1410
|
}
|
|
362
1411
|
|
|
1412
|
+
// #381: a self-recursive numeric function recurses on the native stack (SelfCall → a native call).
|
|
1413
|
+
// Compile it with a trailing `*mut RecurGuard` param so its entry can bail before the stack
|
|
1414
|
+
// overflows; non-recursive functions keep the plain register-f64 ABI (no param, no overhead).
|
|
1415
|
+
// Gated by `TISH_JIT_RECUR_GUARD` (default ON) so trusted hot-recursion workloads can trade the
|
|
1416
|
+
// guard's per-call cost for raw speed — see [`jit_recur_guard_enabled`].
|
|
1417
|
+
let recur_guard = jit_recur_guard_enabled() && chunk_has_self_call(chunk);
|
|
1418
|
+
|
|
1419
|
+
// #189: function-local `f64` arrays. Mutually exclusive with `recur_guard` (both would claim the
|
|
1420
|
+
// trailing param slot); a self-recursive array function just isn't JV-compiled. `None` from the
|
|
1421
|
+
// classifier (an array the JIT can't handle) → empty set → the scan bails on `NewArray` → interpret.
|
|
1422
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1423
|
+
let jv_slots = if !recur_guard && jit_jv_enabled() {
|
|
1424
|
+
classify_jv_slots(chunk).unwrap_or_default()
|
|
1425
|
+
} else {
|
|
1426
|
+
std::collections::HashSet::new()
|
|
1427
|
+
};
|
|
1428
|
+
#[cfg(target_arch = "wasm32")]
|
|
1429
|
+
let jv_slots: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
|
1430
|
+
let is_jv = !jv_slots.is_empty();
|
|
1431
|
+
|
|
363
1432
|
let mut sig = g.module.make_signature();
|
|
364
1433
|
for _ in 0..arity {
|
|
365
1434
|
sig.params.push(AbiParam::new(types::F64));
|
|
366
1435
|
}
|
|
1436
|
+
if recur_guard {
|
|
1437
|
+
sig.params
|
|
1438
|
+
.push(AbiParam::new(g.module.target_config().pointer_type())); // *mut RecurGuard
|
|
1439
|
+
}
|
|
1440
|
+
// #189: a JV function keeps the plain register-`f64` ABI — its deopt flag lives in the per-thread
|
|
1441
|
+
// arena (set by `tish_jv_{get,set,deopt}`), not a trailing pointer param.
|
|
367
1442
|
sig.returns.push(AbiParam::new(types::F64));
|
|
368
1443
|
|
|
369
1444
|
// Declare the function FIRST so its own `FuncRef` is available while building the body — that is
|
|
@@ -381,10 +1456,45 @@ fn compile_chunk(g: &mut JitGlobal, chunk: &Chunk) -> Option<NumericFn> {
|
|
|
381
1456
|
let mut ctx = g.module.make_context();
|
|
382
1457
|
ctx.func.signature = sig.clone();
|
|
383
1458
|
let self_ref = g.module.declare_func_in_func(id, &mut ctx.func);
|
|
1459
|
+
let math_fref = g.module.declare_func_in_func(g.math_call_id, &mut ctx.func);
|
|
1460
|
+
// #189: import the `tish_jv_*` FuncRefs into this function when it has local arrays.
|
|
1461
|
+
let jv_ctx = if is_jv {
|
|
1462
|
+
Some(JvCtx {
|
|
1463
|
+
new: g.module.declare_func_in_func(g.jv_fns.new, &mut ctx.func),
|
|
1464
|
+
push: g.module.declare_func_in_func(g.jv_fns.push, &mut ctx.func),
|
|
1465
|
+
get: g.module.declare_func_in_func(g.jv_fns.get, &mut ctx.func),
|
|
1466
|
+
set: g.module.declare_func_in_func(g.jv_fns.set, &mut ctx.func),
|
|
1467
|
+
len: g.module.declare_func_in_func(g.jv_fns.len, &mut ctx.func),
|
|
1468
|
+
free: g.module.declare_func_in_func(g.jv_fns.free, &mut ctx.func),
|
|
1469
|
+
deopt: g.module.declare_func_in_func(g.jv_fns.deopt, &mut ctx.func),
|
|
1470
|
+
slots: &jv_slots,
|
|
1471
|
+
})
|
|
1472
|
+
} else {
|
|
1473
|
+
None
|
|
1474
|
+
};
|
|
1475
|
+
// #187: import any directly-callable numeric callees this chunk references, so `name(args)` can
|
|
1476
|
+
// lower to a native call. Empty for the vast majority of functions.
|
|
1477
|
+
let resolved = build_resolved_callees(g, chunk, &mut ctx.func);
|
|
384
1478
|
let mut fbctx = FunctionBuilderContext::new();
|
|
385
|
-
let result_bool = match build_body_cfg(
|
|
386
|
-
|
|
1479
|
+
let result_bool = match build_body_cfg(
|
|
1480
|
+
&mut ctx.func,
|
|
1481
|
+
&mut fbctx,
|
|
1482
|
+
chunk,
|
|
1483
|
+
arity,
|
|
1484
|
+
Some(self_ref),
|
|
1485
|
+
0,
|
|
1486
|
+
recur_guard,
|
|
1487
|
+
math_fref,
|
|
1488
|
+
jv_ctx.as_ref(),
|
|
1489
|
+
&resolved,
|
|
1490
|
+
) {
|
|
387
1491
|
Some(b) => b,
|
|
1492
|
+
// A JV function has no straight-line fallback (its ABI has the deopt param `build_body`
|
|
1493
|
+
// doesn't emit), so a `build_body_cfg` bail means "don't JIT" → interpret.
|
|
1494
|
+
None if is_jv => {
|
|
1495
|
+
g.module.clear_context(&mut ctx);
|
|
1496
|
+
return None;
|
|
1497
|
+
}
|
|
388
1498
|
None => {
|
|
389
1499
|
g.module.clear_context(&mut ctx);
|
|
390
1500
|
ctx = g.module.make_context();
|
|
@@ -410,86 +1520,178 @@ fn compile_chunk(g: &mut JitGlobal, chunk: &Chunk) -> Option<NumericFn> {
|
|
|
410
1520
|
return None;
|
|
411
1521
|
}
|
|
412
1522
|
let ptr = g.module.get_finalized_function(id);
|
|
1523
|
+
// #187: a plain register-`f64` top-level function (no arrays / recursion-guard / bool result) with a
|
|
1524
|
+
// provably-stable global name becomes a directly-callable callee. `id` is already finalized above,
|
|
1525
|
+
// so a later caller can `declare_func_in_func(id)` and call it. Re-registering a name (a different
|
|
1526
|
+
// program reusing the address) overwrites with the new `id`/`fp`; callers resolve against the live
|
|
1527
|
+
// registry (and cross-call callers skip the cache), so a stale callee is never invoked.
|
|
1528
|
+
if !is_jv && !recur_guard && !result_bool {
|
|
1529
|
+
if let Some(name) = &chunk.global_name {
|
|
1530
|
+
g.callees.insert(
|
|
1531
|
+
Arc::clone(name),
|
|
1532
|
+
CalleeEntry {
|
|
1533
|
+
id,
|
|
1534
|
+
arity: arity as u8,
|
|
1535
|
+
},
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
413
1539
|
Some(NumericFn {
|
|
414
1540
|
ptr: ptr as usize,
|
|
415
1541
|
arity: arity as u8,
|
|
416
1542
|
result_bool,
|
|
417
1543
|
array_param_mask: 0,
|
|
1544
|
+
array_writable_mask: 0,
|
|
1545
|
+
array_bool_write_mask: 0,
|
|
1546
|
+
recur_guarded: recur_guard,
|
|
1547
|
+
jv: is_jv,
|
|
1548
|
+
uses_xcall: !resolved.is_empty(),
|
|
1549
|
+
void_result: false, // register-f64 functions return a real f64
|
|
418
1550
|
})
|
|
419
1551
|
}
|
|
420
1552
|
|
|
421
|
-
/// Classify each param slot of an array-mode candidate. Returns
|
|
422
|
-
/// an ARRAY** used only as `arr[i]`
|
|
423
|
-
///
|
|
424
|
-
///
|
|
425
|
-
///
|
|
1553
|
+
/// Classify each param slot of an array-mode candidate. Returns `(array_mask, writable_mask,
|
|
1554
|
+
/// bool_write_mask)`: **array bit k ⇒ param k is an ARRAY** used only as `arr[i]` (read, `GetIndex`) or
|
|
1555
|
+
/// `arr[i] = v` (write, `SetIndex`); **writable bit k ⇒ that array is written**; **bool_write bit k ⇒ a
|
|
1556
|
+
/// writable array written with bool consts (`arr[i]=true`)**. Returns `(0, 0, 0)` when there are no
|
|
1557
|
+
/// array params OR the function is ineligible (a param used as both an array and a number, a mix of bool
|
|
1558
|
+
/// and non-bool writes to one array, an index shape the peephole can't consume, etc.). A `0` mask is
|
|
1559
|
+
/// always safe: the caller takes the ordinary
|
|
1560
|
+
/// numeric path (which itself bails on `GetIndex`/`SetIndex`), and `try_call_array_jit` re-checks every
|
|
1561
|
+
/// arg's runtime type, so a misclassification bails to the interpreter rather than miscompiling.
|
|
426
1562
|
#[cfg(not(target_arch = "wasm32"))]
|
|
427
|
-
fn classify_params(chunk: &Chunk, arity: usize) -> u8 {
|
|
1563
|
+
fn classify_params(chunk: &Chunk, arity: usize) -> (u8, u8, u8) {
|
|
428
1564
|
if arity == 0 || arity > 8 || (chunk.num_slots as usize) == 0 {
|
|
429
|
-
return 0;
|
|
1565
|
+
return (0, 0, 0);
|
|
430
1566
|
}
|
|
431
1567
|
let code = &chunk.code;
|
|
432
|
-
|
|
1568
|
+
// #187: which slots hold a Bool value (`arr[i] = boolLocal` must be classified a BOOL write, not a
|
|
1569
|
+
// number write, so a bool local stored into a NUMBER array is caught by the runtime type guard).
|
|
1570
|
+
// Computed unconditionally — a superset only makes the guard bail more (always sound), never miscompile.
|
|
1571
|
+
let bool_slots = classify_bool_slots(chunk);
|
|
1572
|
+
let mut stored = [false; 8]; // param REASSIGNED via StoreLocal (an array param can't be — bail)
|
|
433
1573
|
let mut used_array = [false; 8];
|
|
1574
|
+
let mut written = [false; 8];
|
|
1575
|
+
// #187: per writable array param, was it written with a Bool const (`arr[i]=true`) and/or a
|
|
1576
|
+
// non-bool value (`arr[i]=5`)? The JIT flattens every element to f64, so the writeback must re-box
|
|
1577
|
+
// by the ORIGINAL element type; that is only sound when the whole array stays one type. A mix of
|
|
1578
|
+
// bool and non-bool writes to the SAME array can never be re-boxed uniformly ⇒ bail at compile time.
|
|
1579
|
+
// `wrote_bool` also feeds the runtime guard (write-kind must match the entry array's element type).
|
|
1580
|
+
let mut wrote_bool = [false; 8];
|
|
1581
|
+
let mut wrote_nonbool = [false; 8];
|
|
434
1582
|
let mut ip = 0usize;
|
|
1583
|
+
let simple = |o: Option<Opcode>| matches!(o, Some(Opcode::LoadLocal) | Some(Opcode::LoadConst));
|
|
435
1584
|
while ip < code.len() {
|
|
436
1585
|
let op = match Opcode::from_u8(code[ip]) {
|
|
437
1586
|
Some(o) => o,
|
|
438
|
-
None => return 0,
|
|
1587
|
+
None => return (0, 0, 0),
|
|
439
1588
|
};
|
|
440
1589
|
let size = match op_size(op) {
|
|
441
1590
|
Some(s) => s,
|
|
442
|
-
|
|
1591
|
+
// #187: a bare `LoadVar name` / `Call argc` (a global function call whose result is numeric)
|
|
1592
|
+
// doesn't touch param classification — treat as a sized no-op. `build_body_cfg` is the gate:
|
|
1593
|
+
// it lowers the call only if the callee resolves, else the whole function bails to interp.
|
|
1594
|
+
None if matches!(op, Opcode::LoadVar | Opcode::Call) => 3,
|
|
1595
|
+
None => return (0, 0, 0), // an opcode the array CFG can't handle ⇒ ineligible
|
|
443
1596
|
};
|
|
444
1597
|
match op {
|
|
445
1598
|
Opcode::LoadLocal => {
|
|
446
1599
|
let slot = match peek_u16(code, ip + 1) {
|
|
447
1600
|
Some(s) => s as usize,
|
|
448
|
-
None => return 0,
|
|
1601
|
+
None => return (0, 0, 0),
|
|
449
1602
|
};
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if
|
|
1603
|
+
let at = |off: usize| code.get(ip + off).copied().and_then(Opcode::from_u8);
|
|
1604
|
+
// Read peephole: `LoadLocal(arr) ; (LoadLocal|LoadConst) idx ; GetIndex`.
|
|
1605
|
+
if slot < arity && simple(at(3)) && at(6) == Some(Opcode::GetIndex) {
|
|
1606
|
+
used_array[slot] = true;
|
|
1607
|
+
ip += 7;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
// Write peephole: `LoadLocal(arr) ; (LoadLocal|LoadConst) idx ; (LoadLocal|LoadConst)
|
|
1611
|
+
// val ; Dup ; SetIndex` — a plain `arr[i] = v` with a simple index and value.
|
|
1612
|
+
if slot < arity
|
|
1613
|
+
&& simple(at(3))
|
|
1614
|
+
&& simple(at(6))
|
|
1615
|
+
&& at(9) == Some(Opcode::Dup)
|
|
1616
|
+
&& at(10) == Some(Opcode::SetIndex)
|
|
1617
|
+
{
|
|
460
1618
|
used_array[slot] = true;
|
|
461
|
-
|
|
1619
|
+
written[slot] = true;
|
|
1620
|
+
// Classify the written VALUE (operand at offset 6) so the runtime writeback re-boxes
|
|
1621
|
+
// by the right element type. Only a `LoadConst(Bool)` is a *definite* bool write.
|
|
1622
|
+
// A `LoadLocal(slot)` is trickier: `classify_bool_slots` is a SUPERSET (insert-only —
|
|
1623
|
+
// it never un-tags a slot that is later reassigned a number, and `build_body_cfg`
|
|
1624
|
+
// permits `b = n` into a bool-tagged slot), so a bool-tagged slot may actually hold a
|
|
1625
|
+
// NUMBER at the write site. We can't tell the runtime kind statically, and the flat
|
|
1626
|
+
// `f64` writeback would re-box it wrong — so bail the whole function. (Numeric-slot
|
|
1627
|
+
// writes — e.g. spectral_norm's `out[i] = sum` — are fine: not bool-tagged, non-bool.)
|
|
1628
|
+
let val_is_bool = match at(6) {
|
|
1629
|
+
Some(Opcode::LoadConst) => matches!(
|
|
1630
|
+
peek_u16(code, ip + 7).and_then(|ci| chunk.constants.get(ci as usize)),
|
|
1631
|
+
Some(Constant::Bool(_))
|
|
1632
|
+
),
|
|
1633
|
+
Some(Opcode::LoadLocal)
|
|
1634
|
+
if peek_u16(code, ip + 7)
|
|
1635
|
+
.is_some_and(|s| bool_slots.contains(&(s as usize))) =>
|
|
1636
|
+
{
|
|
1637
|
+
return (0, 0, 0);
|
|
1638
|
+
}
|
|
1639
|
+
_ => false,
|
|
1640
|
+
};
|
|
1641
|
+
if val_is_bool {
|
|
1642
|
+
wrote_bool[slot] = true;
|
|
1643
|
+
} else {
|
|
1644
|
+
wrote_nonbool[slot] = true;
|
|
1645
|
+
}
|
|
1646
|
+
ip += 11;
|
|
462
1647
|
continue;
|
|
463
|
-
} else if slot < arity {
|
|
464
|
-
used_numeric[slot] = true;
|
|
465
1648
|
}
|
|
1649
|
+
// Otherwise a bare `LoadLocal(param)` — either a numeric value (fine; a param is an
|
|
1650
|
+
// array iff INDEXED) or a self-recursive array-call argument (build_body_cfg validates +
|
|
1651
|
+
// consumes it, else bails). Neither affects classification.
|
|
466
1652
|
}
|
|
467
1653
|
Opcode::StoreLocal => {
|
|
468
1654
|
let slot = match peek_u16(code, ip + 1) {
|
|
469
1655
|
Some(s) => s as usize,
|
|
470
|
-
None => return 0,
|
|
1656
|
+
None => return (0, 0, 0),
|
|
471
1657
|
};
|
|
472
1658
|
if slot < arity {
|
|
473
|
-
|
|
1659
|
+
stored[slot] = true; // reassigning the binding — an array param can't be (see tail)
|
|
474
1660
|
}
|
|
475
1661
|
}
|
|
476
|
-
// Any `GetIndex` not
|
|
477
|
-
// handle (
|
|
478
|
-
Opcode::GetIndex => return 0,
|
|
1662
|
+
// Any `GetIndex`/`SetIndex` not consumed by a peephole above ⇒ an index/value shape we don't
|
|
1663
|
+
// handle (`arr[i+1]`, `arr[i] = a + b`, `arr[brr[i]]`, …) ⇒ ineligible.
|
|
1664
|
+
Opcode::GetIndex | Opcode::SetIndex => return (0, 0, 0),
|
|
479
1665
|
_ => {}
|
|
480
1666
|
}
|
|
481
1667
|
ip += size;
|
|
482
1668
|
}
|
|
483
|
-
let mut
|
|
1669
|
+
let mut array_mask = 0u8;
|
|
1670
|
+
let mut writable_mask = 0u8;
|
|
1671
|
+
let mut bool_write_mask = 0u8;
|
|
484
1672
|
for k in 0..arity {
|
|
485
1673
|
if used_array[k] {
|
|
486
|
-
|
|
487
|
-
|
|
1674
|
+
// A REASSIGNED array param (`arr = …`) would break the handle-reuse assumption (esp. the
|
|
1675
|
+
// self-call reusing `handles_ptr`), so bail the whole function. A `arr[i] = v` element write
|
|
1676
|
+
// is `written`, not `stored`, and stays fine.
|
|
1677
|
+
if stored[k] {
|
|
1678
|
+
return (0, 0, 0);
|
|
1679
|
+
}
|
|
1680
|
+
// A single array written with BOTH bool and non-bool values can never be re-boxed uniformly
|
|
1681
|
+
// (the JIT has already flattened everything to f64) ⇒ bail rather than miscompile.
|
|
1682
|
+
if wrote_bool[k] && wrote_nonbool[k] {
|
|
1683
|
+
return (0, 0, 0);
|
|
1684
|
+
}
|
|
1685
|
+
array_mask |= 1u8 << k;
|
|
1686
|
+
if written[k] {
|
|
1687
|
+
writable_mask |= 1u8 << k;
|
|
1688
|
+
if wrote_bool[k] {
|
|
1689
|
+
bool_write_mask |= 1u8 << k;
|
|
1690
|
+
}
|
|
488
1691
|
}
|
|
489
|
-
mask |= 1u8 << k;
|
|
490
1692
|
}
|
|
491
1693
|
}
|
|
492
|
-
|
|
1694
|
+
(array_mask, writable_mask, bool_write_mask)
|
|
493
1695
|
}
|
|
494
1696
|
|
|
495
1697
|
/// Compile an array-mode function: numeric params + array params (read as `arr[i]`). Uses ONE uniform
|
|
@@ -498,23 +1700,60 @@ fn classify_params(chunk: &Chunk, arity: usize) -> u8 {
|
|
|
498
1700
|
/// reads set `*deopt` and bail (the caller re-runs the interpreter); non-numeric arrays never reach
|
|
499
1701
|
/// here (the VM wrapper only calls this when every element is a `Value::Number`).
|
|
500
1702
|
#[cfg(not(target_arch = "wasm32"))]
|
|
501
|
-
fn compile_chunk_arrays(
|
|
1703
|
+
fn compile_chunk_arrays(
|
|
1704
|
+
g: &mut JitGlobal,
|
|
1705
|
+
chunk: &Chunk,
|
|
1706
|
+
arity: usize,
|
|
1707
|
+
mask: u8,
|
|
1708
|
+
writable_mask: u8,
|
|
1709
|
+
bool_write_mask: u8,
|
|
1710
|
+
) -> Option<NumericFn> {
|
|
1711
|
+
// #187: a self-recursive array-mode function (queens' `place`) recurses on the native stack, so —
|
|
1712
|
+
// like #381's register-f64 recursion — it gets a trailing `*mut RecurGuard` param + an entry
|
|
1713
|
+
// SP-bail, keeping unbounded recursion a catchable RangeError instead of a SIGSEGV.
|
|
1714
|
+
let recursive = chunk_has_self_call(chunk) && jit_recur_guard_enabled();
|
|
502
1715
|
let ptr_ty = g.module.target_config().pointer_type();
|
|
503
1716
|
let mut sig = g.module.make_signature();
|
|
504
1717
|
sig.params.push(AbiParam::new(ptr_ty)); // numeric_ptr
|
|
505
1718
|
sig.params.push(AbiParam::new(ptr_ty)); // handles_ptr
|
|
506
1719
|
sig.params.push(AbiParam::new(ptr_ty)); // deopt_ptr
|
|
1720
|
+
if recursive {
|
|
1721
|
+
sig.params.push(AbiParam::new(ptr_ty)); // *mut RecurGuard (#187/#381)
|
|
1722
|
+
}
|
|
507
1723
|
sig.returns.push(AbiParam::new(types::F64));
|
|
508
1724
|
|
|
509
1725
|
let name = format!("tish_arr_{}", g.counter);
|
|
510
1726
|
g.counter += 1;
|
|
511
|
-
let id = g
|
|
1727
|
+
let id = g
|
|
1728
|
+
.module
|
|
1729
|
+
.declare_function(&name, Linkage::Export, &sig)
|
|
1730
|
+
.ok()?;
|
|
512
1731
|
|
|
513
1732
|
let mut ctx = g.module.make_context();
|
|
514
1733
|
ctx.func.signature = sig.clone();
|
|
1734
|
+
// #187: declare this function's own FuncRef so a self-recursive array-mode call (queens' `place`
|
|
1735
|
+
// passing `cols`/`diag1`/`diag2` back to itself) lowers to a native call reusing the same
|
|
1736
|
+
// `handles_ptr`/`deopt_ptr` — only the numeric args are re-marshalled per level.
|
|
1737
|
+
let self_ref = g.module.declare_func_in_func(id, &mut ctx.func);
|
|
1738
|
+
let math_fref = g.module.declare_func_in_func(g.math_call_id, &mut ctx.func);
|
|
1739
|
+
// #187: array-mode functions (e.g. spectral_norm's multiplyAv) may call a register-f64 callee.
|
|
1740
|
+
let resolved = build_resolved_callees(g, chunk, &mut ctx.func);
|
|
515
1741
|
let mut fbctx = FunctionBuilderContext::new();
|
|
516
|
-
// No
|
|
517
|
-
if build_body_cfg(
|
|
1742
|
+
// No JV local arrays in array-param mode → pass None for the JV context.
|
|
1743
|
+
if build_body_cfg(
|
|
1744
|
+
&mut ctx.func,
|
|
1745
|
+
&mut fbctx,
|
|
1746
|
+
chunk,
|
|
1747
|
+
arity,
|
|
1748
|
+
Some(self_ref),
|
|
1749
|
+
mask,
|
|
1750
|
+
recursive, // #187: array-mode recursion guard (the entry SP-bail keys off this)
|
|
1751
|
+
math_fref,
|
|
1752
|
+
None,
|
|
1753
|
+
&resolved,
|
|
1754
|
+
)
|
|
1755
|
+
.is_none()
|
|
1756
|
+
{
|
|
518
1757
|
g.module.clear_context(&mut ctx);
|
|
519
1758
|
return None;
|
|
520
1759
|
}
|
|
@@ -532,13 +1771,20 @@ fn compile_chunk_arrays(g: &mut JitGlobal, chunk: &Chunk, arity: usize, mask: u8
|
|
|
532
1771
|
arity: arity as u8,
|
|
533
1772
|
result_bool: false,
|
|
534
1773
|
array_param_mask: mask,
|
|
1774
|
+
array_writable_mask: writable_mask,
|
|
1775
|
+
array_bool_write_mask: bool_write_mask,
|
|
1776
|
+
recur_guarded: recursive, // #187: array-mode fn has a trailing RecurGuard param → call_arrays passes it
|
|
1777
|
+
jv: false,
|
|
1778
|
+
uses_xcall: !resolved.is_empty(),
|
|
1779
|
+
void_result: chunk_is_void(chunk),
|
|
535
1780
|
})
|
|
536
1781
|
}
|
|
537
1782
|
|
|
538
1783
|
/// Outcome of trying to emit one *straight-line* numeric opcode.
|
|
539
1784
|
enum SimpleOp {
|
|
540
1785
|
/// Handled: bytecode consumed, IR emitted, `bool` flags a comparison/`!` result.
|
|
541
|
-
#[allow(dead_code)]
|
|
1786
|
+
#[allow(dead_code)]
|
|
1787
|
+
// reserved: the flag will carry a comparison/`!` result; currently always false
|
|
542
1788
|
Handled(bool),
|
|
543
1789
|
/// The opcode is control flow / `Return` / non-numeric — NOT consumed; caller decides.
|
|
544
1790
|
NotSimple,
|
|
@@ -606,12 +1852,29 @@ fn emit_simple_op(
|
|
|
606
1852
|
if stack.len() < 2 {
|
|
607
1853
|
return SimpleOp::Unsupported;
|
|
608
1854
|
}
|
|
609
|
-
let (r,
|
|
610
|
-
let (l,
|
|
611
|
-
|
|
1855
|
+
let (r, r_bool) = stack.pop().unwrap();
|
|
1856
|
+
let (l, l_bool) = stack.pop().unwrap();
|
|
1857
|
+
// #187: a bool value (from a bool slot or a `LoadConst Bool`) can't take part in an
|
|
1858
|
+
// EQUALITY compare here — the JIT compares the `f64` 0/1 bits, but JS `===`/`!==` (and
|
|
1859
|
+
// strict `==`/`!=`) treat `0 === false` as FALSE across types. Bail so the interpreter
|
|
1860
|
+
// decides. Relational (`<`/`>`/…) coerces bool→0/1 in both, so those stay JIT'd.
|
|
1861
|
+
let is_eq = matches!(
|
|
612
1862
|
bop,
|
|
613
1863
|
BinOp::Eq | BinOp::Ne | BinOp::StrictEq | BinOp::StrictNe
|
|
614
|
-
|
|
1864
|
+
);
|
|
1865
|
+
if is_eq && (l_bool || r_bool) {
|
|
1866
|
+
return SimpleOp::Unsupported;
|
|
1867
|
+
}
|
|
1868
|
+
let is_cmp = matches!(
|
|
1869
|
+
bop,
|
|
1870
|
+
BinOp::Eq
|
|
1871
|
+
| BinOp::Ne
|
|
1872
|
+
| BinOp::StrictEq
|
|
1873
|
+
| BinOp::StrictNe
|
|
1874
|
+
| BinOp::Lt
|
|
1875
|
+
| BinOp::Le
|
|
1876
|
+
| BinOp::Gt
|
|
1877
|
+
| BinOp::Ge
|
|
615
1878
|
);
|
|
616
1879
|
let v = match bop {
|
|
617
1880
|
BinOp::Add => bcx.ins().fadd(l, r),
|
|
@@ -729,13 +1992,183 @@ fn falsy_flag(bcx: &mut FunctionBuilder, cond: ClifValue) -> ClifValue {
|
|
|
729
1992
|
fn op_size(op: Opcode) -> Option<usize> {
|
|
730
1993
|
use Opcode::*;
|
|
731
1994
|
Some(match op {
|
|
732
|
-
Nop | Pop | Dup | Return | LoopVarsEnd | EnterBlock | ExitBlock | GetIndex => 1,
|
|
1995
|
+
Nop | Pop | Dup | Return | LoopVarsEnd | EnterBlock | ExitBlock | GetIndex | SetIndex => 1,
|
|
733
1996
|
LoadLocal | StoreLocal | LoadConst | BinOp | UnaryOp | Jump | JumpIfFalse | JumpBack
|
|
734
|
-
| LoopVarsBegin | SelfCall => 3,
|
|
1997
|
+
| LoopVarsBegin | SelfCall | MathUnary => 3,
|
|
735
1998
|
_ => return None,
|
|
736
1999
|
})
|
|
737
2000
|
}
|
|
738
2001
|
|
|
2002
|
+
/// Cheap pre-scan: does this chunk contain a `SelfCall`? Decided in `compile_chunk` (before the sig is
|
|
2003
|
+
/// built) so a self-recursive function gets the `RecurGuard` param. Walks by `op_size`; if a size is
|
|
2004
|
+
/// unknown it returns `false`, which is safe: `build_body_cfg` uses the same `op_size` and would itself
|
|
2005
|
+
/// bail on that opcode, so the function isn't JIT'd (it runs on the guarded VM instead). #381
|
|
2006
|
+
fn chunk_has_self_call(chunk: &Chunk) -> bool {
|
|
2007
|
+
let code = &chunk.code;
|
|
2008
|
+
let mut ip = 0;
|
|
2009
|
+
while ip < code.len() {
|
|
2010
|
+
let Some(op) = Opcode::from_u8(code[ip]) else {
|
|
2011
|
+
return false;
|
|
2012
|
+
};
|
|
2013
|
+
if op == Opcode::SelfCall {
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
let Some(size) = op_size(op) else {
|
|
2017
|
+
return false;
|
|
2018
|
+
};
|
|
2019
|
+
ip += size;
|
|
2020
|
+
}
|
|
2021
|
+
false
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/// #189 — identify function-LOCAL slots that hold non-escaping `f64` arrays (JV slots). A slot
|
|
2025
|
+
/// qualifies iff its ONLY definition is an empty array literal (`NewArray 0` immediately followed by
|
|
2026
|
+
/// `StoreLocal slot`) and it is never stored from anything else (which would alias/escape it). Any
|
|
2027
|
+
/// `NewArray` with a non-empty literal, or one not immediately stored to a slot, bails the WHOLE
|
|
2028
|
+
/// function (`None`) — such an array can't be JV-compiled and the function has array opcodes the JIT
|
|
2029
|
+
/// can't handle anyway. `Some(empty)` = "no local arrays" (the normal numeric path).
|
|
2030
|
+
///
|
|
2031
|
+
/// This is a cheap pass over STORES only; the [`build_body_cfg`] emission validates USES by bailing
|
|
2032
|
+
/// #187: slots that ever receive a BOOLEAN value, so the numeric JIT can represent them as `f64`
|
|
2033
|
+
/// `0.0`/`1.0` instead of bailing at `StoreLocal`. Purely syntactic: a slot is tagged when a
|
|
2034
|
+
/// `StoreLocal(slot)` is immediately preceded by an op that pushes a bool — `LoadConst(Bool)`, a
|
|
2035
|
+
/// comparison `BinOp` (`==`/`!=`/`===`/`!==`/`<`/`<=`/`>`/`>=`), or `UnaryOp !`. Over-tagging is safe:
|
|
2036
|
+
/// a tagged slot only makes its `LoadLocal`s carry `is_bool`, which at worst forces a diverging
|
|
2037
|
+
/// `bool === number` compare or a `return bool` to bail to the interpreter (never a miscompile). A
|
|
2038
|
+
/// bytecode decode failure stops the scan early (→ fewer tags → more conservative), never a panic.
|
|
2039
|
+
/// #187: is this a VOID function — one that only ever `return`s the implicit `null` (a side-effect
|
|
2040
|
+
/// function like `multiplyAv`, which writes an array param and falls through)? True iff EVERY `Return`
|
|
2041
|
+
/// is immediately preceded by `LoadConst Null`. The array-mode JIT can then emit a dummy `f64` result
|
|
2042
|
+
/// for such a function and its wrapper returns `Value::Null` (matching the interpreter). Conservative:
|
|
2043
|
+
/// a decode failure or any value-returning path makes it `false`.
|
|
2044
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2045
|
+
fn chunk_is_void(chunk: &Chunk) -> bool {
|
|
2046
|
+
let code = &chunk.code;
|
|
2047
|
+
let mut ip = 0usize;
|
|
2048
|
+
let mut prev_null = false;
|
|
2049
|
+
let mut saw_return = false;
|
|
2050
|
+
while ip < code.len() {
|
|
2051
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
2052
|
+
Some(o) => o,
|
|
2053
|
+
None => return false,
|
|
2054
|
+
};
|
|
2055
|
+
if op == Opcode::Return {
|
|
2056
|
+
saw_return = true;
|
|
2057
|
+
if !prev_null {
|
|
2058
|
+
return false; // a value-returning path ⇒ not void
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
prev_null = op == Opcode::LoadConst
|
|
2062
|
+
&& matches!(
|
|
2063
|
+
peek_u16(code, ip + 1).and_then(|i| chunk.constants.get(i as usize)),
|
|
2064
|
+
Some(Constant::Null)
|
|
2065
|
+
);
|
|
2066
|
+
ip += match op.instruction_size(code, ip) {
|
|
2067
|
+
Some(s) => s,
|
|
2068
|
+
None => return false,
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
saw_return
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
fn classify_bool_slots(chunk: &Chunk) -> std::collections::HashSet<usize> {
|
|
2075
|
+
let code = &chunk.code;
|
|
2076
|
+
let mut set: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
|
2077
|
+
let mut ip = 0usize;
|
|
2078
|
+
let mut prev_pushes_bool = false;
|
|
2079
|
+
while ip < code.len() {
|
|
2080
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
2081
|
+
Some(o) => o,
|
|
2082
|
+
None => break,
|
|
2083
|
+
};
|
|
2084
|
+
let size = match op.instruction_size(code, ip) {
|
|
2085
|
+
Some(s) => s,
|
|
2086
|
+
None => break,
|
|
2087
|
+
};
|
|
2088
|
+
if op == Opcode::StoreLocal && prev_pushes_bool {
|
|
2089
|
+
if let Some(s) = peek_u16(code, ip + 1) {
|
|
2090
|
+
set.insert(s as usize);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
prev_pushes_bool = match op {
|
|
2094
|
+
Opcode::LoadConst => matches!(
|
|
2095
|
+
peek_u16(code, ip + 1).and_then(|i| chunk.constants.get(i as usize)),
|
|
2096
|
+
Some(Constant::Bool(_))
|
|
2097
|
+
),
|
|
2098
|
+
Opcode::BinOp => matches!(
|
|
2099
|
+
peek_u16(code, ip + 1)
|
|
2100
|
+
.map(|r| r as u8)
|
|
2101
|
+
.and_then(u8_to_binop),
|
|
2102
|
+
Some(
|
|
2103
|
+
BinOp::Eq
|
|
2104
|
+
| BinOp::Ne
|
|
2105
|
+
| BinOp::StrictEq
|
|
2106
|
+
| BinOp::StrictNe
|
|
2107
|
+
| BinOp::Lt
|
|
2108
|
+
| BinOp::Le
|
|
2109
|
+
| BinOp::Gt
|
|
2110
|
+
| BinOp::Ge
|
|
2111
|
+
)
|
|
2112
|
+
),
|
|
2113
|
+
Opcode::UnaryOp => matches!(
|
|
2114
|
+
peek_u16(code, ip + 1)
|
|
2115
|
+
.map(|r| r as u8)
|
|
2116
|
+
.and_then(u8_to_unaryop),
|
|
2117
|
+
Some(UnaryOp::Not)
|
|
2118
|
+
),
|
|
2119
|
+
_ => false,
|
|
2120
|
+
};
|
|
2121
|
+
ip += size;
|
|
2122
|
+
}
|
|
2123
|
+
set
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/// on any misuse of a JV ref (it reaching a numeric op, a return, a call arg, a block boundary, …),
|
|
2127
|
+
/// so a mis-shaped use never miscompiles — it just falls back to the interpreter.
|
|
2128
|
+
fn classify_jv_slots(chunk: &Chunk) -> Option<std::collections::HashSet<usize>> {
|
|
2129
|
+
let code = &chunk.code;
|
|
2130
|
+
let mut from_newarray: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
|
2131
|
+
let mut from_other: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
|
2132
|
+
let mut has_newarray = false;
|
|
2133
|
+
let mut ip = 0usize;
|
|
2134
|
+
let mut prev_newarray0 = false;
|
|
2135
|
+
while ip < code.len() {
|
|
2136
|
+
let op = Opcode::from_u8(*code.get(ip)?)?;
|
|
2137
|
+
let size = op.instruction_size(code, ip)?;
|
|
2138
|
+
let is_newarray0 = op == Opcode::NewArray && peek_u16(code, ip + 1)? == 0;
|
|
2139
|
+
if op == Opcode::NewArray {
|
|
2140
|
+
has_newarray = true;
|
|
2141
|
+
if peek_u16(code, ip + 1)? != 0 {
|
|
2142
|
+
return None; // non-empty array literal → not JV-compilable
|
|
2143
|
+
}
|
|
2144
|
+
// An empty `[]` must be stored straight into a slot (`let a = []`); anything else means
|
|
2145
|
+
// the fresh array is used inline / escapes → bail.
|
|
2146
|
+
if Opcode::from_u8(*code.get(ip + size)?)? != Opcode::StoreLocal {
|
|
2147
|
+
return None;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if op == Opcode::StoreLocal {
|
|
2151
|
+
let slot = peek_u16(code, ip + 1)? as usize;
|
|
2152
|
+
if prev_newarray0 {
|
|
2153
|
+
from_newarray.insert(slot);
|
|
2154
|
+
} else {
|
|
2155
|
+
from_other.insert(slot);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
prev_newarray0 = is_newarray0;
|
|
2159
|
+
ip += size;
|
|
2160
|
+
}
|
|
2161
|
+
if !has_newarray {
|
|
2162
|
+
return Some(std::collections::HashSet::new());
|
|
2163
|
+
}
|
|
2164
|
+
// A NewArray-defined slot that is ALSO stored otherwise is aliased/polymorphic — bail the whole
|
|
2165
|
+
// function (its array can't be JV-compiled, and it has `NewArray` the JIT otherwise rejects).
|
|
2166
|
+
if from_newarray.iter().any(|s| from_other.contains(s)) {
|
|
2167
|
+
return None;
|
|
2168
|
+
}
|
|
2169
|
+
Some(from_newarray)
|
|
2170
|
+
}
|
|
2171
|
+
|
|
739
2172
|
/// Read a big-endian u16 operand at `off` without advancing (matches [`read_u16`]).
|
|
740
2173
|
#[inline]
|
|
741
2174
|
fn peek_u16(code: &[u8], off: usize) -> Option<u16> {
|
|
@@ -744,6 +2177,35 @@ fn peek_u16(code: &[u8], off: usize) -> Option<u16> {
|
|
|
744
2177
|
Some((a << 8) | b)
|
|
745
2178
|
}
|
|
746
2179
|
|
|
2180
|
+
/// Read a "simple" operand at byte `at` — a `LoadLocal(slot)` (→ the slot's f64 Variable) or a
|
|
2181
|
+
/// `LoadConst(Number)` (→ an f64 const) — for the array read/write peepholes. `None` for any other
|
|
2182
|
+
/// opcode / a non-numeric const, which makes the caller bail. #187
|
|
2183
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2184
|
+
fn read_simple_operand(
|
|
2185
|
+
bcx: &mut FunctionBuilder,
|
|
2186
|
+
code: &[u8],
|
|
2187
|
+
at: usize,
|
|
2188
|
+
chunk: &Chunk,
|
|
2189
|
+
vars: &[Variable],
|
|
2190
|
+
) -> Option<ClifValue> {
|
|
2191
|
+
match Opcode::from_u8(*code.get(at)?)? {
|
|
2192
|
+
Opcode::LoadLocal => {
|
|
2193
|
+
let s = peek_u16(code, at + 1)? as usize;
|
|
2194
|
+
Some(bcx.use_var(*vars.get(s)?))
|
|
2195
|
+
}
|
|
2196
|
+
Opcode::LoadConst => {
|
|
2197
|
+
let ci = peek_u16(code, at + 1)? as usize;
|
|
2198
|
+
match chunk.constants.get(ci) {
|
|
2199
|
+
Some(Constant::Number(n)) => Some(bcx.ins().f64const(*n)),
|
|
2200
|
+
// #187: a boolean stored into a (bool) array element — `arr[i] = true` — as `f64` 0/1.
|
|
2201
|
+
Some(Constant::Bool(b)) => Some(bcx.ins().f64const(if *b { 1.0 } else { 0.0 })),
|
|
2202
|
+
_ => None,
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
_ => None,
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
747
2209
|
/// Control-flow JIT: lower a slot-based numeric function WITH loops/branches to native code. Uses a
|
|
748
2210
|
/// cranelift `Variable` per slot (loop-carried locals are mutable across blocks; cranelift inserts the
|
|
749
2211
|
/// SSA phis at `seal_all_blocks`), and one block per bytecode jump target. Handles LoadLocal/StoreLocal,
|
|
@@ -754,6 +2216,7 @@ fn peek_u16(code: &[u8], off: usize) -> Option<u16> {
|
|
|
754
2216
|
/// (→ caller → VM) on any other opcode, a non-empty operand stack at a block boundary, boolean slots,
|
|
755
2217
|
/// or a `Call` whose callee is not in `callees`. ADDITIVE: a bail just runs the VM.
|
|
756
2218
|
/// Returns `Some(false)` (Number result) on success.
|
|
2219
|
+
#[allow(clippy::too_many_arguments)] // each param is a distinct, documented lowering mode/handle
|
|
757
2220
|
fn build_body_cfg(
|
|
758
2221
|
func: &mut cranelift::codegen::ir::Function,
|
|
759
2222
|
fbctx: &mut FunctionBuilderContext,
|
|
@@ -764,12 +2227,35 @@ fn build_body_cfg(
|
|
|
764
2227
|
// existing caller passes 0, so that path is byte-identical). Nonzero ⇒ the 3-pointer array ABI:
|
|
765
2228
|
// params are `[numeric_ptr, handles_ptr, deopt_ptr]` and `arr[i]` lowers to a bounds-checked load.
|
|
766
2229
|
array_mask: u8,
|
|
2230
|
+
// #381: when true (self-recursive, plain-numeric path only) the signature has a trailing
|
|
2231
|
+
// `*mut RecurGuard` param, and the function entry emits a stack-pointer check that bails before
|
|
2232
|
+
// the native recursion overflows. `false` for array mode and every non-recursive function.
|
|
2233
|
+
recur_guard: bool,
|
|
2234
|
+
// #186: the imported `tish_math_call` host fn, for lowering `Math.<fn>` (`MathUnary`) intrinsics.
|
|
2235
|
+
math_fref: cranelift::codegen::ir::FuncRef,
|
|
2236
|
+
// #189: `Some` when the function has local `f64` arrays — carries the `tish_jv_*` `FuncRef`s + the
|
|
2237
|
+
// JV slot set. Those slots become `i64` handle Variables and their array ops lower to `tish_jv_*`
|
|
2238
|
+
// calls; an out-of-bounds index sets the per-thread deopt flag the wrapper re-interprets on.
|
|
2239
|
+
jv: Option<&JvCtx>,
|
|
2240
|
+
// #187: `name → (FuncRef, arity)` for directly-callable numeric callees imported into `func`. A
|
|
2241
|
+
// `LoadVar name ; args ; Call` whose name is here lowers to a native call; unresolved names bail.
|
|
2242
|
+
resolved: &HashMap<Arc<str>, (cranelift::codegen::ir::FuncRef, u8)>,
|
|
767
2243
|
) -> Option<bool> {
|
|
768
2244
|
let code = &chunk.code;
|
|
769
2245
|
let num_slots = chunk.num_slots as usize;
|
|
770
2246
|
if num_slots == 0 || num_slots > 256 {
|
|
771
2247
|
return None;
|
|
772
2248
|
}
|
|
2249
|
+
// #187: slots that hold a boolean (represented as `f64` 0/1). A `LoadLocal` of one carries
|
|
2250
|
+
// `is_bool` so a diverging `bool === number` compare or a `return bool` bails to the interpreter.
|
|
2251
|
+
let bool_slots = if jit_bool_slots_enabled() {
|
|
2252
|
+
classify_bool_slots(chunk)
|
|
2253
|
+
} else {
|
|
2254
|
+
std::collections::HashSet::new()
|
|
2255
|
+
};
|
|
2256
|
+
// #187: a VOID array-mode function (only returns the implicit `null`, e.g. a side-effect writer)
|
|
2257
|
+
// gets a dummy `f64` result; `try_call_array_jit` returns `Value::Null` for it (matches interp).
|
|
2258
|
+
let is_void = array_mask != 0 && chunk_is_void(chunk);
|
|
773
2259
|
|
|
774
2260
|
// 1. Validate every opcode is supported + collect block leaders (jump targets, the fall-through
|
|
775
2261
|
// after each branch, entry). Bail on any unsupported opcode (so we never mis-size the scan).
|
|
@@ -780,7 +2266,18 @@ fn build_body_cfg(
|
|
|
780
2266
|
let mut ip = 0;
|
|
781
2267
|
while ip < code.len() {
|
|
782
2268
|
let op = Opcode::from_u8(code[ip])?;
|
|
783
|
-
|
|
2269
|
+
// For a JV function the array opcodes (NewArray/GetMember/GetIndex/SetIndex/Call) are valid
|
|
2270
|
+
// and must be sized via the full instruction table; the translate loop then validates each is
|
|
2271
|
+
// a real JV op (else it bails). Non-JV functions keep the strict `op_size` whitelist.
|
|
2272
|
+
let size = match op_size(op) {
|
|
2273
|
+
Some(s) => s,
|
|
2274
|
+
None if jv.is_some() => op.instruction_size(code, ip)?,
|
|
2275
|
+
// #187: `LoadVar`/`Call` are 3 bytes; the translate loop lowers them to a native call if the
|
|
2276
|
+
// callee resolved, else bails. Sizing them here lets a function with a resolvable call pass
|
|
2277
|
+
// the scan (an unresolvable one still bails in translation).
|
|
2278
|
+
None if matches!(op, Opcode::LoadVar | Opcode::Call) => 3,
|
|
2279
|
+
None => return None,
|
|
2280
|
+
};
|
|
784
2281
|
// A SelfCall whose arity != this function's arity can't be a plain f64-ABI native call → bail.
|
|
785
2282
|
if op == Opcode::SelfCall {
|
|
786
2283
|
if self_ref.is_none() || peek_u16(code, ip + 1)? as usize != arity {
|
|
@@ -820,15 +2317,59 @@ fn build_body_cfg(
|
|
|
820
2317
|
}
|
|
821
2318
|
|
|
822
2319
|
let mut bcx = FunctionBuilder::new(func, fbctx);
|
|
823
|
-
let blocks: std::collections::BTreeMap<usize, Block> =
|
|
2320
|
+
let mut blocks: std::collections::BTreeMap<usize, Block> =
|
|
824
2321
|
leaders.iter().map(|&o| (o, bcx.create_block())).collect();
|
|
825
2322
|
let entry = *blocks.get(&0)?;
|
|
826
2323
|
bcx.append_block_params_for_function_params(entry);
|
|
827
2324
|
bcx.switch_to_block(entry);
|
|
828
2325
|
let params: Vec<ClifValue> = bcx.block_params(entry).to_vec();
|
|
829
2326
|
|
|
830
|
-
//
|
|
831
|
-
|
|
2327
|
+
// #381: self-recursive numeric function → `entry` is a stack-pointer bail check (tish's deopt
|
|
2328
|
+
// pattern, not V8's throw-from-JIT): compare SP to the caller-provided `stack_limit`; if the
|
|
2329
|
+
// recursion has driven SP past it, store `tripped = 1` into the RecurGuard and return a sentinel
|
|
2330
|
+
// instead of recursing further. The body then runs in `body_block`; `entry` is the function's real
|
|
2331
|
+
// entry point, so this check runs on every (including recursive) call. Loops jumping back to offset
|
|
2332
|
+
// 0 land in `body_block` (no re-check needed — a loop adds no native frame). Pointer type is I64
|
|
2333
|
+
// (the JIT targets — x86-64 / aarch64 — are all 64-bit; the module is not built on wasm32).
|
|
2334
|
+
let guard_ptr: Option<ClifValue> = if recur_guard {
|
|
2335
|
+
// #187: the guard pointer is the LAST param — after the `arity` f64s (register-f64 ABI), or
|
|
2336
|
+
// after `[numeric_ptr, handles_ptr, deopt_ptr]` (array-mode ABI, index 3). Array-mode
|
|
2337
|
+
// self-recursion (queens' `place`) recurses on the native stack just like `fib`, so it needs
|
|
2338
|
+
// the SAME SP-bail to stay a catchable RangeError instead of a SIGSEGV (#381).
|
|
2339
|
+
let gp_idx = if array_mask != 0 { 3 } else { arity };
|
|
2340
|
+
let gp = *params.get(gp_idx)?;
|
|
2341
|
+
let stack_limit = bcx.ins().load(types::I64, MemFlags::new(), gp, 0);
|
|
2342
|
+
let sp = bcx.ins().get_stack_pointer(types::I64);
|
|
2343
|
+
let below = bcx.ins().icmp(IntCC::UnsignedLessThan, sp, stack_limit);
|
|
2344
|
+
let bail = bcx.create_block();
|
|
2345
|
+
let body = bcx.create_block();
|
|
2346
|
+
bcx.ins().brif(below, bail, &[], body, &[]);
|
|
2347
|
+
bcx.switch_to_block(bail);
|
|
2348
|
+
bcx.seal_block(bail);
|
|
2349
|
+
let one = bcx.ins().iconst(types::I8, 1);
|
|
2350
|
+
bcx.ins().store(MemFlags::new(), one, gp, 8); // RecurGuard.tripped (offset 8)
|
|
2351
|
+
let nan = bcx.ins().f64const(f64::NAN);
|
|
2352
|
+
bcx.ins().return_(&[nan]);
|
|
2353
|
+
blocks.insert(0, body); // internal jumps to offset 0 → the body, not the SP check
|
|
2354
|
+
bcx.switch_to_block(body);
|
|
2355
|
+
Some(gp)
|
|
2356
|
+
} else {
|
|
2357
|
+
None
|
|
2358
|
+
};
|
|
2359
|
+
let body_start = *blocks.get(&0)?; // `entry` normally; `body_block` when guarded
|
|
2360
|
+
|
|
2361
|
+
// 2. A Variable per slot, all defined at entry so every path defines them. A JV slot (#189) holds
|
|
2362
|
+
// an arena HANDLE (a `u64`, 0 = null) so its Variable is `i64`, not `f64`.
|
|
2363
|
+
let vars: Vec<Variable> = (0..num_slots)
|
|
2364
|
+
.map(|i| {
|
|
2365
|
+
let ty = if jv.is_some_and(|j| j.slots.contains(&i)) {
|
|
2366
|
+
types::I64
|
|
2367
|
+
} else {
|
|
2368
|
+
types::F64
|
|
2369
|
+
};
|
|
2370
|
+
bcx.declare_var(ty)
|
|
2371
|
+
})
|
|
2372
|
+
.collect();
|
|
832
2373
|
// Array-mode state: per-array-param `(ptr,len)` loaded from the handles array + the deopt pad.
|
|
833
2374
|
let mut array_slots: HashMap<usize, (ClifValue, ClifValue)> = HashMap::new();
|
|
834
2375
|
let mut deopt_block: Option<Block> = None;
|
|
@@ -841,7 +2382,8 @@ fn build_body_cfg(
|
|
|
841
2382
|
deopt_ptr = Some(*params.get(2)?);
|
|
842
2383
|
let mut numeric_i = 0i32;
|
|
843
2384
|
let mut array_i = 0i64;
|
|
844
|
-
#[allow(clippy::needless_range_loop)]
|
|
2385
|
+
#[allow(clippy::needless_range_loop)]
|
|
2386
|
+
// `slot` drives bit-mask math (`array_mask >> slot`) + map keys, not just indexing
|
|
845
2387
|
for slot in 0..num_slots {
|
|
846
2388
|
let init = if slot < arity && (array_mask >> slot) & 1 == 1 {
|
|
847
2389
|
let base = bcx.ins().iadd_imm(handles_ptr, array_i * 16);
|
|
@@ -864,18 +2406,37 @@ fn build_body_cfg(
|
|
|
864
2406
|
deopt_block = Some(bcx.create_block());
|
|
865
2407
|
} else {
|
|
866
2408
|
for (i, &v) in vars.iter().enumerate() {
|
|
867
|
-
|
|
868
|
-
|
|
2409
|
+
if jv.is_some_and(|j| j.slots.contains(&i)) {
|
|
2410
|
+
// A JV slot starts null (handle 0); its `[]` def stores the real handle. Null-init
|
|
2411
|
+
// makes the free-on-return safe even if a return precedes the def (`tish_jv_free`
|
|
2412
|
+
// treats handle 0 as a no-op).
|
|
2413
|
+
let null = bcx.ins().iconst(types::I64, 0);
|
|
2414
|
+
bcx.def_var(v, null);
|
|
869
2415
|
} else {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
2416
|
+
let init = if i < arity {
|
|
2417
|
+
params[i]
|
|
2418
|
+
} else {
|
|
2419
|
+
bcx.ins().f64const(0.0)
|
|
2420
|
+
};
|
|
2421
|
+
bcx.def_var(v, init);
|
|
2422
|
+
}
|
|
873
2423
|
}
|
|
874
2424
|
}
|
|
875
2425
|
|
|
876
2426
|
// 3. Translate. The operand stack is empty at every block boundary (statement-level control flow).
|
|
877
2427
|
let mut stack: Vec<(ClifValue, bool)> = Vec::new();
|
|
878
|
-
|
|
2428
|
+
// #189: JV array handles "in flight" (pushed by `LoadLocal`/`NewArray` of a JV slot, consumed by
|
|
2429
|
+
// the very next `GetIndex`/`SetIndex`/`GetMember`), kept off the f64 `stack`; and a pending
|
|
2430
|
+
// `arr.push` awaiting its arg + `Call`. Both must be empty at every block boundary.
|
|
2431
|
+
let mut jv_pending: Vec<ClifValue> = Vec::new();
|
|
2432
|
+
let mut pending_push: Option<ClifValue> = None;
|
|
2433
|
+
// #187: a resolved callee awaiting its args + `Call` (set by `LoadVar name`, consumed by the very
|
|
2434
|
+
// next `Call`). Like the JV pending state, must be empty at every block boundary.
|
|
2435
|
+
let mut pending_callee: Option<(cranelift::codegen::ir::FuncRef, u8)> = None;
|
|
2436
|
+
// #187: array-param slots passed as arguments to a self-recursive array-mode call (queens' `place`),
|
|
2437
|
+
// in argument order — pushed by `LoadLocal(array param)`, consumed by the next `SelfCall`.
|
|
2438
|
+
let mut arr_call_pending: Vec<usize> = Vec::new();
|
|
2439
|
+
let mut cur = body_start;
|
|
879
2440
|
let mut terminated = false;
|
|
880
2441
|
let mut ip = 0usize;
|
|
881
2442
|
while ip < code.len() {
|
|
@@ -887,6 +2448,15 @@ fn build_body_cfg(
|
|
|
887
2448
|
}
|
|
888
2449
|
bcx.ins().jump(blk, &[]);
|
|
889
2450
|
}
|
|
2451
|
+
// A JV ref, a pending push, a pending callee, or a pending array self-call arg must
|
|
2452
|
+
// never span a statement boundary.
|
|
2453
|
+
if !jv_pending.is_empty()
|
|
2454
|
+
|| pending_push.is_some()
|
|
2455
|
+
|| pending_callee.is_some()
|
|
2456
|
+
|| !arr_call_pending.is_empty()
|
|
2457
|
+
{
|
|
2458
|
+
return None;
|
|
2459
|
+
}
|
|
890
2460
|
bcx.switch_to_block(blk);
|
|
891
2461
|
cur = blk;
|
|
892
2462
|
terminated = false;
|
|
@@ -894,37 +2464,61 @@ fn build_body_cfg(
|
|
|
894
2464
|
}
|
|
895
2465
|
}
|
|
896
2466
|
if terminated {
|
|
897
|
-
|
|
2467
|
+
// Skip the unreachable tail; a JV function may have array opcodes here (size via the full
|
|
2468
|
+
// table).
|
|
2469
|
+
let op = Opcode::from_u8(code[ip])?;
|
|
2470
|
+
ip += match op_size(op) {
|
|
2471
|
+
Some(s) => s,
|
|
2472
|
+
None if jv.is_some() => op.instruction_size(code, ip)?,
|
|
2473
|
+
None => return None,
|
|
2474
|
+
};
|
|
898
2475
|
continue;
|
|
899
2476
|
}
|
|
900
2477
|
let op = Opcode::from_u8(code[ip])?;
|
|
901
2478
|
match op {
|
|
902
2479
|
Opcode::LoadLocal => {
|
|
903
2480
|
let slot = peek_u16(code, ip + 1)? as usize;
|
|
904
|
-
//
|
|
905
|
-
//
|
|
2481
|
+
// #189: a JV slot's value is its arena HANDLE — push it to `jv_pending` (the next
|
|
2482
|
+
// GetIndex/SetIndex/GetMember consumes it), never onto the f64 stack.
|
|
2483
|
+
if jv.is_some_and(|j| j.slots.contains(&slot)) {
|
|
2484
|
+
jv_pending.push(bcx.use_var(*vars.get(slot)?));
|
|
2485
|
+
ip += 3;
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
// Array-mode peepholes on an array param, both bounds-checked (OOB → the deopt pad):
|
|
2489
|
+
// READ `LoadLocal(arr) ; (LoadLocal|LoadConst) idx ; GetIndex` → native load
|
|
2490
|
+
// WRITE `LoadLocal(arr) ; idx ; (LoadLocal|LoadConst) val ; Dup ; SetIndex` (#187) → store
|
|
2491
|
+
// ARG a bare `LoadLocal(arr)` → an argument to a self-recursive array call (#187)
|
|
906
2492
|
if array_mask != 0 && slot < arity && (array_mask >> slot) & 1 == 1 {
|
|
907
2493
|
let (aptr, alen) = *array_slots.get(&slot)?;
|
|
908
|
-
|
|
909
|
-
let
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
2494
|
+
// Decide the shape by PEEKING (no cranelift values created yet).
|
|
2495
|
+
let at3 = code.get(ip + 3).copied().and_then(Opcode::from_u8);
|
|
2496
|
+
let idx_simple =
|
|
2497
|
+
matches!(at3, Some(Opcode::LoadLocal) | Some(Opcode::LoadConst));
|
|
2498
|
+
let at6 = code.get(ip + 6).copied().and_then(Opcode::from_u8);
|
|
2499
|
+
let is_read = idx_simple && at6 == Some(Opcode::GetIndex);
|
|
2500
|
+
let is_write = idx_simple
|
|
2501
|
+
&& matches!(at6, Some(Opcode::LoadLocal) | Some(Opcode::LoadConst))
|
|
2502
|
+
&& code.get(ip + 9).copied().and_then(Opcode::from_u8) == Some(Opcode::Dup)
|
|
2503
|
+
&& code.get(ip + 10).copied().and_then(Opcode::from_u8)
|
|
2504
|
+
== Some(Opcode::SetIndex);
|
|
2505
|
+
if !is_read && !is_write {
|
|
2506
|
+
// Bare array-param load — stage it as a self-call argument (the next `SelfCall`
|
|
2507
|
+
// validates + consumes it). A genuine escape leaves it pending at a boundary → bail.
|
|
2508
|
+
arr_call_pending.push(slot);
|
|
2509
|
+
ip += 3;
|
|
2510
|
+
continue;
|
|
925
2511
|
}
|
|
2512
|
+
let idx_f64 = read_simple_operand(&mut bcx, code, ip + 3, chunk, &vars)?;
|
|
926
2513
|
// i = idx as usize (saturating: NaN→0, neg→0 — matches the VM's `n as usize`).
|
|
927
2514
|
let i = bcx.ins().fcvt_to_uint_sat(types::I64, idx_f64);
|
|
2515
|
+
// Read `val` (write only) BEFORE the bounds-check split so its LoadLocal reads the
|
|
2516
|
+
// slot's current SSA value in `cur`, not the fresh `cont` block.
|
|
2517
|
+
let store_val = if is_write {
|
|
2518
|
+
Some(read_simple_operand(&mut bcx, code, ip + 6, chunk, &vars)?)
|
|
2519
|
+
} else {
|
|
2520
|
+
None
|
|
2521
|
+
};
|
|
928
2522
|
let inb = bcx.ins().icmp(IntCC::UnsignedLessThan, i, alen);
|
|
929
2523
|
let cont = bcx.create_block();
|
|
930
2524
|
let db = deopt_block?;
|
|
@@ -933,20 +2527,40 @@ fn build_body_cfg(
|
|
|
933
2527
|
cur = cont; // keep block-boundary tracking accurate after the mid-stream split
|
|
934
2528
|
let off = bcx.ins().imul_imm(i, 8);
|
|
935
2529
|
let addr = bcx.ins().iadd(aptr, off);
|
|
936
|
-
let val =
|
|
937
|
-
|
|
938
|
-
|
|
2530
|
+
if let Some(val) = store_val {
|
|
2531
|
+
bcx.ins().store(MemFlags::new(), val, addr, 0);
|
|
2532
|
+
stack.push((val, false)); // assignment yields the value (a Pop usually discards it)
|
|
2533
|
+
ip += 11; // LoadLocal(arr) + idx + val + Dup + SetIndex
|
|
2534
|
+
} else {
|
|
2535
|
+
let val = bcx.ins().load(types::F64, MemFlags::new(), addr, 0);
|
|
2536
|
+
stack.push((val, false));
|
|
2537
|
+
ip += 7; // LoadLocal(arr) + idx + GetIndex
|
|
2538
|
+
}
|
|
939
2539
|
continue;
|
|
940
2540
|
}
|
|
941
2541
|
let v = *vars.get(slot)?;
|
|
942
|
-
|
|
2542
|
+
// #187: a bool slot's `f64` 0/1 value carries `is_bool` so downstream equality/return
|
|
2543
|
+
// guards fire; a plain numeric slot pushes `false`.
|
|
2544
|
+
stack.push((bcx.use_var(v), bool_slots.contains(&slot)));
|
|
943
2545
|
ip += 3;
|
|
944
2546
|
}
|
|
945
2547
|
Opcode::StoreLocal => {
|
|
946
2548
|
let slot = peek_u16(code, ip + 1)? as usize;
|
|
2549
|
+
// #189: storing a JV array (its `[]` def or a re-store) pops the handle from
|
|
2550
|
+
// `jv_pending` into the slot's i64 Variable.
|
|
2551
|
+
if jv.is_some_and(|j| j.slots.contains(&slot)) {
|
|
2552
|
+
let ptr = jv_pending.pop()?;
|
|
2553
|
+
bcx.def_var(*vars.get(slot)?, ptr);
|
|
2554
|
+
ip += 3;
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
947
2557
|
let (val, is_bool) = stack.pop()?;
|
|
948
|
-
|
|
949
|
-
|
|
2558
|
+
// #187: a boolean value may be stored only into a slot the pre-pass tagged as a bool
|
|
2559
|
+
// slot (represented as `f64` 0/1). Any other bool store → bail (keeps unknown shapes
|
|
2560
|
+
// on the interpreter). A numeric store into a bool-tagged slot is fine — the slot is
|
|
2561
|
+
// still an `f64`; its `LoadLocal`s just carry `is_bool` (conservatively).
|
|
2562
|
+
if is_bool && !bool_slots.contains(&slot) {
|
|
2563
|
+
return None;
|
|
950
2564
|
}
|
|
951
2565
|
let v = *vars.get(slot)?;
|
|
952
2566
|
bcx.def_var(v, val);
|
|
@@ -970,6 +2584,15 @@ fn build_body_cfg(
|
|
|
970
2584
|
if is_bool {
|
|
971
2585
|
return None;
|
|
972
2586
|
}
|
|
2587
|
+
// #189: free every JV array (return its arena slot to the free list) before leaving
|
|
2588
|
+
// the frame — handle 0 (a slot not yet allocated on this path) is a no-op. This is why
|
|
2589
|
+
// JV arrays must never escape.
|
|
2590
|
+
if let Some(jvc) = jv {
|
|
2591
|
+
for &slot in jvc.slots {
|
|
2592
|
+
let handle = bcx.use_var(*vars.get(slot)?);
|
|
2593
|
+
bcx.ins().call(jvc.free, &[handle]);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
973
2596
|
bcx.ins().return_(&[v]);
|
|
974
2597
|
terminated = true;
|
|
975
2598
|
ip += 1;
|
|
@@ -1007,6 +2630,46 @@ fn build_body_cfg(
|
|
|
1007
2630
|
terminated = true;
|
|
1008
2631
|
ip += 3;
|
|
1009
2632
|
}
|
|
2633
|
+
Opcode::SelfCall if array_mask != 0 => {
|
|
2634
|
+
// #187: array-mode recursive self-call (queens' `place` recursing on the SAME arrays).
|
|
2635
|
+
// Array args were staged in `arr_call_pending` and MUST be exactly this function's own
|
|
2636
|
+
// array params in ABI order — then the recursion reuses `handles_ptr`/`deopt_ptr`
|
|
2637
|
+
// unchanged and only re-marshals the numeric args (a fresh `numeric_ptr` per level).
|
|
2638
|
+
let sref = self_ref?;
|
|
2639
|
+
let num_arrays = arr_call_pending.len();
|
|
2640
|
+
let num_numeric = arity.checked_sub(num_arrays)?;
|
|
2641
|
+
let expected: Vec<usize> =
|
|
2642
|
+
(0..arity).filter(|k| (array_mask >> k) & 1 == 1).collect();
|
|
2643
|
+
if arr_call_pending != expected || stack.len() < num_numeric {
|
|
2644
|
+
return None;
|
|
2645
|
+
}
|
|
2646
|
+
arr_call_pending.clear();
|
|
2647
|
+
// Marshal the numeric args (top of the stack, in ABI order) into a fresh stack slot.
|
|
2648
|
+
let slot = bcx.create_sized_stack_slot(cranelift::codegen::ir::StackSlotData::new(
|
|
2649
|
+
cranelift::codegen::ir::StackSlotKind::ExplicitSlot,
|
|
2650
|
+
(num_numeric * 8) as u32,
|
|
2651
|
+
3, // 2^3 = 8-byte alignment for f64
|
|
2652
|
+
));
|
|
2653
|
+
let arg_start = stack.len() - num_numeric;
|
|
2654
|
+
for (j, (v, is_bool)) in stack.drain(arg_start..).enumerate() {
|
|
2655
|
+
if is_bool {
|
|
2656
|
+
return None; // a bool numeric arg doesn't match the f64 ABI
|
|
2657
|
+
}
|
|
2658
|
+
bcx.ins().stack_store(v, slot, (j * 8) as i32);
|
|
2659
|
+
}
|
|
2660
|
+
let num_ptr = bcx.ins().stack_addr(types::I64, slot, 0);
|
|
2661
|
+
let handles_ptr = *params.get(1)?;
|
|
2662
|
+
let deopt_ptr = *params.get(2)?;
|
|
2663
|
+
// #187: thread the RecurGuard (if this recursive array fn was compiled with one) so every
|
|
2664
|
+
// level re-checks the native stack at entry — a catchable RangeError, not a SIGSEGV.
|
|
2665
|
+
let mut call_args = vec![num_ptr, handles_ptr, deopt_ptr];
|
|
2666
|
+
if let Some(gp) = guard_ptr {
|
|
2667
|
+
call_args.push(gp);
|
|
2668
|
+
}
|
|
2669
|
+
let call = bcx.ins().call(sref, &call_args);
|
|
2670
|
+
stack.push((bcx.inst_results(call)[0], false));
|
|
2671
|
+
ip += 3;
|
|
2672
|
+
}
|
|
1010
2673
|
Opcode::SelfCall => {
|
|
1011
2674
|
// Recursive self-call → a native cranelift call to this very function. Args are the
|
|
1012
2675
|
// top `arity` f64 stack values; result is pushed. Validated above (self_ref present,
|
|
@@ -1023,11 +2686,170 @@ fn build_body_cfg(
|
|
|
1023
2686
|
}
|
|
1024
2687
|
call_args.push(v);
|
|
1025
2688
|
}
|
|
2689
|
+
// #381: thread the RecurGuard pointer through the recursive call so every level
|
|
2690
|
+
// re-checks the stack at its entry. Present iff this function was compiled guarded.
|
|
2691
|
+
if let Some(gp) = guard_ptr {
|
|
2692
|
+
call_args.push(gp);
|
|
2693
|
+
}
|
|
1026
2694
|
let call = bcx.ins().call(sref, &call_args);
|
|
1027
2695
|
let result = bcx.inst_results(call)[0];
|
|
1028
2696
|
stack.push((result, false));
|
|
1029
2697
|
ip += 3;
|
|
1030
2698
|
}
|
|
2699
|
+
Opcode::MathUnary => {
|
|
2700
|
+
// #186 — `Math.<fn>(x)`: pop the arg, emit native op / host call, push the result.
|
|
2701
|
+
let id = peek_u16(code, ip + 1)?;
|
|
2702
|
+
let mfn = MathUnaryFn::from_u16(id)?;
|
|
2703
|
+
let (x, _) = stack.pop()?;
|
|
2704
|
+
let r = emit_math_unary(&mut bcx, math_fref, mfn, x);
|
|
2705
|
+
stack.push((r, false));
|
|
2706
|
+
ip += 3;
|
|
2707
|
+
}
|
|
2708
|
+
// #189 — local-array ops, only for JV functions (`jv` = `Some`). Each consumes the array
|
|
2709
|
+
// HANDLE from `jv_pending` (its `LoadLocal`/`NewArray` pushed it) and calls `tish_jv_*`.
|
|
2710
|
+
Opcode::NewArray if jv.is_some() => {
|
|
2711
|
+
let jvc = jv?;
|
|
2712
|
+
if peek_u16(code, ip + 1)? != 0 {
|
|
2713
|
+
return None; // only empty `[]` is JV (classifier already guaranteed this)
|
|
2714
|
+
}
|
|
2715
|
+
let cap = bcx.ins().iconst(types::I64, 0);
|
|
2716
|
+
let call = bcx.ins().call(jvc.new, &[cap]);
|
|
2717
|
+
jv_pending.push(bcx.inst_results(call)[0]);
|
|
2718
|
+
ip += 3;
|
|
2719
|
+
}
|
|
2720
|
+
Opcode::GetIndex if !jv_pending.is_empty() => {
|
|
2721
|
+
let jvc = jv?;
|
|
2722
|
+
let idx = stack.pop()?.0;
|
|
2723
|
+
let handle = jv_pending.pop()?;
|
|
2724
|
+
// `idx as usize` (saturating: NaN/neg → 0), matching the VM's index coercion. OOB sets
|
|
2725
|
+
// the per-thread deopt flag inside `tish_jv_get` and returns NaN.
|
|
2726
|
+
let i = bcx.ins().fcvt_to_uint_sat(types::I64, idx);
|
|
2727
|
+
let call = bcx.ins().call(jvc.get, &[handle, i]);
|
|
2728
|
+
stack.push((bcx.inst_results(call)[0], false));
|
|
2729
|
+
ip += 1;
|
|
2730
|
+
}
|
|
2731
|
+
Opcode::SetIndex if !jv_pending.is_empty() => {
|
|
2732
|
+
let jvc = jv?;
|
|
2733
|
+
// Stack: [ (array→jv_pending), idx, val, dup_val ]. `Dup` left `dup_val` == `val`.
|
|
2734
|
+
let dup_val = stack.pop()?.0;
|
|
2735
|
+
let _val = stack.pop()?.0;
|
|
2736
|
+
let idx = stack.pop()?.0;
|
|
2737
|
+
let handle = jv_pending.pop()?;
|
|
2738
|
+
let i = bcx.ins().fcvt_to_uint_sat(types::I64, idx);
|
|
2739
|
+
bcx.ins().call(jvc.set, &[handle, i, dup_val]); // OOB → deopt flag inside tish_jv_set
|
|
2740
|
+
stack.push((dup_val, false)); // assignment yields the value
|
|
2741
|
+
ip += 1;
|
|
2742
|
+
}
|
|
2743
|
+
Opcode::GetMember if !jv_pending.is_empty() => {
|
|
2744
|
+
let jvc = jv?;
|
|
2745
|
+
let name = chunk.names.get(peek_u16(code, ip + 1)? as usize)?;
|
|
2746
|
+
let ptr = jv_pending.pop()?;
|
|
2747
|
+
match name.as_ref() {
|
|
2748
|
+
"length" => {
|
|
2749
|
+
let call = bcx.ins().call(jvc.len, &[ptr]);
|
|
2750
|
+
let len_i = bcx.inst_results(call)[0];
|
|
2751
|
+
stack.push((bcx.ins().fcvt_from_uint(types::F64, len_i), false));
|
|
2752
|
+
}
|
|
2753
|
+
"push" => pending_push = Some(ptr),
|
|
2754
|
+
_ => return None, // any other member of a JV array → bail
|
|
2755
|
+
}
|
|
2756
|
+
ip += 3;
|
|
2757
|
+
}
|
|
2758
|
+
Opcode::Call if pending_push.is_some() => {
|
|
2759
|
+
let jvc = jv?;
|
|
2760
|
+
if peek_u16(code, ip + 1)? != 1 {
|
|
2761
|
+
return None; // `push` takes exactly one arg in the JV fast path
|
|
2762
|
+
}
|
|
2763
|
+
let ptr = pending_push.take()?;
|
|
2764
|
+
let arg = stack.pop()?.0;
|
|
2765
|
+
bcx.ins().call(jvc.push, &[ptr, arg]);
|
|
2766
|
+
// `Array.push` returns the new length.
|
|
2767
|
+
let call = bcx.ins().call(jvc.len, &[ptr]);
|
|
2768
|
+
let len_i = bcx.inst_results(call)[0];
|
|
2769
|
+
stack.push((bcx.ins().fcvt_from_uint(types::F64, len_i), false));
|
|
2770
|
+
ip += 3;
|
|
2771
|
+
}
|
|
2772
|
+
// #187: `LoadVar name` where `name` is a resolved directly-callable callee — stage it for the
|
|
2773
|
+
// very next `Call`. Any other `LoadVar` (an unresolved global) bails to the interpreter.
|
|
2774
|
+
Opcode::LoadVar => {
|
|
2775
|
+
if pending_callee.is_some() {
|
|
2776
|
+
return None; // no nested resolved calls in v1
|
|
2777
|
+
}
|
|
2778
|
+
let name = chunk.names.get(peek_u16(code, ip + 1)? as usize)?;
|
|
2779
|
+
match resolved.get(name) {
|
|
2780
|
+
Some(&(fref, callee_arity)) => pending_callee = Some((fref, callee_arity)),
|
|
2781
|
+
None => return None,
|
|
2782
|
+
}
|
|
2783
|
+
ip += 3;
|
|
2784
|
+
}
|
|
2785
|
+
// #187: `Call argc` consuming a staged callee — a native cranelift call (like `SelfCall`,
|
|
2786
|
+
// minus the guard). Pops `argc` f64 args from the stack, pushes the f64 result.
|
|
2787
|
+
Opcode::Call if pending_callee.is_some() => {
|
|
2788
|
+
let (fref, callee_arity) = pending_callee.take()?;
|
|
2789
|
+
if peek_u16(code, ip + 1)? as u8 != callee_arity
|
|
2790
|
+
|| stack.len() < callee_arity as usize
|
|
2791
|
+
{
|
|
2792
|
+
return None;
|
|
2793
|
+
}
|
|
2794
|
+
let arg_start = stack.len() - callee_arity as usize;
|
|
2795
|
+
let mut call_args = Vec::with_capacity(callee_arity as usize);
|
|
2796
|
+
for (v, is_bool) in stack.drain(arg_start..) {
|
|
2797
|
+
if is_bool {
|
|
2798
|
+
return None; // a bool arg doesn't match the callee's f64 ABI
|
|
2799
|
+
}
|
|
2800
|
+
call_args.push(v);
|
|
2801
|
+
}
|
|
2802
|
+
let call = bcx.ins().call(fref, &call_args);
|
|
2803
|
+
stack.push((bcx.inst_results(call)[0], false));
|
|
2804
|
+
ip += 3;
|
|
2805
|
+
}
|
|
2806
|
+
// #187: a VOID array-mode function's implicit `return null` (the fall-through of a
|
|
2807
|
+
// side-effect writer like `multiplyAv`). Emit a dummy `f64` result — the wrapper returns
|
|
2808
|
+
// `Value::Null` for a void function, matching the interpreter — so the whole native body
|
|
2809
|
+
// (the array reads/writes + any cross-calls) runs instead of bailing on the null. MUST be
|
|
2810
|
+
// an actual return-null (next op is `Return`): a MID-BODY `let x = null` is also a
|
|
2811
|
+
// non-numeric `LoadConst`, and terminating on it would drop the rest of the function.
|
|
2812
|
+
Opcode::LoadConst
|
|
2813
|
+
if is_void
|
|
2814
|
+
&& Opcode::from_u8(*code.get(ip + 3)?)? == Opcode::Return
|
|
2815
|
+
&& !matches!(
|
|
2816
|
+
chunk.constants.get(peek_u16(code, ip + 1)? as usize),
|
|
2817
|
+
Some(Constant::Number(_)) | Some(Constant::Bool(_))
|
|
2818
|
+
) =>
|
|
2819
|
+
{
|
|
2820
|
+
let zero = bcx.ins().f64const(0.0);
|
|
2821
|
+
bcx.ins().return_(&[zero]);
|
|
2822
|
+
terminated = true; // the following `Return` (and any dead tail) is now skipped
|
|
2823
|
+
ip += 3;
|
|
2824
|
+
}
|
|
2825
|
+
// #189: a non-numeric constant load in a JV function — almost always the compiler's
|
|
2826
|
+
// trailing implicit `LoadConst Null; Return` epilogue after a `while (true)` loop. That
|
|
2827
|
+
// epilogue is the loop's never-taken exit edge: statically reachable (so it becomes a
|
|
2828
|
+
// cranelift block that must be filled), dynamically dead. A `null`/string can't live in
|
|
2829
|
+
// an f64 register, so instead of bailing the whole function we emit a deopt here — free
|
|
2830
|
+
// the live arrays, set the flag, and return. If the block truly never runs (infinite loop
|
|
2831
|
+
// with an inner `return`), cranelift keeps a dead branch and the hot path stays native;
|
|
2832
|
+
// if a non-numeric return is ever actually reached, the VM wrapper re-interprets and
|
|
2833
|
+
// produces the correct value. Sound either way, since the deopt path reproduces semantics.
|
|
2834
|
+
Opcode::LoadConst
|
|
2835
|
+
if jv.is_some()
|
|
2836
|
+
&& Opcode::from_u8(*code.get(ip + 3)?)? == Opcode::Return
|
|
2837
|
+
&& !matches!(
|
|
2838
|
+
chunk.constants.get(peek_u16(code, ip + 1)? as usize),
|
|
2839
|
+
Some(Constant::Number(_)) | Some(Constant::Bool(_))
|
|
2840
|
+
) =>
|
|
2841
|
+
{
|
|
2842
|
+
let jvc = jv?;
|
|
2843
|
+
for &slot in jvc.slots {
|
|
2844
|
+
let handle = bcx.use_var(*vars.get(slot)?);
|
|
2845
|
+
bcx.ins().call(jvc.free, &[handle]);
|
|
2846
|
+
}
|
|
2847
|
+
bcx.ins().call(jvc.deopt, &[]);
|
|
2848
|
+
let zero = bcx.ins().f64const(0.0);
|
|
2849
|
+
bcx.ins().return_(&[zero]);
|
|
2850
|
+
terminated = true; // the following `Return` (and any dead tail) is now skipped
|
|
2851
|
+
ip += 3;
|
|
2852
|
+
}
|
|
1031
2853
|
_ => match emit_simple_op(&mut bcx, chunk, code, &mut ip, &mut stack, ¶ms, arity) {
|
|
1032
2854
|
SimpleOp::Handled(_) => {}
|
|
1033
2855
|
_ => return None, // LoadConst/BinOp/UnaryOp handled; anything else → VM
|
|
@@ -1098,8 +2920,9 @@ fn build_body(
|
|
|
1098
2920
|
// THEN arm: straight-line ops until the trailing `Jump`.
|
|
1099
2921
|
let mut tip = p;
|
|
1100
2922
|
loop {
|
|
1101
|
-
match emit_simple_op(
|
|
1102
|
-
|
|
2923
|
+
match emit_simple_op(
|
|
2924
|
+
&mut bcx, chunk, code, &mut tip, &mut stack, ¶ms, arity,
|
|
2925
|
+
) {
|
|
1103
2926
|
SimpleOp::Handled(_) => continue,
|
|
1104
2927
|
SimpleOp::Unsupported => return None,
|
|
1105
2928
|
SimpleOp::NotSimple => break,
|
|
@@ -1120,8 +2943,9 @@ fn build_body(
|
|
|
1120
2943
|
// ELSE arm: straight-line ops from `jp` up to the merge point.
|
|
1121
2944
|
let mut eip = jp;
|
|
1122
2945
|
while eip < merge_target {
|
|
1123
|
-
match emit_simple_op(
|
|
1124
|
-
|
|
2946
|
+
match emit_simple_op(
|
|
2947
|
+
&mut bcx, chunk, code, &mut eip, &mut stack, ¶ms, arity,
|
|
2948
|
+
) {
|
|
1125
2949
|
SimpleOp::Handled(_) => continue,
|
|
1126
2950
|
_ => return None, // nested control flow / unsupported → VM
|
|
1127
2951
|
}
|
|
@@ -1140,6 +2964,12 @@ fn build_body(
|
|
|
1140
2964
|
stack.push((sel, then_b));
|
|
1141
2965
|
ip = merge_target;
|
|
1142
2966
|
}
|
|
2967
|
+
// #187: a `function name(x) { … }` block body wraps its statements in EnterBlock/ExitBlock
|
|
2968
|
+
// (+ loop-var markers) — pure scope bookkeeping with no runtime effect on a straight-line
|
|
2969
|
+
// numeric body. Skip them so leaf functions (e.g. a `sq(x)`/`evalA(i,j)` cross-call callee)
|
|
2970
|
+
// compile instead of bailing on the first marker. The real `Return` still ends the path.
|
|
2971
|
+
Opcode::EnterBlock | Opcode::ExitBlock | Opcode::LoopVarsEnd => ip += 1,
|
|
2972
|
+
Opcode::LoopVarsBegin => ip += 3,
|
|
1143
2973
|
// Loops / member / index / call / array / object → VM.
|
|
1144
2974
|
_ => return None,
|
|
1145
2975
|
}
|
|
@@ -1212,6 +3042,471 @@ mod tests {
|
|
|
1212
3042
|
find(&chunk).expect("the JIT must compile this arity-2 numeric fn (did it start bailing?)")
|
|
1213
3043
|
}
|
|
1214
3044
|
|
|
3045
|
+
/// #189: compile the first JV (function-local `f64` array) nested fn in `src` via `compile_chunk`,
|
|
3046
|
+
/// bypassing the address-keyed cache (see [`jit_arity2`]). Panics if none compiles JV — so a change
|
|
3047
|
+
/// that makes the classifier or lowering silently stop accepting the target fails loudly.
|
|
3048
|
+
fn jit_jv(src: &str) -> NumericFn {
|
|
3049
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3050
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3051
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
3052
|
+
fn compile_uncached(c: &Chunk) -> Option<NumericFn> {
|
|
3053
|
+
if !c.slot_based
|
|
3054
|
+
|| c.rest_param_index != NO_REST_PARAM
|
|
3055
|
+
|| c.param_count == 0
|
|
3056
|
+
|| c.param_count > 8
|
|
3057
|
+
{
|
|
3058
|
+
return None;
|
|
3059
|
+
}
|
|
3060
|
+
let lock = jit()?;
|
|
3061
|
+
let mut g = lock.lock().ok()?;
|
|
3062
|
+
compile_chunk(&mut g, c)
|
|
3063
|
+
}
|
|
3064
|
+
fn find(c: &Chunk) -> Option<NumericFn> {
|
|
3065
|
+
for n in &c.nested {
|
|
3066
|
+
if let Some(f) = compile_uncached(n) {
|
|
3067
|
+
if f.is_jv() {
|
|
3068
|
+
return Some(f);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
if let Some(f) = find(n) {
|
|
3072
|
+
return Some(f);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
None
|
|
3076
|
+
}
|
|
3077
|
+
find(&chunk).expect("the JIT must JV-compile this local-array fn (did it start bailing?)")
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
/// #189: the core JV path — a function-local array built with `push`, then read and written by
|
|
3081
|
+
/// index in a loop. `sum_{i=0}^{n-1}(2i+1) == n^2`, so the JIT'd result is checked against a
|
|
3082
|
+
/// closed form (independent of the interpreter) across sizes. `deopt` must stay clear (in-bounds).
|
|
3083
|
+
#[test]
|
|
3084
|
+
fn jit_jv_local_array_sum_matches_closed_form() {
|
|
3085
|
+
let f = jit_jv(
|
|
3086
|
+
"function build(n) {\n\
|
|
3087
|
+
let a = []\n\
|
|
3088
|
+
for (let i = 0; i < n; i = i + 1) { a.push(i * 2) }\n\
|
|
3089
|
+
let s = 0\n\
|
|
3090
|
+
let j = 0\n\
|
|
3091
|
+
while (j < n) { a[j] = a[j] + 1; s = s + a[j]; j = j + 1 }\n\
|
|
3092
|
+
return s\n\
|
|
3093
|
+
}\n\
|
|
3094
|
+
build(0)\n",
|
|
3095
|
+
);
|
|
3096
|
+
assert!(f.is_jv(), "expected a JV-compiled fn");
|
|
3097
|
+
for n in [1.0f64, 5.0, 20.0, 100.0] {
|
|
3098
|
+
jv_reset_deopt();
|
|
3099
|
+
let r = f.call(&[n]);
|
|
3100
|
+
assert!(!jv_take_deopt(), "no OOB expected for n={n}");
|
|
3101
|
+
assert_eq!(r, n * n, "JV local-array sum wrong for n={n}");
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/// #189: a `while (true)` whose only exit is an inner `return` — the compiler still emits a trailing
|
|
3106
|
+
/// implicit `LoadConst Null; Return` epilogue (the loop's never-taken exit edge). That epilogue must
|
|
3107
|
+
/// NOT bail JV compilation; it's routed to a deopt. Without the fix this fn falls back to the VM.
|
|
3108
|
+
#[test]
|
|
3109
|
+
fn jit_jv_while_true_epilogue_compiles() {
|
|
3110
|
+
let f = jit_jv(
|
|
3111
|
+
"function f(n) {\n\
|
|
3112
|
+
let a = []\n\
|
|
3113
|
+
a.push(0)\n\
|
|
3114
|
+
let i = 0\n\
|
|
3115
|
+
while (true) {\n\
|
|
3116
|
+
a[0] = a[0] + 1\n\
|
|
3117
|
+
i = i + 1\n\
|
|
3118
|
+
if (i >= n) { return a[0] }\n\
|
|
3119
|
+
}\n\
|
|
3120
|
+
}\n\
|
|
3121
|
+
f(0)\n",
|
|
3122
|
+
);
|
|
3123
|
+
assert!(f.is_jv(), "while(true)+return JV fn must compile, not bail");
|
|
3124
|
+
jv_reset_deopt();
|
|
3125
|
+
assert_eq!(f.call(&[7.0]), 7.0);
|
|
3126
|
+
assert!(!jv_take_deopt());
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
/// #189: an out-of-bounds index makes the host `tish_jv_get` set the deopt flag (and return a
|
|
3130
|
+
/// sentinel); the VM wrapper then discards the result and re-interprets. Sound because JV arrays
|
|
3131
|
+
/// never escape, so the partially-run native attempt mutated nothing observable.
|
|
3132
|
+
#[test]
|
|
3133
|
+
fn jit_jv_oob_read_sets_deopt_flag() {
|
|
3134
|
+
// A loop that reads `a[i]` for `i` in `0..n`, but the array only has 2 elements — so `n > 2`
|
|
3135
|
+
// reads out of bounds. (Needs a loop: the CFG JIT only engages on loop/self-call fns.)
|
|
3136
|
+
let f = jit_jv(
|
|
3137
|
+
"function f(n) {\n\
|
|
3138
|
+
let a = []\n\
|
|
3139
|
+
a.push(10)\n\
|
|
3140
|
+
a.push(20)\n\
|
|
3141
|
+
let s = 0\n\
|
|
3142
|
+
for (let i = 0; i < n; i = i + 1) { s = s + a[i] }\n\
|
|
3143
|
+
return s\n\
|
|
3144
|
+
}\n\
|
|
3145
|
+
f(0)\n",
|
|
3146
|
+
);
|
|
3147
|
+
jv_reset_deopt();
|
|
3148
|
+
assert_eq!(f.call(&[2.0]), 30.0, "a[0]+a[1] == 30");
|
|
3149
|
+
assert!(!jv_take_deopt(), "n=2 stays in bounds");
|
|
3150
|
+
jv_reset_deopt();
|
|
3151
|
+
let _ = f.call(&[5.0]); // reads a[2] (past the 2-element array) → OOB
|
|
3152
|
+
assert!(jv_take_deopt(), "OOB array access must set the deopt flag");
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
/// #187: compile the first arity-1 plain-numeric (non-JV, non-array) fn in `src` via `compile_chunk`.
|
|
3156
|
+
fn jit_numeric1(src: &str) -> NumericFn {
|
|
3157
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3158
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3159
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
3160
|
+
fn compile_uncached(c: &Chunk) -> Option<NumericFn> {
|
|
3161
|
+
if !c.slot_based || c.rest_param_index != NO_REST_PARAM || c.param_count != 1 {
|
|
3162
|
+
return None;
|
|
3163
|
+
}
|
|
3164
|
+
let lock = jit()?;
|
|
3165
|
+
let mut g = lock.lock().ok()?;
|
|
3166
|
+
compile_chunk(&mut g, c)
|
|
3167
|
+
}
|
|
3168
|
+
fn find(c: &Chunk) -> Option<NumericFn> {
|
|
3169
|
+
for n in &c.nested {
|
|
3170
|
+
if let Some(f) = compile_uncached(n) {
|
|
3171
|
+
if !f.is_jv() && f.array_param_mask() == 0 {
|
|
3172
|
+
return Some(f);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
if let Some(f) = find(n) {
|
|
3176
|
+
return Some(f);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
None
|
|
3180
|
+
}
|
|
3181
|
+
find(&chunk).expect("the JIT must compile this arity-1 numeric fn (did it start bailing?)")
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/// #187: a boolean SCALAR local (`let done = false; … done = true; if (done) …`) must JIT — the
|
|
3185
|
+
/// bool is represented as an `f64` 0/1 and drives `if (done)` via the falsy check. Result is the
|
|
3186
|
+
/// numeric accumulator, checked against a closed form (`sum_{i<n} i == n(n-1)/2`), independent of
|
|
3187
|
+
/// the interpreter. Without bool slots this fn bails at the `done = false` StoreLocal → interprets.
|
|
3188
|
+
#[test]
|
|
3189
|
+
fn jit_bool_scalar_slot_flips_and_matches() {
|
|
3190
|
+
// Bounded loop (not `while(true)`) so this exercises the bool slot in isolation — a bool slot
|
|
3191
|
+
// `done` set inside a branch, then driving `if (done)`. `sum_{i<n} i == n(n-1)/2` for all n≥0.
|
|
3192
|
+
let f = jit_numeric1(
|
|
3193
|
+
"function f(n) {\n\
|
|
3194
|
+
let done = false\n\
|
|
3195
|
+
let acc = 0\n\
|
|
3196
|
+
for (let i = 0; i < n; i = i + 1) {\n\
|
|
3197
|
+
acc = acc + i\n\
|
|
3198
|
+
if (i + 1 >= n) { done = true }\n\
|
|
3199
|
+
}\n\
|
|
3200
|
+
if (done) { return acc }\n\
|
|
3201
|
+
return 0\n\
|
|
3202
|
+
}\n\
|
|
3203
|
+
f(0)\n",
|
|
3204
|
+
);
|
|
3205
|
+
for n in [0i64, 1, 2, 10, 100, 1000] {
|
|
3206
|
+
let expect = (n * (n - 1) / 2) as f64;
|
|
3207
|
+
assert_eq!(
|
|
3208
|
+
f.call(&[n as f64]),
|
|
3209
|
+
expect,
|
|
3210
|
+
"bool-slot loop wrong for n={n}"
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
/// #187 soundness: a `bool === number` compare must NOT be JIT'd to an `f64` compare (JS says
|
|
3216
|
+
/// `0 === false` / `true === 1` is FALSE across types, but the bits `1.0 === 1.0` are equal). The
|
|
3217
|
+
/// equality guard in `emit_simple_op` bails such a function to the interpreter. This uses a LOOP so
|
|
3218
|
+
/// it reaches the CFG JIT (+ the guard); WITHOUT the guard it would compile and wrongly count every
|
|
3219
|
+
/// iteration (returning n), so the closed-form assertion catches a regression. WITH the guard it
|
|
3220
|
+
/// bails (no `NumericFn`), and the assertion is vacuously satisfied — either way it never miscompiles.
|
|
3221
|
+
#[test]
|
|
3222
|
+
fn jit_bool_eq_number_never_miscompiles() {
|
|
3223
|
+
let src = "function g(n) {\n\
|
|
3224
|
+
let flag = false\n\
|
|
3225
|
+
let hits = 0\n\
|
|
3226
|
+
for (let i = 0; i < n; i = i + 1) {\n\
|
|
3227
|
+
flag = i >= 0\n\
|
|
3228
|
+
if (flag === 1) { hits = hits + 1 }\n\
|
|
3229
|
+
}\n\
|
|
3230
|
+
return hits\n\
|
|
3231
|
+
}\n\
|
|
3232
|
+
g(0)\n";
|
|
3233
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3234
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3235
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
3236
|
+
fn first_num1(c: &Chunk) -> Option<NumericFn> {
|
|
3237
|
+
for n in &c.nested {
|
|
3238
|
+
if n.slot_based && n.param_count == 1 {
|
|
3239
|
+
if let Some(g) =
|
|
3240
|
+
jit().and_then(|l| l.lock().ok().and_then(|mut g| compile_chunk(&mut g, n)))
|
|
3241
|
+
{
|
|
3242
|
+
return Some(g);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if let Some(g) = first_num1(n) {
|
|
3246
|
+
return Some(g);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
None
|
|
3250
|
+
}
|
|
3251
|
+
if let Some(g) = first_num1(&chunk) {
|
|
3252
|
+
// `(i>=0) === 1` is always FALSE in JS, so `hits` must stay 0 — never `n`.
|
|
3253
|
+
assert_eq!(
|
|
3254
|
+
g.call(&[5.0]),
|
|
3255
|
+
0.0,
|
|
3256
|
+
"`bool === 1` must be false → hits stays 0"
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
/// #187: compile the first array-mode fn (`array_param_mask != 0`) in `src` via `compile_chunk`.
|
|
3262
|
+
fn jit_arrays(src: &str) -> NumericFn {
|
|
3263
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3264
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3265
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
3266
|
+
fn compile_uncached(c: &Chunk) -> Option<NumericFn> {
|
|
3267
|
+
if !c.slot_based || c.rest_param_index != NO_REST_PARAM || c.param_count == 0 {
|
|
3268
|
+
return None;
|
|
3269
|
+
}
|
|
3270
|
+
let lock = jit()?;
|
|
3271
|
+
let mut g = lock.lock().ok()?;
|
|
3272
|
+
compile_chunk(&mut g, c)
|
|
3273
|
+
}
|
|
3274
|
+
fn find(c: &Chunk) -> Option<NumericFn> {
|
|
3275
|
+
for n in &c.nested {
|
|
3276
|
+
if let Some(f) = compile_uncached(n) {
|
|
3277
|
+
if f.array_param_mask() != 0 {
|
|
3278
|
+
return Some(f);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
if let Some(f) = find(n) {
|
|
3282
|
+
return Some(f);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
None
|
|
3286
|
+
}
|
|
3287
|
+
find(&chunk)
|
|
3288
|
+
.expect("the JIT must array-compile this fn (did classify_params stop matching?)")
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
/// #187: an array param written as `dst[i] = v` is stored into and copied back. Here `copy` reads
|
|
3292
|
+
/// `src[i]` into `dst[i]`; after `call_arrays` the caller's `dst` buffer must equal `src`, and the
|
|
3293
|
+
/// writable mask must flag `dst` (so [`try_call_array_jit`] knows to write it back).
|
|
3294
|
+
#[test]
|
|
3295
|
+
fn jit_array_param_write_and_readback() {
|
|
3296
|
+
let f = jit_arrays(
|
|
3297
|
+
"function copy(n, src, dst) {\n\
|
|
3298
|
+
let i = 0\n\
|
|
3299
|
+
while (i < n) { let x = src[i]; dst[i] = x; i = i + 1 }\n\
|
|
3300
|
+
return dst[0]\n\
|
|
3301
|
+
}\n\
|
|
3302
|
+
copy(0, [], [])\n",
|
|
3303
|
+
);
|
|
3304
|
+
assert!(f.array_param_mask() != 0, "src/dst are array params");
|
|
3305
|
+
assert_ne!(f.array_writable_mask(), 0, "dst must be flagged writable");
|
|
3306
|
+
let mut src = [10.0f64, 20.0, 30.0];
|
|
3307
|
+
let mut dst = [0.0f64; 3];
|
|
3308
|
+
// arity 3 = [n (numeric), src (array), dst (array)]. numeric = [n]; arrays in param order.
|
|
3309
|
+
let handles = [
|
|
3310
|
+
ArrayHandle {
|
|
3311
|
+
ptr: src.as_mut_ptr(),
|
|
3312
|
+
len: 3,
|
|
3313
|
+
},
|
|
3314
|
+
ArrayHandle {
|
|
3315
|
+
ptr: dst.as_mut_ptr(),
|
|
3316
|
+
len: 3,
|
|
3317
|
+
},
|
|
3318
|
+
];
|
|
3319
|
+
let (res, deopt) = f.call_arrays(&[3.0], &handles);
|
|
3320
|
+
assert!(!deopt, "in-bounds writes never deopt");
|
|
3321
|
+
assert_eq!(dst, [10.0, 20.0, 30.0], "dst must be overwritten with src");
|
|
3322
|
+
assert_eq!(res, 10.0, "returns dst[0]");
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
/// #187: an out-of-bounds array-param WRITE sets the deopt flag (the wrapper then discards the
|
|
3326
|
+
/// scratch — no partial writeback — and re-interprets).
|
|
3327
|
+
#[test]
|
|
3328
|
+
fn jit_array_param_write_oob_deopts() {
|
|
3329
|
+
let f = jit_arrays(
|
|
3330
|
+
"function copy(n, src, dst) {\n\
|
|
3331
|
+
let i = 0\n\
|
|
3332
|
+
while (i < n) { let x = src[i]; dst[i] = x; i = i + 1 }\n\
|
|
3333
|
+
return dst[0]\n\
|
|
3334
|
+
}\n\
|
|
3335
|
+
copy(0, [], [])\n",
|
|
3336
|
+
);
|
|
3337
|
+
let mut src = [1.0f64; 5];
|
|
3338
|
+
let mut dst = [0.0f64; 2]; // only 2 elements — a write to dst[2] is OOB
|
|
3339
|
+
let handles = [
|
|
3340
|
+
ArrayHandle {
|
|
3341
|
+
ptr: src.as_mut_ptr(),
|
|
3342
|
+
len: 5,
|
|
3343
|
+
},
|
|
3344
|
+
ArrayHandle {
|
|
3345
|
+
ptr: dst.as_mut_ptr(),
|
|
3346
|
+
len: 2,
|
|
3347
|
+
},
|
|
3348
|
+
];
|
|
3349
|
+
let (_res, deopt) = f.call_arrays(&[5.0], &handles); // writes dst[0..5) into a len-2 array
|
|
3350
|
+
assert!(deopt, "OOB array-param write must deopt");
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
/// #187: a caller lowers a `name(args)` call to a stable register-`f64` callee into a native
|
|
3354
|
+
/// cranelift call. Compiles `sq` first (registering it), then `sumSq` which calls it; the result
|
|
3355
|
+
/// (`sum_{i<n} i^2 == (n-1)n(2n-1)/6`) is checked against a closed form independent of the interp.
|
|
3356
|
+
#[test]
|
|
3357
|
+
fn jit_cross_function_call_matches_closed_form() {
|
|
3358
|
+
let src = "function sq(x) { return x * x }\n\
|
|
3359
|
+
function sumSq(n) {\n\
|
|
3360
|
+
let s = 0\n\
|
|
3361
|
+
let i = 0\n\
|
|
3362
|
+
while (i < n) { s = s + sq(i); i = i + 1 }\n\
|
|
3363
|
+
return s\n\
|
|
3364
|
+
}\n\
|
|
3365
|
+
sumSq(0)\n";
|
|
3366
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3367
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3368
|
+
let chunk = tishlang_bytecode::compile(&opt).expect("compile");
|
|
3369
|
+
// Compile every nested fn in source order (sq before sumSq) so sq registers before sumSq
|
|
3370
|
+
// resolves it. `compile_chunk` bypasses the address cache, and cross-call fns aren't cached.
|
|
3371
|
+
let lock = jit().expect("jit available");
|
|
3372
|
+
let mut sumsq: Option<NumericFn> = None;
|
|
3373
|
+
for n in &chunk.nested {
|
|
3374
|
+
let mut g = lock.lock().unwrap();
|
|
3375
|
+
if let Some(f) = compile_chunk(&mut g, n) {
|
|
3376
|
+
if n.global_name.as_deref() == Some("sumSq") {
|
|
3377
|
+
sumsq = Some(f);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
let f =
|
|
3382
|
+
sumsq.expect("sumSq must compile with the resolved `sq` call (did resolution break?)");
|
|
3383
|
+
for n in [1i64, 5, 20, 50] {
|
|
3384
|
+
let expect = ((n - 1) * n * (2 * n - 1) / 6) as f64;
|
|
3385
|
+
assert_eq!(
|
|
3386
|
+
f.call(&[n as f64]),
|
|
3387
|
+
expect,
|
|
3388
|
+
"cross-call sumSq wrong for n={n}"
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
/// #187: an array-mode function that is SELF-RECURSIVE and passes a BOOL array param back to itself
|
|
3394
|
+
/// (queens' `place` shape). Exercises: bool-array element read (`!cols[col]`), bool-array write
|
|
3395
|
+
/// (`cols[col] = true/false`), and the native array self-call (reusing `handles_ptr`, re-marshalling
|
|
3396
|
+
/// the numeric args). `place(n, 0, all-false)` counts permutations = n!.
|
|
3397
|
+
#[test]
|
|
3398
|
+
fn jit_array_mode_recursion_over_bool_array() {
|
|
3399
|
+
let f = jit_arrays(
|
|
3400
|
+
"function place(n, row, cols) {\n\
|
|
3401
|
+
if (row === n) { return 1 }\n\
|
|
3402
|
+
let count = 0\n\
|
|
3403
|
+
for (let col = 0; col < n; col = col + 1) {\n\
|
|
3404
|
+
if (!cols[col]) {\n\
|
|
3405
|
+
cols[col] = true\n\
|
|
3406
|
+
count = count + place(n, row + 1, cols)\n\
|
|
3407
|
+
cols[col] = false\n\
|
|
3408
|
+
}\n\
|
|
3409
|
+
}\n\
|
|
3410
|
+
return count\n\
|
|
3411
|
+
}\n\
|
|
3412
|
+
place(0, 0, [])\n",
|
|
3413
|
+
);
|
|
3414
|
+
assert!(f.array_param_mask() != 0, "cols is an array param");
|
|
3415
|
+
assert!(
|
|
3416
|
+
f.recur_guarded(),
|
|
3417
|
+
"place is self-recursive → compiled with a trailing RecurGuard param"
|
|
3418
|
+
);
|
|
3419
|
+
// n! for n = 0..6 (cols starts all-false = 0.0). Each call resets `cols` to all-false on return.
|
|
3420
|
+
let factorial = [1.0f64, 1.0, 2.0, 6.0, 24.0, 120.0, 720.0];
|
|
3421
|
+
for (n, &expect) in factorial.iter().enumerate() {
|
|
3422
|
+
let mut cols = vec![0.0f64; n.max(1)];
|
|
3423
|
+
let handles = [ArrayHandle {
|
|
3424
|
+
ptr: cols.as_mut_ptr(),
|
|
3425
|
+
len: n,
|
|
3426
|
+
}];
|
|
3427
|
+
// `stack_limit = 0` ⇒ the entry SP-check never trips (a real SP is always ≥ 0).
|
|
3428
|
+
let mut guard = RecurGuard {
|
|
3429
|
+
stack_limit: 0,
|
|
3430
|
+
tripped: 0,
|
|
3431
|
+
};
|
|
3432
|
+
let (res, deopt) = f.call_arrays_guarded(&[n as f64, 0.0], &handles, &mut guard);
|
|
3433
|
+
assert!(!deopt, "in-bounds, no deopt for n={n}");
|
|
3434
|
+
assert_eq!(
|
|
3435
|
+
guard.tripped, 0,
|
|
3436
|
+
"shallow recursion must not trip the guard"
|
|
3437
|
+
);
|
|
3438
|
+
assert_eq!(res, expect, "place({n},0,cols) = {n}! = {expect}");
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
/// #187: the write-KIND classification that pins the array-mode writeback's soundness. The JIT
|
|
3443
|
+
/// flattens every element to `f64`, so `try_call_array_jit` re-boxes each written element by the
|
|
3444
|
+
/// runtime array's element type — which is only sound when the statically written kind (bool vs
|
|
3445
|
+
/// number) matches. `classify_params` must therefore report, per writable array, whether it is
|
|
3446
|
+
/// written with a bool (a `LoadConst(Bool)` or a `LoadLocal(bool-slot)`), and reject a single array
|
|
3447
|
+
/// written with BOTH a bool and a number (unrepresentable as one flat re-boxing).
|
|
3448
|
+
#[test]
|
|
3449
|
+
fn classify_params_bool_write_kinds() {
|
|
3450
|
+
// The arity-2 array fn `f(a, n)` from `src` (already the nested chunk).
|
|
3451
|
+
fn arr_fn(src: &str) -> Chunk {
|
|
3452
|
+
fn_chunk(src)
|
|
3453
|
+
}
|
|
3454
|
+
let bit0 = |m: u8| m & 1;
|
|
3455
|
+
|
|
3456
|
+
// A bool CONST write ⇒ array bit, writable bit, AND bool-write bit all set.
|
|
3457
|
+
let c = arr_fn(
|
|
3458
|
+
"function f(a, n) { for (let i = 0; i < n; i = i + 1) { a[i] = true } return 0 }\nf([0], 1)\n",
|
|
3459
|
+
);
|
|
3460
|
+
let (am, wm, bm) = classify_params(&c, c.param_count as usize);
|
|
3461
|
+
assert_eq!(
|
|
3462
|
+
(bit0(am), bit0(wm), bit0(bm)),
|
|
3463
|
+
(1, 1, 1),
|
|
3464
|
+
"bool-const write"
|
|
3465
|
+
);
|
|
3466
|
+
|
|
3467
|
+
// A number CONST write ⇒ array + writable set, bool-write CLEAR.
|
|
3468
|
+
let c = arr_fn(
|
|
3469
|
+
"function f(a, n) { for (let i = 0; i < n; i = i + 1) { a[i] = 7 } return 0 }\nf([0], 1)\n",
|
|
3470
|
+
);
|
|
3471
|
+
let (am, wm, bm) = classify_params(&c, c.param_count as usize);
|
|
3472
|
+
assert_eq!(
|
|
3473
|
+
(bit0(am), bit0(wm), bit0(bm)),
|
|
3474
|
+
(1, 1, 0),
|
|
3475
|
+
"number-const write"
|
|
3476
|
+
);
|
|
3477
|
+
|
|
3478
|
+
// A bool LOCAL write ⇒ the whole fn bails: `classify_bool_slots` is a superset (a bool-tagged
|
|
3479
|
+
// slot may be reassigned a number), so a `LoadLocal(bool-slot)` written value can't be typed for
|
|
3480
|
+
// the flat writeback. Reject rather than risk re-boxing wrong.
|
|
3481
|
+
let c = arr_fn(
|
|
3482
|
+
"function f(a, n) { let b = true; for (let i = 0; i < n; i = i + 1) { a[i] = b } return 0 }\nf([0], 1)\n",
|
|
3483
|
+
);
|
|
3484
|
+
assert_eq!(
|
|
3485
|
+
classify_params(&c, c.param_count as usize),
|
|
3486
|
+
(0, 0, 0),
|
|
3487
|
+
"bool-local write must bail (bool-tagged slot may hold a number)"
|
|
3488
|
+
);
|
|
3489
|
+
|
|
3490
|
+
// A number LOCAL write ⇒ bool-write CLEAR.
|
|
3491
|
+
let c = arr_fn(
|
|
3492
|
+
"function f(a, n) { let x = n + 1; for (let i = 0; i < n; i = i + 1) { a[i] = x } return 0 }\nf([0], 1)\n",
|
|
3493
|
+
);
|
|
3494
|
+
let (am, wm, bm) = classify_params(&c, c.param_count as usize);
|
|
3495
|
+
assert_eq!(
|
|
3496
|
+
(bit0(am), bit0(wm), bit0(bm)),
|
|
3497
|
+
(1, 1, 0),
|
|
3498
|
+
"number-local write"
|
|
3499
|
+
);
|
|
3500
|
+
|
|
3501
|
+
// Mixed bool + number writes to the SAME array ⇒ the whole function is rejected.
|
|
3502
|
+
let c = arr_fn("function f(a, n) { a[0] = true; a[1] = 5; return 0 }\nf([0, 0], 1)\n");
|
|
3503
|
+
assert_eq!(
|
|
3504
|
+
classify_params(&c, c.param_count as usize),
|
|
3505
|
+
(0, 0, 0),
|
|
3506
|
+
"mixed bool+number writes must bail"
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
|
|
1215
3510
|
/// Regression for the address-reuse stale hit (#247): compile one function, then overwrite the SAME
|
|
1216
3511
|
/// heap `Chunk` (same address = the cache key) with a *different* function — what a long-lived
|
|
1217
3512
|
/// process (REPL / multi-script embedder) does when a freed chunk address is reused. Before the
|
|
@@ -1239,14 +3534,116 @@ mod tests {
|
|
|
1239
3534
|
let shr = jit_arity2("const f = (a, b) => a >> b\nf(0, 0)\n");
|
|
1240
3535
|
let ushr = jit_arity2("const f = (a, b) => a >>> b\nf(0, 0)\n");
|
|
1241
3536
|
let cases = [
|
|
1242
|
-
(1.0, 4.0),
|
|
1243
|
-
(
|
|
1244
|
-
(
|
|
3537
|
+
(1.0, 4.0),
|
|
3538
|
+
(1.0, 32.0),
|
|
3539
|
+
(1.0, 33.0),
|
|
3540
|
+
(1.0, -1.0),
|
|
3541
|
+
(-8.0, 1.0),
|
|
3542
|
+
(-1.0, 0.0),
|
|
3543
|
+
(-2.0, 1.0),
|
|
3544
|
+
(4294967295.0, 0.0),
|
|
3545
|
+
(3.9, 0.0),
|
|
3546
|
+
(4294967297.0, 0.0),
|
|
3547
|
+
(-123456789.0, 5.0),
|
|
3548
|
+
(65535.0, 16.0),
|
|
1245
3549
|
];
|
|
1246
3550
|
for (a, b) in cases {
|
|
1247
|
-
assert_eq!(
|
|
1248
|
-
|
|
1249
|
-
|
|
3551
|
+
assert_eq!(
|
|
3552
|
+
shl.call(&[a, b]),
|
|
3553
|
+
to_int32(a).wrapping_shl(to_uint32(b)) as f64,
|
|
3554
|
+
"<< {a} {b}"
|
|
3555
|
+
);
|
|
3556
|
+
assert_eq!(
|
|
3557
|
+
shr.call(&[a, b]),
|
|
3558
|
+
to_int32(a).wrapping_shr(to_uint32(b)) as f64,
|
|
3559
|
+
">> {a} {b}"
|
|
3560
|
+
);
|
|
3561
|
+
assert_eq!(
|
|
3562
|
+
ushr.call(&[a, b]),
|
|
3563
|
+
to_uint32(a).wrapping_shr(to_uint32(b)) as f64,
|
|
3564
|
+
">>> {a} {b}"
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
/// Top-level chunk of compiled `src` (where hot loops live) — #190 OSR targets these.
|
|
3570
|
+
fn top_chunk(src: &str) -> Chunk {
|
|
3571
|
+
let prog = tishlang_parser::parse(src).expect("parse");
|
|
3572
|
+
let opt = tishlang_opt::optimize(&prog);
|
|
3573
|
+
tishlang_bytecode::compile(&opt).expect("compile")
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
/// `(header_ip, region_end)` of the FIRST loop in `chunk` — its first `JumpBack` names the region
|
|
3577
|
+
/// the OSR trigger would compile.
|
|
3578
|
+
fn first_region(chunk: &Chunk) -> (usize, usize) {
|
|
3579
|
+
let code = &chunk.code;
|
|
3580
|
+
let mut ip = 0;
|
|
3581
|
+
while ip < code.len() {
|
|
3582
|
+
let op = Opcode::from_u8(code[ip]).expect("op");
|
|
3583
|
+
if op == Opcode::JumpBack {
|
|
3584
|
+
let dist = peek_u16(code, ip + 1).unwrap() as usize;
|
|
3585
|
+
let region_end = ip + 3;
|
|
3586
|
+
return (region_end - dist, region_end);
|
|
3587
|
+
}
|
|
3588
|
+
ip += op.instruction_size(code, ip).unwrap_or(1);
|
|
1250
3589
|
}
|
|
3590
|
+
panic!("no JumpBack (loop) in chunk");
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
/// #190 — the region compiler + `LoopFn` ABI run a real numeric loop end-to-end (independent of
|
|
3594
|
+
/// the VM dispatch loop): `while (i < 10) { s = s + i; i = i + 1 }` from all-zero live-ins must
|
|
3595
|
+
/// leave `s = 45`, `i = 10` in the live slots and return an in-range exit id, without deopting.
|
|
3596
|
+
#[test]
|
|
3597
|
+
fn osr_region_runs_numeric_loop() {
|
|
3598
|
+
let chunk =
|
|
3599
|
+
top_chunk("let s = 0.0\nlet i = 0.0\nwhile (i < 10.0) { s = s + i; i = i + 1.0 }\n");
|
|
3600
|
+
let (header, end) = first_region(&chunk);
|
|
3601
|
+
let lf = try_compile_loop(&chunk, header, end).expect("pure-numeric loop must OSR-compile");
|
|
3602
|
+
assert!(!lf.used_slots.is_empty() && !lf.exits.is_empty());
|
|
3603
|
+
let mut buf = vec![0.0f64; lf.used_slots.len()];
|
|
3604
|
+
let mut deopt = 0u8;
|
|
3605
|
+
let exit = lf.call(&mut buf, &mut deopt);
|
|
3606
|
+
assert!((exit as usize) < lf.exits.len(), "exit id in range");
|
|
3607
|
+
assert_eq!(deopt, 0, "v1 region never sets the deopt flag");
|
|
3608
|
+
let mut got = buf.clone();
|
|
3609
|
+
got.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
3610
|
+
assert_eq!(
|
|
3611
|
+
got,
|
|
3612
|
+
vec![10.0, 45.0],
|
|
3613
|
+
"s=45 (sum 0..9), i=10 after the loop"
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
/// #190 — a loop that touches a non-slot value (a general call) is not pure-numeric slot math, so
|
|
3618
|
+
/// the region must be rejected (negative-cached) and the VM keeps interpreting. `Math.max` is a
|
|
3619
|
+
/// 2-arg call (NOT the #186 unary intrinsic), so it stays a `Call` the region must reject.
|
|
3620
|
+
#[test]
|
|
3621
|
+
fn osr_region_rejects_calls() {
|
|
3622
|
+
let chunk = top_chunk(
|
|
3623
|
+
"let a = 0.0\nfor (let i = 0; i < 100; i = i + 1) { a = a + Math.max(i, 2.0) }\n",
|
|
3624
|
+
);
|
|
3625
|
+
let (header, end) = first_region(&chunk);
|
|
3626
|
+
assert!(
|
|
3627
|
+
try_compile_loop(&chunk, header, end).is_none(),
|
|
3628
|
+
"a loop containing a general call must not OSR-compile"
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
/// #190 — a loop with nested branches compiles and computes correctly through multiple blocks:
|
|
3633
|
+
/// `while (i < 20) { if (i % 2 == 0) s = s + i; i = i + 1 }` → s = sum of evens in 0..19 = 90.
|
|
3634
|
+
#[test]
|
|
3635
|
+
fn osr_region_handles_branches() {
|
|
3636
|
+
let chunk = top_chunk(
|
|
3637
|
+
"let s = 0.0\nlet i = 0.0\nwhile (i < 20.0) { if (i % 2.0 == 0.0) { s = s + i }; i = i + 1.0 }\n",
|
|
3638
|
+
);
|
|
3639
|
+
let (header, end) = first_region(&chunk);
|
|
3640
|
+
let lf =
|
|
3641
|
+
try_compile_loop(&chunk, header, end).expect("branchy numeric loop must OSR-compile");
|
|
3642
|
+
let mut buf = vec![0.0f64; lf.used_slots.len()];
|
|
3643
|
+
let mut deopt = 0u8;
|
|
3644
|
+
lf.call(&mut buf, &mut deopt);
|
|
3645
|
+
let mut got = buf.clone();
|
|
3646
|
+
got.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
3647
|
+
assert_eq!(got, vec![20.0, 90.0], "s=90 (0+2+…+18), i=20");
|
|
1251
3648
|
}
|
|
1252
3649
|
}
|