logicaffeine_compile/codegen_sva/soft_core_model.rs
1//! The cycle-accurate Rust model of `benchmarks/silicon/soft_core.v` — the executable spec of the
2//! LOGOS universal interpreter in silicon. Fetch/decode/execute over a chip-word program
3//! (`[opcode:8][dst:8][a:8][b:8]`; wide/float/heap ops per the doc on [`run_soft_core_typed`]),
4//! returning the `Show` sequence. This is the differential ORACLE the whole spatial-interpreter
5//! campaign checks against: `compile_to_full_words` -> [`run_soft_core_typed`] must equal
6//! `compile::vm_outcome` for every program, and the iverilog testbenches agree with it. Lifted here
7//! (from `rtl.rs`'s test module) so both the in-crate RTL tests and the `logicaffeine_tests`
8//! integration locks share ONE oracle.
9
10/// The Rust model of `alu.v` (32-bit), mirroring `emit_alu_datapath_verilog` exactly (wrapping /
11/// signed). Div / ExactDiv / Mod / FloorDiv are NOT here — they route to [`soft_core_divider`]
12/// (mirroring the soft-core's `shared_divider`). DivPow2 stays (a cheap arithmetic shift).
13pub fn soft_core_alu(opcode: u8, a: i32, b: i32) -> i32 {
14 match opcode {
15 8 | 9 => a.wrapping_add(b), // Add / AddAssign
16 10 => a.wrapping_sub(b), // Sub
17 11 => a.wrapping_mul(b), // Mul
18 16 => {
19 // DivPow2 (signed ÷2^b, toward zero)
20 let k = (b & 31) as u32;
21 let bias = if a < 0 {
22 (1u32 << k).wrapping_sub(1) as i32
23 } else {
24 0
25 };
26 a.wrapping_add(bias) >> k
27 }
28 19 => a ^ b,
29 20 => a & b,
30 21 => a | b,
31 24 => !a,
32 22 => a.wrapping_shl((b & 31) as u32), // Shl
33 23 => a.wrapping_shr((b & 31) as u32), // Shr (arithmetic on i32)
34 25 => (a < b) as i32, // signed comparisons → 0/1
35 26 => (a > b) as i32,
36 27 => (a <= b) as i32,
37 28 => (a >= b) as i32,
38 29 | 30 => (a == b) as i32, // Eq / ApproxEq (integer sub-domain: below the 1e-9 relative-tolerance floor, ≈ collapses to =; float-tolerance is the FPU coprocessor)
39 31 => (a != b) as i32,
40 _ => 0,
41 }
42}
43
44/// The Rust model of the soft-core's `shared_divider` unit (truncated signed division): Div /
45/// ExactDiv → quotient, Mod → remainder (dividend's sign), FloorDiv → floor-adjusted quotient;
46/// b==0 → 0. `run_soft_core` issues opcodes 12/13/14/15 here instead of the combinational ALU,
47/// mirroring the RTL (the divider is proven == this by iverilog `tb_shared_divider`).
48pub fn soft_core_divider(opcode: u8, a: i32, b: i32) -> i32 {
49 if b == 0 {
50 return 0;
51 }
52 match opcode {
53 12 | 13 => a.wrapping_div(b), // Div / ExactDiv → quotient
54 15 => a.wrapping_rem(b), // Mod → remainder
55 14 => {
56 // FloorDiv (toward −∞)
57 let q = a.wrapping_div(b);
58 if a.wrapping_rem(b) != 0 && ((a < 0) != (b < 0)) {
59 q - 1
60 } else {
61 q
62 }
63 }
64 _ => 0,
65 }
66}
67
68// ── WIDTH TOGGLE (VM⇄Fabric parity) — the datapath width is a PARAMETER, not hard-wired to 32 bits.
69// The chip and the VM diverge only because the scalar core is 32-bit while the VM's Int is unbounded (it
70// promotes to BigInt): a benchmark PRNG doing `(seed * 1103515245) % 2147483648` needs a 64-bit intermediate
71// and a `2^31` constant that a 32-bit core cannot hold. So the fix is a WIDTH toggle: one datapath that builds
72// a 32-bit core (small, Sky130 tape-out) or a 64-bit core (VM parity for the i64 range), by computing in i64
73// and SIGN-EXTENDING to `width` bits after each op. `soft_core_alu`/`soft_core_divider` (above) are exactly the
74// `width == 32` case, kept byte-identical; these are the parametric forms the 64-bit core + the future
75// `run_soft_core_w` use.
76
77/// Sign-extend the low `width` bits of a value to a full i64 — a fixed-width datapath's result register.
78/// `width >= 64` is the identity; `width == 32` reproduces the 32-bit core's `as i32` truncation exactly. The
79/// SAME logic yields a 32-, 64-, or any-power-of-two-width core — the toggle.
80pub fn sign_extend_to_width(v: i64, width: u32) -> i64 {
81 if width >= 64 {
82 v
83 } else {
84 let s = 64 - width;
85 (v << s) >> s
86 }
87}
88
89/// Width-parametric model of `alu.v` — computes in i64 and sign-extends to `width` bits. [`soft_core_alu`] is
90/// the `width == 32` case (proven equal by `alu_width_toggle_is_32bit_faithful_and_64bit_wide`). Div/Mod/
91/// FloorDiv route to [`soft_core_divider_w`], mirroring the shared divider.
92pub fn soft_core_alu_w(opcode: u8, a: i64, b: i64, width: u32) -> i64 {
93 let sh = (b as u64 & u64::from(width - 1)) as u32; // shift amount, masked to the datapath width
94 let r = match opcode {
95 8 | 9 => a.wrapping_add(b), // Add / AddAssign
96 10 => a.wrapping_sub(b), // Sub
97 11 => a.wrapping_mul(b), // Mul
98 16 => {
99 // DivPow2 (signed ÷2^b, toward zero)
100 let bias = if a < 0 {
101 1i64.wrapping_shl(sh).wrapping_sub(1)
102 } else {
103 0
104 };
105 a.wrapping_add(bias) >> sh
106 }
107 19 => a ^ b,
108 20 => a & b,
109 21 => a | b,
110 24 => !a,
111 22 => a.wrapping_shl(sh), // Shl
112 23 => a.wrapping_shr(sh), // Shr (arithmetic)
113 25 => return i64::from(a < b), // signed comparisons → 0/1 (already width-safe)
114 26 => return i64::from(a > b),
115 27 => return i64::from(a <= b),
116 28 => return i64::from(a >= b),
117 29 | 30 => return i64::from(a == b),
118 31 => return i64::from(a != b),
119 _ => return 0,
120 };
121 sign_extend_to_width(r, width)
122}
123
124/// Width-parametric model of the shared divider (truncated signed division), sign-extended to `width`.
125/// [`soft_core_divider`] is the `width == 32` case.
126pub fn soft_core_divider_w(opcode: u8, a: i64, b: i64, width: u32) -> i64 {
127 if b == 0 {
128 return 0;
129 }
130 let r = match opcode {
131 12 | 13 => a.wrapping_div(b), // Div / ExactDiv → quotient
132 15 => a.wrapping_rem(b), // Mod → remainder
133 14 => {
134 // FloorDiv (toward −∞)
135 let q = a.wrapping_div(b);
136 if a.wrapping_rem(b) != 0 && ((a < 0) != (b < 0)) {
137 q - 1
138 } else {
139 q
140 }
141 }
142 _ => return 0,
143 };
144 sign_extend_to_width(r, width)
145}
146
147// ── AUTO-PROMOTING VALUE-INTEGER ALU/DIVIDER (VM-EXACT) ──────────────────────────────────────────────
148// The datapath's VALUE integers are unbounded, EXACTLY as the VM's `Int` (`crate::vm::value::Value` /
149// `RuntimeValue`): a FAST `Small(i64)` path (the common case, keeping the model fast + the 32-bit oracle
150// byte-identical) that AUTO-PROMOTES to an exact `Big(BigInt)` on i64 overflow, then normalizes back to
151// `Small` when the result fits `i64` again (`RegVal::from_big`, mirroring `RuntimeValue::from_bigint`).
152// So the chip differs NOWHERE from the VM on integers — no `mod 2^width`, no range restriction. The width
153// no longer masks value arithmetic; it is a perf/area knob for the fast path and the raw-i64 output view
154// (`run_soft_core_w`), never a correctness switch. These mirror `crate::semantics::arith`'s `add`/
155// `subtract`/`multiply`/`divide`/`modulo`/`floor_divide` line-for-line (checked-i64 fast path, BigInt
156// exact fallback, truncating Div/Mod vs floor FloorDiv, bitwise/shift on the i64 pattern only — the VM's
157// bitwise/shift path is i64-only and never promotes).
158
159/// The value-integer ALU: `Add`/`Sub`/`Mul` promote to `BigInt` on i64 overflow (VM-exact); `DivPow2`,
160/// bitwise (`^ & | ~`), and shifts compute on the i64 pattern (the VM's bitwise/shift path is i64-only —
161/// SHA/Word programs stay in range); comparisons return `Small(0/1)`. Mirrors `arith::{add,subtract,
162/// multiply}` and `binary_op`'s bitwise/shift arms exactly.
163fn soft_core_alu_regval(opcode: u8, a: &RegVal, b: &RegVal) -> RegVal {
164 match opcode {
165 8 | 9 => match (a, b) {
166 (RegVal::Small(x), RegVal::Small(y)) => match x.checked_add(*y) {
167 Some(s) => RegVal::Small(s),
168 None => RegVal::from_big(a.to_big().add(&b.to_big())),
169 },
170 _ => RegVal::from_big(a.to_big().add(&b.to_big())),
171 },
172 10 => match (a, b) {
173 (RegVal::Small(x), RegVal::Small(y)) => match x.checked_sub(*y) {
174 Some(s) => RegVal::Small(s),
175 None => RegVal::from_big(a.to_big().sub(&b.to_big())),
176 },
177 _ => RegVal::from_big(a.to_big().sub(&b.to_big())),
178 },
179 11 => match (a, b) {
180 (RegVal::Small(x), RegVal::Small(y)) => match x.checked_mul(*y) {
181 Some(p) => RegVal::Small(p),
182 None => RegVal::from_big(a.to_big().mul(&b.to_big())),
183 },
184 _ => RegVal::from_big(a.to_big().mul(&b.to_big())),
185 },
186 16 => {
187 // DivPow2 (signed ÷2^b, toward zero)
188 // The optimizer's lowering of `Div` by a power of two — so it must equal the VM's TRUNCATING
189 // `Div` (`arith::divide`). `a >> sh` with the round-to-zero bias is exactly `a / 2^sh` toward
190 // zero for a `Small`; a `Big` dividend divides exactly through the BigInt truncating `div_rem`.
191 let sh = (b.sm() as u64 & 63) as u32;
192 match a {
193 RegVal::Small(x) => {
194 let bias = if *x < 0 {
195 1i64.wrapping_shl(sh).wrapping_sub(1)
196 } else {
197 0
198 };
199 RegVal::Small(x.wrapping_add(bias) >> sh)
200 }
201 RegVal::Big(x) => {
202 let divisor = BigInt::from_i64(2).pow(sh);
203 RegVal::from_big(x.div_rem(&divisor).expect("2^sh is nonzero").0)
204 }
205 }
206 }
207 19 => RegVal::Small(a.sm() ^ b.sm()), // BitXor — VM: Int ^ Int (i64 only)
208 20 => RegVal::Small(a.sm() & b.sm()), // BitAnd
209 21 => RegVal::Small(a.sm() | b.sm()), // BitOr
210 24 => RegVal::Small(!a.sm()), // Not (bitwise on the i64 pattern)
211 22 => RegVal::Small(a.sm().wrapping_shl(b.sm() as u32)), // Shl — VM: a.wrapping_shl(b as u32)
212 23 => RegVal::Small(a.sm().wrapping_shr(b.sm() as u32)), // Shr — VM: a.wrapping_shr(b as u32)
213 25 => RegVal::Small(i64::from(regval_cmp(a, b) == std::cmp::Ordering::Less)),
214 26 => RegVal::Small(i64::from(regval_cmp(a, b) == std::cmp::Ordering::Greater)),
215 27 => RegVal::Small(i64::from(regval_cmp(a, b) != std::cmp::Ordering::Greater)),
216 28 => RegVal::Small(i64::from(regval_cmp(a, b) != std::cmp::Ordering::Less)),
217 29 | 30 => RegVal::Small(i64::from(regval_cmp(a, b) == std::cmp::Ordering::Equal)),
218 31 => RegVal::Small(i64::from(regval_cmp(a, b) != std::cmp::Ordering::Equal)),
219 _ => RegVal::Small(0),
220 }
221}
222
223/// Exact ordering of two value integers — the fast i64 compare when both are `Small`, else an exact
224/// `BigInt` compare. Mirrors the VM's `compare` (which promotes both operands to the exact tower).
225fn regval_cmp(a: &RegVal, b: &RegVal) -> std::cmp::Ordering {
226 match (a, b) {
227 (RegVal::Small(x), RegVal::Small(y)) => x.cmp(y),
228 _ => a.to_big().cmp(&b.to_big()),
229 }
230}
231
232/// The value-integer shared divider — TRUNCATING `Div`/`ExactDiv` (op 12/13, toward zero), `Mod` (op 15,
233/// remainder carries the dividend's sign), `FloorDiv` (op 14, toward −∞); `b == 0` → `Small(0)` (mirrors
234/// the RTL divider's zero guard). Promotes to `BigInt` EXACTLY as `arith::{divide,modulo,floor_divide}`:
235/// truncating Div's only i64-overflow case is `i64::MIN / -1 = 2^63`, and FloorDiv floors the exact
236/// rational quotient. Big operands divide exactly through `BigInt::div_rem`.
237fn soft_core_divider_regval(opcode: u8, a: &RegVal, b: &RegVal) -> RegVal {
238 if let RegVal::Small(0) = b {
239 return RegVal::Small(0);
240 }
241 if let RegVal::Big(bb) = b {
242 if bb.is_zero() {
243 return RegVal::Small(0);
244 }
245 }
246 match (a, b) {
247 (RegVal::Small(x), RegVal::Small(y)) => match opcode {
248 12 | 13 => match x.checked_div(*y) {
249 // Div / ExactDiv — only i64::MIN/-1 overflows
250 Some(q) => RegVal::Small(q),
251 None => RegVal::from_big(a.to_big().div_rem(&b.to_big()).expect("nonzero").0),
252 },
253 15 => RegVal::Small(x.checked_rem(*y).unwrap_or(0)), // Mod — i64::MIN % -1 == 0
254 14 => match x.checked_div(*y) {
255 // FloorDiv (toward −∞)
256 // The only i64 overflow is `i64::MIN / -1` (an EXACT division, remainder 0 → no floor
257 // adjustment), handled exactly on the BigInt path. Otherwise: the truncating quotient,
258 // decremented when the division is inexact and the operands have opposite signs.
259 Some(q) => {
260 if x.wrapping_rem(*y) != 0 && ((*x < 0) != (*y < 0)) {
261 RegVal::Small(q - 1)
262 } else {
263 RegVal::Small(q)
264 }
265 }
266 None => RegVal::from_big(bigint_floor_div(&a.to_big(), &b.to_big())),
267 },
268 _ => RegVal::Small(0),
269 },
270 _ => {
271 let (aa, bb) = (a.to_big(), b.to_big());
272 match opcode {
273 12 | 13 => RegVal::from_big(aa.div_rem(&bb).expect("nonzero").0),
274 15 => RegVal::from_big(aa.div_rem(&bb).expect("nonzero").1),
275 14 => RegVal::from_big(bigint_floor_div(&aa, &bb)),
276 _ => RegVal::Small(0),
277 }
278 }
279 }
280}
281
282/// `⌊a / b⌋` on `BigInt`s (toward −∞) — the exact floor of the rational quotient, matching the VM's
283/// `arith::floor_divide` (`(a/b).floor()`): the truncating quotient, decremented by one when the division
284/// is inexact and the operands have opposite signs.
285fn bigint_floor_div(a: &BigInt, b: &BigInt) -> BigInt {
286 let (q, r) = a.div_rem(b).expect("nonzero divisor");
287 if !r.is_zero() && (a.is_negative() != b.is_negative()) {
288 q.sub(&BigInt::from_i64(1))
289 } else {
290 q
291 }
292}
293
294use logicaffeine_base::BigInt;
295
296/// A VALUE integer register cell — the chip's exact mirror of the VM's `Int`/`BigInt` tower. The common
297/// case is a machine word (`Small`, the FAST path that keeps the 32-bit oracle byte-identical and the
298/// model fast over the 5e8-cycle budget); an integer that overflows the datapath promotes to an exact
299/// `Big` (unbounded [`BigInt`]), EXACTLY as `crate::vm::value::Value` / `RuntimeValue::Int` promotes to
300/// `RuntimeValue::BigInt`. INVARIANT (mirrors `RuntimeValue::from_bigint`): a `Big` never holds a value
301/// that fits `i64` — every arithmetic result normalizes back to `Small` when it fits — so a value has one
302/// canonical representation and the printed decimal is unique. Pointers, list lengths, map tags, iterator
303/// cursors, and register indices stay `Small` (they are addresses, never over-i64), so the structural
304/// heap/pointer machinery reads them through `RegVal::sm` with no BigInt on the hot structural path.
305#[derive(Clone, PartialEq, Debug)]
306pub enum RegVal {
307 Small(i64),
308 Big(BigInt),
309}
310
311impl Default for RegVal {
312 fn default() -> Self {
313 RegVal::Small(0)
314 }
315}
316
317impl RegVal {
318 /// Build a value integer from a `BigInt`, DOWNSIZING to [`RegVal::Small`] whenever it fits `i64` —
319 /// the chip's mirror of `RuntimeValue::from_bigint`, which is what keeps the representation canonical
320 /// (a `Big` is always genuinely over-i64) so the printed decimal is identical to the VM's.
321 fn from_big(b: BigInt) -> RegVal {
322 match b.to_i64() {
323 Some(n) => RegVal::Small(n),
324 None => RegVal::Big(b),
325 }
326 }
327
328 /// This value as a `BigInt` (exact for both arms) — the operand view the overflow path promotes into.
329 fn to_big(&self) -> BigInt {
330 match self {
331 RegVal::Small(n) => BigInt::from_i64(*n),
332 RegVal::Big(b) => b.clone(),
333 }
334 }
335
336 /// The `Small` machine word — the address/length/tag/cursor view used by the structural heap and
337 /// pointer machinery. A `Big` value never reaches here (it is only ever a VALUE integer, never an
338 /// address); the `to_i64().unwrap_or` narrows defensively (the low bits) so a stray over-i64 index
339 /// wraps rather than panicking, exactly as a fixed-width address register would.
340 fn sm(&self) -> i64 {
341 match self {
342 RegVal::Small(n) => *n,
343 RegVal::Big(b) => b.to_i64().unwrap_or(0),
344 }
345 }
346
347 /// The `Small` machine word as a heap/register index.
348 #[inline]
349 fn idx(&self) -> usize {
350 self.sm() as usize
351 }
352}
353
354/// One `Show` output of the soft-core, tagged by the datapath that produced it: an integer `Show`
355/// (opcode 89, the 32-bit ALU/heap result) or a FLOAT `Show` (opcode 212 `ShowF`, an f64 from the
356/// FPU float bank). The chip is dynamically typed at the VM level, but the encoder statically lowers
357/// each `Show` to the one whose operand type it proved — so the model carries the same distinction and
358/// [`ShowVal::fmt`] renders each exactly as the VM does (ints bare, floats via `fmt_f64`).
359#[derive(Clone, PartialEq, Debug)]
360pub enum ShowVal {
361 Int(i64),
362 /// A BIGINT integer `Show` — the same integer datapath as [`ShowVal::Int`], but the value OVERFLOWED
363 /// the machine word and promoted to an exact [`BigInt`], mirroring the VM's `RuntimeValue::BigInt`. The
364 /// 32-bit oracle and every in-range program stay on `Int` (the fast path is unchanged); only a value
365 /// genuinely exceeding `i64` lands here, and [`ShowVal::fmt`] prints its full decimal byte-identically
366 /// to `logicaffeine_base::BigInt`'s `Display` (the same string the VM's `Show` emits).
367 BigInt(BigInt),
368 Float(f64),
369 /// A TEXT `Show` (opcode 217 `ShowText`) — the string decoded from the char-code list on the heap.
370 /// The chip sends the char bytes over UART (reply tag 2); the host reassembles the string. The model
371 /// carries the reconstituted `String` so `fmt` renders it byte-identically to the VM's Text `Show`.
372 Text(String),
373 /// A WORD32 `Show` (opcode 221 `ShowWord32`) — the same 32-bit ALU/heap result as an integer `Show`,
374 /// but the register holds a `Word32` (or an `intOfWord32` unsigned value), so it renders UNSIGNED
375 /// (0..2³²−1) rather than the `i32` two's-complement reinterpretation. Carries the `u32` so `fmt`
376 /// matches the VM's `Word32`/unsigned-`Int` `Show` exactly (SHA-256's hash words exceed 2³¹).
377 Word32(u32),
378}
379impl ShowVal {
380 /// Render exactly as the VM's `Show` does: an integer bare, a float through the ONE float-display
381 /// authority (`logicaffeine_data::fmt::fmt_f64` — the same path `RuntimeValue::to_display_string`
382 /// uses), a Text as its raw characters (`RuntimeValue::Text(s) => s`), a `Word32` as its UNSIGNED
383 /// decimal value (`Word32`'s `Display` prints the underlying `u32`), so the model's output string is
384 /// byte-identical to `vm_outcome`.
385 pub fn fmt(&self) -> String {
386 match self {
387 ShowVal::Int(v) => v.to_string(),
388 ShowVal::BigInt(b) => b.to_string(),
389 ShowVal::Float(f) => logicaffeine_data::fmt::fmt_f64(*f),
390 ShowVal::Text(s) => s.clone(),
391 ShowVal::Word32(w) => w.to_string(),
392 }
393 }
394 /// The integer payload of an integer `Show` — panics on a float / text / word `Show`. Used only by
395 /// the pure integer differential/testbench paths (`run_soft_core`), which never emit
396 /// `ShowF`/`ShowText`/`ShowWord32`. Returns the full `i64` (the datapath width toggle carries 64-bit
397 /// values at `width == 64`; the 32-bit core's values are the low 32 bits sign-extended, identical).
398 pub fn as_i64(&self) -> i64 {
399 match self {
400 ShowVal::Int(v) => *v,
401 ShowVal::BigInt(b) => panic!("integer soft-core run emitted a bigint Show ({b})"),
402 ShowVal::Float(f) => panic!("integer soft-core run emitted a float Show ({f})"),
403 ShowVal::Text(s) => panic!("integer soft-core run emitted a text Show ({s:?})"),
404 ShowVal::Word32(w) => panic!("integer soft-core run emitted a word32 Show ({w})"),
405 }
406 }
407}
408
409/// A cycle-accurate Rust model of `benchmarks/silicon/soft_core.v` — fetch/decode/execute over a
410/// chip-word program (`[opcode:8][dst:8][a:8][b:8]`; LoadConst/Jump use `[op][dst][imm:16]`). Returns
411/// the sequence of `Show` outputs (integer or float — see [`ShowVal`]). Mirrors the RTL `case` exactly;
412/// the executable spec the ≡VM differential checks and the iverilog testbench agrees with.
413///
414/// FLOAT ARCHITECTURE (mirrors `soft_core.v`'s `fregs`/`fpu.v` integration): a parallel 64-bit float
415/// bank `fregs` sits beside the 32-bit `regs`, same index space — VM register R holds either an int in
416/// `regs[R]` or an f64 in `fregs[R]`, disambiguated by the op's statically-lowered type. The float
417/// opcodes (chip-synthetic, above the ISA table like `LoadConstWide`=200, so no `Op` variant / no ISA
418/// drift): `FConst`=201 (3-word: op + 2 immediate words = the f64 bits), `FAdd`=202 `FSub`=203
419/// `FMul`=204 `FDiv`=205 `FSqrt`=206 (dispatch to fpu.v, bit-exact via real `f64`), `FLt`=207 `FEq`=208
420/// (write an int 0/1 into `regs` so branches work), `IntToF`=209 `FToInt`=210 (the Int↔Float boundary),
421/// `FMove`=211, `ShowF`=212. The FLOAT-HEAP PLANE (213..=215) lets a `Seq of Float` ride the heap: the
422/// structure (header + int cells) lives in `heap` and the f64 payloads on the parallel `fheap` plane
423/// (`FListPush`=213 grows both + COWs both, `FIndex`=214 reads a payload, `FSetIndex`=215 writes one).
424///
425/// The TEXT / SEQ BUILTIN opcodes (chip-synthetic 233..=236) lower the deterministic string/fill/parse
426/// builtins the CHIP compiler emits from source: `Chr`=233 (a 1-char Text `[1, codepoint]` from a codepoint,
427/// trap on an invalid one), `TextContains`=234 (a substring char-scan over two Text char-lists → 0/1),
428/// `RepeatSeq`=235 (an `n`-slot fresh Seq, every slot the same SCALAR element), `ParseInt`=236 (a bounded
429/// decimal char-scan over a Text char-list → the i64 value, trap on a non-numeric / empty / overflowing
430/// input). Each is byte-identical to the matching `semantics::builtins::call_builtin` arm; the remaining
431/// reference/host builtin families (parseFloat/format/uuid/…) stay off the chip (the encoder rejects them
432/// loudly — see `full_asm::encode_op`).
433///
434/// The integer datapath WIDTH is fixed at 32 bits (the tape-out core). For VM parity on 64-bit-range
435/// arithmetic, use [`run_soft_core_typed_w`] with `width == 64`.
436thread_local! {
437 /// DATA-segment preload for [`run_soft_core_typed_preloaded`]: seeds `heap[0..]` before the run so the
438 /// model mirrors the SoC's UART DATA phase (weights streamed into the heap; `loaded()` = the Seq at ptr 0).
439 static HEAP_PRELOAD: std::cell::RefCell<Vec<i64>> = const { std::cell::RefCell::new(Vec::new()) };
440}
441
442/// Run with `preload` seeded into the heap as `heap[0..preload.len()]` — the model twin of the SoC's weight
443/// DATA phase. With `preload = [len, w0, w1, …]` that is a `Seq of Int` at pointer 0, so the compiled
444/// `loaded()` (pointer 0) makes `item i of loaded()` read `heap[i]`. The bump allocator continues after it.
445pub fn run_soft_core_typed_preloaded(
446 imem: &[u32],
447 max_cycles: usize,
448 preload: &[i64],
449) -> Vec<ShowVal> {
450 HEAP_PRELOAD.with(|h| *h.borrow_mut() = preload.to_vec());
451 let out = run_soft_core_typed_w(imem, max_cycles, 32);
452 HEAP_PRELOAD.with(|h| h.borrow_mut().clear());
453 out
454}
455
456pub fn run_soft_core_typed(imem: &[u32], max_cycles: usize) -> Vec<ShowVal> {
457 run_soft_core_typed_w(imem, max_cycles, 32)
458}
459
460/// The WIDTH-PARAMETRIC form of [`run_soft_core_typed`] — the real fetch/decode/execute loop. Every
461/// integer VALUE is an auto-promoting [`RegVal`] (fast `Small(i64)` + exact `Big(BigInt)` on overflow),
462/// so the datapath's `Int` is UNBOUNDED exactly like the VM's — the chip differs NOWHERE from the VM on
463/// integers at ANY `width`. This is a REFRAME of the old width toggle: `width` no longer masks value
464/// arithmetic (a product that overflows i32/i64 PROMOTES, matching the VM, rather than wrapping), so
465/// `width == 32` and `width == 64` both equal `vm_outcome` on the `ShowVal` output. `width` survives as a
466/// perf/area knob and the datapath-width parameter for the raw-i64 OUTPUT VIEW ([`run_soft_core_w`], which
467/// masks each value to `width` bits to model a fixed-width hardware output register). Addresses/iterators
468/// stay `usize`; the float bank stays f64 (width-independent).
469pub fn run_soft_core_typed_w(imem: &[u32], max_cycles: usize, width: u32) -> Vec<ShowVal> {
470 run_soft_core_traced(imem, max_cycles, width).0
471}
472
473/// How the core stopped with a FAILURE (a trap-halt rather than a clean `Halt`).
474#[derive(Clone, Debug, PartialEq, Eq)]
475pub enum ChipFailure {
476 /// A `FailWith` whose reason the program streamed over the Text port just before it (`reasoned`).
477 Reason(String),
478 /// A trap with no text: a failed policy check, an invalid codepoint, a `parseInt` error.
479 Trap,
480}
481
482/// A run of the core: everything it showed, and how it failed if it did.
483#[derive(Clone, Debug)]
484pub struct SoftCoreRun {
485 pub shows: Vec<ShowVal>,
486 pub failure: Option<ChipFailure>,
487}
488
489/// Run `imem` and report the failure contract the RTL's `failed`/`reasoned` ports carry: a reasoned
490/// trap's last Text record IS its reason (so it leaves `shows`); a trap without text is `Trap`.
491pub fn run_soft_core_outcome(imem: &[u32], max_cycles: usize) -> SoftCoreRun {
492 let (mut shows, trap) = run_soft_core_traced(imem, max_cycles, 32);
493 let failure = match trap {
494 None => None,
495 Some(false) => Some(ChipFailure::Trap),
496 Some(true) => match shows.pop() {
497 Some(ShowVal::Text(reason)) => Some(ChipFailure::Reason(reason)),
498 other => {
499 shows.extend(other);
500 Some(ChipFailure::Trap)
501 }
502 },
503 };
504 SoftCoreRun { shows, failure }
505}
506
507/// The fetch/decode/execute loop: the shows, and `Some(reasoned)` when it trap-halted.
508fn run_soft_core_traced(
509 imem: &[u32],
510 max_cycles: usize,
511 width: u32,
512) -> (Vec<ShowVal>, Option<bool>) {
513 let mut trap: Option<bool> = None;
514 // The typed loop is now width-TRANSPARENT (value integers are unbounded, VM-exact). `width` is kept in
515 // the signature for API stability and for the raw-i64 output view; the loop itself ignores it.
516 let _ = width;
517 // The value-integer register file — each cell an auto-promoting `RegVal` (a FAST `Small(i64)` common
518 // case + an exact `Big(BigInt)` overflow fallback), so a VALUE integer is UNBOUNDED exactly like the
519 // VM's `Int`. Pointers, lengths, tags, and cursors live here too but always as `Small` (they are
520 // addresses, never over-i64), read through `.sm()`/`.idx()`.
521 let mut regs: [RegVal; 256] = std::array::from_fn(|_| RegVal::Small(0));
522 // The 64-bit FLOAT BANK: same 256-entry index space as `regs`, mirroring soft_core.v's `fregs`.
523 // A register holds an f64 here when the op that wrote it was a float op; an int lives in `regs`.
524 // Bits are carried EXACTLY (`f64::from_bits`/`to_bits`) so the model == the RTL == Rust f64. The float
525 // bank is WIDTH-INDEPENDENT (IEEE-754 f64) — the width toggle only governs the integer datapath.
526 let mut fregs = [0.0f64; 256];
527 // The 256-BIT BIGINT LANE: an 8×256-bit wide register bank (4 little-endian u64 limbs each),
528 // beside the 32-bit regfile. `BigIntWide` (op 114) routes it to the bigint_alu/div/mul blocks.
529 let mut wide: [[u64; 4]; 8] = [[0u64; 4]; 8];
530 let mut pc: usize = 0;
531 let mut out: Vec<ShowVal> = Vec::new();
532 // Call/return frame stack: (saved int window, saved float window, return pc, result reg) — mirrors
533 // soft_core.v (the frame engine snapshots the whole 256-register window, both banks, per call).
534 let mut frames: Vec<([RegVal; 256], [f64; 256], usize, usize)> = Vec::new();
535 // The LOAD-STORE unit's heap: a flat data memory holding structured values. A list at pointer `p`
536 // is `heap[p] = length` followed by the `length` elements at `heap[p+1..=p+length]`, so the 1-based
537 // `item i of xs` is `heap[p + i]` (mirrors `mem_unit.v` + the VM's `RuntimeValue::List`). A cell holds
538 // a `RegVal` because a collection ELEMENT can be an over-i64 `Big` value (a `Seq of Int` of big ints);
539 // headers/tags/pointers stay `Small`, read through `.sm()`.
540 let mut heap: Vec<RegVal> =
541 HEAP_PRELOAD.with(|h| h.borrow().iter().map(|&x| RegVal::Small(x)).collect());
542 // The FLOAT-HEAP PLANE — a dense f64 payload plane running PARALLEL to `heap` at the SAME indices
543 // (RTL: `fdmem`, two 32-bit BRAMs beside `dmem`). A `Seq of Float` handle is still an INT pointer into
544 // `heap` (which keeps the `[length, …]` header + all structure); its element PAYLOADS ride `fheap` at
545 // the same slots. INVARIANT: `fheap.len() == heap.len()` ALWAYS — enforced by a single resize at the
546 // bottom of the fetch loop, so every op that grows `heap` (int lists, maps, ranges, …) leaves a
547 // matching `0.0` float cell behind with no per-op bookkeeping. The float-list ops (213/214/215) read
548 // and write this plane; the COW in FListPush copies BOTH planes so a float seq's payload survives.
549 let mut fheap: Vec<f64> = vec![0.0; heap.len()]; // INVARIANT: fheap.len() == heap.len() (preload too)
550 // The iterator stack: each `for x in xs` pushes a `(collection pointer, 1-based cursor)`; IterNext
551 // reads `heap[ptr + cursor]` and advances, jumping to `exit` when the cursor passes the length.
552 let mut iters: Vec<(usize, usize)> = Vec::new();
553 // A MAP is a header-tagged heap block `[MAP_TAG|npairs, k0, v0, k1, v1, …]`. The tag bit (never set by a
554 // plain list length) lets the shared Length/Contains ops branch to key-scan behaviour for maps and stay
555 // positional for lists. Each key cell `kᵢ` is EITHER a char-list POINTER (a `Map of Text to …`, accessed
556 // through the string-key ops SetIndex/Index 57/59 which compare with `key_eq`) OR a direct INTEGER key (a
557 // `Map of Int to …`, accessed through the int-key ops MapSetIndexInt/MapIndexInt 224/223 which compare with
558 // `==`). The encoder picks the op family from the key expression's static type — the run loop never mixes
559 // them within one map, so the key cell's interpretation is unambiguous per access op.
560 const MAP_TAG: i64 = 0x4000_0000;
561 // char-list key equality: the string at `p` equals the string at `q` (same length + same codepoints).
562 let key_eq = |heap: &[RegVal], p: usize, q: usize| -> bool {
563 if heap[p].sm() != heap[q].sm() {
564 return false;
565 }
566 for j in 1..=(heap[p].sm() as usize) {
567 if heap[p + j].sm() != heap[q + j].sm() {
568 return false;
569 }
570 }
571 true
572 };
573 // The globals table — Main top-level `Let`s promoted to globals when a function body references them.
574 // It lives OUTSIDE the register-window frame stack, so a global survives Call/Return (a callee reads
575 // the same slot the caller wrote), exactly as the VM's runtime globals table does.
576 let mut globals: [RegVal; 256] = std::array::from_fn(|_| RegVal::Small(0));
577 // Select arms: `Await the first of:` registers `(chan_reg, var_reg)` recv arms (chan_reg = usize::MAX
578 // marks an `After ticks` timeout arm); SelectWait fires the FIRST ready recv (deterministic), else the
579 // timeout arm, and reports the fired arm index.
580 let mut sel_arms: Vec<(usize, usize)> = Vec::new();
581 // The networking coprocessor's OFFLINE LOOPBACK inbox — the single-node projection of the relay:
582 // `NetSend to <peer>` PUBLISHES `(peer key, message)` here; `NetAwait from <peer>` DRAINS the entry
583 // whose key matches (the VM's `offline_loopback` — a message a node sends to a peer it also awaits
584 // comes back to it). The peer key is the `NetMakePeer` handle (Send and Await name the same peer).
585 let mut inbox: Vec<(RegVal, RegVal)> = Vec::new();
586 let mut peer_seq: i64 = 0;
587 for _ in 0..max_cycles {
588 if pc >= imem.len() {
589 break;
590 }
591 let instr = imem[pc];
592 let opcode = (instr >> 24) as u8;
593 let dst = ((instr >> 16) & 0xff) as usize;
594 let ra = ((instr >> 8) & 0xff) as usize;
595 let rb = (instr & 0xff) as usize;
596 let imm = i64::from(instr as i16); // LoadConst immediate: low 16 bits, SIGN-extended (−32768..=32767); fits any width, no mask
597 let target = ((instr >> 16) & 0xff) as usize; // 8-bit entry byte (Spawn/SpawnHandle; wide targets read below)
598 // Branch truthiness (`av == 0`) tests the VALUE: a `Big` is never zero (canonical repr), so
599 // `is_zero_val` is the exact truthiness the VM's branch uses. Arithmetic arms take the full
600 // `RegVal` (`®s[..]`) so an over-i64 `Big` operand promotes correctly; structural arms read the
601 // `.sm()` i64 address view.
602 let is_zero_val = |v: &RegVal| matches!(v, RegVal::Small(0));
603 match opcode {
604 // Reversible/adiabatic charge-recovery core (wire opcodes 0..4) — the abelian bijection
605 // datapath (`rev_alu_core.v`), now integrated into the CPU so ONE core executes every op.
606 // The reversible core's Add/Sub/Neg AUTO-PROMOTE like every value add/sub/neg (VM-exact); Xor
607 // is the i64-only bitwise the VM's `Int ^ Int` computes. `place op= delta` in-place.
608 0 => {
609 let r = soft_core_alu_regval(8, ®s[dst], ®s[ra]);
610 regs[dst] = r;
611 pc += 1;
612 } // RevAdd { place=dst, delta=ra }
613 1 => {
614 let r = soft_core_alu_regval(10, ®s[dst], ®s[ra]);
615 regs[dst] = r;
616 pc += 1;
617 } // RevSub
618 2 => {
619 let r = RegVal::Small(regs[dst].sm() ^ regs[ra].sm());
620 regs[dst] = r;
621 pc += 1;
622 } // RevXor (i64 pattern)
623 3 => {
624 let r = soft_core_alu_regval(10, &RegVal::Small(0), ®s[dst]);
625 regs[dst] = r;
626 pc += 1;
627 } // RevNeg { place=dst } = 0 - place
628 4 => {
629 regs.swap(dst, ra);
630 pc += 1;
631 } // RevSwap { a=dst, b=ra }
632 5 => {
633 regs[dst] = RegVal::Small(imm);
634 pc += 1;
635 } // LoadConst — sign-extended 16-bit inline immediate (fits any width)
636 200 => {
637 regs[dst] = RegVal::Small(i64::from(imem[pc + 1] as i32));
638 pc += 2;
639 } // LoadConstWide — sign-extended 32-bit immediate in the SECOND word (chip-synthetic opcode, no VM Op)
640 222 => {
641 // LoadConstWide64 { dst } — 3-word: op + [bits 31:0][bits 63:32] = the full i64 constant
642 // The full i64 constant is retired EXACTLY, irrespective of datapath width — NO sign-extension
643 // to `width`. This is what lets the full core reproduce a constant wider than i32 (`% 2^31`,
644 // `% 2^32` in the benchmark PRNGs, or a full-range i64 literal) at width 32 as faithfully as at
645 // 64: the value plane auto-promotes to BigInt on overflow like the VM's unbounded `Int`, so the
646 // width is a fast-path/area knob, not a correctness switch. Mirrors `FConst` (op 201): low word
647 // then high word. The encoder emits op 222 for every over-i32 constant at every width.
648 let bits = (imem[pc + 1] as u64) | ((imem[pc + 2] as u64) << 32);
649 regs[dst] = RegVal::Small(bits as i64);
650 pc += 3;
651 }
652 // ── FLOAT DATAPATH (chip-synthetic opcodes 201..=212, mirroring soft_core.v's fregs + fpu.v).
653 // A float register lives in `fregs`; ints stay in `regs`. Every arithmetic result is computed
654 // with real Rust `f64` (bit-identical to fpu.v's IEEE-754 RNE units) via from_bits/to_bits.
655 201 => {
656 // FConst { dst } — 3-word: op + [bits 31:0][bits 63:32]
657 let bits = (imem[pc + 1] as u64) | ((imem[pc + 2] as u64) << 32);
658 fregs[dst] = f64::from_bits(bits);
659 pc += 3;
660 }
661 202 => {
662 fregs[dst] = fregs[ra] + fregs[rb];
663 pc += 1;
664 } // FAdd (fp_addsub sub=0)
665 203 => {
666 fregs[dst] = fregs[ra] - fregs[rb];
667 pc += 1;
668 } // FSub (fp_addsub sub=1)
669 204 => {
670 fregs[dst] = fregs[ra] * fregs[rb];
671 pc += 1;
672 } // FMul (fp_mul)
673 205 => {
674 fregs[dst] = fregs[ra] / fregs[rb];
675 pc += 1;
676 } // FDiv (fp_div, multi-cycle)
677 206 => {
678 fregs[dst] = fregs[ra].sqrt();
679 pc += 1;
680 } // FSqrt (fp_sqrt, multi-cycle)
681 207 => {
682 regs[dst] = RegVal::Small(i64::from(fregs[ra] < fregs[rb]));
683 pc += 1;
684 } // FLt -> int 0/1 (fp_cmp lt)
685 208 => {
686 regs[dst] = RegVal::Small(i64::from(fregs[ra] == fregs[rb]));
687 pc += 1;
688 } // FEq -> int 0/1 (fp_cmp eq)
689 229 => {
690 regs[dst] = RegVal::Small(i64::from(fregs[ra] <= fregs[rb]));
691 pc += 1;
692 } // FLe -> int 0/1 (fp_cmp le = lt|eq; NaN-false, matching the VM's f64 `<=`)
693 230 => {
694 regs[dst] = RegVal::Small(i64::from(fregs[ra] != fregs[rb]));
695 pc += 1;
696 } // FNe -> int 0/1 (fp_cmp ne = !eq; NaN-true, matching the VM's f64 `!=`)
697 // `is approximately` has NO dedicated opcode: the encoder DECOMPOSES it into FEq/FSub/FAbs (36)/
698 // FMax (38)/FConst/FMul/FLe + an int BitOr, reproducing `logos_approx_eq` bit-for-bit with the SAME
699 // ops the model already runs above and in the op-38 float coprocessor below.
700 209 => {
701 fregs[dst] = regs[ra].to_big().to_f64();
702 pc += 1;
703 } // IntToF { dst, src=ra } — Int -> Float boundary (exact for Big too, VM: `n as f64`/`b.to_f64()`)
704 210 => {
705 regs[dst] = RegVal::Small(fregs[ra] as i64);
706 pc += 1;
707 } // FToInt { dst, src=ra } — Float -> Int (truncation toward zero)
708 211 => {
709 fregs[dst] = fregs[ra];
710 pc += 1;
711 } // FMove { dst, src=ra } — float register copy
712 212 => {
713 out.push(ShowVal::Float(fregs[ra]));
714 pc += 1;
715 } // ShowF { src=ra } — float Show
716 // ── FLOAT-HEAP PLANE (chip-synthetic opcodes 213..=215): a `Seq of Float`'s structure lives
717 // in the INT heap (header `[length, …]` + int placeholder cells), its f64 element payloads on
718 // the parallel `fheap` plane at the SAME slots. Mirrors soft_core.v's `fdmem` beside `dmem`.
719 213 => {
720 // FListPush { list=dst, value=ra } — integer ListPush structure + float payload
721 let ptr = regs[dst].idx();
722 let len = heap[ptr].sm() as usize;
723 let slot;
724 if ptr + 1 + len == heap.len() {
725 // list is on top of the heap: append in place
726 slot = heap.len();
727 heap.push(RegVal::Small(0)); // int placeholder cell (the payload rides fheap)
728 fheap.push(0.0); // keep the plane aligned within the op
729 heap[ptr] = RegVal::Small((len + 1) as i64);
730 } else {
731 // else copy-on-write to a fresh block (value semantics)
732 let newptr = heap.len();
733 heap.push(RegVal::Small((len + 1) as i64)); // header
734 fheap.push(0.0);
735 for k in 1..=len {
736 // COPY BOTH PLANES — the float seq's payload survives
737 let v = heap[ptr + k].clone();
738 let f = fheap[ptr + k];
739 heap.push(v);
740 fheap.push(f);
741 }
742 slot = heap.len();
743 heap.push(RegVal::Small(0)); // the appended int placeholder
744 fheap.push(0.0);
745 regs[dst] = RegVal::Small(newptr as i64);
746 }
747 fheap[slot] = fregs[ra]; // write the f64 payload at the appended slot
748 pc += 1;
749 }
750 214 => {
751 // FIndex { dst, coll=ra, index=rb } — read the f64 payload (1-based)
752 fregs[dst] = fheap[regs[ra].idx() + regs[rb].idx()];
753 pc += 1;
754 }
755 215 => {
756 // FSetIndex { coll=dst, index=ra, value=rb } — write the f64 payload (1-based)
757 fheap[regs[dst].idx() + regs[ra].idx()] = fregs[rb];
758 pc += 1;
759 }
760 // ── TEXT (string) PLANE (chip-synthetic opcodes 216/217): a `Text` is a char-code list on the
761 // int heap. `TextConst` copies a string's char block out of the imem STRING-CONSTANT POOL into
762 // a fresh heap char-list `[len, chars…]`; `ShowText` decodes such a list to a UTF-8 string.
763 216 => {
764 // TextConst { dst } — 2-word: [216][dst][len:16] ‖ pool_word_offset
765 let len = ((instr & 0xffff) as usize) & 0xffff;
766 let off = imem[pc + 1] as usize; // imem index of c0 (into the pool, laid out after the code)
767 let ptr = heap.len();
768 heap.push(RegVal::Small(len as i64)); // header
769 for k in 0..len {
770 heap.push(RegVal::Small(i64::from(imem[off + k])));
771 } // copy the codepoints (unsigned codepoint)
772 regs[dst] = RegVal::Small(ptr as i64);
773 pc += 2;
774 }
775 217 => {
776 // ShowText { src=ra } — decode the char list to a UTF-8 string
777 let ptr = regs[ra].idx();
778 let len = heap[ptr].sm() as usize;
779 let mut s = String::with_capacity(len);
780 for k in 1..=len {
781 // A char cell is an i32 CODE POINT (as `TextConst`/`FormatValue`/Concat store them).
782 if let Some(ch) = char::from_u32(heap[ptr + k].sm() as u32) {
783 s.push(ch);
784 }
785 }
786 out.push(ShowVal::Text(s));
787 pc += 1;
788 }
789 218 => {
790 // TextCharAt { dst, coll=ra, index=rb } — a single-char Text
791 // The VM's `item i of text` returns a ONE-character string; the chip builds a fresh
792 // `[1, codepoint]` char-list so a subsequent `char == " "` routes to the char-list compare.
793 let cp = heap[regs[ra].idx() + regs[rb].idx()].clone(); // the 1-based codepoint
794 let ptr = heap.len();
795 heap.push(RegVal::Small(1));
796 heap.push(cp);
797 regs[dst] = RegVal::Small(ptr as i64);
798 pc += 1;
799 }
800 219 | 220 => {
801 // TextEq (219) / TextNe (220) — char-list CONTENT compare
802 // Text `equals`/`is not` compares by content (same length + same codepoints), mirroring the
803 // VM's `values_equal` for two Texts. 219 yields 1 on equal; 220 yields its negation.
804 let p = regs[ra].idx();
805 let q = regs[rb].idx();
806 let mut equal = heap[p] == heap[q];
807 if equal {
808 for j in 1..=(heap[p].sm() as usize) {
809 if heap[p + j] != heap[q + j] {
810 equal = false;
811 break;
812 }
813 }
814 }
815 regs[dst] = RegVal::Small(if opcode == 219 {
816 i64::from(equal)
817 } else {
818 i64::from(!equal)
819 });
820 pc += 1;
821 }
822 6 => {
823 regs[dst] = regs[ra].clone();
824 pc += 1;
825 } // Move
826 // Branch targets are 16-bit, SPLIT so the low byte sits in the scalar core's byte-1 target
827 // position: a scalar program (target ≤255, high byte 0) decodes byte-identically, so the wide
828 // format is a strict superset of the deployed scalar ISA and the scalar core is untouched.
829 34 => pc = (((instr >> 16) & 0xff) | (((instr >> 8) & 0xff) << 8)) as usize, // Jump {t_lo=byte1, t_hi=byte2}
830 35 => {
831 if is_zero_val(®s[ra]) {
832 pc = (((instr >> 16) & 0xff) | ((instr & 0xff) << 8)) as usize
833 } else {
834 pc += 1
835 }
836 } // JumpIfFalse {t_lo=byte1, cond=byte2, t_hi=byte3}
837 36 => {
838 if !is_zero_val(®s[ra]) {
839 pc = (((instr >> 16) & 0xff) | ((instr & 0xff) << 8)) as usize
840 } else {
841 pc += 1
842 }
843 } // JumpIfTrue {t_lo=byte1, cond=byte2, t_hi=byte3}
844 89 => {
845 out.push(match ®s[ra] {
846 // Show (integer datapath) — full value, exact
847 RegVal::Small(v) => ShowVal::Int(*v),
848 RegVal::Big(b) => ShowVal::BigInt(b.clone()),
849 });
850 pc += 1;
851 }
852 221 => {
853 out.push(ShowVal::Word32(regs[ra].sm() as u32));
854 pc += 1;
855 } // ShowWord32 — same 32-bit result, rendered UNSIGNED (Word32/intOfWord32 Show)
856 43 => break, // Halt
857 // FailWith {reasoned=rb} — the unrecoverable trap: the core halts with the error strobe raised;
858 // rb=1 says the reason was streamed over the Text port just before. Pre-trap output stands.
859 91 => {
860 trap = Some(rb != 0);
861 break;
862 }
863 37 => {
864 // Call — TWO words: [37][result:8][args_start:8][arg_count:8] then [entry:32]
865 // Hardware arg-copy calling convention: the callee runs in a FRESH 256-register frame with
866 // the caller's r[args_start..args_start+arg_count] seeded into r0.., matching the VM's
867 // windowed convention (a function reads its params at its window base r0..). The caller's
868 // whole frame is saved and restored on Return, so argument passing never clobbers a caller
869 // register. result=dst byte, args_start=ra byte, arg_count=rb byte; the callee's imem entry
870 // index is the full 32-bit SECOND word (so it reaches a program of any length).
871 // The arg-copy seeds BOTH banks (int + float) at each arg slot — a float param rides
872 // `fregs`, an int param rides `regs`; only the statically-typed one is read in the body.
873 let arg_cnt = rb;
874 let entry = imem[pc + 1] as usize;
875 let mut callee: [RegVal; 256] = std::array::from_fn(|_| RegVal::Small(0));
876 let mut callee_f = [0.0f64; 256];
877 for i in 0..arg_cnt {
878 callee[i] = regs[ra + i].clone();
879 callee_f[i] = fregs[ra + i];
880 }
881 frames.push((regs, fregs, pc + 2, dst));
882 regs = callee;
883 fregs = callee_f;
884 pc = entry;
885 }
886 41 => {
887 // Return {src=ra}
888 // Capture BOTH banks at the source slot; restore the caller window and place both into
889 // its result register. The caller reads only the correctly-typed one (the encoder knows
890 // dst's type at the call site), so a type-agnostic Return needs no float-return opcode.
891 let rv = regs[ra].clone();
892 let rvf = fregs[ra];
893 match frames.pop() {
894 Some((saved, saved_f, ret_pc, d)) => {
895 regs = saved;
896 fregs = saved_f;
897 regs[d] = rv;
898 fregs[d] = rvf;
899 pc = ret_pc;
900 }
901 None => break,
902 }
903 }
904 42 => match frames.pop() {
905 // ReturnNothing
906 Some((saved, saved_f, ret_pc, _)) => {
907 regs = saved;
908 fregs = saved_f;
909 pc = ret_pc;
910 }
911 None => break,
912 },
913 40 => {
914 // MakeClosure { dst, func=a-byte } — closure = [entry] (non-capturing)
915 let entry = ((instr >> 8) & 0xff) as i64;
916 let ptr = heap.len();
917 heap.push(RegVal::Small(entry));
918 regs[dst] = RegVal::Small(ptr as i64);
919 pc += 1;
920 }
921 39 => {
922 // CallValue { dst=result, callee=ra, b: args_start(hi nibble) | arg_count(lo nibble) }
923 // Indirect call through a closure value `[entry]`. The frame is the CALLER's (cloned, then
924 // restored on Return), so captures and the closure's static body share one register window —
925 // but the ARGS ride the caller's window at `args_start`, while a compiled closure body reads
926 // its parameters at its window base r0. So window-copy R[args_start..+arg_count] → R[0..] in
927 // the cloned frame before entering (arg_count 0 = same-frame, the hand-built closure form).
928 let entry = heap[regs[ra].idx()].idx();
929 let args_start = ((instr >> 4) & 0xf) as usize;
930 let arg_count = (instr & 0xf) as usize;
931 let mut callee = regs.clone();
932 let mut callee_f = fregs;
933 for k in 0..arg_count {
934 callee[k] = regs[args_start + k].clone();
935 callee_f[k] = fregs[args_start + k]; // seed BOTH banks (a float closure arg rides fregs)
936 }
937 frames.push((regs, fregs, pc + 1, dst));
938 regs = callee;
939 fregs = callee_f;
940 pc = entry;
941 }
942 // ── LOAD-STORE unit (structured heap) ──────────────────────────────────────────
943 48 | 67 => {
944 // NewList / NewTuple { dst, start=ra, count=rb }
945 let count = rb; // element count (≤ regfile width); tuple ≡ list layout
946 let ptr = heap.len();
947 heap.push(RegVal::Small(count as i64));
948 for i in 0..count {
949 heap.push(regs[(ra + i) & 0xff].clone());
950 }
951 regs[dst] = RegVal::Small(ptr as i64);
952 pc += 1;
953 }
954 49 | 50 | 90 | 82 => {
955 let ptr = heap.len();
956 heap.push(RegVal::Small(0));
957 regs[dst] = RegVal::Small(ptr as i64);
958 pc += 1;
959 } // NewEmptyList / NewEmptyListI32 / Args (offline argv) / NewCrdt (seq/set = empty collection; single-replica projection)
960 54 | 83 => {
961 // ListPush { list=dst, value=ra } / CrdtAppend (seq CRDT = ordered list, single-replica projection)
962 let ptr = regs[dst].idx();
963 let len = heap[ptr].sm() as usize;
964 if ptr + 1 + len == heap.len() {
965 // list is on top of the heap: append in place
966 heap.push(regs[ra].clone());
967 heap[ptr] = RegVal::Small((len + 1) as i64);
968 } else {
969 // else copy-on-write to a fresh block (value semantics)
970 let newptr = heap.len();
971 heap.push(RegVal::Small((len + 1) as i64));
972 for k in 1..=len {
973 let v = heap[ptr + k].clone();
974 heap.push(v);
975 }
976 heap.push(regs[ra].clone());
977 regs[dst] = RegVal::Small(newptr as i64);
978 }
979 pc += 1;
980 }
981 59 | 60 => {
982 // Index / IndexUnchecked { dst, coll=ra, index/key=rb }
983 let ptr = regs[ra].idx();
984 if heap[ptr].sm() & MAP_TAG != 0 {
985 // MAP: look up value by char-list key
986 let np = (heap[ptr].sm() & !MAP_TAG) as usize;
987 let key = regs[rb].idx();
988 let mut val = RegVal::Small(0);
989 for i in 0..np {
990 if key_eq(&heap, key, heap[ptr + 1 + 2 * i].idx()) {
991 val = heap[ptr + 2 + 2 * i].clone();
992 break;
993 }
994 }
995 regs[dst] = val;
996 } else {
997 // LIST: positional (1-based)
998 regs[dst] = heap[ptr + regs[rb].idx()].clone();
999 }
1000 pc += 1;
1001 }
1002 223 => {
1003 // MapIndexInt { dst, map=ra, key=rb } — lookup by INTEGER key
1004 // `item k of m` for a `Map of Int to …`: scan the key cells with a direct integer `==` (the key
1005 // cell holds the int key itself, NOT a heap pointer — the encoder emits this op only when the key
1006 // is statically non-Text). A missing key yields 0 here — the fabric engine's hardware-natural
1007 // default. NOTE: this DIVERGES from the scalar tiers, where `item k of m` on an absent key is a
1008 // runtime TRAP (interpreter/VM/C/riscv); the whole-program corpora reach this op only through
1009 // `contains`-guarded reads, so the gap is latent. Aligning the fabric to trap is a fabric call.
1010 let ptr = regs[ra].idx();
1011 let np = (heap[ptr].sm() & !MAP_TAG) as usize;
1012 let key = regs[rb].clone();
1013 let mut val = RegVal::Small(0);
1014 for i in 0..np {
1015 if heap[ptr + 1 + 2 * i] == key {
1016 val = heap[ptr + 2 + 2 * i].clone();
1017 break;
1018 }
1019 }
1020 regs[dst] = val;
1021 pc += 1;
1022 }
1023 62 => {
1024 // Length { dst, coll=ra }
1025 let ptr = regs[ra].idx();
1026 let h = heap[ptr].sm();
1027 regs[dst] = RegVal::Small(if h & MAP_TAG != 0 { h & !MAP_TAG } else { h });
1028 pc += 1;
1029 }
1030 52 => {
1031 let ptr = heap.len();
1032 heap.push(RegVal::Small(MAP_TAG));
1033 regs[dst] = RegVal::Small(ptr as i64);
1034 pc += 1;
1035 } // NewEmptyMap — tagged, 0 pairs
1036 64 => {
1037 // FormatValue { dst, src=ra } — the decimal string of regs[ra]
1038 // The full BigInt decimal (exact for a `Big` value too) — byte-identical to
1039 // `logicaffeine_base::BigInt`'s `Display`, the same string the VM renders in interpolation.
1040 let s = match ®s[ra] {
1041 RegVal::Small(v) => v.to_string(),
1042 RegVal::Big(b) => b.to_string(),
1043 };
1044 let ptr = heap.len();
1045 heap.push(RegVal::Small(s.chars().count() as i64)); // Text ≡ char-code list [len, chars…]
1046 for ch in s.chars() {
1047 heap.push(RegVal::Small(i64::from(ch as u32)));
1048 }
1049 regs[dst] = RegVal::Small(ptr as i64);
1050 pc += 1;
1051 }
1052 225 => {
1053 // FormatValueFloat { dst, src=ra, prec=rb } — the fixed-precision decimal string of fregs[ra]
1054 // A float `Show "{f:.N}"` renders `fregs[ra]` to `rb` decimal places EXACTLY as the VM does
1055 // (`crate::semantics::format::apply_format_spec` on a `.N` spec = `format!("{:.prec$}", f, prec)`),
1056 // then lays the UTF-8 bytes out as a Text char-code list `[len, chars…]` on the heap — the same
1057 // `[len, codepoints…]` shape `TextConst`/op 64/Concat use, so a downstream `ShowText` (op 217)
1058 // decodes it byte-identically. The digits/sign/rounding come from Rust's `f64` formatter (the
1059 // single float-display authority the VM shares), so the model string == the VM's `Show` line.
1060 let s = format!("{:.prec$}", fregs[ra], prec = rb);
1061 let ptr = heap.len();
1062 heap.push(RegVal::Small(s.chars().count() as i64)); // Text ≡ char-code list [len, chars…]
1063 for ch in s.chars() {
1064 heap.push(RegVal::Small(i64::from(ch as u32)));
1065 }
1066 regs[dst] = RegVal::Small(ptr as i64);
1067 pc += 1;
1068 }
1069 63 => {
1070 // Contains { dst, coll=ra, value=rb }
1071 let ptr = regs[ra].idx();
1072 let target = regs[rb].clone();
1073 let mut found = 0i64;
1074 if heap[ptr].sm() & MAP_TAG != 0 {
1075 // MAP: membership tests the KEYS (int keys)
1076 let np = (heap[ptr].sm() & !MAP_TAG) as usize;
1077 for i in 0..np {
1078 if heap[ptr + 1 + 2 * i] == target {
1079 found = 1;
1080 break;
1081 }
1082 }
1083 } else {
1084 // LIST/SET: membership tests the elements
1085 let len = heap[ptr].sm() as usize;
1086 for k in 1..=len {
1087 if heap[ptr + k] == target {
1088 found = 1;
1089 break;
1090 }
1091 }
1092 }
1093 regs[dst] = RegVal::Small(found);
1094 pc += 1;
1095 }
1096 70 => {
1097 // ListPop { dst, list=ra }
1098 let ptr = regs[ra].idx();
1099 let len = heap[ptr].sm() as usize;
1100 regs[dst] = heap[ptr + len].clone(); // last element
1101 heap[ptr] = RegVal::Small((len - 1) as i64);
1102 pc += 1;
1103 }
1104 74 => {
1105 // NewStruct { dst, num_fields=rb } — record: [n, f0..f_{n-1}]
1106 let n = rb;
1107 let ptr = heap.len();
1108 heap.push(RegVal::Small(n as i64));
1109 for _ in 0..n {
1110 heap.push(RegVal::Small(0));
1111 }
1112 regs[dst] = RegVal::Small(ptr as i64);
1113 pc += 1;
1114 }
1115 75 | 84 => {
1116 // StructInsert { obj=dst, value=ra, field=rb } / CrdtResolve (register last-write = field overwrite, single-replica)
1117 let ptr = regs[dst].idx();
1118 heap[ptr + 1 + rb] = regs[ra].clone();
1119 pc += 1;
1120 }
1121 76 => {
1122 // GetField { dst, obj=ra, field=rb }
1123 let ptr = regs[ra].idx();
1124 regs[dst] = heap[ptr + 1 + rb].clone();
1125 pc += 1;
1126 }
1127 57 | 58 => {
1128 // SetIndex/SetIndexUnchecked { coll=dst, index/key=ra, value=rb }
1129 let ptr = regs[dst].idx();
1130 if heap[ptr].sm() & MAP_TAG != 0 {
1131 // MAP: update the key's value, or append a new pair
1132 let np = (heap[ptr].sm() & !MAP_TAG) as usize;
1133 let key = regs[ra].idx();
1134 let mut found = None;
1135 for i in 0..np {
1136 if key_eq(&heap, key, heap[ptr + 1 + 2 * i].idx()) {
1137 found = Some(i);
1138 break;
1139 }
1140 }
1141 match found {
1142 Some(i) => heap[ptr + 2 + 2 * i] = regs[rb].clone(),
1143 None => {
1144 // append a new `(key_ptr, value)` pair
1145 // A map is a CONTIGUOUS block `[header, k0,v0, …]`; appending in place is only sound
1146 // when the block is on top of the heap. The string-key path INTERLEAVES a fresh
1147 // `TextConst` key char-list before each `Set item "k" of m`, so the map is usually
1148 // NOT on top — a bare push would scatter the pair past unrelated cells and the
1149 // `ptr+1+2i` addressing would read garbage (the read returns 0). COPY-ON-WRITE the
1150 // whole map to a fresh contiguous block (header + all `2*np` pair cells) and repoint
1151 // `m`, exactly as ListPush COWs a non-top list; when the map IS on top this still
1152 // appends contiguously (the copy is a no-op relocation).
1153 if ptr + 1 + 2 * np == heap.len() {
1154 // on top: grow in place
1155 heap.push(regs[ra].clone());
1156 heap.push(regs[rb].clone());
1157 heap[ptr] = RegVal::Small(MAP_TAG | ((np + 1) as i64));
1158 } else {
1159 // else COW to a fresh contiguous block
1160 let newptr = heap.len();
1161 heap.push(RegVal::Small(MAP_TAG | ((np + 1) as i64))); // header (with the +1 pair)
1162 for k in 1..=(2 * np) {
1163 let v = heap[ptr + k].clone();
1164 heap.push(v);
1165 }
1166 heap.push(regs[ra].clone());
1167 heap.push(regs[rb].clone());
1168 regs[dst] = RegVal::Small(newptr as i64);
1169 }
1170 }
1171 }
1172 } else {
1173 // LIST: positional (1-based)
1174 heap[ptr + regs[ra].idx()] = regs[rb].clone();
1175 }
1176 pc += 1;
1177 }
1178 224 => {
1179 // MapSetIndexInt { map=dst, key=ra, value=rb } — set by INTEGER key
1180 // `Set item k of m to v` for a `Map of Int to …`: find the key cell by a direct integer `==` and
1181 // overwrite its value, else append the pair `[key, value]` (the map is on top of the heap so the
1182 // append grows it in place) and bump the header's pair count. Mirrors the string-key SetIndex, but
1183 // the key cell holds the int key itself. The encoder emits this only for a statically non-Text key.
1184 let ptr = regs[dst].idx();
1185 let np = (heap[ptr].sm() & !MAP_TAG) as usize;
1186 let key = regs[ra].clone();
1187 let mut found = None;
1188 for i in 0..np {
1189 if heap[ptr + 1 + 2 * i] == key {
1190 found = Some(i);
1191 break;
1192 }
1193 }
1194 match found {
1195 Some(i) => heap[ptr + 2 + 2 * i] = regs[rb].clone(),
1196 None => {
1197 // Same CONTIGUITY rule as the string-key SetIndex: append in place only when the map is
1198 // on top of the heap, else COW the whole block. An int key rarely allocates (so the map
1199 // usually stays on top), but a `Map of Int to <heap value>` can interleave allocations —
1200 // the COW keeps the pair block contiguous so `ptr+1+2i` addressing stays sound.
1201 if ptr + 1 + 2 * np == heap.len() {
1202 // on top: grow in place
1203 heap.push(key);
1204 heap.push(regs[rb].clone());
1205 heap[ptr] = RegVal::Small(MAP_TAG | ((np + 1) as i64));
1206 } else {
1207 // else COW to a fresh contiguous block
1208 let newptr = heap.len();
1209 heap.push(RegVal::Small(MAP_TAG | ((np + 1) as i64)));
1210 for k in 1..=(2 * np) {
1211 let v = heap[ptr + k].clone();
1212 heap.push(v);
1213 }
1214 heap.push(key);
1215 heap.push(regs[rb].clone());
1216 regs[dst] = RegVal::Small(newptr as i64);
1217 }
1218 }
1219 }
1220 pc += 1;
1221 }
1222 33 | 32 => {
1223 // SeqConcat / Concat (Text ≡ char-code list) — fresh lhs ++ rhs
1224 let lp = regs[ra].idx();
1225 let rp = regs[rb].idx();
1226 let ll = heap[lp].sm() as usize;
1227 let rl = heap[rp].sm() as usize;
1228 let ptr = heap.len();
1229 heap.push(RegVal::Small((ll + rl) as i64));
1230 for k in 1..=ll {
1231 let v = heap[lp + k].clone();
1232 heap.push(v);
1233 }
1234 for k in 1..=rl {
1235 let v = heap[rp + k].clone();
1236 heap.push(v);
1237 }
1238 regs[dst] = RegVal::Small(ptr as i64);
1239 pc += 1;
1240 }
1241 66 => {
1242 // DeepClone { dst, src=ra } — independent copy
1243 // A MAP's header carries MAP_TAG in its high bits, so its cell COUNT is `1 + 2*npairs` (key/value
1244 // pairs), not `heap[sp]` — cloning it as a plain list would read the tagged header as a giant
1245 // length. Copy the header VERBATIM (preserving the tag) and the `2*npairs` key/value cells; a
1246 // plain list/set/struct clones `heap[sp]` cells as before. The isolation pass emits this to keep
1247 // value semantics when a map is mutated in a loop (`Set item k of m`), so a map clone must survive.
1248 // A `Seq of Float`'s element PAYLOADS ride the parallel `fheap` plane at the same slots, so a deep
1249 // copy must carry BOTH planes (mirroring FListPush's two-plane COW) — copying only `heap` clones
1250 // the structure but leaves the cloned block's float payloads 0.0, silently zeroing a float seq
1251 // passed into (and mutated inside) a function. `fheap` stays length-aligned with `heap` (the
1252 // bottom-of-loop resize), so we push a matching float cell for every int cell.
1253 let sp = regs[ra].idx();
1254 let hdr = heap[sp].sm();
1255 let cells = if hdr & MAP_TAG != 0 {
1256 2 * (hdr & !MAP_TAG) as usize
1257 } else {
1258 hdr as usize
1259 };
1260 let ptr = heap.len();
1261 heap.push(RegVal::Small(hdr));
1262 fheap.push(fheap[sp]);
1263 for k in 1..=cells {
1264 let v = heap[sp + k].clone();
1265 let f = fheap[sp + k];
1266 heap.push(v);
1267 fheap.push(f);
1268 }
1269 regs[dst] = RegVal::Small(ptr as i64);
1270 pc += 1;
1271 }
1272 85 => {
1273 iters.push((regs[ra].idx(), 1));
1274 pc += 1;
1275 } // IterPrepare { iterable=ra }
1276 86 => {
1277 // IterNext { dst, exit:16 } — exit_lo=byte2, exit_hi=byte3
1278 let exit = (((instr >> 8) & 0xff) | ((instr & 0xff) << 8)) as usize;
1279 let (ptr, cursor) = *iters.last().unwrap();
1280 let len = heap[ptr].sm() as usize;
1281 if cursor > len {
1282 pc = exit; // exhausted → the loop's exit (IterPop)
1283 } else {
1284 regs[dst] = heap[ptr + cursor].clone();
1285 iters.last_mut().unwrap().1 = cursor + 1;
1286 pc += 1;
1287 }
1288 }
1289 231 => {
1290 // FIterNext { dst, exit:16 } — float-seq iteration: read the f64 PAYLOAD off `fheap` into `fregs[dst]`
1291 // A `Repeat for v in xs` over a `Seq of Float` iterates the SAME `[length, …]` int structure,
1292 // but each element's value rides the parallel `fheap` plane. This mirrors op 86 (IterNext) but
1293 // reads `fheap[ptr+cursor]` into the float bank, so the loop variable `v` is a float scalar and
1294 // a downstream `s + v` lowers to the FPU — byte-identical to the VM's f64 iteration.
1295 let exit = (((instr >> 8) & 0xff) | ((instr & 0xff) << 8)) as usize;
1296 let (ptr, cursor) = *iters.last().unwrap();
1297 let len = heap[ptr].sm() as usize;
1298 if cursor > len {
1299 pc = exit;
1300 } else {
1301 fregs[dst] = fheap[ptr + cursor];
1302 iters.last_mut().unwrap().1 = cursor + 1;
1303 pc += 1;
1304 }
1305 }
1306 87 => {
1307 iters.pop();
1308 pc += 1;
1309 } // IterPop
1310 45 => {
1311 let idx = ((instr >> 8) & 0xff) as usize;
1312 regs[dst] = globals[idx].clone();
1313 pc += 1;
1314 } // GlobalGet { dst, idx=a-byte }
1315 46 => {
1316 globals[dst] = regs[ra].clone();
1317 pc += 1;
1318 } // GlobalSet { idx=dst-field, src=ra }
1319 // ── CONCURRENCY coprocessor: FIFO channels (the deterministic NoC) ─────────────
1320 // A channel is a fixed 8-slot ring buffer on the heap: `[head, count, s0..s7]`. Send enqueues
1321 // at `(head+count) mod 8`, Recv dequeues at `head` — FIFO order, exactly the VM's deterministic
1322 // pipe semantics for a single task (buffered send-then-receive).
1323 92 => {
1324 // ChanNew { dst, cap ignored }
1325 let ptr = heap.len();
1326 heap.push(RegVal::Small(0));
1327 heap.push(RegVal::Small(0)); // head, count
1328 for _ in 0..8 {
1329 heap.push(RegVal::Small(0));
1330 } // 8 ring slots
1331 regs[dst] = RegVal::Small(ptr as i64);
1332 pc += 1;
1333 }
1334 93 => {
1335 // ChanSend { chan=dst, val=ra } — enqueue
1336 let ptr = regs[dst].idx();
1337 let head = heap[ptr].sm() as usize;
1338 let count = heap[ptr + 1].sm() as usize;
1339 let slot = (head + count) % 8;
1340 heap[ptr + 2 + slot] = regs[ra].clone();
1341 heap[ptr + 1] = RegVal::Small((count + 1) as i64);
1342 pc += 1;
1343 }
1344 94 => {
1345 // ChanRecv { dst, chan=ra } — dequeue (FIFO)
1346 let ptr = regs[ra].idx();
1347 let head = heap[ptr].sm() as usize;
1348 let count = heap[ptr + 1].sm() as usize;
1349 regs[dst] = heap[ptr + 2 + head].clone();
1350 heap[ptr] = RegVal::Small(((head + 1) % 8) as i64);
1351 heap[ptr + 1] = RegVal::Small((count - 1) as i64);
1352 pc += 1;
1353 }
1354 95 => {
1355 // ChanTrySend { dst=success, chan=ra, val=rb } — non-blocking
1356 let ptr = regs[ra].idx();
1357 let head = heap[ptr].sm() as usize;
1358 let count = heap[ptr + 1].sm() as usize;
1359 if count < 8 {
1360 heap[ptr + 2 + (head + count) % 8] = regs[rb].clone();
1361 heap[ptr + 1] = RegVal::Small((count + 1) as i64);
1362 regs[dst] = RegVal::Small(1); // sent
1363 } else {
1364 regs[dst] = RegVal::Small(0); // channel full
1365 }
1366 pc += 1;
1367 }
1368 96 => {
1369 // ChanTryRecv { dst=value, chan=ra } — non-blocking
1370 let ptr = regs[ra].idx();
1371 let head = heap[ptr].sm() as usize;
1372 let count = heap[ptr + 1].sm() as usize;
1373 if count > 0 {
1374 regs[dst] = heap[ptr + 2 + head].clone();
1375 heap[ptr] = RegVal::Small(((head + 1) % 8) as i64);
1376 heap[ptr + 1] = RegVal::Small((count - 1) as i64);
1377 } else {
1378 regs[dst] = RegVal::Small(0); // nothing
1379 }
1380 pc += 1;
1381 }
1382 88 => {
1383 pc += 1;
1384 } // Sleep { duration } — deterministic scheduler no-op
1385 72 | 73 => {
1386 regs[dst] = RegVal::Small(0);
1387 pc += 1;
1388 } // LoadToday / LoadNow — placeholder epoch (a real chip reads an RTC)
1389 // Value-transparent metadata ops: EnsureOwned (ownership), RegionBoundsGuard (hoist marker),
1390 // Obs (observability emit) — the interpreter treats them as no-ops for VALUES, so the chip does too.
1391 // Networking (105 NetConnect / 106 NetListen / 107 NetSend / 108 NetStream / 111 NetSync):
1392 // on the single-node deterministic chip there is no peer or wire, so — exactly like the VM's
1393 // documented offline model (tw==VM-net==AOT) — they are no-ops. NetSync leaves its synced
1394 // variable unchanged (no peer to merge). The real NIC/relay is an off-chip transport.
1395 7 | 61 | 112 | 105 | 106 | 108 | 111 => {
1396 pc += 1;
1397 } // EnsureOwned / RegionBoundsGuard / Obs / offline Net no-ops (Connect/Listen/Stream/Sync)
1398 110 => {
1399 regs[dst] = RegVal::Small(peer_seq);
1400 peer_seq += 1;
1401 pc += 1;
1402 } // NetMakePeer { dst, addr } — a fresh offline peer key (identity for the loopback inbox)
1403 107 => {
1404 inbox.push((regs[ra].clone(), regs[rb].clone()));
1405 pc += 1;
1406 } // NetSend { to=ra, msg=rb } — PUBLISH (peer key, message) to the loopback inbox
1407 109 => {
1408 // NetAwait { dst, from=ra } — DRAIN the inbox entry keyed by the peer, delivering its message
1409 let want = regs[ra].clone();
1410 if let Some(i) = inbox.iter().position(|(k, _)| *k == want) {
1411 regs[dst] = inbox.remove(i).1;
1412 }
1413 pc += 1;
1414 }
1415 98 => {
1416 // Spawn { entry=target, args_start=ra, arg_count=rb } — deterministic sync launch (fire-and-forget; pairs with ReturnNothing)
1417 // Launch runs the task SYNCHRONOUSLY (deterministic offline semantics), so a spawn is a
1418 // fire-and-forget Call: the launched body runs in a FRESH frame with R[args_start..+arg_count]
1419 // window-copied into r0.. (exactly the Call convention), its channel side-effects persist on
1420 // the shared heap, and its `ReturnNothing` pops back to Main. This matches the VM's windowed
1421 // task-arg passing (the arg — a channel handle — reaches the body's parameter at its base r0).
1422 let arg_cnt = rb;
1423 let mut callee: [RegVal; 256] = std::array::from_fn(|_| RegVal::Small(0));
1424 let mut callee_f = [0.0f64; 256];
1425 for k in 0..arg_cnt {
1426 callee[k] = regs[ra + k].clone();
1427 callee_f[k] = fregs[ra + k];
1428 }
1429 frames.push((regs, fregs, pc + 1, 0));
1430 regs = callee;
1431 fregs = callee_f;
1432 pc = target;
1433 }
1434 99 => {
1435 // SpawnHandle { handle=dst, entry=a-byte } — sync launch + bind a handle
1436 let entry = ((instr >> 8) & 0xff) as usize;
1437 regs[dst] = RegVal::Small(0); // the task handle (placeholder; the frame preserves it across the sync run)
1438 frames.push((regs.clone(), fregs, pc + 1, 0));
1439 pc = entry;
1440 }
1441 101 => {
1442 pc += 1;
1443 } // TaskAbort { handle } — no-op (the deterministic task already ran to completion)
1444 100 => {
1445 pc += 1;
1446 } // TaskAwait { dst, handle } — the instant join: the sync child already completed at spawn, so control passes through (the VM delivers `Nothing` as the result — task returns aren't propagated — so the result reg is not a meaningful observable)
1447 97 => {
1448 pc += 1;
1449 } // ChanClose { chan } — sets a closed flag no receive path reads; output-transparent
1450 44 => {
1451 // CheckPolicy { subject=ra, cap field=rb } — capability engine (boolean-capability class)
1452 // The policy grants iff the subject's capability field is truthy; else the check TRAPS
1453 // (`Security Check Failed`) — enforcing denial, never a no-op. The policy compiler resolves
1454 // the predicate to the capability field index (rb).
1455 if heap[regs[ra].idx() + 1 + rb].sm() != 0 {
1456 pc += 1;
1457 } else {
1458 trap = Some(false);
1459 break;
1460 }
1461 }
1462 38 => {
1463 // CallBuiltin { dst, args_start=ra, builtin=rb } — kernel-builtin coprocessor
1464 // The builtin ORDINAL rides the b-byte (a chip-internal numbering, NOT the BuiltinId
1465 // discriminant — the encoder's `builtin_chip_ordinal` is the single source of truth,
1466 // mirrored here). Two-arg ops read the second operand from regs[args_start+1]. The WORD
1467 // coprocessor ordinals (30/31/32) compute over the register's 32-bit pattern AS UNSIGNED:
1468 // a rotate is `rotate_left/right`, a logical shift zero-fills the vacated high bits
1469 // (distinct from the ALU's ARITHMETIC `Shr`). Bit-exact to `logicaffeine_base::Word32`.
1470 let arg1 = regs[(ra + 1) & 0xff].clone();
1471 let src = regs[ra].clone();
1472 // FLOAT-plane ordinals (36..=41) read `fregs` — the operand was proven float by the type
1473 // oracle. `abs`/`min`/`max` on floats write `fregs[dst]` (a float scalar); `floor`/`ceil`/
1474 // `round` write `regs[dst]` (an INT result: `f.floor() as i64`, round-half-away). All are
1475 // byte-identical to `call_builtin`'s float arms. They `continue` past the integer store below.
1476 if (36..=41).contains(&rb) {
1477 let a = fregs[ra];
1478 let b = fregs[(ra + 1) & 0xff];
1479 match rb {
1480 36 => fregs[dst] = a.abs(), // FAbs (VM: `f.abs()`)
1481 37 => fregs[dst] = a.min(b), // FMin (VM: `x.min(y)`)
1482 38 => fregs[dst] = a.max(b), // FMax (VM: `x.max(y)`)
1483 39 => regs[dst] = RegVal::Small(a.floor() as i64), // Floor → Int
1484 40 => regs[dst] = RegVal::Small(a.ceil() as i64), // Ceil → Int
1485 _ => regs[dst] = RegVal::Small(a.round() as i64), // Round → Int (round-half-away)
1486 }
1487 pc += 1;
1488 fheap.resize(heap.len(), 0.0);
1489 continue;
1490 }
1491 regs[dst] = match rb {
1492 // Abs promotes on `|i64::MIN|` = 2^63 (exact via BigInt), and takes a `Big` value's exact
1493 // abs — mirroring the VM's `BuiltinId::Abs`.
1494 5 => match &src {
1495 RegVal::Small(n) => match n.checked_abs() {
1496 Some(a) => RegVal::Small(a),
1497 None => RegVal::from_big(src.to_big().abs()),
1498 },
1499 RegVal::Big(b) => RegVal::from_big(b.abs()),
1500 },
1501 7 => {
1502 if regval_cmp(&src, &arg1) != std::cmp::Ordering::Greater {
1503 src
1504 } else {
1505 arg1
1506 }
1507 } // Min
1508 8 => {
1509 if regval_cmp(&src, &arg1) != std::cmp::Ordering::Less {
1510 src
1511 } else {
1512 arg1
1513 }
1514 } // Max
1515 // `count_ones(n)` — set bits over the full 64-bit two's-complement pattern, EXACTLY the
1516 // VM's `(n as u64).count_ones()`. A `Big` operand narrows to its low i64 first (a chip
1517 // register is 64-bit; `count_ones` is defined only over `Int` in `call_builtin`).
1518 9 => RegVal::Small(i64::from((src.sm() as u64).count_ones())),
1519 // The WORD32 ordinals operate on the register's low 32-bit pattern (the `Word32` type is
1520 // ALWAYS 32-bit, independent of the datapath width) and sign-extend the 32-bit result.
1521 30 => RegVal::Small(i64::from(
1522 (src.sm() as u32).rotate_left(arg1.sm() as u32) as i32
1523 )), // BuiltinId::Rotl
1524 31 => RegVal::Small(i64::from(
1525 (src.sm() as u32).rotate_right(arg1.sm() as u32) as i32,
1526 )), // BuiltinId::Rotr
1527 32 => RegVal::Small(i64::from(
1528 (src.sm() as u32).wrapping_shr(arg1.sm() as u32) as i32,
1529 )), // BuiltinId::Word32Shr (LOGICAL)
1530 _ => RegVal::Small(0),
1531 };
1532 pc += 1;
1533 }
1534 102 => {
1535 sel_arms.push((dst, ra));
1536 pc += 1;
1537 } // SelectArmRecv { chan=dst, var=ra } — register a recv arm
1538 103 => {
1539 sel_arms.push((usize::MAX, ra));
1540 pc += 1;
1541 } // SelectArmTimeout { ticks=ra } — register a timeout arm
1542 104 => {
1543 // SelectWait { dst_arm } — fire first-ready recv, else timeout
1544 let mut fired = usize::MAX;
1545 for (i, &(cr, vr)) in sel_arms.iter().enumerate() {
1546 if cr != usize::MAX {
1547 // a recv arm
1548 let ch = regs[cr].idx();
1549 if heap[ch + 1].sm() > 0 {
1550 // channel has a buffered value
1551 let head = heap[ch].sm() as usize;
1552 regs[vr] = heap[ch + 2 + head].clone();
1553 heap[ch] = RegVal::Small(((head + 1) % 8) as i64);
1554 heap[ch + 1] = RegVal::Small(heap[ch + 1].sm() - 1);
1555 fired = i;
1556 break;
1557 }
1558 }
1559 }
1560 if fired == usize::MAX {
1561 // nothing ready → the timeout arm, if any
1562 for (i, &(cr, _)) in sel_arms.iter().enumerate() {
1563 if cr == usize::MAX {
1564 fired = i;
1565 break;
1566 }
1567 }
1568 }
1569 regs[dst] = RegVal::Small(fired as i64);
1570 sel_arms.clear();
1571 pc += 1;
1572 }
1573 // ── CRDT coprocessor: convergent counter ──────────────────────────────────────
1574 // A `Counter` is a plain struct (`[nfields, f0..]`); `Increase c's n by k` accumulates the
1575 // field (`crdt_counter_bump` = field ± amount) and `Merge b into a` joins field-wise —
1576 // `crdt_merge_field(Int,Int)` is wrapping-ADD, so counters converge by summing. Counter
1577 // arithmetic is INTENTIONALLY wrapping (modular, over the cyclic group — see the VM's
1578 // `crdt_counter_bump`/`crdt_merge_field`), so these fields stay `Small` and wrap, NOT promote.
1579 80 => {
1580 // CrdtBump { obj=dst, amount=ra, field=b[3:0], negate=b[4] }
1581 let fb = (instr & 0xff) as usize;
1582 let field = fb & 0xf;
1583 let neg = (fb >> 4) & 1;
1584 let amt = regs[ra].sm();
1585 let ptr = regs[dst].idx();
1586 let delta = if neg == 1 { -amt } else { amt };
1587 heap[ptr + 1 + field] =
1588 RegVal::Small(heap[ptr + 1 + field].sm().wrapping_add(delta));
1589 pc += 1;
1590 }
1591 81 => {
1592 // CrdtMerge { target=dst, source=ra } — field-wise sum-join
1593 let tp = regs[dst].idx();
1594 let sp = regs[ra].idx();
1595 let nf = heap[sp].sm() as usize; // source field count
1596 for f in 0..nf {
1597 heap[tp + 1 + f] =
1598 RegVal::Small(heap[tp + 1 + f].sm().wrapping_add(heap[sp + 1 + f].sm()));
1599 }
1600 pc += 1;
1601 }
1602 113 => {
1603 // CrdtJoin { target=dst, source=ra } — per-replica MAX
1604 let tp = regs[dst].idx(); // the state-based G-Counter lattice join
1605 let sp = regs[ra].idx(); // (idempotent), issued to the crdt_merge block
1606 let nf = heap[sp].sm() as usize; // source lane count (== target's for a G-Counter)
1607 for f in 0..nf {
1608 heap[tp + 1 + f] =
1609 RegVal::Small(heap[tp + 1 + f].sm().max(heap[sp + 1 + f].sm()));
1610 }
1611 pc += 1;
1612 }
1613 114 => {
1614 // BigIntWide — the 256-bit lane (kind = dst-byte hi nibble)
1615 use crate::codegen_sva::bignum_rtl::{
1616 limb_add, limb_lt, limb_sub, wdiv256, wmul256,
1617 };
1618 let kind = ((instr >> 20) & 0xf) as u8; // operation rides the dst byte's high nibble
1619 match kind {
1620 0 => wide[dst & 7] = [(regs[ra].sm() as u32) as u64, 0, 0, 0], // FromInt: W[dst] = zext(R[a])
1621 1 => wide[dst & 7] = limb_add(&wide[ra & 7], &wide[rb & 7]).0, // Add (bigint_alu)
1622 2 => wide[dst & 7] = limb_sub(&wide[ra & 7], &wide[rb & 7]).0, // Sub (bigint_alu)
1623 3 => {
1624 regs[dst & 0xf] =
1625 RegVal::Small(i64::from(limb_lt(&wide[ra & 7], &wide[rb & 7])))
1626 } // Cmp: R[dst] = (W[a] < W[b])
1627 4 => {
1628 regs[dst & 0xf] = RegVal::Small(i64::from((wide[ra & 7][0] as u32) as i32))
1629 } // Low: R[dst] = W[a][31:0] (sign-extended)
1630 5 => wide[dst & 7] = wdiv256(&wide[ra & 7], &wide[rb & 7]), // Div (bigint_div)
1631 6 => wide[dst & 7] = wmul256(&wide[ra & 7], &wide[rb & 7]), // Mul (bigint_mul composed)
1632 _ => {}
1633 }
1634 pc += 1;
1635 }
1636 77 => {
1637 // NewInductive { dst, ctor=a-byte, args_start=b[7:4], count=b[3:0] }
1638 let ctor = ((instr >> 8) & 0xff) as i64; // the variant tag (a valid lowering of the VM's ctor id)
1639 let args_start = ((instr >> 4) & 0xf) as usize;
1640 let count = (instr & 0xf) as usize;
1641 let ptr = heap.len();
1642 heap.push(RegVal::Small(ctor)); // record: [tag, payload0, …]
1643 for i in 0..count {
1644 heap.push(regs[(args_start + i) & 0xff].clone());
1645 }
1646 regs[dst] = RegVal::Small(ptr as i64);
1647 pc += 1;
1648 }
1649 78 => {
1650 // TestArm { dst, target=ra, variant=b-byte } → tag == variant
1651 let variant = (instr & 0xff) as i64;
1652 let ptr = regs[ra].idx();
1653 regs[dst] = RegVal::Small(if heap[ptr].sm() == variant { 1 } else { 0 });
1654 pc += 1;
1655 }
1656 79 => {
1657 // BindArm { dst, target=ra, index=b-byte } → payload[index]
1658 let index = (instr & 0xff) as usize;
1659 let ptr = regs[ra].idx();
1660 regs[dst] = heap[ptr + 1 + index].clone();
1661 pc += 1;
1662 }
1663 51 => {
1664 let ptr = heap.len();
1665 heap.push(RegVal::Small(0));
1666 regs[dst] = RegVal::Small(ptr as i64);
1667 pc += 1;
1668 } // NewEmptySet
1669 55 => {
1670 // SetAdd { set=dst, value=ra } — dedup on insert
1671 let ptr = regs[dst].idx();
1672 let len = heap[ptr].sm() as usize;
1673 let v = regs[ra].clone();
1674 let mut found = false;
1675 for k in 1..=len {
1676 if heap[ptr + k] == v {
1677 found = true;
1678 break;
1679 }
1680 }
1681 if !found {
1682 if ptr + 1 + len == heap.len() {
1683 // on top: append in place
1684 heap.push(v);
1685 heap[ptr] = RegVal::Small((len + 1) as i64);
1686 } else {
1687 // else copy-on-write (value semantics)
1688 let newptr = heap.len();
1689 heap.push(RegVal::Small((len + 1) as i64));
1690 for k in 1..=len {
1691 let x = heap[ptr + k].clone();
1692 heap.push(x);
1693 }
1694 heap.push(v);
1695 regs[dst] = RegVal::Small(newptr as i64);
1696 }
1697 }
1698 pc += 1;
1699 }
1700 53 => {
1701 // NewRange { dst, start=ra, end=rb } — inclusive [start..=end]
1702 let start = regs[ra].sm();
1703 let end = regs[rb].sm();
1704 let n = if end >= start {
1705 (end - start + 1) as usize
1706 } else {
1707 0
1708 };
1709 let ptr = heap.len();
1710 heap.push(RegVal::Small(n as i64));
1711 for i in 0..n {
1712 heap.push(RegVal::Small(start + i as i64));
1713 }
1714 regs[dst] = RegVal::Small(ptr as i64);
1715 pc += 1;
1716 }
1717 68 => {
1718 // UnionOp { dst, lhs=ra, rhs=rb } — fresh lhs ∪ rhs (dedup)
1719 let lp = regs[ra].idx();
1720 let rp = regs[rb].idx();
1721 let ll = heap[lp].sm() as usize;
1722 let rl = heap[rp].sm() as usize;
1723 let ptr = heap.len();
1724 heap.push(RegVal::Small(0));
1725 let mut count = 0usize;
1726 for k in 1..=ll {
1727 let v = heap[lp + k].clone();
1728 heap.push(v);
1729 count += 1;
1730 }
1731 for k in 1..=rl {
1732 let v = heap[rp + k].clone();
1733 let mut found = false;
1734 for j in 0..count {
1735 if heap[ptr + 1 + j] == v {
1736 found = true;
1737 break;
1738 }
1739 }
1740 if !found {
1741 heap.push(v);
1742 count += 1;
1743 }
1744 }
1745 heap[ptr] = RegVal::Small(count as i64);
1746 regs[dst] = RegVal::Small(ptr as i64);
1747 pc += 1;
1748 }
1749 69 => {
1750 // IntersectOp { dst, lhs=ra, rhs=rb } — fresh lhs ∩ rhs
1751 let lp = regs[ra].idx();
1752 let rp = regs[rb].idx();
1753 let ll = heap[lp].sm() as usize;
1754 let rl = heap[rp].sm() as usize;
1755 let ptr = heap.len();
1756 heap.push(RegVal::Small(0));
1757 let mut count = 0usize;
1758 for k in 1..=ll {
1759 let v = heap[lp + k].clone();
1760 let mut in_rhs = false;
1761 for j in 1..=rl {
1762 if heap[rp + j] == v {
1763 in_rhs = true;
1764 break;
1765 }
1766 }
1767 if in_rhs {
1768 heap.push(v);
1769 count += 1;
1770 }
1771 }
1772 heap[ptr] = RegVal::Small(count as i64);
1773 regs[dst] = RegVal::Small(ptr as i64);
1774 pc += 1;
1775 }
1776 56 => {
1777 // RemoveFrom { collection=dst, value=ra } — drop value, shift down
1778 let ptr = regs[dst].idx();
1779 let len = heap[ptr].sm() as usize;
1780 let want = regs[ra].clone();
1781 let mut idx = 0usize;
1782 for k in 1..=len {
1783 if heap[ptr + k] == want {
1784 idx = k;
1785 break;
1786 }
1787 }
1788 if idx > 0 {
1789 for k in idx..len {
1790 let nv = heap[ptr + k + 1].clone();
1791 heap[ptr + k] = nv;
1792 }
1793 heap[ptr] = RegVal::Small((len - 1) as i64);
1794 }
1795 pc += 1;
1796 }
1797 71 => {
1798 // DestructureTuple { start=dst, src=ra, count=rb } — spread into regs
1799 let sp = regs[ra].idx();
1800 for i in 0..rb {
1801 regs[(dst + i) & 0xff] = heap[sp + 1 + i].clone();
1802 }
1803 pc += 1;
1804 }
1805 65 => {
1806 // SliceOp { dst, coll=ra, start=b[7:4], end=b[3:0] } — 1-based inclusive
1807 // `items i through j of xs`: the 1-based-inclusive bounds ride the b-byte nibbles (the encoder
1808 // constant-folds them; a runtime/large bound has no chip lowering and is rejected loudly). The
1809 // VM's `collections::slice` is 1-based-inclusive with an EMPTY out-of-range result: `start0 =
1810 // (start-1)` (0-based inclusive), `end` (1-based inclusive == 0-based exclusive); empty when
1811 // `start0 ≥ end` or `end > len`. Copy `[cp+1+start0 ..< cp+1+end]`.
1812 let start = ((instr >> 4) & 0xf) as usize;
1813 let end = (instr & 0xf) as usize;
1814 let cp = regs[ra].idx();
1815 let len = heap[cp].sm() as usize;
1816 let start0 = start.saturating_sub(1);
1817 let (lo, hi) = if start0 < end && end <= len {
1818 (start0, end)
1819 } else {
1820 (0, 0)
1821 };
1822 let ptr = heap.len();
1823 heap.push(RegVal::Small((hi - lo) as i64));
1824 for i in lo..hi {
1825 let v = heap[cp + 1 + i].clone();
1826 heap.push(v);
1827 }
1828 regs[dst] = RegVal::Small(ptr as i64);
1829 pc += 1;
1830 }
1831 47 => {
1832 // ListPushField { obj=dst, src=ra, field=rb } — push to obj's list field
1833 let obj_ptr = regs[dst].idx();
1834 let list_ptr = heap[obj_ptr + 1 + rb].idx();
1835 let len = heap[list_ptr].sm() as usize;
1836 if list_ptr + 1 + len == heap.len() {
1837 // list on top: append in place
1838 heap.push(regs[ra].clone());
1839 heap[list_ptr] = RegVal::Small((len + 1) as i64);
1840 } else {
1841 // else copy-on-write, and repoint the field
1842 let newptr = heap.len();
1843 heap.push(RegVal::Small((len + 1) as i64));
1844 for k in 1..=len {
1845 let v = heap[list_ptr + k].clone();
1846 heap.push(v);
1847 }
1848 heap.push(regs[ra].clone());
1849 heap[obj_ptr + 1 + rb] = RegVal::Small(newptr as i64);
1850 }
1851 pc += 1;
1852 }
1853 // Div / ExactDiv / Mod / FloorDiv issue to the shared divider (not the ALU) — the
1854 // RTL stalls until `done`; the result is identical, so the model retires it directly. The
1855 // value-integer divider/ALU AUTO-PROMOTE to BigInt on overflow, EXACTLY like the VM (`arith`),
1856 // so the chip differs nowhere on integers — the `width` no longer masks value arithmetic.
1857 12 | 13 | 14 | 15 => {
1858 let r = soft_core_divider_regval(opcode, ®s[ra], ®s[rb]);
1859 regs[dst] = r;
1860 pc += 1;
1861 }
1862 // Pow (op 18): `base ** exp` for a non-negative integer exponent — the sequential-multiply power
1863 // unit (a stall-based FSM in the RTL, mirroring the shared divider), retired directly by the model.
1864 // The accumulator starts at 1 and multiplies by `base` `exp` times through the SAME auto-promoting
1865 // value-integer multiply the ALU uses (`soft_core_alu_regval(11, …)`), so it is BigInt-exact —
1866 // byte-identical to the VM's `int_power`/`BigInt::pow` (`base^0 == 1`; overflow promotes, never
1867 // wraps). A negative exponent errors in the VM (an Int can't hold a fraction), so no differential
1868 // program raises to one; the unsigned loop count reflects that the chip's power unit takes exp ≥ 0.
1869 18 => {
1870 let base = regs[ra].clone();
1871 let exp = regs[rb].sm();
1872 let mut acc = RegVal::Small(1);
1873 let mut k = if exp < 0 { 0 } else { exp };
1874 while k > 0 {
1875 acc = soft_core_alu_regval(11, &acc, &base);
1876 k -= 1;
1877 }
1878 regs[dst] = acc;
1879 pc += 1;
1880 }
1881 // ── WORD32 (ℤ/2³²) ARITHMETIC (chip-synthetic 226/227/228) — a Word32 `+`/`-`/`*` wraps in the
1882 // ring, NOT the unbounded value-integer ALU. The register holds the 32-bit pattern; compute on
1883 // the low 32 bits, wrap (`u32::wrapping_op`), and stay `Small` — a Word32 is NEVER a BigInt.
1884 // Bit-exact to the VM's `Word32::add`/`sub`/`mul` (`u32::wrapping_*`). Sign-extended back to i64
1885 // (`as i32 as i64`) so a Word32 whose high bit is set reads as a negative i64 pattern, exactly
1886 // as the 32-bit datapath carried it; `ShowWord32` re-reads the low 32 bits UNSIGNED.
1887 226 => {
1888 regs[dst] = RegVal::Small(i64::from(
1889 (regs[ra].sm() as u32).wrapping_add(regs[rb].sm() as u32) as i32,
1890 ));
1891 pc += 1;
1892 }
1893 227 => {
1894 regs[dst] = RegVal::Small(i64::from(
1895 (regs[ra].sm() as u32).wrapping_sub(regs[rb].sm() as u32) as i32,
1896 ));
1897 pc += 1;
1898 }
1899 228 => {
1900 regs[dst] = RegVal::Small(i64::from(
1901 (regs[ra].sm() as u32).wrapping_mul(regs[rb].sm() as u32) as i32,
1902 ));
1903 pc += 1;
1904 }
1905 233 => {
1906 // Chr { dst, code=ra } — a 1-char Text from a codepoint
1907 // `chr(code)` builds a ONE-character Text — the char-code list `[1, codepoint]` on the heap,
1908 // the SAME shape TextConst/op 64/Concat produce, so a downstream `ShowText` decodes it
1909 // byte-identically to the VM's `RuntimeValue::Text(c.to_string())`. An INVALID codepoint (a
1910 // surrogate 0xD800..=0xDFFF or a value > 0x10FFFF) has no character — the VM's `call_builtin`
1911 // returns `Err` there, aborting the run with the output produced so far standing, so the chip
1912 // TRAPS (halts, pre-trap output stands), byte-identical to the VM's error-path output.
1913 let code = regs[ra].sm();
1914 match u32::try_from(code).ok().and_then(char::from_u32) {
1915 Some(_) => {
1916 let ptr = heap.len();
1917 heap.push(RegVal::Small(1)); // header: a 1-char string
1918 heap.push(RegVal::Small(code)); // the codepoint (a valid `char`)
1919 regs[dst] = RegVal::Small(ptr as i64);
1920 pc += 1;
1921 }
1922 None => {
1923 trap = Some(false);
1924 break;
1925 } // invalid codepoint → trap (VM returns Err)
1926 }
1927 }
1928 234 => {
1929 // TextContains { dst, hay=ra, needle=rb } — substring scan
1930 // `contains(haystack, needle)` over two Text char-lists: `regs[dst] = 1` iff `needle` occurs
1931 // as a contiguous run of codepoints in `haystack`, else 0 — byte-identical to the VM's
1932 // `h.contains(n.as_str())` (Rust `str::contains` on the reconstructed strings). The empty
1933 // needle is contained in EVERYTHING (`"".contains` is always true), matching Rust. The scan
1934 // compares CODEPOINTS (the char-list cells), which is exactly Rust's UTF-8 substring test
1935 // since a codepoint match is a byte match for the canonical UTF-8 both sides carry.
1936 let hp = regs[ra].idx();
1937 let np = regs[rb].idx();
1938 let hl = heap[hp].sm() as usize; // haystack char count
1939 let nl = heap[np].sm() as usize; // needle char count
1940 let mut found = 0i64;
1941 if nl == 0 {
1942 found = 1; // the empty needle is always contained
1943 } else if nl <= hl {
1944 for start in 0..=(hl - nl) {
1945 // every window of `nl` chars in the haystack
1946 let mut all = true;
1947 for k in 0..nl {
1948 if heap[hp + 1 + start + k].sm() != heap[np + 1 + k].sm() {
1949 all = false;
1950 break;
1951 }
1952 }
1953 if all {
1954 found = 1;
1955 break;
1956 }
1957 }
1958 }
1959 regs[dst] = RegVal::Small(found);
1960 pc += 1;
1961 }
1962 235 => {
1963 // RepeatSeq { dst, elem=ra, count=rb } — n-slot scalar fill
1964 // `repeatSeq(x, n)` builds a fresh `n`-element Seq, every slot the SAME scalar `regs[ra]`
1965 // (`n ≤ 0` is empty), byte-identical to the VM's `(0..n).map(|_| element.deep_clone())` for a
1966 // SCALAR element (an Int/Word32 register value deep-clones to itself — the encoder only emits
1967 // this op for a provably-scalar element, rejecting a float/reference element inside, since a
1968 // heap-reference element would need per-slot deep copies the flat fill can't express). The
1969 // count is a runtime register value; the chip's fill unit is a sequential state machine (like
1970 // the divider), so the model loops here.
1971 let n = regs[rb].sm().max(0) as usize;
1972 let elem = regs[ra].clone();
1973 let ptr = heap.len();
1974 heap.push(RegVal::Small(n as i64)); // header
1975 for _ in 0..n {
1976 heap.push(elem.clone());
1977 }
1978 regs[dst] = RegVal::Small(ptr as i64);
1979 pc += 1;
1980 }
1981 236 => {
1982 // ParseInt { dst, text=ra } — decimal char-scan → i64
1983 // `parseInt(text)` reads the Text char-list at `regs[ra]` (`[len, cp0, cp1, …]`) and computes
1984 // the i64 value EXACTLY as the VM's `s.trim().parse::<i64>()`: trim Unicode whitespace off both
1985 // ends, accept an optional single leading `+`/`-`, then require ≥1 ASCII digit, accumulating in
1986 // the NEGATIVE magnitude (`acc = acc*10 − digit`) so `-9223372036854775808` (= i64::MIN) parses
1987 // where its positive twin overflows — the exact boundary Rust's parser draws. A non-numeric,
1988 // empty, or OVERFLOWING input has no i64 value: the VM's `parse` returns `Err`, whose `?` aborts
1989 // the run with the output so far standing, so the chip TRAPS (`break`, pre-trap output stands) —
1990 // byte-identical. The VM does NOT promote an overflow to BigInt; it errors, so neither do we.
1991 let ptr = regs[ra].idx();
1992 let len = heap[ptr].sm() as usize;
1993 // Reconstruct the codepoint slice, then trim by Unicode whitespace (Rust's `str::trim`).
1994 let cps: Vec<u32> = (0..len).map(|k| heap[ptr + 1 + k].sm() as u32).collect();
1995 let is_ws = |cp: u32| char::from_u32(cp).map_or(false, |c| c.is_whitespace());
1996 let mut lo = 0usize;
1997 let mut hi = cps.len();
1998 while lo < hi && is_ws(cps[lo]) {
1999 lo += 1;
2000 }
2001 while hi > lo && is_ws(cps[hi - 1]) {
2002 hi -= 1;
2003 }
2004 let body = &cps[lo..hi];
2005 // Optional single leading sign, then the digit run (matching `i64::from_str_radix(_, 10)`).
2006 let (neg, digits) = match body.first().copied() {
2007 Some(c) if c == '-' as u32 => (true, &body[1..]),
2008 Some(c) if c == '+' as u32 => (false, &body[1..]),
2009 _ => (false, body),
2010 };
2011 // Accumulate as the NEGATIVE magnitude; any non-digit char or an i64 overflow → trap.
2012 let mut acc: Option<i64> = Some(0);
2013 let mut saw_digit = false;
2014 for &cp in digits {
2015 if !(('0' as u32)..=('9' as u32)).contains(&cp) {
2016 acc = None;
2017 break;
2018 }
2019 saw_digit = true;
2020 let d = (cp - '0' as u32) as i64;
2021 acc = acc
2022 .and_then(|a| a.checked_mul(10))
2023 .and_then(|a| a.checked_sub(d));
2024 if acc.is_none() {
2025 break;
2026 }
2027 }
2028 // Empty input (no digits at all) is an error too — Rust's parser rejects `""`/`"-"`/`"+"`.
2029 let parsed = if !saw_digit {
2030 None
2031 } else {
2032 acc.and_then(|a| if neg { Some(a) } else { a.checked_neg() })
2033 };
2034 match parsed {
2035 Some(v) => {
2036 regs[dst] = RegVal::Small(v);
2037 pc += 1;
2038 }
2039 None => {
2040 trap = Some(false);
2041 break;
2042 } // non-numeric / empty / overflow → trap (VM returns Err)
2043 }
2044 }
2045 237 => {
2046 // TextBytes { dst, text=ra } — UTF-8 encode a Text char-list
2047 // `text_bytes(text)` reads the char-list at `regs[ra]` (`[len, cp0, cp1, …]`) and builds a fresh
2048 // `Seq of Int` `[n, b0, b1, …]` of its UTF-8 bytes, byte-identical to the VM's
2049 // `bytes_to_seq(s.as_bytes())`. Each codepoint encodes by its range ([`utf8_of_codepoint`], the
2050 // RTL's `tbx` encoder); the bytes follow the header in order, and the header, written last,
2051 // carries the total byte count.
2052 let ptr = regs[ra].idx();
2053 let len = heap[ptr].sm() as usize;
2054 let base = heap.len();
2055 heap.push(RegVal::Small(0));
2056 for k in 0..len {
2057 let (bytes, n) = utf8_of_codepoint(heap[ptr + 1 + k].sm() as u32);
2058 for &b in &bytes[..n] {
2059 heap.push(RegVal::Small(i64::from(b)));
2060 }
2061 }
2062 heap[base] = RegVal::Small((heap.len() - base - 1) as i64);
2063 regs[dst] = RegVal::Small(base as i64);
2064 pc += 1;
2065 }
2066 _ => {
2067 let r = soft_core_alu_regval(opcode, ®s[ra], ®s[rb]);
2068 regs[dst] = r;
2069 pc += 1;
2070 }
2071 }
2072 // FLOAT-HEAP PLANE INVARIANT: keep `fheap` the SAME LENGTH as `heap` after every op. Any op that
2073 // grew `heap` (an int list/map/range/struct/…) leaves a matching `0.0` float cell — the float-list
2074 // ops (213/214/215) keep their own float cells aligned as they push, so this only backfills the
2075 // integer-structure growth. Mirrors the RTL `fdmem` bump tracking `dmem`.
2076 fheap.resize(heap.len(), 0.0);
2077 }
2078 (out, trap)
2079}
2080
2081/// The UTF-8 encoding of one codepoint by its range — one byte below 0x80, two below 0x800, three below
2082/// 0x10000, else four — returned as the bytes and their count. On every Unicode scalar value this is
2083/// exactly `char::encode_utf8`; it is written by range because it is the RTL's encoder too (`tbx` in
2084/// `soft_core.v`), so the model and the silicon share one spec rather than one calling the standard library.
2085fn utf8_of_codepoint(cp: u32) -> ([u8; 4], usize) {
2086 let cont = |shift: u32| 0x80 | ((cp >> shift) & 0x3F) as u8;
2087 if cp < 0x80 {
2088 ([cp as u8, 0, 0, 0], 1)
2089 } else if cp < 0x800 {
2090 ([0xC0 | (cp >> 6) as u8, cont(0), 0, 0], 2)
2091 } else if cp < 0x1_0000 {
2092 ([0xE0 | (cp >> 12) as u8, cont(6), cont(0), 0], 3)
2093 } else {
2094 (
2095 [0xF0 | ((cp >> 18) & 0x07) as u8, cont(12), cont(6), cont(0)],
2096 4,
2097 )
2098 }
2099}
2100
2101/// The integer-differential view of [`run_soft_core_typed`]: the `Show` sequence as raw `i64` values.
2102/// The pure integer programs (the hand-assembled ≡VM differential, the testbench emitter, the silicon
2103/// demo) never emit a float `Show`, so at the 32-bit width this stays value-identical to the pre-FPU
2104/// model (a 32-bit result carried in the low bits of an `i64`). A float program uses
2105/// [`run_soft_core_typed`] + [`ShowVal::fmt`] instead (panics loudly here if one leaks a float).
2106pub fn run_soft_core(imem: &[u32], max_cycles: usize) -> Vec<i64> {
2107 run_soft_core_w(imem, max_cycles, 32)
2108}
2109
2110/// The WIDTH-PARAMETRIC integer-differential view of [`run_soft_core_typed_w`]: the `Show` sequence as raw
2111/// `i64` values MASKED to the datapath `width` — the fixed-width tape-out core's OUTPUT REGISTER view
2112/// (`width == 32` truncates each value to i32, `width == 64` is the full i64 pattern). This is DISTINCT
2113/// from [`run_soft_core_typed_w`], whose `ShowVal` carries the UNBOUNDED value (VM-exact via BigInt): the
2114/// typed path is the chip that "differs nowhere from the VM"; this raw view models what a `width`-bit
2115/// hardware output register would show if you read the low bits, so the 32-bit tape-out core's wrap is
2116/// still observable here. A value that overflowed to `Big` contributes its low-64-bit two's-complement
2117/// pattern, then sign-extended to `width`. (Float/Text/Word32 `Show`s panic — the pure-integer callers
2118/// never emit them.)
2119pub fn run_soft_core_w(imem: &[u32], max_cycles: usize, width: u32) -> Vec<i64> {
2120 run_soft_core_typed_w(imem, max_cycles, width)
2121 .iter()
2122 .map(|sv| sign_extend_to_width(showval_low_i64(sv), width))
2123 .collect()
2124}
2125
2126/// The low-64-bit two's-complement pattern of an integer `Show` — the `i64` for a `Small`, the low limb
2127/// (with sign) for a `Big`. The width-masked raw view ([`run_soft_core_w`]) reads this; a Float/Text/
2128/// Word32 `Show` panics (the pure-integer differential/testbench callers never emit one).
2129fn showval_low_i64(sv: &ShowVal) -> i64 {
2130 match sv {
2131 ShowVal::Int(v) => *v,
2132 ShowVal::BigInt(b) => {
2133 // The low 64 bits as a two's-complement i64: reduce the exact value mod 2^64 via the
2134 // signed remainder against 2^64 (an over-i64 `Big` never comes from the 32-bit oracle, so
2135 // this only serves the width-64 raw view of a genuinely huge value).
2136 let mask = BigInt::from_u64(u64::MAX).add(&BigInt::from_i64(1)); // 2^64
2137 let (_, r) = b.div_rem(&mask).expect("2^64 is nonzero");
2138 // `r` carries the dividend's sign and |r| < 2^64; fold to the u64 bit pattern, then i64.
2139 let (neg, bytes) = r.to_le_bytes();
2140 let mut limb = [0u8; 8];
2141 for (i, byte) in bytes.iter().take(8).enumerate() {
2142 limb[i] = *byte;
2143 }
2144 let u = u64::from_le_bytes(limb);
2145 let u = if neg { u.wrapping_neg() } else { u };
2146 u as i64
2147 }
2148 ShowVal::Float(f) => panic!("integer soft-core run emitted a float Show ({f})"),
2149 ShowVal::Text(s) => panic!("integer soft-core run emitted a text Show ({s:?})"),
2150 ShowVal::Word32(w) => panic!("integer soft-core run emitted a word32 Show ({w})"),
2151 }
2152}
2153
2154#[cfg(test)]
2155#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/soft_core_model/width_toggle_tests.rs"]
2156mod width_toggle_tests;
2157
2158#[cfg(test)]
2159#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/soft_core_model/text_bytes_tests.rs"]
2160mod text_bytes_tests;