@tishlang/tish-format 1.0.13 → 2.0.1

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