logicaffeine_compile/codegen_sva/full_asm.rs
1//! full_asm — assemble a compiled LOGOS program (register bytecode `CompiledProgram`) into the
2//! instruction words the FULL soft-core (`soft_core.v`) executes: the whole 113-op ISA — user functions
3//! (hardware arg-copy `Call`), division/modulo (the sequential shared divider), and the structured heap
4//! (lists, tuples, ranges, sets, maps, structs) — so the ENTIRE benchmark suite compiles from `.lg` source
5//! to words that run on silicon byte-identically to the VM.
6//!
7//! Unlike `scalar_asm` (the function-free combinational subset), this targets the full datapath and lays
8//! out Main + every function body in one imem image. The op BYTE is always `binisa::opcode(op)` — the
9//! append-only ISA ABI the hardware decoder keys on — and every operand field is packed exactly as
10//! `codegen_sva::rtl::run_soft_core` decodes it (the ≡VM software model is the field-layout spec).
11//!
12//! Locked ≡ VM by `rtl.rs::tests::encode_full_matches_vm`.
13
14use crate::vm::CompiledProgram;
15
16#[cfg(test)]
17#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/full_asm/survey.rs"]
18mod survey;
19
20use crate::vm::binisa::opcode;
21use crate::vm::{Constant, Op, Reg};
22
23/// A register index must fit the full core's 256-entry file (8-bit operand fields).
24fn reg(r: Reg) -> Result<u32, String> {
25 if r > 255 {
26 return Err(format!(
27 "register r{r} exceeds the full core's 256-register file"
28 ));
29 }
30 Ok(r as u32)
31}
32
33/// `[opcode:8][dst:8][a:8][b:8]` — the general full-core word (8-bit operand fields).
34fn w(oc: u16, dst: u32, a: u32, b: u32) -> u32 {
35 ((oc as u32) << 24) | (dst << 16) | (a << 8) | b
36}
37
38// ── CHIP-SYNTHETIC FLOAT OPCODES ──────────────────────────────────────────────────────────────────
39// The soft-core is dynamically typed at the VM level (`Add`/`Mul`/… are polymorphic over Int and f64),
40// but silicon needs a fixed datapath per op. These opcodes drive the 64-bit float bank + `fpu.v`. They
41// sit ABOVE the append-only ISA table (like `LoadConstWide`=200) so they are pure chip-internal decode
42// with NO `Op` variant and NO ISA-map entry — the ISA totality/append-only locks stay untouched. The
43// encoder emits them by STATICALLY lowering a float-typed op (see [`FloatTyping`]); `run_soft_core`
44// models each with real Rust `f64` (bit-identical to the RTL), and `soft_core.v` dispatches them to
45// `fpu.v`. Numbers must match `codegen_sva::rtl::run_soft_core_typed` and `soft_core.v` exactly.
46const FCONST: u16 = 201; // 3-word: [201][dst][0][0] ‖ bits[31:0] ‖ bits[63:32]
47const FADD: u16 = 202;
48const FSUB: u16 = 203;
49const FMUL: u16 = 204;
50const FDIV: u16 = 205;
51#[allow(dead_code)]
52const FSQRT: u16 = 206; // reached via the `sqrt(x)` builtin lowering (spectral_norm / nbody)
53const FLT: u16 = 207; // float `<` -> int 0/1 in regs
54const FEQ: u16 = 208; // float `==` -> int 0/1 in regs
55const FLE: u16 = 229; // float `<=` -> int 0/1 in regs (le = lt|eq; `>=` is FLe with swapped operands)
56const FNE: u16 = 230; // float `!=` -> int 0/1 in regs (ne = !eq)
57 // The `is approximately` (isclose) compare has NO dedicated float opcode: `float_shape` DECOMPOSES it into
58 // the primitive float ops the RTL already sequences (FEq/FSub/FAbs/FMax/FConst/FMul/FLe) combined by an int
59 // `BitOr` — so no bespoke multi-cycle isclose FSM is needed and the SAME image runs on the model and RTL.
60const BITOR: u16 = 21; // `eq | le` — the ISA `BitOr` ALU op (combines the two 0/1 isclose results)
61
62// The FLOAT-plane CallBuiltin (opcode 38) coprocessor ordinals — a chip-internal numbering DISTINCT from
63// the integer-plane ordinals in `builtin_chip_ordinal` (5/7/8/9): these read `fregs` (an operand proven
64// float by the type oracle). 36/37/38 are `abs`/`min`/`max` on floats (fregs → fregs). 39/40/41 are
65// `floor`/`ceil`/`round` on a float — fregs → REGS (an INT result: the VM's `f.floor() as i64` etc). The
66// model's `38 =>` arm branches on the ordinal to pick the plane; `soft_core.v`'s fpu case mirrors it.
67const CB_FABS: u32 = 36;
68const CB_FMIN: u32 = 37;
69const CB_FMAX: u32 = 38;
70const CB_FLOOR: u32 = 39;
71const CB_FCEIL: u32 = 40;
72const CB_FROUND: u32 = 41;
73const INT_TO_F: u16 = 209; // fregs[dst] = regs[src] as f64 (Int operand used in a float context)
74#[allow(dead_code)]
75const F_TO_INT: u16 = 210; // regs[dst] = fregs[src] as i32
76const FMOVE: u16 = 211; // fregs[dst] = fregs[src]
77const SHOWF: u16 = 212; // Show a float
78 // ── THE FLOAT-HEAP PLANE (chip-synthetic opcodes 213..=215) ──────────────────────────────────────────
79 // A `Seq of Float` handle is an INT pointer into the integer heap (which keeps the `[length, …]` header +
80 // structure); its f64 element payloads ride a PARALLEL float plane (`fheap`/`fdmem`) at the SAME indices.
81 // The encoder emits these when the float-seq type oracle proves a heap op operates on a `Seq of Float`:
82 // a float `Push`/`Add to`, a float `Set item`, and an `item i of (Seq of Float)` read.
83const FLIST_PUSH: u16 = 213; // FListPush { list=dst, value=ra } — int ListPush structure + set fheap[slot]=fregs[ra]
84const FINDEX: u16 = 214; // FIndex { dst, coll=ra, index=rb } — fregs[dst] = fheap[regs[ra]+regs[rb]]
85const FSET_INDEX: u16 = 215; // FSetIndex { coll=dst, index=ra, value=rb } — fheap[regs[dst]+regs[ra]] = fregs[rb]
86const FITER_NEXT: u16 = 231; // FIterNext { dst, exit:16 } — `Repeat for v in (Seq of Float)`: fregs[dst] = fheap[ptr+cursor]
87 // ── THE TEXT (string) PLANE (chip-synthetic opcodes 216/217) ─────────────────────────────────────────
88 // A `Text` is a char-code list `[len, c0, c1, …]` on the integer heap — so its OPERATIONS (Concat = a Text
89 // `Add`, Length, `item i of`, FormatValue) already ride the list/heap datapath. The two things a char-list
90 // identity does NOT supply are a Text CONSTANT (there is no 32-bit-integer encoding of "hello") and a Text
91 // `Show` (the string must be reconstituted from the char list). These two chip-synthetic opcodes fill the
92 // gap; the char data of every distinct Text constant is appended AFTER the code as a STRING-CONSTANT POOL in
93 // imem (compact — one shared block per string), and `TextConst` copies a block into a fresh heap char-list.
94/// Pool keys of the Text a failure may need that no program constant spells: the two Bool renderings
95/// (`Fail with false`) and the refusal of a non-Text contract reason, one per kind the chip carries.
96const POOL_TRUE: u32 = u32::MAX - 1;
97const POOL_FALSE: u32 = u32::MAX - 2;
98const POOL_REFUSE_INT: u32 = u32::MAX - 3;
99const POOL_REFUSE_BOOL: u32 = u32::MAX - 4;
100const FAILURE_POOL: &[(u32, &str)] = &[
101 (POOL_TRUE, "true"),
102 (POOL_FALSE, "false"),
103 (
104 POOL_REFUSE_INT,
105 "The reason after `otherwise` must be Text, but it was Int.",
106 ),
107 (
108 POOL_REFUSE_BOOL,
109 "The reason after `otherwise` must be Text, but it was Bool.",
110 ),
111];
112
113/// How a failure reaches the chip's failure contract: stream the reason over the Text port
114/// (`ShowText`), then trap-halt with `reasoned` set (`FailWith`, rb = 1).
115enum FailPlan {
116 /// The reason is already a Text in `src`.
117 Text { src: Reg },
118 /// `Fail with <Int>` — render it (`FormatValue`) first.
119 Int { src: Reg },
120 /// `Fail with <Bool>` — select the pooled `true`/`false` by arithmetic on the 0/1 value, using the
121 /// never-returning call's `dst` as the second temporary.
122 Bool { src: Reg, dst: Reg },
123 /// A pooled Text (a `FailWith` message, a refusal) — its pool key.
124 Pooled { key: u32 },
125}
126
127impl FailPlan {
128 fn width(&self) -> usize {
129 match self {
130 FailPlan::Text { .. } => 2,
131 FailPlan::Int { .. } => 3,
132 FailPlan::Bool { .. } => 9,
133 FailPlan::Pooled { .. } => 4,
134 }
135 }
136}
137
138/// The `format(x)` builtin on the chip: `x`'s text exactly as `Show` renders it.
139enum FormatPlan {
140 /// Already Text — the same handle.
141 Text { dst: Reg, src: Reg },
142 /// An Int — its decimal (`FormatValue`).
143 Int { dst: Reg, src: Reg },
144 /// A Bool — the pooled `true`/`false`, selected by arithmetic on the 0/1 value.
145 Bool { dst: Reg, src: Reg },
146}
147
148impl FormatPlan {
149 fn width(&self) -> usize {
150 match self {
151 FormatPlan::Text { .. } | FormatPlan::Int { .. } => 1,
152 FormatPlan::Bool { .. } => 7,
153 }
154 }
155}
156
157fn format_plan(i: usize, op: &Op, ft: &FloatTyping) -> Result<Option<FormatPlan>, String> {
158 use crate::semantics::builtins::BuiltinId;
159 let Op::CallBuiltin {
160 dst,
161 builtin: BuiltinId::Format,
162 args_start,
163 arg_count,
164 } = op
165 else {
166 return Ok(None);
167 };
168 if *arg_count != 1 {
169 return Err(format!("instruction {i}: format() takes one value"));
170 }
171 let (dst, src) = (*dst, *args_start);
172 Ok(Some(if ft.is_text(i, src) {
173 FormatPlan::Text { dst, src }
174 } else if ft.is_float(i, src) {
175 return Err(format!(
176 "instruction {i}: format() of a Real has no chip rendering (the chip's float formatter is \
177 fixed-precision, not `Show`'s)"
178 ));
179 } else if ft.is_bool(i, src) {
180 FormatPlan::Bool { dst, src }
181 } else {
182 FormatPlan::Int { dst, src }
183 }))
184}
185
186/// The failure lowering of op `i`, if it is one: a `FailWith` with a Text message, or the `Fail` /
187/// `FailReason` builtins. A reason this chip cannot render as `Show` would (a Float) is refused.
188fn failure_plan(
189 i: usize,
190 op: &Op,
191 constants: &[Constant],
192 ft: &FloatTyping,
193) -> Result<Option<FailPlan>, String> {
194 use crate::semantics::builtins::BuiltinId;
195 Ok(match op {
196 Op::FailWith { msg } if matches!(constants.get(*msg as usize), Some(Constant::Text(_))) => {
197 Some(FailPlan::Pooled { key: *msg })
198 }
199 Op::CallBuiltin {
200 dst,
201 builtin: b @ (BuiltinId::Fail | BuiltinId::FailReason),
202 args_start,
203 ..
204 } => {
205 let a = *args_start;
206 Some(if ft.is_text(i, a) {
207 FailPlan::Text { src: a }
208 } else if ft.is_float(i, a) {
209 return Err(format!(
210 "instruction {i}: a failure reason that is a Real has no chip rendering (the chip's float \
211 formatter is fixed-precision, not `Show`'s)"
212 ));
213 } else if *b == BuiltinId::Fail {
214 if ft.is_bool(i, a) {
215 FailPlan::Bool { src: a, dst: *dst }
216 } else {
217 FailPlan::Int { src: a }
218 }
219 } else if ft.is_bool(i, a) {
220 FailPlan::Pooled {
221 key: POOL_REFUSE_BOOL,
222 }
223 } else {
224 FailPlan::Pooled {
225 key: POOL_REFUSE_INT,
226 }
227 })
228 }
229 _ => None,
230 })
231}
232
233/// The string-pool key of the program name a chip's `args()` answers with ([`crate::semantics::PROGRAM_NAME`],
234/// the host-less argv); no real constant index reaches it.
235const ARGV0_POOL_KEY: u32 = u32::MAX;
236const TEXTCONST: u16 = 216; // TextConst { dst } — 2-word: word0=[216][dst][len:16], word1=pool_word_offset (imem index of c0). Copies len chars imem[off..off+len] into a fresh heap [len, chars…]; regs[dst]=ptr.
237const SHOWTEXT: u16 = 217; // ShowText { src=ra } — decode the char list at regs[ra] to a UTF-8 string and Show it
238const TEXTCHARAT: u16 = 218; // TextCharAt { dst, coll=ra, index=rb } — a single-char Text: fresh heap [1, heap[regs[ra]+regs[rb]]] (the VM's `item i of text` returns a 1-char string)
239const TEXTEQ: u16 = 219; // TextEq { dst, a=ra, b=rb } — char-list CONTENT equality (regs[dst] = 1/0), matching the VM's Text `equals`
240const TEXTNE: u16 = 220; // TextNe { dst, a=ra, b=rb } — char-list content INEQUALITY (the negation of TextEq)
241 // ── THE WORD32 (ℤ/2³²) PLANE (chip-synthetic opcode 221) ─────────────────────────────────────────────
242 // A `Word32` is the 32-bit register's own bit pattern, so its VALUE rides the ordinary integer datapath —
243 // `word32(n)`/`intOfWord32(w)` are the identity (`Move`), `word_and`/`word_or`/`word_not` are the existing
244 // `BitAnd`/`BitOr`/`Not` ALU ops (bit-identical to the ring), and a Word32 `+` is `Add` (wrapping i32 ==
245 // wrapping u32). The ONLY things the integer datapath does NOT supply are a ROTATE and a LOGICAL right shift
246 // (the ALU's `Shr` is ARITHMETIC) — routed through the `CallBuiltin` coprocessor (opcode 38) — and an
247 // UNSIGNED `Show`: the integer `Show`=89 prints `i32` SIGNED, but a Word32 (SHA-256 hash word > 2³¹) must
248 // print unsigned. `ShowWord32` is that unsigned Show; the model renders `ShowVal::Word32`, and `soft_core.v`
249 // routes it to a separate output port so the host formats it unsigned (int/float/string Shows unchanged).
250const SHOWWORD32: u16 = 221; // ShowWord32 { src=ra } — Show the 32-bit result at regs[ra] as UNSIGNED (0..2³²−1)
251 // ── THE 64-BIT CONSTANT (chip-synthetic opcode 222) ──────────────────────────────────────────────────
252 // The inline 16-bit (opcode 5) and wide 32-bit (opcode 200) LoadConst forms cannot hold a constant that
253 // exceeds i32 (the benchmark PRNGs' `% 2147483648`/`% 4294967296` moduli, 2³¹ and 2³², or a full-range i64
254 // literal). Such a constant is emitted as a 3-word LoadConstWide64 at EVERY datapath width: word0 =
255 // [222][dst][0][0], word1 = bits[31:0], word2 = bits[63:32] — the full i64, mirroring `FConst`'s
256 // two-immediate-word layout. `run_soft_core_typed_w`'s `222 =>` arm retires the FULL i64 irrespective of
257 // width, and the value plane auto-promotes to BigInt on overflow exactly like the VM's unbounded `Int`, so
258 // the full core reproduces every constant — and every arithmetic result — EXACTLY at width 32 as at 64. The
259 // width is a fast-path/area knob (how wide the native adder is), not a correctness switch: BigInt handles
260 // exactness, so the full-core image is width-independent. (The separate 32-bit tape-out core in
261 // `scalar_asm.rs` genuinely wraps at 2³² — that is the small-area chip, distinct from this exact full core.)
262const LOADCONSTWIDE64: u16 = 222;
263// ── THE INT-KEY MAP PLANE (chip-synthetic opcodes 223/224) ───────────────────────────────────────────
264// A `Map of Int to …` stores its keys as DIRECT integers in the key cells (not char-list pointers). The
265// shared map ops SetIndex/Index (57/59) compare keys with `key_eq` — a char-list CONTENT compare that reads
266// the key as a heap pointer — which is correct for a `Map of Text` key but WRONG for an integer key (it would
267// dereference the int as an address). These two chip-synthetic ops carry the int-key path: `MapIndexInt`
268// (223) looks up by integer `==`, `MapSetIndexInt` (224) sets/appends by integer `==`. The encoder emits them
269// for an `Index`/`SetIndex` whose collection is a MAP and whose key is statically NON-Text; a `Map of Text`
270// key (a Text-typed key register) keeps the string-key 57/59 form, and a plain LIST index stays positional
271// 57/59. So the string-key map path (locked by `the_soft_core_runs_logos_programs_identically`) is untouched.
272const MAPINDEXINT: u16 = 223; // MapIndexInt { dst, map=ra, key=rb } — map lookup by integer key (==)
273const MAPSETINDEXINT: u16 = 224; // MapSetIndexInt { map=dst, key=ra, value=rb } — map set/append by integer key (==)
274 // ── THE FLOAT FORMAT PLANE (chip-synthetic opcode 225) ───────────────────────────────────────────────
275 // A `Show "{f:.N}"` on a FLOAT renders `fregs[src]` to N decimal places as a Text char-list. The integer
276 // `FormatValue` (op 64) reads `regs[src]` and builds the decimal string of an i64; a float needs the FPU's
277 // value and a fixed-precision decimal formatting the integer path can't express. `FormatValueFloat` reads
278 // the f64 in `fregs[src]`, formats it EXACTLY as the VM (`apply_format_spec` on a `.N` spec = Rust's
279 // `format!("{:.N$}", f)`), and lays the UTF-8 char-codes out as a fresh heap `[len, chars…]` — the same Text
280 // char-list shape op 64 / `TextConst` / `Concat` produce, so a downstream `ShowText` (217) decodes it
281 // byte-identically. The precision N (0..255; the benchmarks use 9 / 15) rides the word's b-byte.
282const FORMATVALUE_FLOAT: u16 = 225; // FormatValueFloat { dst, src=ra, prec=rb } — fixed-precision decimal Text of fregs[ra]
283 // ── THE WORD32 (ℤ/2³²) ARITHMETIC PLANE (chip-synthetic opcodes 226/227/228) ─────────────────────────
284 // A `Word32 +`/`-`/`*` adds/subtracts/multiplies IN THE RING ℤ/2³² (wrapping mod 2³²), exactly as the VM's
285 // `Word32::add`/`sub`/`mul` (`u32::wrapping_add`/…). This is DISTINCT from the value-integer ALU (opcodes
286 // 8/10/11), which is now UNBOUNDED (auto-promoting to BigInt on overflow) so that a plain `Int +` matches
287 // the VM's exact `Int` — a Word32 sum must NOT promote, it must wrap to 32 bits (SHA-256's `s1 + s0 + …`
288 // accumulator would otherwise drift past 2³² round over round and corrupt the hash). The model computes
289 // `((a as u32) wrapping_op (b as u32)) as i32` and stays `Small`; the result Shows UNSIGNED via ShowWord32.
290const WORD32ADD: u16 = 226; // Word32Add { dst, a=ra, b=rb } — regs[dst] = (regs[ra] +ᵤ regs[rb]) mod 2³²
291const WORD32SUB: u16 = 227; // Word32Sub { dst, a=ra, b=rb } — regs[dst] = (regs[ra] -ᵤ regs[rb]) mod 2³²
292const WORD32MUL: u16 = 228; // Word32Mul { dst, a=ra, b=rb } — regs[dst] = (regs[ra] ·ᵤ regs[rb]) mod 2³²
293 // ── THE TEXT / SEQ BUILTIN PLANE (chip-synthetic opcodes 233–237) ──────────────────────────────────
294 // The deterministic string / fill builtins the CHIP compiler emits from source, each producing an on-heap
295 // value from register operands — byte-identical to the matching `semantics::builtins::call_builtin` arm:
296 // • `Chr(code)` (233) — a ONE-character Text: a fresh heap char-list `[1, codepoint]` (the same shape
297 // `TextConst`/op 64/`Concat` produce, so a downstream `ShowText` decodes it identically). An invalid
298 // codepoint (a surrogate / > 0x10FFFF) TRAPS (the VM's `char::from_u32` → `Err`), pre-trap output stands.
299 // • `TextContains(hay, needle)` (234) — a SUBSTRING char-scan over two Text char-lists → an int 0/1 in
300 // regs[dst] (the empty needle is contained in everything), matching the VM's `h.contains(n)`.
301 // • `RepeatSeq(elem, count)` (235) — a fresh `count`-slot Seq, every slot the SAME scalar `regs[elem]`
302 // (`count ≤ 0` empty), matching the VM's `(0..n).map(|_| element.deep_clone())` for a SCALAR element.
303 // The encoder emits it only when the element is provably a scalar (not float/text/seq/map); a
304 // reference element would need per-slot deep copies the flat fill can't express, so it rejects inside.
305 // • `ParseInt(text)` (236) — a bounded DECIMAL char-scan over the Text char-list at `regs[text]` → the
306 // i64 value in `regs[dst]`: trim Unicode whitespace, an optional single leading `+`/`-`, then ≥1 ASCII
307 // digit accumulating `acc = acc*10 − digit` in the NEGATIVE magnitude (so `-9223372036854775808` = i64::MIN
308 // parses), negating for a positive sign. A non-numeric / empty / OVERFLOWING input TRAPS (halts, pre-trap
309 // output stands) — byte-identical to the VM's `str::trim().parse::<i64>()` returning `Err` (the VM does NOT
310 // promote an overflow to BigInt, it errors, and that `?` aborts the run just like the chip's `break`).
311 // • `TextBytes(text)` (237) — the UTF-8 encoding of the Text char-list at `regs[text]` as a fresh `Seq of
312 // Int` `[n, b0, b1, …]` (each byte 0..=255), matching the VM's `bytes_to_seq(s.as_bytes())`. A codepoint
313 // encodes to 1–4 bytes by its range (< 0x80, < 0x800, < 0x10000, else four); a char-list holds only
314 // scalar values (every producer — TextConst, Chr, FormatValue, Concat — validates), so no surrogate
315 // reaches the encoder and the op never traps. The byte count is known only after the scan, so the
316 // header is written last, the same order the RTL's single write port uses.
317const CHR: u16 = 233; // Chr { dst, code=ra } — regs[dst] = ptr to a fresh 1-char Text `[1, code]`; trap on an invalid codepoint
318const TEXTCONTAINS: u16 = 234; // TextContains { dst, hay=ra, needle=rb } — regs[dst] = 1/0 (needle is a substring of hay)
319const REPEATSEQ: u16 = 235; // RepeatSeq { dst, elem=ra, count=rb } — regs[dst] = ptr to a fresh `count`-slot Seq filled with regs[elem]
320const PARSEINT: u16 = 236; // ParseInt { dst, text=ra } — regs[dst] = the i64 value of the decimal Text at regs[ra]; trap on a non-numeric / empty / overflowing input (matches the VM's `str::trim().parse::<i64>()` error)
321const TEXTBYTES: u16 = 237; // TextBytes { dst, text=ra } — regs[dst] = ptr to a fresh Seq of the UTF-8 bytes of the Text at regs[ra]
322
323// The CallBuiltin (opcode 38) coprocessor ORDINAL for each chip-lowered builtin, carried in the word's
324// b-byte. A chip-internal numbering (NOT the `BuiltinId` discriminant) shared with `run_soft_core_typed`'s
325// `38 =>` arm and `soft_core.v`. This function names the INTEGER-plane ordinals (they read/write `regs`):
326// 5/7/8 are Abs/Min/Max, 9 is `count_ones`; 30/31/32 are the Word32 rotate / logical-shift the integer ALU
327// can't express. A builtin with a direct ALU lowering (word32/intOfWord32 → Move, word_and → BitAnd, …) is
328// NOT here — it never reaches opcode 38. The FLOAT-plane numeric builtins (`floor`/`ceil`/`round` on a
329// float, and `abs`/`min`/`max` on floats) ride `float_shape` with their own ordinals (36..=41), reading
330// `fregs`; they are NOT integer-plane ops, so they are not returned here.
331fn builtin_chip_ordinal(id: crate::semantics::builtins::BuiltinId) -> Option<u8> {
332 use crate::semantics::builtins::BuiltinId::*;
333 Some(match id {
334 Abs => 5,
335 Min => 7,
336 Max => 8,
337 CountOnes => 9,
338 Rotl => 30,
339 Rotr => 31,
340 Word32Shr => 32,
341 _ => return None,
342 })
343}
344
345/// The chip-synthetic opcode a DETERMINISTIC string / fill builtin lowers to (`Chr`=233 / `contains` on
346/// TEXT=234 / `repeatSeq`=235 / `parseInt`=236 / `text_bytes`=237), with the static TYPE GATE that keeps the lowering sound;
347/// `Ok(None)` for a builtin handled elsewhere (the op-38 numeric coprocessor, the Word32 vocabulary via
348/// `word_shape`) or a genuine host/reference-value boundary (`format`/uuid/…) that reaches the loud reject. An
349/// `Err` is the HONEST reject for a supported builtin whose OPERAND SHAPE has no chip form — a `contains`
350/// on a non-Text operand (the list `contains` rides `Op::Contains`, not this builtin) or a `repeatSeq` of
351/// a float / reference element (the flat fill can't deep-copy a heap element per slot). Both `imem_width`
352/// and `encode_op` consult this so their widths and rejects agree exactly. Mirrors `builtin_chip_ordinal`.
353fn text_builtin_opcode(
354 i: usize,
355 builtin: crate::semantics::builtins::BuiltinId,
356 args_start: Reg,
357 ft: &FloatTyping,
358) -> Result<Option<u16>, String> {
359 use crate::semantics::builtins::BuiltinId;
360 Ok(match builtin {
361 // `chr(code)` — a 1-char Text from an INTEGER codepoint. The argument is an ordinary integer
362 // register; a float/text/seq codepoint is a type error the VM also rejects (`chr` requires an Int),
363 // so gate it to a scalar-int operand (not float / not text / not float-seq).
364 BuiltinId::Chr => {
365 let code = args_start;
366 if ft.is_float(i, code) || ft.is_text(i, code) || ft.is_float_seq(i, code) {
367 return Err(format!(
368 "instruction {i}: `chr` needs an integer codepoint (r{code} is not an integer) — no \
369 full-core chip lowering for a non-integer `chr` argument"
370 ));
371 }
372 Some(CHR)
373 }
374 // `contains(hay, needle)` on TEXT — both operands must be Text char-lists (the LIST `contains` rides
375 // `Op::Contains`, opcode 63, never this builtin). A non-Text operand has no substring semantics, so
376 // reject it loudly rather than mis-scanning a numeric heap block as codepoints.
377 BuiltinId::Contains => {
378 let hay = args_start;
379 let needle = args_start.saturating_add(1);
380 if !(ft.is_text(i, hay) && ft.is_text(i, needle)) {
381 return Err(format!(
382 "instruction {i}: `contains` on the full core lowers only for TEXT operands (r{hay} / \
383 r{needle} are not both Text) — a list `contains` uses `Op::Contains` (op 63), not this builtin"
384 ));
385 }
386 Some(TEXTCONTAINS)
387 }
388 // `repeatSeq(elem, count)` — an `n`-slot fill of a SCALAR element. A float element would need the
389 // float heap plane per slot, and a Text/seq/map element a per-slot deep copy the flat fill can't do,
390 // so gate to a provably-scalar element (not float / not text / not float-seq / not map). The count is
391 // an ordinary integer register (a runtime value the model's sequential fill unit reads).
392 BuiltinId::RepeatSeq => {
393 let elem = args_start;
394 if ft.is_float(i, elem)
395 || ft.is_text(i, elem)
396 || ft.is_float_seq(i, elem)
397 || ft.is_map(i, elem)
398 {
399 return Err(format!(
400 "instruction {i}: `repeatSeq` on the full core lowers only for a SCALAR element (r{elem} \
401 is a float / reference value) — a float or heap-reference element has no flat-fill chip form"
402 ));
403 }
404 Some(REPEATSEQ)
405 }
406 // `parseInt(text)` — a bounded decimal char-scan over the Text char-list at `regs[args_start]` → an
407 // i64 in `regs[dst]`. The operand MUST be a Text char-list (the char-scan reads it as `[len, cp…]`);
408 // the VM itself errors on a non-Text argument (`parseInt requires a Text argument`), and a non-Text
409 // register on the chip has no char-list to scan, so gate it to a provably-Text operand rather than
410 // mis-reading a numeric/pointer register as a string. The scan itself TRAPS on a non-numeric / empty /
411 // overflowing input inside the model (op 236), byte-identical to the VM's `str::parse::<i64>()` `Err`.
412 BuiltinId::ParseInt => {
413 let text = args_start;
414 if !ft.is_text(i, text) {
415 return Err(format!(
416 "instruction {i}: `parseInt` on the full core lowers only for a TEXT operand (r{text} is \
417 not a Text char-list) — the decimal char-scan reads its argument as a string; a non-Text \
418 `parseInt` argument (which the VM also rejects) has no chip lowering"
419 ));
420 }
421 Some(PARSEINT)
422 }
423 // `text_bytes(text)` — the UTF-8 bytes of the Text char-list at `regs[args_start]` as a fresh Int Seq.
424 // The scan reads its operand as `[len, cp…]`; the VM rejects a non-Text argument (`text_bytes()
425 // requires text`), and a non-Text register has no char-list to encode, so gate it to a provably-Text
426 // operand. The result is a plain integer-element Seq, so it sets no plane.
427 BuiltinId::TextBytes => {
428 let text = args_start;
429 if !ft.is_text(i, text) {
430 return Err(format!(
431 "instruction {i}: `text_bytes` on the full core lowers only for a TEXT operand (r{text} is \
432 not a Text char-list) — the UTF-8 encoder reads its argument as a string; a non-Text \
433 `text_bytes` argument (which the VM also rejects) has no chip lowering"
434 ));
435 }
436 Some(TEXTBYTES)
437 }
438 _ => None,
439 })
440}
441
442/// True when `v` fits the sign-extended 16-bit inline `LoadConst` immediate (1 word); otherwise the
443/// encoder emits the 2-word wide `LoadConst` (opcode 200) whose second word is the full 32-bit value.
444fn narrow(v: i32) -> bool {
445 (-32768..=32767).contains(&v)
446}
447
448/// The FULL integer value of a constant (no truncation): an `Int` as its `i64`, a `Bool` as 0/1. This is
449/// what a 64-bit datapath must hold — the PRNG moduli `2147483648` (2³¹) / `4294967296` (2³²) exceed i32, so
450/// truncating them to `i32` is the 32-bit divergence source. The 32-bit encoder path still applies `as i32`
451/// at the emission site (the honest 32-bit limitation); this returns the value in full so a 64-bit datapath
452/// can hold it. `Err` on a Float (needs the FPU) or a Text/other constant with no integer representation.
453fn const_i64(c: &Constant) -> Result<i64, String> {
454 match c {
455 Constant::Int(v) => Ok(*v),
456 Constant::Bool(b) => Ok(i64::from(*b)),
457 // `Nothing` is the compiler's default-fill placeholder for an as-yet-unbumped counter field (a
458 // `Tally`/`Counter` struct field starts `Nothing`, then `CrdtBump` accumulates onto it). The VM's
459 // `crdt_counter_bump(Nothing, k)` returns `Int(k)` — i.e. Nothing behaves as the additive identity 0
460 // — so the chip loads it as 0 and the first bump computes `0 + k == k`, byte-identical to the VM.
461 Constant::Nothing => Ok(0),
462 // A `Duration` is the nanosecond count carried by a `Sleep`/`After ticks` timeout arm. The chip's
463 // deterministic scheduler ignores the actual wall-clock quantity (a `Sleep` is a no-op, a `SelectWait`
464 // timeout arm only fires when no recv is ready — see the model's ops 88/103/104), so the value rides
465 // the integer datapath as a plain nanosecond count with no effect on output; loading it losslessly
466 // keeps the constant pool honest (the over-i32 nanos count uses the 3-word op-222 form like any i64).
467 Constant::Duration(nanos) => Ok(*nanos),
468 Constant::Float(_) => {
469 Err("float constant needs the FPU (P3) — this is a float program".into())
470 }
471 other => Err(format!(
472 "constant {other:?} is not representable on the integer core"
473 )),
474 }
475}
476
477/// True when `v` fits a signed 32-bit field — the wide `LoadConst` (opcode 200) can hold it losslessly, so
478/// both the 32-bit and 64-bit datapaths encode it the SAME 1-word/2-word way (width-32 output stays byte-
479/// identical). A value OUTSIDE this range needs the 3-word 64-bit constant (opcode 222) on a 64-bit core;
480/// on a 32-bit core it is the honest limitation (the historical i32 truncation stands).
481fn fits_i32(v: i64) -> bool {
482 v >= i64::from(i32::MIN) && v <= i64::from(i32::MAX)
483}
484
485/// The fixed decimal precision of a float `FormatValue` — the `N` of a `.N` format spec. The VM's
486/// `apply_format_spec` renders a `Float` with a `.N` spec as `format!("{:.N$}", f)`; the soft-core's
487/// `FormatValueFloat` (op 225) does the SAME, carrying `N` in the word's b-byte, so the chip's Text is
488/// byte-identical to the VM's `Show` line. `spec == u32::MAX` (a bare `{f}`, no spec) or any spec that is
489/// not a plain `.N` (currency `$`, alignment `>N`) has no float chip lowering yet — `None`, so a float
490/// `FormatValue` outside the `.N` family falls through to the loud rejection rather than mis-rendering.
491fn float_format_precision(spec: u32, debug_prefix: u32, constants: &[Constant]) -> Option<u8> {
492 if spec == u32::MAX || debug_prefix != u32::MAX {
493 return None; // a bare `{f}` (full-precision display) / a `{f=}` debug prefix isn't the `.N` path
494 }
495 let s = match constants.get(spec as usize) {
496 Some(Constant::Text(s)) => s.as_str(),
497 _ => return None,
498 };
499 let n: usize = s.strip_prefix('.')?.parse().ok()?;
500 u8::try_from(n).ok() // the b-byte holds 0..=255; the benchmarks use 9 / 15
501}
502
503// ── THE TYPE ORACLE: static Int-vs-Float register typing (P3) ─────────────────────────────────────
504// The VM is dynamically typed, so nothing in the bytecode says whether a given `Add`/`Mul`/`Div`/`Show`
505// operates on integers or f64 — the runtime decides from the value tags. Silicon can't: each op needs a
506// fixed datapath. This forward FIXPOINT dataflow recovers, per code index, which registers PROVABLY hold
507// an f64 (they live in the chip's parallel `fregs` bank). The seeds are the ground truth the language
508// already carries: a `Constant::Float` literal, a `Float`/`Seq of Float` function parameter or return, a
509// float global, and the `sqrt` builtin. Float then PROPAGATES: an arithmetic op is float iff either
510// operand is float; a `Move` copies floatness; an `item i of (Seq of Float)` reads a float. The loop
511// back-edges are handled by the worklist (a register is float on entry iff it is float on ALL reaching
512// paths — the join is AND, so a register used as both int and float on different paths stays int and the
513// op falls to the integer datapath, which is the conservative, always-sound direction).
514
515/// True when a `BoundaryType` is (or unwraps to) a scalar `Float`. A `Seq of Float` element read yields a
516/// float scalar, so its ELEMENT kind matters at `Index`; the seq HANDLE itself is an integer pointer.
517fn boundary_is_float_scalar(t: &crate::vm::BoundaryType) -> bool {
518 matches!(t, crate::vm::BoundaryType::Float)
519}
520
521/// True when a `BoundaryType` is a `Seq of Float` — a heap handle whose element PAYLOADS ride the float
522/// plane. The handle register itself is an integer pointer (into the int heap), but `item i of` it reads a
523/// float and `Push`/`Set item` on it writes the float plane, so it needs its own lattice bit.
524fn boundary_is_float_seq(t: &crate::vm::BoundaryType) -> bool {
525 matches!(t, crate::vm::BoundaryType::Seq(inner) if boundary_is_float_scalar(inner))
526}
527
528/// True when a `BoundaryType` is `Text`. A Text handle is an int pointer to a char-code list; the encoder
529/// carries a `text` lattice bit so a Text `Add`/`AddAssign`/`Show`/param/return lowers to the string ops.
530fn boundary_is_text(t: &crate::vm::BoundaryType) -> bool {
531 matches!(t, crate::vm::BoundaryType::Text)
532}
533
534/// True when a `BoundaryType` is a `Map` — an int-pointer handle to a key-scan heap block. The `map` plane
535/// carries this so an `Index`/`SetIndex` on a map lowers to the KEY-SCAN chip ops (int- or string-key by the
536/// key register's type) instead of the positional list ops.
537fn boundary_is_map(t: &crate::vm::BoundaryType) -> bool {
538 matches!(t, crate::vm::BoundaryType::Map(_, _))
539}
540
541/// True when a `BoundaryType` is a scalar `Word32` — a native `i32` register whose VALUE is its own bit
542/// pattern, but whose `Show` renders UNSIGNED (0..2³²−1). The `word` plane carries this so a Word32
543/// param/return/`Show` lowers to `ShowWord32`.
544fn boundary_is_word32_scalar(t: &crate::vm::BoundaryType) -> bool {
545 matches!(t, crate::vm::BoundaryType::Word32)
546}
547
548/// True when a `BoundaryType` is a `Seq of Word32` — an int-pointer handle whose ELEMENTS are Word32 (the
549/// SHA-256 message schedule / hash state array). The handle rides the int heap; an `item i of` it reads a
550/// Word32 scalar, so it seeds the `wseq` plane (mirroring `boundary_is_float_seq`).
551fn boundary_is_word32_seq(t: &crate::vm::BoundaryType) -> bool {
552 matches!(t, crate::vm::BoundaryType::Seq(inner) if boundary_is_word32_scalar(inner))
553}
554
555/// One register's static float typing at a program point: a `scalar` f64 (rides `fregs`) OR a `seq` handle
556/// to a `Seq of Float` (an int pointer whose element payloads ride the float heap plane) OR a `text` handle
557/// (an int pointer to a char-code list) OR a `word` scalar (a `Word32`, ℤ/2³²) OR a `wseq` handle to a
558/// `Seq of Word32`. The bits are disjoint in practice (a register is one of them), but carried independently
559/// so the per-plane join is correct. `Bottom` is all-false — an integer / pointer / unreached register.
560///
561/// A `Word32` is the 32-bit register's own bit pattern, so its VALUE is indistinguishable from an `i32` on
562/// the chip; the plane exists ONLY to render a Word32 `Show` UNSIGNED (SHA-256's hash words exceed 2³¹) — a
563/// Word32-typed `Show` lowers to `ShowWord32`. The `word`/`wseq` split mirrors the float `scalar`/`seq`
564/// split: the SEQ handle is an int pointer, but an `item i of` it reads a Word32 scalar.
565#[derive(Clone, Copy, PartialEq, Eq)]
566struct FloatState {
567 scalar: [bool; 256], // register holds an f64 scalar
568 seq: [bool; 256], // register holds a `Seq of Float` handle (its elements are floats)
569 text: [bool; 256], // register holds a `Text` handle (a char-code list) — the string plane
570 word: [bool; 256], // register holds a `Word32` scalar (ℤ/2³²) — the unsigned-Show plane
571 wseq: [bool; 256], // register holds a `Seq of Word32` handle (its elements are Word32)
572 map: [bool; 256], // register holds a `Map` handle — the key-scan plane (int- vs string-key routing)
573}
574
575impl FloatState {
576 fn bottom() -> Self {
577 FloatState {
578 scalar: [false; 256],
579 seq: [false; 256],
580 text: [false; 256],
581 word: [false; 256],
582 wseq: [false; 256],
583 map: [false; 256],
584 }
585 }
586}
587
588/// Which registers provably hold a Bool at each code index — a MUST analysis (AND at joins, so a
589/// register is Bool only when it is Bool on every path reaching the op). Bools come from a Bool
590/// constant, a comparison, `not`, a Bool parameter, or a Bool-declared call return / global / struct
591/// field; a `Move` carries the fact; any other write to a register ends it.
592fn bool_typing(program: &CompiledProgram, structs: &StructTyping) -> Vec<[bool; 256]> {
593 use crate::vm::BoundaryType;
594 let n = program.code.len();
595 let mut entry: Vec<Option<[bool; 256]>> = vec![None; n];
596 let mut work: std::collections::VecDeque<usize> = Default::default();
597 let join = |entry: &mut Vec<Option<[bool; 256]>>,
598 work: &mut std::collections::VecDeque<usize>,
599 pc: usize,
600 st: [bool; 256]| {
601 if pc >= n {
602 return;
603 }
604 let next = match entry[pc] {
605 None => st,
606 Some(old) => std::array::from_fn(|r| old[r] && st[r]),
607 };
608 if entry[pc] != Some(next) {
609 entry[pc] = Some(next);
610 work.push_back(pc);
611 }
612 };
613 let seed = |params: &[Option<BoundaryType>]| -> [bool; 256] {
614 std::array::from_fn(|r| matches!(params.get(r), Some(Some(BoundaryType::Bool))))
615 };
616 join(&mut entry, &mut work, 0, [false; 256]);
617 for f in &program.functions {
618 join(&mut entry, &mut work, f.entry_pc, seed(&f.param_types));
619 }
620 while let Some(i) = work.pop_front() {
621 let mut out = entry[i].expect("a queued op has an entry state");
622 let op = &program.code[i];
623 match op {
624 Op::LoadConst { dst, idx } => {
625 out[*dst as usize & 255] = matches!(
626 program.constants.get(*idx as usize),
627 Some(Constant::Bool(_))
628 );
629 }
630 Op::Lt { dst, .. }
631 | Op::Gt { dst, .. }
632 | Op::LtEq { dst, .. }
633 | Op::GtEq { dst, .. }
634 | Op::Eq { dst, .. }
635 | Op::NotEq { dst, .. }
636 | Op::ApproxEq { dst, .. }
637 | Op::Not { dst, .. } => out[*dst as usize & 255] = true,
638 Op::Move { dst, src } | Op::DeepClone { dst, src } => {
639 out[*dst as usize & 255] = out[*src as usize & 255]
640 }
641 Op::RevSwap { a, b } => out.swap(*a as usize & 255, *b as usize & 255),
642 Op::Call { dst, func, .. } => {
643 out[*dst as usize & 255] = matches!(
644 program
645 .functions
646 .get(*func as usize)
647 .and_then(|f| f.return_type.as_ref()),
648 Some(BoundaryType::Bool)
649 );
650 }
651 Op::GlobalGet { dst, idx } => {
652 out[*dst as usize & 255] = matches!(
653 program.global_types.get(*idx as usize),
654 Some(Some(BoundaryType::Bool))
655 );
656 }
657 Op::GetField { dst, obj, field } => {
658 out[*dst as usize & 255] = matches!(
659 declared_field_type(program, structs, i, *obj, *field),
660 Some(BoundaryType::Bool)
661 );
662 }
663 other if rebinds_same_kind(other) => {}
664 other => {
665 for d in written_regs(other) {
666 out[d as usize & 255] = false;
667 }
668 }
669 }
670 for s in successors(i, op, n, |ci| ci) {
671 join(&mut entry, &mut work, s, out);
672 }
673 }
674 entry
675 .into_iter()
676 .map(|e| e.unwrap_or([false; 256]))
677 .collect()
678}
679
680/// The whole-program float register typing: for each code index, which registers provably hold an f64
681/// scalar (`fregs`) and which hold a `Seq of Float` handle (the float heap plane). Computed once; both
682/// encoder passes consult it so their imem widths and emitted words agree. Mirrors the structure of
683/// [`isolation_targets`] (same seed/successor logic).
684struct FloatTyping {
685 entry: Vec<FloatState>,
686 /// Per code index, which registers provably hold a Bool ([`bool_typing`]). The chip carries a Bool
687 /// as 0/1; rendering it as `true`/`false` needs this static fact.
688 bools: Vec<[bool; 256]>,
689}
690
691impl FloatTyping {
692 fn analyze(program: &CompiledProgram, structs: &StructTyping) -> Self {
693 let n = program.code.len();
694 const R: usize = 256;
695 let mut entry: Vec<FloatState> = vec![FloatState::bottom(); n];
696 let mut seen: Vec<bool> = vec![false; n]; // reached at least once (Bottom = false everywhere is ambiguous)
697 let mut work: std::collections::VecDeque<usize> = Default::default();
698
699 // Seed a region entry (Main or a function body) with its parameters' float-ness. A float scalar
700 // param rides `fregs`; a `Seq of Float` param is an integer pointer whose ELEMENTS are float — it
701 // seeds the `seq` plane (its handle isn't a scalar float, but its `item i of` reads a float).
702 let seed = |entry: &mut Vec<FloatState>,
703 seen: &mut Vec<bool>,
704 work: &mut std::collections::VecDeque<usize>,
705 pc: usize,
706 param_types: &[Option<crate::vm::BoundaryType>]| {
707 if pc >= entry.len() {
708 return;
709 }
710 for (r, pt) in param_types.iter().enumerate().take(R) {
711 if let Some(t) = pt {
712 entry[pc].scalar[r] = boundary_is_float_scalar(t);
713 entry[pc].seq[r] = boundary_is_float_seq(t);
714 entry[pc].text[r] = boundary_is_text(t);
715 entry[pc].word[r] = boundary_is_word32_scalar(t);
716 entry[pc].wseq[r] = boundary_is_word32_seq(t);
717 entry[pc].map[r] = boundary_is_map(t);
718 }
719 }
720 seen[pc] = true;
721 work.push_back(pc);
722 };
723 // (Region entries are (re)seeded at the top of every fixpoint sweep below.)
724
725 // The transfer of op `i` given the entry state `st`: the (register, scalar-float, float-seq, text,
726 // word32, word32-seq, map) writes this op performs. Reads consult `st` on the matching plane. A write of
727 // all-false CLEARS every plane at `r` (the op reassigned that register to an int / pointer), which is
728 // essential for the AND-join to converge — a register rebound to an int on one path stops being float
729 // / word32. The heap-handle planes (seq / text / wseq / map) join with OR: a handle typing is established
730 // once (a constant, a producing op, or a boundary) and, as a heap-object property, isn't lost on a
731 // re-derive.
732 let transfer = |i: usize,
733 st: &FloatState|
734 -> Vec<(usize, bool, bool, bool, bool, bool, bool)> {
735 use Op::*;
736 let sf = |r: u16| st.scalar[r as usize & 255]; // scalar-float?
737 let qf = |r: u16| st.seq[r as usize & 255]; // float-seq?
738 let tx = |r: u16| st.text[r as usize & 255]; // text?
739 let wd = |r: u16| st.word[r as usize & 255]; // word32 scalar?
740 let wq = |r: u16| st.wseq[r as usize & 255]; // word32-seq?
741 let mp = |r: u16| st.map[r as usize & 255]; // map handle?
742 match &program.code[i] {
743 LoadConst { dst, idx } => {
744 let c = program.constants.get(*idx as usize);
745 let is_f = matches!(c, Some(Constant::Float(_)));
746 let is_t = matches!(c, Some(Constant::Text(_)));
747 vec![(*dst as usize & 255, is_f, false, is_t, false, false, false)]
748 }
749 // Arithmetic: float iff either operand is float; TEXT iff either operand is Text (a Text `Add`
750 // is string concat — the non-Text operand is FormatValue'd, so `+` on a mixed Text/int is a
751 // Text result); WORD32 iff either operand is Word32 (a Word32 `+`/`-`/`*` stays Word32 —
752 // SHA-256's `s1 + s0 + …` accumulator, so the final Show renders unsigned). A Word32 operand
753 // is never simultaneously float / text, so the planes stay disjoint.
754 Add { dst, lhs, rhs } => {
755 let f = sf(*lhs) || sf(*rhs);
756 let t = tx(*lhs) || tx(*rhs);
757 let w = (wd(*lhs) || wd(*rhs)) && !f && !t;
758 vec![(*dst as usize & 255, f, false, t, w, false, false)]
759 }
760 Sub { dst, lhs, rhs } | Mul { dst, lhs, rhs } => {
761 let f = sf(*lhs) || sf(*rhs);
762 let w = (wd(*lhs) || wd(*rhs)) && !f;
763 vec![(*dst as usize & 255, f, false, false, w, false, false)]
764 }
765 Div { dst, lhs, rhs } | ExactDiv { dst, lhs, rhs } | FloorDiv { dst, lhs, rhs } => {
766 vec![(
767 *dst as usize & 255,
768 sf(*lhs) || sf(*rhs),
769 false,
770 false,
771 false,
772 false,
773 false,
774 )]
775 }
776 // Bitwise `xor`/`and`/`or`/`not` — the Word32 crypto operators (`rotr(x,7) xor …`) that lower
777 // straight to the ALU. The result stays Word32 iff an operand is Word32, so the diffusion
778 // chain's final value Shows unsigned.
779 BitXor { dst, lhs, rhs }
780 | BitAnd { dst, lhs, rhs }
781 | BitOr { dst, lhs, rhs }
782 | Shl { dst, lhs, rhs }
783 | Shr { dst, lhs, rhs } => {
784 vec![(
785 *dst as usize & 255,
786 false,
787 false,
788 false,
789 wd(*lhs) || wd(*rhs),
790 false,
791 false,
792 )]
793 }
794 Not { dst, src } => vec![(
795 *dst as usize & 255,
796 false,
797 false,
798 false,
799 wd(*src),
800 false,
801 false,
802 )],
803 AddAssign { dst, src } => {
804 let f = sf(*dst) || sf(*src);
805 let t = tx(*dst) || tx(*src);
806 let w = (wd(*dst) || wd(*src)) && !f && !t;
807 vec![(*dst as usize & 255, f, false, t, w, false, false)]
808 }
809 // Move copies ALL planes — a `Let ys be xs` on a float / Word32 / text / map handle keeps its typing;
810 // a `DeepClone` is the same value in a fresh heap block.
811 Move { dst, src } | DeepClone { dst, src } => {
812 vec![(
813 *dst as usize & 255,
814 sf(*src),
815 qf(*src),
816 tx(*src),
817 wd(*src),
818 wq(*src),
819 mp(*src),
820 )]
821 }
822 // `RevSwap` exchanges two registers — and with them, every plane.
823 RevSwap { a, b } => {
824 let plane = |r: u16| (sf(r), qf(r), tx(r), wd(r), wq(r), mp(r));
825 let ((a0, a1, a2, a3, a4, a5), (b0, b1, b2, b3, b4, b5)) =
826 (plane(*a), plane(*b));
827 vec![
828 (*a as usize & 255, b0, b1, b2, b3, b4, b5),
829 (*b as usize & 255, a0, a1, a2, a3, a4, a5),
830 ]
831 }
832 // A slice is the same kind of sequence as the one it cuts: a Text slice is Text, a `Seq of Float`
833 // slice a float seq.
834 SliceOp {
835 dst, collection, ..
836 } => {
837 vec![(
838 *dst as usize & 255,
839 false,
840 qf(*collection),
841 tx(*collection),
842 false,
843 wq(*collection),
844 false,
845 )]
846 }
847 // `Concat` joins two renderings into a Text; `SeqConcat` keeps the element typing of either side.
848 Concat { dst, .. } => {
849 vec![(*dst as usize & 255, false, false, true, false, false, false)]
850 }
851 SeqConcat { dst, lhs, rhs } => {
852 vec![(
853 *dst as usize & 255,
854 false,
855 qf(*lhs) || qf(*rhs),
856 false,
857 false,
858 wq(*lhs) || wq(*rhs),
859 false,
860 )]
861 }
862 // `p's f`: the field's DECLARED type — a Text field is a Text handle, a Real field a float. A field
863 // whose object type or declaration the oracles cannot name is an integer (all planes clear).
864 GetField { dst, obj, field } => {
865 match declared_field_type(program, structs, i, *obj, *field) {
866 Some(t) => vec![(
867 *dst as usize & 255,
868 boundary_is_float_scalar(t),
869 boundary_is_float_seq(t),
870 boundary_is_text(t),
871 boundary_is_word32_scalar(t),
872 boundary_is_word32_seq(t),
873 boundary_is_map(t),
874 )],
875 None => vec![(
876 *dst as usize & 255,
877 false,
878 false,
879 false,
880 false,
881 false,
882 false,
883 )],
884 }
885 }
886 // Comparisons write an INT bool (never float / text / word / map), regardless of operand types.
887 Lt { dst, .. }
888 | Gt { dst, .. }
889 | LtEq { dst, .. }
890 | GtEq { dst, .. }
891 | Eq { dst, .. }
892 | NotEq { dst, .. }
893 | ApproxEq { dst, .. } => {
894 vec![(
895 *dst as usize & 255,
896 false,
897 false,
898 false,
899 false,
900 false,
901 false,
902 )]
903 }
904 // `item i of xs`: a float SCALAR result iff xs is a float seq, a WORD32 scalar iff xs is a
905 // Word32 seq (`item i of (Seq of Word32)`). `item i of text` is a single-CHARACTER Text. A map
906 // read (`item k of m`) yields the VALUE — not a map handle — so the map plane is never set here.
907 Index {
908 dst, collection, ..
909 }
910 | IndexUnchecked {
911 dst, collection, ..
912 } => {
913 vec![(
914 *dst as usize & 255,
915 qf(*collection),
916 false,
917 tx(*collection),
918 wq(*collection),
919 false,
920 false,
921 )]
922 }
923 // `Repeat for v in xs`: the loop variable `v` is a float SCALAR iff the governing `IterPrepare`
924 // (the compiler emits it at `i-1`, before the loop-head `IterNext`) names a `Seq of Float`
925 // iterable — its element rides `fheap`, so the body's `s + v` lowers to the FPU. The iterable's
926 // float-seq bit (established at the `IterPrepare`, a read) flows unchanged into this op's entry
927 // state `st`, so `qf(iterable)` here IS its float-seq-ness. Else `v` is an integer element.
928 IterNext { dst, .. } => {
929 let f = i > 0
930 && matches!(&program.code[i - 1], Op::IterPrepare { .. })
931 && match &program.code[i - 1] {
932 Op::IterPrepare { iterable } => qf(*iterable),
933 _ => false,
934 };
935 vec![(*dst as usize & 255, f, false, false, false, false, false)]
936 }
937 // A float / Word32 `Push`/`Add to` MARKS its list register as a float-seq / word-seq (the
938 // value's plane decides; the list handle stays an int pointer, its seq bit records the element
939 // typing). SetAdd (Set membership) rides the same detector.
940 ListPush { list, value } => {
941 if sf(*value) {
942 vec![(
943 *list as usize & 255,
944 false,
945 true,
946 false,
947 false,
948 false,
949 false,
950 )]
951 } else if wd(*value) {
952 vec![(
953 *list as usize & 255,
954 false,
955 false,
956 false,
957 false,
958 true,
959 false,
960 )]
961 } else {
962 vec![]
963 }
964 }
965 SetAdd { set, value } => {
966 if sf(*value) {
967 vec![(*set as usize & 255, false, true, false, false, false, false)]
968 } else if wd(*value) {
969 vec![(*set as usize & 255, false, false, false, false, true, false)]
970 } else {
971 vec![]
972 }
973 }
974 // A float / Word32 `Set item i of xs to v` likewise CONFIRMS xs's element typing. It does NOT
975 // touch the collection's map plane (a map handle stays a map after a `Set item k of m`).
976 SetIndex {
977 collection, value, ..
978 }
979 | SetIndexUnchecked {
980 collection, value, ..
981 } => {
982 if sf(*value) {
983 vec![(
984 *collection as usize & 255,
985 false,
986 true,
987 false,
988 false,
989 false,
990 false,
991 )]
992 } else if wd(*value) {
993 vec![(
994 *collection as usize & 255,
995 false,
996 false,
997 false,
998 false,
999 true,
1000 false,
1001 )]
1002 } else {
1003 vec![]
1004 }
1005 }
1006 // A `[a, b, …]` list literal is a float / Word32 seq iff its first element is that scalar kind.
1007 NewList { dst, start, count } if *count > 0 => {
1008 vec![(
1009 *dst as usize & 255,
1010 false,
1011 sf(*start),
1012 false,
1013 false,
1014 wd(*start),
1015 false,
1016 )]
1017 }
1018 // `a new Map of …` — the map handle plane's producer (seeds the key-scan routing).
1019 NewEmptyMap { dst } | NewEmptyMapWithCapacity { dst, .. } => {
1020 vec![(*dst as usize & 255, false, false, false, false, false, true)]
1021 }
1022 // Call: the result is a float scalar / float seq / Text / Word32 / Word32 seq per the callee's
1023 // declared return type (the `ssig0(x: Word32) -> Word32` shape).
1024 Call { dst, func, .. } => {
1025 let rt = program
1026 .functions
1027 .get(*func as usize)
1028 .and_then(|f| f.return_type.as_ref());
1029 let is_scalar = rt.map(boundary_is_float_scalar).unwrap_or(false);
1030 let is_seq = rt.map(boundary_is_float_seq).unwrap_or(false);
1031 let is_text = rt.map(boundary_is_text).unwrap_or(false);
1032 let is_word = rt.map(boundary_is_word32_scalar).unwrap_or(false);
1033 let is_wseq = rt.map(boundary_is_word32_seq).unwrap_or(false);
1034 let is_map = rt.map(boundary_is_map).unwrap_or(false);
1035 vec![(
1036 *dst as usize & 255,
1037 is_scalar,
1038 is_seq,
1039 is_text,
1040 is_word,
1041 is_wseq,
1042 is_map,
1043 )]
1044 }
1045 CallBuiltin {
1046 dst,
1047 builtin,
1048 args_start,
1049 ..
1050 } => {
1051 use crate::semantics::builtins::BuiltinId::*;
1052 let is_f = match builtin {
1053 Sqrt => true, // sqrt of a number is always a float (the FPU result)
1054 // `floor`/`ceil`/`round` of a FLOAT return an INTEGER (`f.floor() as i64`), so their
1055 // dst leaves the float plane — a `Show floor(x)` must render an int, not a float.
1056 Floor | Ceil | Round => false,
1057 Abs => sf(*args_start), // |x| preserves the operand's kind (float in → float out)
1058 // `min`/`max` promote to float if EITHER operand is float (`(x as f64).min(y)`), so the
1059 // result is float when either arg is — matching `float_shape`'s either-operand routing.
1060 Min | Max => sf(*args_start) || sf(args_start.saturating_add(1)),
1061 _ => false,
1062 };
1063 // The Word32 vocabulary: `word32(n)`/`intOfWord32(w)` and the rotate/shift/bitwise word
1064 // ops all yield a Word32 (the register's own 32-bit pattern), so their result Shows
1065 // UNSIGNED. `intOfWord32` is an unsigned Int in the VM, but on a 32-bit register its value
1066 // is the Word32 bit pattern — indistinguishable except at Show, so it rides the word plane.
1067 let is_w = matches!(
1068 builtin,
1069 Word32 | IntOfWord32 | Rotl | Rotr | Word32Shr | Wand | Wor | Wnot
1070 );
1071 // `chr(code)` yields a ONE-character TEXT (a char-code list), so its dst is a Text handle —
1072 // a downstream `Show`/`item i of`/`contains` on it must route to the string plane. `contains`
1073 // yields an int 0/1, and `repeatSeq` and `text_bytes` a plain integer-element Seq (an int
1074 // pointer, positionally indexed), so none sets a plane — the all-false default is correct.
1075 // `format(x)` renders its argument to Text (a char-code list), like `FormatValue`.
1076 let is_t = matches!(builtin, Chr | Format);
1077 vec![(*dst as usize & 255, is_f, false, is_t, is_w, false, false)]
1078 }
1079 GlobalGet { dst, idx } => {
1080 let gt = program
1081 .global_types
1082 .get(*idx as usize)
1083 .and_then(|t| t.as_ref());
1084 let is_scalar = gt.map(boundary_is_float_scalar).unwrap_or(false);
1085 let is_seq = gt.map(boundary_is_float_seq).unwrap_or(false);
1086 let is_text = gt.map(boundary_is_text).unwrap_or(false);
1087 let is_word = gt.map(boundary_is_word32_scalar).unwrap_or(false);
1088 let is_wseq = gt.map(boundary_is_word32_seq).unwrap_or(false);
1089 let is_map = gt.map(boundary_is_map).unwrap_or(false);
1090 vec![(
1091 *dst as usize & 255,
1092 is_scalar,
1093 is_seq,
1094 is_text,
1095 is_word,
1096 is_wseq,
1097 is_map,
1098 )]
1099 }
1100 // `FormatValue` renders a value to a Text char-list (integer op 64 or float op 225), so its dst
1101 // is a TEXT handle — the downstream `Concat` (`"" ++ formatted`) must see a Text-Text concat, not
1102 // re-FormatValue a register the chip already stringified. Marks only the text plane.
1103 FormatValue { dst, .. } => {
1104 vec![(*dst as usize & 255, false, false, true, false, false, false)]
1105 }
1106 op if rebinds_same_kind(op) => vec![],
1107 // Every other register an op writes holds an integer / pointer: all planes clear.
1108 op => written_regs(op)
1109 .into_iter()
1110 .map(|d| (d as usize & 255, false, false, false, false, false, false))
1111 .collect(),
1112 }
1113 };
1114
1115 // The two planes have OPPOSITE lattice directions — SCALAR joins with AND (a register is a float
1116 // scalar only if float on EVERY reaching path; the conservative direction that lets a genuine
1117 // int/float register reuse fall to the integer datapath), SEQ joins with OR (a `Seq of Float` handle's
1118 // element typing is a property of the heap object, established once a float is pushed and never lost).
1119 // A single worklist sweep can't reconcile them: when a seq is first typed by a push INSIDE a loop, the
1120 // seq bit rises late, but a scalar bit DERIVED from it (an `item i of seq` read) may already have been
1121 // AND-latched at `false` on an earlier visit (false AND true = false, never rises again) — a loop-built
1122 // float seq then reads back all-zero. The fix is to iterate the whole sweep to a fixpoint: the seq
1123 // plane converges first (OR is monotone), and each subsequent sweep re-derives the scalar bits against
1124 // the now-stable seq, so nothing stays latched. Convergence is monotone, so a handful of sweeps suffice;
1125 // the bound is a hard backstop.
1126 for _sweep in 0..16 {
1127 let before = entry.clone();
1128 for x in seen.iter_mut() {
1129 *x = false;
1130 }
1131 work.clear();
1132 seed(&mut entry, &mut seen, &mut work, 0, &[]); // Main: no params
1133 for f in &program.functions {
1134 seed(&mut entry, &mut seen, &mut work, f.entry_pc, &f.param_types);
1135 }
1136 while let Some(i) = work.pop_front() {
1137 let mut out = entry[i];
1138 for (r, sc, sq, tx, wd, wq, mp) in transfer(i, &entry[i]) {
1139 out.scalar[r] = sc;
1140 out.seq[r] = sq;
1141 out.text[r] = tx;
1142 out.word[r] = wd;
1143 out.wseq[r] = wq;
1144 out.map[r] = mp;
1145 }
1146 let succs = successors(i, &program.code[i], n, |ci| ci);
1147 for s in succs {
1148 if s >= n {
1149 continue;
1150 }
1151 let mut changed = false;
1152 if !seen[s] {
1153 seen[s] = true;
1154 // SCALAR planes (float `scalar`, `word`): (re)initialize from this predecessor — the
1155 // must-analysis start, met with any later predecessors below. On a re-sweep this
1156 // DISCARDS the previous sweep's scalar so a bit that was latched low can be re-derived
1157 // correctly against the now-stable handle planes. HANDLE planes (seq / text / wseq):
1158 // OR-preserve — NEVER drop a `Seq of Float`/`Text`/`Seq of Word32` typing carried over
1159 // from a prior sweep (that carried-over, converged handle plane is what un-latches the
1160 // scalars — e.g. a loop-built `Seq of Word32` whose element read must stay word-typed).
1161 entry[s].scalar = out.scalar;
1162 entry[s].word = out.word;
1163 for r in 0..R {
1164 entry[s].seq[r] |= out.seq[r];
1165 entry[s].text[r] |= out.text[r];
1166 entry[s].wseq[r] |= out.wseq[r];
1167 entry[s].map[r] |= out.map[r];
1168 }
1169 changed = true;
1170 } else {
1171 for r in 0..R {
1172 let m = entry[s].scalar[r] && out.scalar[r]; // SCALAR = AND
1173 if m != entry[s].scalar[r] {
1174 entry[s].scalar[r] = m;
1175 changed = true;
1176 }
1177 let mw = entry[s].word[r] && out.word[r]; // WORD32 SCALAR = AND
1178 if mw != entry[s].word[r] {
1179 entry[s].word[r] = mw;
1180 changed = true;
1181 }
1182 let mq = entry[s].seq[r] || out.seq[r]; // SEQ = OR
1183 if mq != entry[s].seq[r] {
1184 entry[s].seq[r] = mq;
1185 changed = true;
1186 }
1187 let mt = entry[s].text[r] || out.text[r]; // TEXT = OR
1188 if mt != entry[s].text[r] {
1189 entry[s].text[r] = mt;
1190 changed = true;
1191 }
1192 let mwq = entry[s].wseq[r] || out.wseq[r]; // WORD32 SEQ = OR
1193 if mwq != entry[s].wseq[r] {
1194 entry[s].wseq[r] = mwq;
1195 changed = true;
1196 }
1197 let mm = entry[s].map[r] || out.map[r]; // MAP = OR
1198 if mm != entry[s].map[r] {
1199 entry[s].map[r] = mm;
1200 changed = true;
1201 }
1202 }
1203 }
1204 if changed {
1205 work.push_back(s);
1206 }
1207 }
1208 }
1209 if entry == before {
1210 break;
1211 }
1212 }
1213 FloatTyping {
1214 entry,
1215 bools: bool_typing(program, structs),
1216 }
1217 }
1218
1219 /// Is register `r` a float SCALAR on entry to code index `i`?
1220 fn is_float(&self, i: usize, r: u16) -> bool {
1221 self.entry
1222 .get(i)
1223 .map(|st| st.scalar[r as usize & 255])
1224 .unwrap_or(false)
1225 }
1226
1227 /// Is register `r` a `Seq of Float` handle on entry to code index `i`?
1228 fn is_float_seq(&self, i: usize, r: u16) -> bool {
1229 self.entry
1230 .get(i)
1231 .map(|st| st.seq[r as usize & 255])
1232 .unwrap_or(false)
1233 }
1234
1235 /// Is register `r` a `Text` (char-code list) handle on entry to code index `i`?
1236 fn is_bool(&self, i: usize, r: u16) -> bool {
1237 self.bools
1238 .get(i)
1239 .map(|b| b[r as usize & 255])
1240 .unwrap_or(false)
1241 }
1242
1243 fn is_text(&self, i: usize, r: u16) -> bool {
1244 self.entry
1245 .get(i)
1246 .map(|st| st.text[r as usize & 255])
1247 .unwrap_or(false)
1248 }
1249
1250 /// Is register `r` a `Word32` SCALAR on entry to code index `i`? (Its `Show` renders UNSIGNED.) The
1251 /// `Seq of Word32` handle plane (`wseq`) has no separate accessor: an `item i of (Seq of Word32)` rides
1252 /// the ordinary integer heap datapath (the value is the 32-bit pattern), so only the RESULT's word
1253 /// typing matters — the transfer's `Index` arm derives it from the collection's `wseq` bit directly.
1254 fn is_word32(&self, i: usize, r: u16) -> bool {
1255 self.entry
1256 .get(i)
1257 .map(|st| st.word[r as usize & 255])
1258 .unwrap_or(false)
1259 }
1260
1261 /// Is register `r` a `Map` handle on entry to code index `i`? An `Index`/`SetIndex` on a map lowers to the
1262 /// KEY-SCAN chip ops (int- or string-key by the key register's own type) rather than the positional list
1263 /// ops; a register that is not provably a map keeps the positional (list) lowering.
1264 fn is_map(&self, i: usize, r: u16) -> bool {
1265 self.entry
1266 .get(i)
1267 .map(|st| st.map[r as usize & 255])
1268 .unwrap_or(false)
1269 }
1270}
1271
1272/// True when the `IterNext` at code index `i` iterates a `Seq of Float`: the loop's governing `IterPrepare`
1273/// (the compiler emits it at `i - 1`, immediately before the loop-head `IterNext`) names a float-seq iterable.
1274/// A float iteration reads each element's f64 PAYLOAD off `fheap` (op 231 `FIterNext`) into `fregs[dst]`, so
1275/// the loop variable is a float scalar. A `None`/non-float-seq iterable is the ordinary integer `IterNext`.
1276fn is_float_iternext(i: usize, program: &CompiledProgram, ft: &FloatTyping) -> bool {
1277 if i == 0 {
1278 return false;
1279 }
1280 match &program.code[i - 1] {
1281 Op::IterPrepare { iterable } => ft.is_float_seq(i - 1, *iterable),
1282 _ => false,
1283 }
1284}
1285
1286/// The single dst register an op writes (for the integer-default float transfer), or `None` for a control
1287/// op / a multi- or no-write op we don't need to type (its dst, if any, is left non-float).
1288/// The registers `op` writes — the ISA's exhaustive write set. Every register typing below re-derives a
1289/// written register from the writing op ALONE: a fact the register held before the write never survives it.
1290fn written_regs(op: &Op) -> Vec<Reg> {
1291 let mut out = Vec::new();
1292 op.for_each_written_reg(|r| out.push(r));
1293 out
1294}
1295
1296/// A write that rebinds its register to a value of the SAME kind, so every typing carries the register's
1297/// facts through it: `EnsureOwned` (a copy-on-write clone), `NetSync` (the merged replica written back), and
1298/// the reversible ring ops (`xor`/`+`/`-`/negation keep a Word a Word and an Int an Int).
1299fn rebinds_same_kind(op: &Op) -> bool {
1300 matches!(
1301 op,
1302 Op::EnsureOwned { .. }
1303 | Op::NetSync { .. }
1304 | Op::RevAdd { .. }
1305 | Op::RevSub { .. }
1306 | Op::RevXor { .. }
1307 | Op::RevNeg { .. }
1308 )
1309}
1310
1311/// The declared type of field `field` (a field-name const) of the struct in register `obj` at code index
1312/// `i`: the struct oracle names the object's type, the program's registry declares the field.
1313fn declared_field_type<'p>(
1314 program: &'p CompiledProgram,
1315 st: &StructTyping,
1316 i: usize,
1317 obj: Reg,
1318 field: u32,
1319) -> Option<&'p crate::vm::BoundaryType> {
1320 let type_name = st.type_names.get(st.struct_of(i, obj)? as usize)?;
1321 let Some(Constant::Text(fname)) = program.constants.get(field as usize) else {
1322 return None;
1323 };
1324 let def = program.struct_types.iter().find(|d| &d.name == type_name)?;
1325 def.fields.iter().find(|(n, _)| n == fname).map(|(_, t)| t)
1326}
1327
1328// ── THE STRUCT-TYPE ORACLE + USAGE-DERIVED FIELD LAYOUT ────────────────────────────────────────────
1329// A LOGOS struct (and a bare-int `Counter`/`Tally`, which the compiler lowers to a plain struct) is a heap
1330// record `[nfields, f0, …]` addressed POSITIONALLY on the chip (model ops 74/75/76/80/81/84). But the
1331// bytecode addresses a field by NAME (`StructInsert`/`GetField`/`CrdtBump`/`CrdtResolve` carry the
1332// field-name `ConstIdx`, never a slot index) — the dynamically-typed VM/tree-walker resolve names in a
1333// `HashMap`. To lower a field op the encoder must map (struct type, field name) → a slot position that is
1334// (a) CONSISTENT across every op on that struct type (so `StructInsert` writes the slot `GetField` reads),
1335// and (b) a compact `[0, nfields)` layout (so `NewStruct { num_fields }` allocates enough slots). The slot
1336// number is a pure chip-internal detail — the program's OUTPUT depends only on WHICH value lands in the
1337// field, not on its heap offset — so any consistent bijection works.
1338//
1339// The struct-type NAME of each object register comes from the ground truth the bytecode carries: a
1340// `NewStruct { type_name }` const, a `Struct(name)` parameter / global / return type. `StructTyping` is
1341// the forward dataflow that recovers it (propagating through `Move`). The layout is then DERIVED from
1342// actual usage — for each struct type, its field names in first-reference order, assigned positions 0.. —
1343// which is complete by construction (it never depends on the lossy/partial `struct_types` registry) and
1344// self-consistent (the same field name in the same type always resolves to the same slot).
1345struct StructTyping {
1346 entry: Vec<[Option<u16>; 256]>, // per code index, per register: Some(type-name index) or None
1347 /// Program-global struct-type names (index → name); `NewStruct` consts + `Struct(name)` boundary types.
1348 type_names: Vec<String>,
1349 /// Per struct type (by `type_names` index): field name → slot position, in first-reference order.
1350 field_slots: Vec<std::collections::HashMap<String, usize>>,
1351 /// Per struct type: field count (== `field_slots[t].len()`) — the `NewStruct { num_fields }` operand.
1352 field_counts: Vec<usize>,
1353}
1354
1355impl StructTyping {
1356 fn analyze(program: &CompiledProgram) -> Self {
1357 let n = program.code.len();
1358 const R: usize = 256;
1359 // Program-global struct-type name table: every name a `NewStruct` const or a `Struct(_)` boundary
1360 // type mentions. The index is the register-plane's stable type id.
1361 let mut type_names: Vec<String> = Vec::new();
1362 let mut name_index: std::collections::HashMap<String, u16> =
1363 std::collections::HashMap::new();
1364 let intern = |name: &str,
1365 names: &mut Vec<String>,
1366 idx: &mut std::collections::HashMap<String, u16>|
1367 -> Option<u16> {
1368 if let Some(&i) = idx.get(name) {
1369 return Some(i);
1370 }
1371 let i = u16::try_from(names.len()).ok()?;
1372 names.push(name.to_string());
1373 idx.insert(name.to_string(), i);
1374 Some(i)
1375 };
1376 for op in &program.code {
1377 if let Op::NewStruct { type_name, .. } = op {
1378 if let Some(Constant::Text(s)) = program.constants.get(*type_name as usize) {
1379 intern(s, &mut type_names, &mut name_index);
1380 }
1381 }
1382 }
1383 let note_bt = |t: &crate::vm::BoundaryType,
1384 names: &mut Vec<String>,
1385 idx: &mut std::collections::HashMap<String, u16>| {
1386 if let crate::vm::BoundaryType::Struct(name) = t {
1387 intern(name, names, idx);
1388 }
1389 };
1390 for f in &program.functions {
1391 for pt in f.param_types.iter().flatten() {
1392 note_bt(pt, &mut type_names, &mut name_index);
1393 }
1394 if let Some(rt) = &f.return_type {
1395 note_bt(rt, &mut type_names, &mut name_index);
1396 }
1397 }
1398 for gt in program.global_types.iter().flatten() {
1399 note_bt(gt, &mut type_names, &mut name_index);
1400 }
1401 // A `BoundaryType::Struct(name)` → its type-name index (a struct-typed param/global/return).
1402 let bt_index = |t: &crate::vm::BoundaryType| -> Option<u16> {
1403 match t {
1404 crate::vm::BoundaryType::Struct(name) => name_index.get(name.as_str()).copied(),
1405 _ => None,
1406 }
1407 };
1408
1409 let mut entry: Vec<[Option<u16>; 256]> = vec![[None; R]; n];
1410 let mut seen: Vec<bool> = vec![false; n];
1411 let mut work: std::collections::VecDeque<usize> = Default::default();
1412 let seed = |entry: &mut Vec<[Option<u16>; R]>,
1413 seen: &mut Vec<bool>,
1414 work: &mut std::collections::VecDeque<usize>,
1415 pc: usize,
1416 param_types: &[Option<crate::vm::BoundaryType>]| {
1417 if pc >= entry.len() {
1418 return;
1419 }
1420 for (r, pt) in param_types.iter().enumerate().take(R) {
1421 if let Some(t) = pt {
1422 entry[pc][r] = bt_index(t);
1423 }
1424 }
1425 seen[pc] = true;
1426 work.push_back(pc);
1427 };
1428
1429 // The transfer: which register this op re-types (Some(type index) or None = cleared to a non-struct).
1430 let transfer = |i: usize, st: &[Option<u16>; R]| -> Vec<(usize, Option<u16>)> {
1431 use Op::*;
1432 match &program.code[i] {
1433 // `a new T …` (a struct, or a bare-int Counter/Tally lowered to a struct): the dst holds T.
1434 NewStruct { dst, type_name } => {
1435 let ty = match program.constants.get(*type_name as usize) {
1436 Some(Constant::Text(s)) => name_index.get(s.as_str()).copied(),
1437 _ => None,
1438 };
1439 vec![(*dst as usize & 255, ty)]
1440 }
1441 // `Let q be p` keeps p's struct typing.
1442 Move { dst, src } => vec![(*dst as usize & 255, st[*src as usize & 255])],
1443 // A struct-typed call return / global read seeds the dst from the declared type.
1444 Call { dst, func, .. } => {
1445 let ty = program
1446 .functions
1447 .get(*func as usize)
1448 .and_then(|f| f.return_type.as_ref())
1449 .and_then(bt_index);
1450 vec![(*dst as usize & 255, ty)]
1451 }
1452 GlobalGet { dst, idx } => {
1453 let ty = program
1454 .global_types
1455 .get(*idx as usize)
1456 .and_then(|t| t.as_ref())
1457 .and_then(bt_index);
1458 vec![(*dst as usize & 255, ty)]
1459 }
1460 DeepClone { dst, src } => vec![(*dst as usize & 255, st[*src as usize & 255])],
1461 RevSwap { a, b } => vec![
1462 (*a as usize & 255, st[*b as usize & 255]),
1463 (*b as usize & 255, st[*a as usize & 255]),
1464 ],
1465 // `p's inner` where `inner` is declared a struct: the dst holds that struct.
1466 GetField { dst, obj, field } => {
1467 let ty = st[*obj as usize & 255]
1468 .and_then(|t| type_names.get(t as usize))
1469 .and_then(|tn| program.struct_types.iter().find(|d| &d.name == tn))
1470 .zip(match program.constants.get(*field as usize) {
1471 Some(Constant::Text(f)) => Some(f),
1472 _ => None,
1473 })
1474 .and_then(|(d, f)| d.fields.iter().find(|(n, _)| n == f))
1475 .and_then(|(_, t)| bt_index(t));
1476 vec![(*dst as usize & 255, ty)]
1477 }
1478 // A struct is value-typed: `StructInsert`/`CrdtBump`/`CrdtMerge`/`CrdtResolve` mutate the SAME
1479 // object in place (no register write), and a same-kind rebind keeps its struct.
1480 op if rebinds_same_kind(op) => vec![],
1481 // Every other register an op writes holds a non-struct value.
1482 op => written_regs(op)
1483 .into_iter()
1484 .map(|d| (d as usize & 255, None))
1485 .collect(),
1486 }
1487 };
1488
1489 for _sweep in 0..16 {
1490 let before = entry.clone();
1491 for x in seen.iter_mut() {
1492 *x = false;
1493 }
1494 work.clear();
1495 seed(&mut entry, &mut seen, &mut work, 0, &[]);
1496 for f in &program.functions {
1497 seed(&mut entry, &mut seen, &mut work, f.entry_pc, &f.param_types);
1498 }
1499 while let Some(i) = work.pop_front() {
1500 let mut out = entry[i];
1501 for (r, ty) in transfer(i, &entry[i]) {
1502 out[r] = ty;
1503 }
1504 let succs = successors(i, &program.code[i], n, |ci| ci);
1505 for s in succs {
1506 if s >= n {
1507 continue;
1508 }
1509 let mut changed = false;
1510 if !seen[s] {
1511 seen[s] = true;
1512 entry[s] = out;
1513 changed = true;
1514 } else {
1515 for r in 0..R {
1516 // MUST-agree join: keep the type only if both sides agree, else clear to None.
1517 let m = if entry[s][r] == out[r] {
1518 entry[s][r]
1519 } else {
1520 None
1521 };
1522 if m != entry[s][r] {
1523 entry[s][r] = m;
1524 changed = true;
1525 }
1526 }
1527 }
1528 if changed {
1529 work.push_back(s);
1530 }
1531 }
1532 }
1533 if entry == before {
1534 break;
1535 }
1536 }
1537
1538 // Build the usage-derived field layout: for each field op, resolve its object's struct type (from
1539 // the now-converged register plane) and record its field name in first-reference order. The result
1540 // is a compact `[0, nfields)` slot table per struct type, complete + self-consistent by construction.
1541 let mut field_slots: Vec<std::collections::HashMap<String, usize>> =
1542 vec![std::collections::HashMap::new(); type_names.len()];
1543 for (i, op) in program.code.iter().enumerate() {
1544 let (obj, field) = match op {
1545 Op::StructInsert { obj, field, .. } => (*obj, *field),
1546 Op::GetField { obj, field, .. } => (*obj, *field),
1547 Op::CrdtBump { obj, field, .. } => (*obj, *field),
1548 Op::CrdtResolve { obj, field, .. } => (*obj, *field),
1549 _ => continue,
1550 };
1551 let Some(ty) = entry.get(i).and_then(|st| st[obj as usize & 255]) else {
1552 continue;
1553 };
1554 let Some(Constant::Text(fname)) = program.constants.get(field as usize) else {
1555 continue;
1556 };
1557 let slots = &mut field_slots[ty as usize];
1558 if !slots.contains_key(fname.as_str()) {
1559 let pos = slots.len();
1560 slots.insert(fname.clone(), pos);
1561 }
1562 }
1563 let field_counts = field_slots.iter().map(|s| s.len()).collect();
1564 StructTyping {
1565 entry,
1566 type_names,
1567 field_slots,
1568 field_counts,
1569 }
1570 }
1571
1572 /// The type-name index of the struct in register `r` on entry to code index `i`, if provably known.
1573 fn struct_of(&self, i: usize, r: u16) -> Option<u16> {
1574 self.entry.get(i).and_then(|st| st[r as usize & 255])
1575 }
1576
1577 /// The type-name index of a struct type by NAME (a `NewStruct`'s `type_name` const), if it is a struct
1578 /// type this program uses. `NewStruct` reads its type from the const directly (the ground truth) rather
1579 /// than the register plane, which only types the dst on the op's EXIT (successor entry), not at the op.
1580 fn index_of_name(&self, name: &str) -> Option<u16> {
1581 self.type_names
1582 .iter()
1583 .position(|n| n == name)
1584 .and_then(|i| u16::try_from(i).ok())
1585 }
1586
1587 /// The field count (`NewStruct` operand) of struct type `ty` — the usage-derived slot count.
1588 fn field_count(&self, ty: u16) -> usize {
1589 self.field_counts.get(ty as usize).copied().unwrap_or(0)
1590 }
1591}
1592
1593/// Resolve the (slot position, field count) for a field op on register `obj` at code index `i`: the object's
1594/// struct type (from the oracle) + the field NAME (from the `field` const) via the usage-derived layout.
1595/// `Err` names the missing piece so an unlowerable struct op fails LOUDLY rather than mis-addressing a heap slot.
1596fn resolve_field(
1597 i: usize,
1598 constants: &[Constant],
1599 st: &StructTyping,
1600 obj: Reg,
1601 field: u32,
1602) -> Result<(usize, usize), String> {
1603 let ty = st.struct_of(i, obj).ok_or_else(|| {
1604 format!(
1605 "instruction {i}: struct field op on r{obj} whose struct type is not statically known — \
1606 no full-core chip lowering (the positional slot cannot be resolved)"
1607 )
1608 })?;
1609 let field_name = match constants.get(field as usize) {
1610 Some(Constant::Text(s)) => s.as_str(),
1611 _ => {
1612 return Err(format!(
1613 "instruction {i}: struct field name const #{field} is not Text"
1614 ))
1615 }
1616 };
1617 let slot = st
1618 .field_slots
1619 .get(ty as usize)
1620 .and_then(|m| m.get(field_name))
1621 .copied()
1622 .ok_or_else(|| {
1623 format!(
1624 "instruction {i}: field `{field_name}` not in the usage-derived layout of struct type `{}`",
1625 st.type_names.get(ty as usize).map(String::as_str).unwrap_or("?")
1626 )
1627 })?;
1628 Ok((slot, st.field_count(ty)))
1629}
1630
1631/// An 8-bit variant TAG for an inductive constructor: the constructor-name `ConstIdx` itself. Constants are
1632/// content-deduped (`Compiler::add_const`), so the SAME constructor name shares ONE index — `NewInductive`'s
1633/// `ctor` and every `TestArm`'s `variant` referencing that name get the SAME tag, exactly the VM's name
1634/// equality. Must fit the model's 8-bit tag field; a program with >255 distinct constants that puts a
1635/// constructor name past 255 is rejected loudly (the tag would truncate).
1636fn inductive_tag(i: usize, ctor: u32) -> Result<u32, String> {
1637 if ctor > 255 {
1638 return Err(format!(
1639 "instruction {i}: inductive constructor const index {ctor} exceeds the 8-bit tag field"
1640 ));
1641 }
1642 Ok(ctor)
1643}
1644
1645/// The FLOAT lowering of a single op, produced from the type oracle: a list of `IntToF` conversions
1646/// (`(dst_fbank, src_intbank)` — each one imem word) that must precede the main op so its integer
1647/// operands enter the float bank, then the main float word (self-contained, no branch target). `None`
1648/// means the op is NOT float-typed and the integer path (`imem_width`/`encode_op`) handles it unchanged.
1649/// The conversions write `fregs[reg] = regs[reg] as f64` IN PLACE (same register number, disjoint bank),
1650/// so a subsequent integer use of that register is unaffected.
1651struct FloatPlan {
1652 convs: Vec<(u32, u32)>, // (reg to fill in the float bank) — src==dst==the reg, converted in place
1653 body: Vec<u32>, // the float instruction word(s): one for a primitive op, a whole sequence for a
1654 // multi-op lowering (`is approximately` decomposes into FEq/FSub/FAbs/FMax/FConst/FMul/FLe/BitOr).
1655}
1656
1657/// Decide the float lowering of code index `i`, or `None` for the integer path. Emits `IntToF` for each
1658/// integer operand feeding a float op (the Int→Float boundary the type oracle located — e.g. `2.0 * k`
1659/// converts `k`). Comparisons with a float operand lower to `FLt`/`FEq` (int 0/1 result, so branches work).
1660/// `scratch` is the program-wide-unused base register (`max index any op touches + 1`); a multi-op float
1661/// lowering (`is approximately`) claims a small contiguous block starting there for its intermediates.
1662fn float_shape(
1663 i: usize,
1664 op: &Op,
1665 ft: &FloatTyping,
1666 scratch: Reg,
1667) -> Result<Option<FloatPlan>, String> {
1668 let isf = |r: Reg| ft.is_float(i, r);
1669 // The `CallBuiltin` opcode (38) carries the FLOAT-plane numeric coprocessor calls (abs/min/max/floor/
1670 // ceil/round on floats), the ordinal in the b-byte. Same opcode the integer-plane path uses; the
1671 // model's `38 =>` arm branches on the ordinal to read `fregs` vs `regs`.
1672 let cb_oc = crate::vm::binisa::opcode_of_name("CallBuiltin")
1673 .expect("CallBuiltin is a registered opcode");
1674 // A conversion for an integer operand feeding a float op: fill fregs[r] from regs[r] in place.
1675 let conv = |r: Reg, plan: &mut Vec<(u32, u32)>| -> Result<(), String> {
1676 if !isf(r) {
1677 plan.push((reg(r)?, reg(r)?));
1678 }
1679 Ok(())
1680 };
1681 let isq = |r: Reg| ft.is_float_seq(i, r);
1682 Ok(match op {
1683 Op::LoadConst { .. } => None, // handled in encode_op (needs the constant table); width special-cased
1684 // ── FLOAT-HEAP PLANE: a `Seq of Float` structure lives in the int heap; its f64 payloads ride the
1685 // parallel float plane. A float `Push`/`Add to` lowers to `FListPush` (int structure + fheap[slot] =
1686 // fregs[value]); a float `Set item` lowers to `FSetIndex`. The value is a proven scalar float (that
1687 // is the detector), so no `IntToF` conversion is needed. The list handle + index stay int registers.
1688 Op::ListPush { list, value } | Op::SetAdd { set: list, value } if isf(*value) => {
1689 Some(FloatPlan {
1690 convs: Vec::new(),
1691 body: vec![w(FLIST_PUSH, reg(*list)?, reg(*value)?, 0)],
1692 })
1693 }
1694 Op::SetIndex {
1695 collection,
1696 index,
1697 value,
1698 }
1699 | Op::SetIndexUnchecked {
1700 collection,
1701 index,
1702 value,
1703 } if isf(*value) => Some(FloatPlan {
1704 convs: Vec::new(),
1705 body: vec![w(FSET_INDEX, reg(*collection)?, reg(*index)?, reg(*value)?)],
1706 }),
1707 // `item i of (Seq of Float)` — read the f64 payload off the float plane into `fregs[dst]`. The
1708 // collection handle + index are int registers (the pointer + 1-based position); the type oracle
1709 // proved the collection is a float seq so the RESULT is a scalar float.
1710 Op::Index {
1711 dst,
1712 collection,
1713 index,
1714 }
1715 | Op::IndexUnchecked {
1716 dst,
1717 collection,
1718 index,
1719 } if isq(*collection) => Some(FloatPlan {
1720 convs: Vec::new(),
1721 body: vec![w(FINDEX, reg(*dst)?, reg(*collection)?, reg(*index)?)],
1722 }),
1723 Op::Add { dst, lhs, rhs }
1724 | Op::Sub { dst, lhs, rhs }
1725 | Op::Mul { dst, lhs, rhs }
1726 | Op::Div { dst, lhs, rhs }
1727 if isf(*lhs) || isf(*rhs) =>
1728 {
1729 let oc = match op {
1730 Op::Add { .. } => FADD,
1731 Op::Sub { .. } => FSUB,
1732 Op::Mul { .. } => FMUL,
1733 _ => FDIV, // `/` on floats is IEEE-754 division
1734 };
1735 let mut convs = Vec::new();
1736 conv(*lhs, &mut convs)?;
1737 conv(*rhs, &mut convs)?;
1738 Some(FloatPlan {
1739 convs,
1740 body: vec![w(oc, reg(*dst)?, reg(*lhs)?, reg(*rhs)?)],
1741 })
1742 }
1743 // Exact/floor division and modulo have no float datapath (they are integer-division semantics);
1744 // a float operand reaching one is a real type error, rejected rather than silently mis-lowered.
1745 Op::ExactDiv { lhs, rhs, .. }
1746 | Op::FloorDiv { lhs, rhs, .. }
1747 | Op::Mod { lhs, rhs, .. }
1748 if isf(*lhs) || isf(*rhs) =>
1749 {
1750 return Err(format!(
1751 "instruction {i}: integer division/modulo op `{}` on a float operand has no chip lowering",
1752 op.name()
1753 ));
1754 }
1755 // `Set x to x + y` where x is a float accumulator: FAdd dst, dst, src (dst already float).
1756 Op::AddAssign { dst, src } if isf(*dst) || isf(*src) => {
1757 let mut convs = Vec::new();
1758 conv(*dst, &mut convs)?;
1759 conv(*src, &mut convs)?;
1760 Some(FloatPlan {
1761 convs,
1762 body: vec![w(FADD, reg(*dst)?, reg(*dst)?, reg(*src)?)],
1763 })
1764 }
1765 Op::Move { dst, src } if isf(*src) => Some(FloatPlan {
1766 convs: Vec::new(),
1767 body: vec![w(FMOVE, reg(*dst)?, reg(*src)?, 0)],
1768 }),
1769 // Comparisons where at least one side is float → FLt/FEq (int 0/1 result in regs).
1770 Op::Lt { dst, lhs, rhs } | Op::Gt { dst, lhs, rhs } if isf(*lhs) || isf(*rhs) => {
1771 let mut convs = Vec::new();
1772 conv(*lhs, &mut convs)?;
1773 conv(*rhs, &mut convs)?;
1774 // Gt a b == Lt b a.
1775 let (a, b) = if matches!(op, Op::Gt { .. }) {
1776 (*rhs, *lhs)
1777 } else {
1778 (*lhs, *rhs)
1779 };
1780 Some(FloatPlan {
1781 convs,
1782 body: vec![w(FLT, reg(*dst)?, reg(a)?, reg(b)?)],
1783 })
1784 }
1785 // `a <= b` → FLe(a,b); `a >= b` → `b <= a` → FLe(b,a) (operand swap, no float NOT needed).
1786 Op::LtEq { dst, lhs, rhs } | Op::GtEq { dst, lhs, rhs } if isf(*lhs) || isf(*rhs) => {
1787 let mut convs = Vec::new();
1788 conv(*lhs, &mut convs)?;
1789 conv(*rhs, &mut convs)?;
1790 let (a, b) = if matches!(op, Op::GtEq { .. }) {
1791 (*rhs, *lhs)
1792 } else {
1793 (*lhs, *rhs)
1794 };
1795 Some(FloatPlan {
1796 convs,
1797 body: vec![w(FLE, reg(*dst)?, reg(a)?, reg(b)?)],
1798 })
1799 }
1800 // `a == b` → FEq; `a != b` → FNe (native `!=`, NaN-true like the VM's f64 `!=`).
1801 Op::Eq { dst, lhs, rhs } | Op::NotEq { dst, lhs, rhs } if isf(*lhs) || isf(*rhs) => {
1802 let mut convs = Vec::new();
1803 conv(*lhs, &mut convs)?;
1804 conv(*rhs, &mut convs)?;
1805 let oc = if matches!(op, Op::NotEq { .. }) {
1806 FNE
1807 } else {
1808 FEQ
1809 };
1810 Some(FloatPlan {
1811 convs,
1812 body: vec![w(oc, reg(*dst)?, reg(*lhs)?, reg(*rhs)?)],
1813 })
1814 }
1815 // `a is approximately b` — the tolerant isclose compare, DECOMPOSED into the primitive float ops the
1816 // RTL already sequences per instruction (no bespoke multi-cycle isclose FSM). Bit-for-bit reproduces
1817 // the ONE shared definition `logicaffeine_data::ops::logos_approx_eq`, the SAME IEEE-754 RNE ops in the
1818 // SAME order, so the SAME image runs on the model AND `soft_core.v`:
1819 // eq = FEq(a,b) ; diff = FSub(a,b) ; adiff = FAbs(diff) ; m = FMax(FAbs(a), FAbs(b)) ;
1820 // scaled = 1e-9·m ; thresh = FMax(scaled, 1e-12) ; le = FLe(adiff, thresh) ; result = eq | le.
1821 // `logos_approx_eq(a,b) = (a==b) || |a−b| ≤ max(1e-9·max(|a|,|b|), 1e-12)`; the `eq | le` fast path
1822 // reproduces the `a==b` short-circuit (and stays 0-on-NaN, since `==`/`<=` are both false on NaN).
1823 // The float coprocessor `FMax` (op 38, ordinal 38) reads its two operands from CONSECUTIVE float
1824 // registers (`fregs[ra]`, `fregs[ra+1]`), so the two `|a|`/`|b|` and `scaled`/`1e-12` pairs are placed
1825 // adjacently. Intermediates live in a contiguous block above `scratch` (program-wide-unused), disjoint
1826 // per bank: the int results `eq`/`le` reuse indices in the int file that the float temps occupy in the
1827 // float file (the banks never alias). The final `BitOr` writes the int 0/1 into `dst`.
1828 Op::ApproxEq { dst, lhs, rhs } if isf(*lhs) || isf(*rhs) => {
1829 let mut convs = Vec::new();
1830 conv(*lhs, &mut convs)?;
1831 conv(*rhs, &mut convs)?;
1832 let (a, b, d) = (reg(*lhs)?, reg(*rhs)?, reg(*dst)?);
1833 // Contiguous scratch block s..=s+8 (all above every used register). The two FMax pairs must be
1834 // adjacent: (fa=|a|, fa+1=|b|) and (fsc=scaled, fsc+1=1e-12).
1835 let s = reg(scratch)?;
1836 let need_hi = s + 8;
1837 if need_hi > 255 {
1838 return Err(format!(
1839 "instruction {i}: `is approximately` needs a 9-register scratch block for its isclose \
1840 decomposition, but the program's live registers leave none free (base r{s})"
1841 ));
1842 }
1843 let fa = s; // |a| (FMax pair lo)
1844 let fb = s + 1; // |b| (FMax pair hi = fa+1)
1845 let fm = s + 2; // m = max(|a|,|b|)
1846 let fc9 = s + 3; // 1e-9
1847 let fsc = s + 4; // scaled = 1e-9·m (FMax pair lo)
1848 let fc12 = s + 5; // 1e-12 (FMax pair hi = fsc+1)
1849 let fth = s + 6; // thresh = max(scaled, 1e-12)
1850 let fdiff = s + 7; // a − b
1851 let fadiff = s + 8; // |a − b|
1852 let ieq = s; // int bank: eq = (a==b) (disjoint bank — reuses the |a| index)
1853 let ile = s + 1; // int bank: le = diff ≤ thresh (reuses the |b| index)
1854 let fconst = |dst: u32, bits: u64| -> [u32; 3] {
1855 [
1856 w(FCONST, dst, 0, 0),
1857 (bits & 0xffff_ffff) as u32,
1858 (bits >> 32) as u32,
1859 ]
1860 };
1861 let mut body: Vec<u32> = Vec::with_capacity(18);
1862 body.push(w(FEQ, ieq, a, b)); // eq = FEq(a,b) → regs[ieq]
1863 body.push(w(FSUB, fdiff, a, b)); // diff = a − b
1864 body.push(w(cb_oc, fadiff, fdiff, CB_FABS)); // adiff = |diff|
1865 body.push(w(cb_oc, fa, a, CB_FABS)); // |a|
1866 body.push(w(cb_oc, fb, b, CB_FABS)); // |b| (= fa+1)
1867 body.push(w(cb_oc, fm, fa, CB_FMAX)); // m = FMax(fregs[fa], fregs[fa+1])
1868 body.extend(fconst(fc9, (1e-9f64).to_bits())); // 1e-9
1869 body.push(w(FMUL, fsc, fc9, fm)); // scaled = 1e-9 · m
1870 body.extend(fconst(fc12, (1e-12f64).to_bits())); // 1e-12 (= fsc+1)
1871 body.push(w(cb_oc, fth, fsc, CB_FMAX)); // thresh = FMax(fregs[fsc], fregs[fsc+1])
1872 body.push(w(FLE, ile, fadiff, fth)); // le = FLe(adiff, thresh) → regs[ile]
1873 body.push(w(BITOR, d, ieq, ile)); // result = eq | le → regs[dst]
1874 Some(FloatPlan { convs, body })
1875 }
1876 Op::Show { src } if isf(*src) => Some(FloatPlan {
1877 convs: Vec::new(),
1878 body: vec![w(SHOWF, 0, reg(*src)?, 0)],
1879 }),
1880 Op::CallBuiltin {
1881 dst,
1882 builtin,
1883 args_start,
1884 ..
1885 } if matches!(builtin, crate::semantics::builtins::BuiltinId::Sqrt) && isf(*args_start) => {
1886 Some(FloatPlan {
1887 convs: Vec::new(),
1888 body: vec![w(FSQRT, reg(*dst)?, reg(*args_start)?, 0)],
1889 })
1890 }
1891 // `abs(x)` on a FLOAT — the FPU magnitude (`f.abs()`), fregs → fregs. Result stays a float scalar.
1892 Op::CallBuiltin {
1893 dst,
1894 builtin,
1895 args_start,
1896 ..
1897 } if matches!(builtin, crate::semantics::builtins::BuiltinId::Abs) && isf(*args_start) => {
1898 Some(FloatPlan {
1899 convs: Vec::new(),
1900 body: vec![w(cb_oc, reg(*dst)?, reg(*args_start)?, CB_FABS)],
1901 })
1902 }
1903 // `min(a, b)` / `max(a, b)` where EITHER operand is a float — the VM promotes the pair to f64
1904 // (`(x as f64).min(y)` etc), so any int operand is converted (IntToF) and the compare runs on the
1905 // float plane (fregs → fregs). The two args are the consecutive registers `args_start` / `args_start+1`.
1906 Op::CallBuiltin {
1907 dst,
1908 builtin,
1909 args_start,
1910 arg_count,
1911 } if matches!(
1912 builtin,
1913 crate::semantics::builtins::BuiltinId::Min | crate::semantics::builtins::BuiltinId::Max
1914 ) && *arg_count == 2
1915 && (isf(*args_start) || isf(args_start.saturating_add(1))) =>
1916 {
1917 let mut convs = Vec::new();
1918 conv(*args_start, &mut convs)?;
1919 conv(args_start.saturating_add(1), &mut convs)?;
1920 let ord = if matches!(builtin, crate::semantics::builtins::BuiltinId::Min) {
1921 CB_FMIN
1922 } else {
1923 CB_FMAX
1924 };
1925 Some(FloatPlan {
1926 convs,
1927 body: vec![w(cb_oc, reg(*dst)?, reg(*args_start)?, ord)],
1928 })
1929 }
1930 // `floor(x)` / `ceil(x)` / `round(x)` on a FLOAT — the FPU rounds to an INTEGER (`f.floor() as i64`,
1931 // round-half-away via Rust's `f64::round`), fregs → REGS. The result is an int scalar, so the dst
1932 // leaves the float plane (the type oracle marks it int); the coprocessor writes `regs[dst]`.
1933 Op::CallBuiltin {
1934 dst,
1935 builtin,
1936 args_start,
1937 ..
1938 } if matches!(
1939 builtin,
1940 crate::semantics::builtins::BuiltinId::Floor
1941 | crate::semantics::builtins::BuiltinId::Ceil
1942 | crate::semantics::builtins::BuiltinId::Round
1943 ) && isf(*args_start) =>
1944 {
1945 use crate::semantics::builtins::BuiltinId;
1946 let ord = match builtin {
1947 BuiltinId::Floor => CB_FLOOR,
1948 BuiltinId::Ceil => CB_FCEIL,
1949 _ => CB_FROUND,
1950 };
1951 Some(FloatPlan {
1952 convs: Vec::new(),
1953 body: vec![w(cb_oc, reg(*dst)?, reg(*args_start)?, ord)],
1954 })
1955 }
1956 _ => None,
1957 })
1958}
1959
1960// ── THE WORD32 (ℤ/2³²) LOWERING ─────────────────────────────────────────────────────────────────────
1961// A `Word32` is the register's own 32-bit pattern, so almost the whole vocabulary is FREE on the integer
1962// datapath. This shape covers the parts that need a distinct lowering:
1963// • the CONSTRUCTOR/COERCION builtins `word32(n)` / `intOfWord32(w)` → `Move` (the value is the bit
1964// pattern, already in the register; the coercion is a no-op copy);
1965// • the BITWISE word builtins `word_and`/`word_or`/`word_not` → the `BitAnd`/`BitOr`/`Not` ALU ops
1966// (bit-identical to the ring), since the source's `xor` OPERATOR already lowers to `BitXor`;
1967// • the ROTATE and LOGICAL-SHIFT builtins `rotl`/`rotr`/`word32Shr` → the `CallBuiltin` coprocessor
1968// (opcode 38, by ordinal) — the integer ALU has no rotate and only an ARITHMETIC `Shr`. The shift/
1969// rotate count rides `regs[args_start+1]` (a plain int), matching the model's coprocessor arm;
1970// • a Word32 `+`/`-`/`*` → the WORD32 ARITHMETIC ops (226/227/228), which wrap in the ring ℤ/2³² — the
1971// value-integer ALU (8/10/11) is now UNBOUNDED (BigInt on overflow, to match a plain `Int`), so a
1972// Word32 sum routed there would drift past 2³² instead of wrapping (SHA-256's `s1 + s0 + …` chain).
1973// The oracle's word32-result marking (`wd(lhs) || wd(rhs)`) is on the op's EXIT; here we read the same
1974// signal from the ENTRY state (an operand is already Word32), which is exactly when the result wraps.
1975// • a Word32-typed `Show` → `ShowWord32` (opcode 221), which renders the value UNSIGNED.
1976// All are single-word (no operand conversions), so the width is always 1. `None` means the op is not a
1977// Word32 op and the integer path handles it unchanged (a Word32 `==`/`Move`/`Push`/bitwise already rides
1978// the integer datapath byte-identically — only its element/Show TYPING differs, carried by the type oracle).
1979fn word_shape(i: usize, op: &Op, ft: &FloatTyping) -> Result<Option<u32>, String> {
1980 use crate::semantics::builtins::BuiltinId;
1981 let move_oc = crate::vm::binisa::opcode_of_name("Move").expect("Move is a registered opcode");
1982 let and_oc =
1983 crate::vm::binisa::opcode_of_name("BitAnd").expect("BitAnd is a registered opcode");
1984 let or_oc = crate::vm::binisa::opcode_of_name("BitOr").expect("BitOr is a registered opcode");
1985 let not_oc = crate::vm::binisa::opcode_of_name("Not").expect("Not is a registered opcode");
1986 let cb_oc = crate::vm::binisa::opcode_of_name("CallBuiltin")
1987 .expect("CallBuiltin is a registered opcode");
1988 // A Word32 `+`/`-`/`*` (either operand already Word32) wraps in ℤ/2³²: the wrapping opcodes 226/227/228.
1989 // NOT a Text/float `Add` (those planes take precedence in the transfer; a Text `+` has a Text operand,
1990 // a float `+` a float operand — neither is Word32-typed here, so the guard `is_word32(operand)` is exact).
1991 let word32_arith = |dst: &Reg, a: &Reg, b: &Reg, oc: u16| -> Result<Option<u32>, String> {
1992 if ft.is_word32(i, *a) || ft.is_word32(i, *b) {
1993 Ok(Some(w(oc, reg(*dst)?, reg(*a)?, reg(*b)?)))
1994 } else {
1995 Ok(None)
1996 }
1997 };
1998 Ok(match op {
1999 Op::Add { dst, lhs, rhs } => return word32_arith(dst, lhs, rhs, WORD32ADD),
2000 Op::Sub { dst, lhs, rhs } => return word32_arith(dst, lhs, rhs, WORD32SUB),
2001 Op::Mul { dst, lhs, rhs } => return word32_arith(dst, lhs, rhs, WORD32MUL),
2002 // `Set w to w + x` on a Word32 accumulator — the in-place add wraps too (lhs = dst).
2003 Op::AddAssign { dst, src } => return word32_arith(dst, dst, src, WORD32ADD),
2004 Op::CallBuiltin {
2005 dst,
2006 builtin,
2007 args_start,
2008 ..
2009 } => match builtin {
2010 // The value IS the register's bit pattern — construction / int-coercion is an identity copy.
2011 BuiltinId::Word32 | BuiltinId::IntOfWord32 => {
2012 Some(w(move_oc, reg(*dst)?, reg(*args_start)?, 0))
2013 }
2014 // Bitwise word ops → the existing ALU datapath. The two operands ride args_start, args_start+1.
2015 BuiltinId::Wand => Some(w(
2016 and_oc,
2017 reg(*dst)?,
2018 reg(*args_start)?,
2019 reg(args_start.saturating_add(1))?,
2020 )),
2021 BuiltinId::Wor => Some(w(
2022 or_oc,
2023 reg(*dst)?,
2024 reg(*args_start)?,
2025 reg(args_start.saturating_add(1))?,
2026 )),
2027 BuiltinId::Wnot => Some(w(not_oc, reg(*dst)?, reg(*args_start)?, 0)),
2028 // Rotate / logical shift → the CallBuiltin coprocessor (opcode 38) by ordinal. dst = result,
2029 // ra = args_start (the word), and the coprocessor reads the count from regs[args_start+1].
2030 BuiltinId::Rotl | BuiltinId::Rotr | BuiltinId::Word32Shr => {
2031 let ord = builtin_chip_ordinal(*builtin).ok_or_else(|| {
2032 format!("instruction {i}: word builtin {builtin:?} has no chip ordinal")
2033 })?;
2034 // The count register (args_start+1) must fit the 8-bit operand field too.
2035 reg(args_start.saturating_add(1))?;
2036 Some(w(cb_oc, reg(*dst)?, reg(*args_start)?, ord as u32))
2037 }
2038 _ => None,
2039 },
2040 // A Word32-typed `Show` renders UNSIGNED (0..2³²−1) — the integer `Show` would print `i32` signed.
2041 Op::Show { src } if ft.is_word32(i, *src) => Some(w(SHOWWORD32, 0, reg(*src)?, 0)),
2042 _ => None,
2043 })
2044}
2045
2046// ── THE TEXT (string) LOWERING ────────────────────────────────────────────────────────────────────
2047// A `Text` handle is an int pointer to a char-code list. Three op shapes carry Text on the chip:
2048// • a Text `LoadConst` → `TextConst` (2 words: op+len then the pool word-offset), copying the string's
2049// char block from the imem string-constant pool into a fresh heap char-list;
2050// • a Text `Add`/`AddAssign` → the `Concat` datapath (op 32) — the VM's `Text + x` rule is string concat,
2051// with a non-Text operand rendered via `to_display_string()`. An Int operand is FormatValue'd first (its
2052// decimal char-list); a Text operand feeds the Concat directly. (`AddAssign` = `Set r to r + x`.)
2053// • a Text `Show` → `ShowText`, which decodes the char list to a UTF-8 string.
2054// This mirrors the VM's kernel add: `(Text,Text)` concat, `(Text,other)`/`(other,Text)` stringify-then-concat.
2055enum TextLower {
2056 /// A Text `LoadConst`: copy the pooled char block into a fresh heap char-list. `idx` = constant index.
2057 Const { dst: Reg, idx: u32 },
2058 /// A Text concat (`Add`/`AddAssign`). `lhs`/`rhs` are the concat operands; `fmt` names the ONE operand
2059 /// (if any) that is not itself Text and must be FormatValue'd to its decimal char-list first (Int only).
2060 Concat {
2061 dst: Reg,
2062 lhs: Reg,
2063 rhs: Reg,
2064 fmt: Option<Reg>,
2065 },
2066 /// A Text `Show` — decode the char list at `src` and emit the string.
2067 Show { src: Reg },
2068 /// `item i of text` — a single-character Text (a fresh `[1, codepoint]` char-list), matching the VM.
2069 CharAt {
2070 dst: Reg,
2071 collection: Reg,
2072 index: Reg,
2073 },
2074 /// A Text `equals`/`is not` (`Eq`/`NotEq` with a Text operand) — char-list CONTENT compare. `eq` selects
2075 /// equality (`TextEq`) vs inequality (`TextNe`); the result is an int 0/1.
2076 Compare {
2077 dst: Reg,
2078 lhs: Reg,
2079 rhs: Reg,
2080 eq: bool,
2081 },
2082 /// A Text `contains` (`Op::Contains` on a Text collection, or the `contains(hay, needle)` builtin) —
2083 /// a SUBSTRING char-scan (`TextContains`, op 234) → int 0/1. Distinct from the LIST/SET/MAP `Op::Contains`
2084 /// (op 63, element membership): a Text `contains` tests a contiguous run of codepoints, not a single cell.
2085 Contains { dst: Reg, hay: Reg, needle: Reg },
2086}
2087
2088impl TextLower {
2089 /// The imem word count of this lowering. `Const` = 2 (op word + pool offset word); `Concat` = 1
2090 /// (a bare Concat) or 2 (a FormatValue then the Concat); every other Text op = 1.
2091 fn width(&self) -> usize {
2092 match self {
2093 TextLower::Const { .. } => 2,
2094 TextLower::Concat { fmt, .. } => 1 + usize::from(fmt.is_some()),
2095 TextLower::Show { .. }
2096 | TextLower::CharAt { .. }
2097 | TextLower::Compare { .. }
2098 | TextLower::Contains { .. } => 1,
2099 }
2100 }
2101}
2102
2103/// Classify code index `i` as a Text op, or `None` for the non-Text path. Uses the Text type plane to decide
2104/// a Text `Add`/`AddAssign`/`Show`; a Text `LoadConst` is decided from the constant kind (ground truth). A
2105/// Text concat whose non-Text operand is neither a plain Int (FormatValue-able) nor a Text is rejected — the
2106/// chip has no `to_display_string` for a Float/Bool inside a string, so it fails loudly rather than mis-lower.
2107fn text_shape(
2108 i: usize,
2109 op: &Op,
2110 constants: &[Constant],
2111 ft: &FloatTyping,
2112) -> Result<Option<TextLower>, String> {
2113 let ist = |r: Reg| ft.is_text(i, r);
2114 // The non-Text operand of a Text concat must be a plain Int at runtime (FormatValue renders the decimal
2115 // string, matching the VM's `Int::to_display_string`). A float there would need a float→string path the
2116 // chip doesn't have; reject it. We can only prove int-ness by exclusion (not float / not text / not seq),
2117 // which the benchmarks satisfy (they concat only ints and strings).
2118 let non_text_ok = |r: Reg| -> Result<(), String> {
2119 if ft.is_float(i, r) || ft.is_float_seq(i, r) {
2120 return Err(format!(
2121 "instruction {i}: a Text concat with a non-integer operand (r{r} is float) has no chip lowering — \
2122 only `Text + Int` interpolation and `Text + Text` concat are supported"
2123 ));
2124 }
2125 Ok(())
2126 };
2127 Ok(match op {
2128 Op::LoadConst { dst, idx } => match constants.get(*idx as usize) {
2129 Some(Constant::Text(_)) => Some(TextLower::Const {
2130 dst: *dst,
2131 idx: *idx,
2132 }),
2133 _ => None,
2134 },
2135 Op::Add { dst, lhs, rhs } if ist(*lhs) || ist(*rhs) => {
2136 let fmt = if ist(*lhs) && ist(*rhs) {
2137 None
2138 } else if ist(*lhs) {
2139 non_text_ok(*rhs)?;
2140 Some(*rhs)
2141 } else {
2142 non_text_ok(*lhs)?;
2143 Some(*lhs)
2144 };
2145 Some(TextLower::Concat {
2146 dst: *dst,
2147 lhs: *lhs,
2148 rhs: *rhs,
2149 fmt,
2150 })
2151 }
2152 // An explicit `Concat` — the interpolation `"{x:.N}"` accumulator (`"" ++ formatted ++ …`) emits this,
2153 // not `Add`. Both operands are Text on the chip (the accumulator started as a Text `LoadConst`; each
2154 // piece is a Text literal or a `FormatValue`'d char-list, already marked Text by the type oracle), so it
2155 // is a bare Text-Text `Concat`. A non-Text operand (a raw int piece) is FormatValue'd first, mirroring
2156 // the `Add` path — so a mixed interpolation lowers the same way regardless of which concat op the
2157 // compiler chose.
2158 Op::Concat { dst, lhs, rhs } if ist(*lhs) || ist(*rhs) => {
2159 let fmt = if ist(*lhs) && ist(*rhs) {
2160 None
2161 } else if ist(*lhs) {
2162 non_text_ok(*rhs)?;
2163 Some(*rhs)
2164 } else {
2165 non_text_ok(*lhs)?;
2166 Some(*lhs)
2167 };
2168 Some(TextLower::Concat {
2169 dst: *dst,
2170 lhs: *lhs,
2171 rhs: *rhs,
2172 fmt,
2173 })
2174 }
2175 // `Set r to r + x` on a Text accumulator: Concat into r (lhs = dst). The VM's `add_assign` for a Text
2176 // dst is exactly `r + x`, so this is `Concat r = r ++ x` (FormatValue-ing a non-Text x).
2177 Op::AddAssign { dst, src } if ist(*dst) || ist(*src) => {
2178 let fmt = if ist(*dst) && ist(*src) {
2179 None
2180 } else if ist(*dst) {
2181 non_text_ok(*src)?;
2182 Some(*src)
2183 } else {
2184 // A Text src added into a non-Text dst — `stringify(dst) ++ src`; the dst is the fmt operand.
2185 non_text_ok(*dst)?;
2186 Some(*dst)
2187 };
2188 Some(TextLower::Concat {
2189 dst: *dst,
2190 lhs: *dst,
2191 rhs: *src,
2192 fmt,
2193 })
2194 }
2195 Op::Show { src } if ist(*src) => Some(TextLower::Show { src: *src }),
2196 // `item i of text` — a single-char Text (the VM returns a 1-char string). The collection handle +
2197 // 1-based index are int registers; the result is a fresh char-list.
2198 Op::Index {
2199 dst,
2200 collection,
2201 index,
2202 }
2203 | Op::IndexUnchecked {
2204 dst,
2205 collection,
2206 index,
2207 } if ist(*collection) => Some(TextLower::CharAt {
2208 dst: *dst,
2209 collection: *collection,
2210 index: *index,
2211 }),
2212 // A Text `equals`/`is not` — char-list content compare (both operands are Text handles). The VM
2213 // compares Text by content; the chip does the same via `TextEq`/`TextNe`.
2214 Op::Eq { dst, lhs, rhs } if ist(*lhs) || ist(*rhs) => Some(TextLower::Compare {
2215 dst: *dst,
2216 lhs: *lhs,
2217 rhs: *rhs,
2218 eq: true,
2219 }),
2220 Op::NotEq { dst, lhs, rhs } if ist(*lhs) || ist(*rhs) => Some(TextLower::Compare {
2221 dst: *dst,
2222 lhs: *lhs,
2223 rhs: *rhs,
2224 eq: false,
2225 }),
2226 // A Text `contains` (`s contains "sub"`) — the compiler lowers the `contains` OPERATOR to `Op::Contains`
2227 // (op 63) for EVERY collection kind, but Text membership is SUBSTRING containment, not the element scan
2228 // op 63 performs (which would compare a codepoint cell against the needle's Text pointer and always
2229 // miss). When the COLLECTION is a Text char-list, route it to the substring scanner `TextContains`
2230 // (op 234) — byte-identical to the VM's `Value::contains` for two Texts (`h.contains(needle)`). The
2231 // NEEDLE must be provably Text too: the VM errors on a non-Text needle against a Text (`Cannot check if
2232 // Text contains …`), and the char-scan reads it as a char-list, so a non-Text needle is rejected loudly
2233 // rather than mis-scanned (never silently falling to the op-63 element scan, which is wrong for a Text).
2234 Op::Contains {
2235 dst,
2236 collection,
2237 value,
2238 } if ist(*collection) => {
2239 if !ist(*value) {
2240 return Err(format!(
2241 "instruction {i}: `contains` on a Text needs a Text needle (r{value} is not Text) — the \
2242 substring scanner reads the needle as a char-list; a non-Text needle has no chip lowering"
2243 ));
2244 }
2245 Some(TextLower::Contains {
2246 dst: *dst,
2247 hay: *collection,
2248 needle: *value,
2249 })
2250 }
2251 _ => None,
2252 })
2253}
2254
2255/// The imem word count an op lowers to: 2 for `Call` (word2 = callee entry) and a wide `LoadConst`
2256/// (word2 = 32-bit immediate), 1 for everything else. Target-independent (depends only on the op kind,
2257/// its float typing, and constant magnitude), so pass 1 can build the pc→imem map before any target is
2258/// resolved. A float op costs `1 + <IntToF conversions>` (see [`float_shape`]); a float `LoadConst` is a
2259/// 3-word `FConst`. A Text op costs [`TextLower::width`]. Returns `Err` for an op with no chip lowering, so
2260/// an unsupported program fails loudly.
2261fn imem_width(
2262 i: usize,
2263 op: &Op,
2264 constants: &[Constant],
2265 ft: &FloatTyping,
2266 slice_fold: &dyn Fn(usize) -> Option<(u8, u8)>,
2267 scratch: Reg,
2268) -> Result<usize, String> {
2269 // A float-typed op: the lowering body (one word for a primitive op, a full sequence for `is
2270 // approximately`) plus one IntToF per integer operand feeding it.
2271 if let Some(plan) = float_shape(i, op, ft, scratch)? {
2272 return Ok(plan.convs.len() + plan.body.len());
2273 }
2274 // A Word32 op (construct/coerce/bitwise/rotate/shift/Show): always a single chip word.
2275 if word_shape(i, op, ft)?.is_some() {
2276 return Ok(1);
2277 }
2278 if let Some(plan) = failure_plan(i, op, constants, ft)? {
2279 return Ok(plan.width());
2280 }
2281 if let Some(plan) = format_plan(i, op, ft)? {
2282 return Ok(plan.width());
2283 }
2284 // A Text-typed op: a TextConst (2), a Concat (1) / FormatValue+Concat (2), or a ShowText (1).
2285 if let Some(t) = text_shape(i, op, constants, ft)? {
2286 return Ok(t.width());
2287 }
2288 Ok(match op {
2289 Op::Call { .. } => 2,
2290 // `args()` materializes `["logos"]`: NewEmptyList + TextConst (2) + ListPush.
2291 Op::Args { .. } => 4,
2292 // A `NewList` of a `Seq of Float` literal builds through the float plane: `NewEmptyList` + one
2293 // `FListPush` per element (`1 + count` words), so `item i of` reads the f64 payload. A non-float
2294 // `NewList` is the single-word structure op. (See the matching `encode_op` arm.)
2295 Op::NewList { start, count, .. } if (0..*count).any(|k| ft.is_float(i, start.saturating_add(k))) => {
2296 1 + *count as usize
2297 }
2298 Op::NewList { .. } => 1,
2299 Op::LoadConst { idx, .. } => {
2300 let c = constants
2301 .get(*idx as usize)
2302 .ok_or_else(|| format!("LoadConst references missing constant #{idx}"))?;
2303 // A float literal is ALWAYS a 3-word `FConst` (op + the two immediate words = the f64 bits) —
2304 // the constant kind is ground truth, so it needs no dataflow lookup.
2305 if let Constant::Float(_) = c {
2306 let _ = i; // (i unused here; kept in the signature for the float ops)
2307 return Ok(3);
2308 }
2309 let v = const_i64(c)?;
2310 // A constant wider than i32 (the PRNG moduli 2³¹ / 2³², or a full-range i64 literal) is held in a
2311 // 3-word LoadConstWide64 (op 222) at EVERY width. The full core's value datapath auto-promotes to
2312 // BigInt on overflow exactly like the VM, and op 222 retires the full i64 regardless of datapath
2313 // width, so the chip reproduces the constant EXACTLY at width 32 as well as 64 — the width is a
2314 // fast-path/area knob, not a correctness switch (BigInt handles exactness). An i32-fitting constant
2315 // uses the narrow (1) / 2-word (2) inline immediate.
2316 if !fits_i32(v) {
2317 3
2318 } else if narrow(v as i32) {
2319 1
2320 } else {
2321 2
2322 }
2323 }
2324 // Ops with a clean single-word lowering (validated by `encode_op` below).
2325 Op::Add { .. } | Op::Sub { .. } | Op::Mul { .. } | Op::Div { .. } | Op::Mod { .. }
2326 | Op::ExactDiv { .. } | Op::FloorDiv { .. } | Op::AddAssign { .. }
2327 // `Swap a and b` (RevSwap), the tolerant `is approximately` (ApproxEq — int `==` here, float via
2328 // `float_shape`), fresh set union/intersection (UnionOp/IntersectOp), swap-remove (RemoveFrom),
2329 // and struct-field push (ListPushField) all lower to one word.
2330 | Op::RevSwap { .. } | Op::ApproxEq { .. } | Op::UnionOp { .. } | Op::IntersectOp { .. }
2331 | Op::RemoveFrom { .. } | Op::ListPushField { .. }
2332 | Op::Lt { .. } | Op::Gt { .. } | Op::LtEq { .. } | Op::GtEq { .. } | Op::Eq { .. } | Op::NotEq { .. }
2333 | Op::BitXor { .. } | Op::BitAnd { .. } | Op::BitOr { .. } | Op::Shl { .. } | Op::Shr { .. } | Op::Not { .. }
2334 | Op::Move { .. } | Op::Jump { .. } | Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. }
2335 | Op::Return { .. } | Op::ReturnNothing | Op::Show { .. } | Op::Halt | Op::FailWith { .. }
2336 | Op::NewTuple { .. } | Op::NewEmptyList { .. } | Op::NewEmptyListI32 { .. }
2337 | Op::NewEmptyListWithCapacity { .. } | Op::NewEmptyListFixed { .. }
2338 | Op::NewEmptyMapWithCapacity { .. }
2339 | Op::NewEmptyMap { .. } | Op::NewEmptySet { .. } | Op::NewRange { .. }
2340 | Op::ListPush { .. } | Op::SetAdd { .. } | Op::Length { .. } | Op::Contains { .. } | Op::ListPop { .. }
2341 | Op::DeepClone { .. } | Op::SeqConcat { .. }
2342 | Op::Index { .. } | Op::IndexUnchecked { .. } | Op::SetIndex { .. } | Op::SetIndexUnchecked { .. }
2343 | Op::IterPrepare { .. } | Op::IterNext { .. } | Op::IterPop
2344 | Op::GlobalGet { .. } | Op::GlobalSet { .. }
2345 // Structs (74/75/76), inductives + match (77/78/79), CRDTs (80/81/84/82/83/113), non-capturing
2346 // closures (40/39), and deterministic concurrency (channels 92/93/94, Spawn 98) all lower to a
2347 // single self-contained chip word. `encode_op` validates each operand fits its field. (A field op
2348 // needs the struct oracle to resolve the slot; a closure/spawn resolves an imem entry — none add a word.)
2349 | Op::NewStruct { .. } | Op::StructInsert { .. } | Op::GetField { .. }
2350 | Op::NewInductive { .. } | Op::TestArm { .. } | Op::BindArm { .. }
2351 | Op::CrdtBump { .. } | Op::CrdtMerge { .. } | Op::CrdtJoin { .. }
2352 | Op::NewCrdt { .. } | Op::CrdtAppend { .. } | Op::CrdtResolve { .. }
2353 | Op::MakeClosure { .. } | Op::CallValue { .. }
2354 | Op::ChanNew { .. } | Op::ChanSend { .. } | Op::ChanRecv { .. } | Op::Spawn { .. }
2355 // The residual deterministic ops the model + RTL already decode, each a single self-contained chip
2356 // word (validated by `encode_op`): tuple destructure (71), the integer power unit (18), the non-
2357 // blocking channels (95/96/97), the handle-bound sync launch + task join/abort (99/100/101), the
2358 // `select` arms + wait (102/103/104), the deterministic scheduler `Sleep` no-op (88), and the offline
2359 // single-node networking no-ops / loopback inbox (105..=111 — Connect/Listen/Send/Stream/Await/
2360 // MakePeer/Sync). Each rides the ordinary integer register file and heap; none adds an imem word.
2361 | Op::DestructureTuple { .. } | Op::Pow { .. } | Op::Sleep { .. }
2362 | Op::ChanTrySend { .. } | Op::ChanTryRecv { .. } | Op::ChanClose { .. }
2363 | Op::SpawnHandle { .. } | Op::TaskAwait { .. } | Op::TaskAbort { .. }
2364 | Op::SelectArmRecv { .. } | Op::SelectArmTimeout { .. } | Op::SelectWait { .. }
2365 | Op::NetConnect { .. } | Op::NetListen { .. } | Op::NetSend { .. } | Op::NetStream { .. }
2366 | Op::NetAwait { .. } | Op::NetMakePeer { .. } | Op::NetSync { .. }
2367 | Op::EnsureOwned { .. } | Op::RegionBoundsGuard { .. } => 1,
2368 // A standalone `FormatValue` (the `Show "{x:.N}"` render): a single chip word — op 64 (integer
2369 // decimal) or op 225 (float fixed-precision decimal), decided by the operand's static type in
2370 // `encode_op`. A float operand needs a `.N` spec (the only float format the chip renders).
2371 Op::FormatValue { src, spec, debug_prefix, .. } => {
2372 if ft.is_float(i, *src) && float_format_precision(*spec, *debug_prefix, constants).is_none() {
2373 return Err(format!(
2374 "instruction {i}: float `FormatValue` needs a `.N` precision spec — a bare `{{f}}` / \
2375 currency / alignment on a float has no full-core chip lowering yet"
2376 ));
2377 }
2378 1
2379 }
2380 // The integer-plane numeric builtin coprocessor (abs/min/max/count_ones) is a single op-38 word. A
2381 // builtin with no integer-plane ordinal falls through to the reject (its float-plane / Word32 forms
2382 // were already caught by `float_shape`/`word_shape` above, which return a width before this match).
2383 Op::CallBuiltin { builtin, .. } if builtin_chip_ordinal(*builtin).is_some() => 1,
2384 // The deterministic string / fill / parse / encode builtins (`chr`=233 / TEXT `contains`=234 /
2385 // `repeatSeq`=235 / `parseInt`=236 / `text_bytes`=237) are a single chip word; the type-gate reject
2386 // inside `text_builtin_opcode` matches `encode_op` exactly.
2387 Op::CallBuiltin { builtin, args_start, .. } if text_builtin_opcode(i, *builtin, *args_start, ft)?.is_some() => 1,
2388 // `items i through j of xs` (SliceOp) is a single op-65 word IFF its bounds fold to small constant
2389 // nibbles; a non-foldable (runtime / large) bound has no chip form — reject to match `encode_op`.
2390 Op::SliceOp { .. } => {
2391 if slice_fold(i).is_some() {
2392 1
2393 } else {
2394 return Err(format!(
2395 "instruction {i}: `SliceOp` bounds are not provably small integer constants (1..=15) — the \
2396 chip's slice unit takes immediate nibble bounds, so a runtime or large slice bound has no \
2397 full-core chip lowering yet"
2398 ));
2399 }
2400 }
2401 other => {
2402 return Err(format!(
2403 "`{}` has no full-core chip lowering yet (outside the integer benchmark subset)",
2404 other.name()
2405 ))
2406 }
2407 })
2408}
2409
2410/// Emit one op's imem word(s). `entry_of` maps a callee's `FuncIdx` to its imem entry index, and
2411/// `imem_of` maps a code index (a Jump/branch/iter target) to its imem word index (both via the pass-1
2412/// pc→imem table). Word count MUST equal [`imem_width`] for the same op. A float-typed op (per the type
2413/// oracle `ft` at code index `i`) lowers to its `IntToF` conversions then the float word.
2414fn encode_op(
2415 i: usize,
2416 op: &Op,
2417 constants: &[Constant],
2418 ft: &FloatTyping,
2419 st: &StructTyping,
2420 entry_of: &dyn Fn(u16) -> Result<u32, String>,
2421 imem_of: &dyn Fn(usize) -> Result<u32, String>,
2422 pool_of: &dyn Fn(u32) -> Result<(u32, u32), String>,
2423 captures_nonempty: &dyn Fn(u16) -> bool,
2424 is_float_iter: &dyn Fn(usize) -> bool,
2425 slice_fold: &dyn Fn(usize) -> Option<(u8, u8)>,
2426 scratch: Reg,
2427) -> Result<Vec<u32>, String> {
2428 // ── FLOAT DATAPATH: the type oracle proved this op operates on f64. Emit each integer operand's
2429 // Int→Float conversion (fregs[r] = regs[r]) then the float instruction. Keeps the integer path below
2430 // byte-identical (a non-float op returns `None` here and falls through).
2431 if let Some(plan) = float_shape(i, op, ft, scratch)? {
2432 let mut words = Vec::with_capacity(plan.convs.len() + plan.body.len());
2433 for (d, s) in plan.convs {
2434 words.push(w(INT_TO_F, d, s, 0)); // IntToF fregs[d] <- regs[s] (d==s: convert in place)
2435 }
2436 words.extend(plan.body);
2437 return Ok(words);
2438 }
2439 // ── WORD32 (ℤ/2³²) DATAPATH: `word32`/`intOfWord32` → Move (identity), `word_and`/`word_or`/`word_not`
2440 // → the BitAnd/BitOr/Not ALU ops, `rotl`/`rotr`/`word32Shr` → the CallBuiltin coprocessor (opcode 38),
2441 // and a Word32 `Show` → `ShowWord32` (unsigned). A single self-contained word; the integer path below is
2442 // untouched when nothing is a Word32 op (a Word32 `+`/`Move`/`Push` already rides the integer datapath).
2443 if let Some(word) = word_shape(i, op, ft)? {
2444 return Ok(vec![word]);
2445 }
2446 // ── TEXT (string) DATAPATH: a Text constant → `TextConst` (pool copy); a Text `Add`/`AddAssign` → the
2447 // `format(x)` → its Show text (see [`FormatPlan`]).
2448 if let Some(plan) = format_plan(i, op, ft)? {
2449 let oc = |name: &str| crate::vm::binisa::opcode_of_name(name).expect("a registered opcode");
2450 return Ok(match plan {
2451 FormatPlan::Text { dst, src } => vec![w(oc("Move"), reg(dst)?, reg(src)?, 0)],
2452 FormatPlan::Int { dst, src } => vec![w(oc("FormatValue"), reg(dst)?, reg(src)?, 0)],
2453 FormatPlan::Bool { dst, src } => {
2454 let (t_off, t_len) = pool_of(POOL_TRUE)?;
2455 let (f_off, f_len) = pool_of(POOL_FALSE)?;
2456 vec![
2457 ((TEXTCONST as u32) << 24) | (reg(dst)? << 16) | (t_len & 0xffff),
2458 t_off,
2459 ((TEXTCONST as u32) << 24) | (reg(scratch)? << 16) | (f_len & 0xffff),
2460 f_off,
2461 w(oc("Sub"), reg(dst)?, reg(dst)?, reg(scratch)?), // true − false
2462 w(oc("Mul"), reg(dst)?, reg(dst)?, reg(src)?), // × the 0/1 value
2463 w(oc("Add"), reg(dst)?, reg(dst)?, reg(scratch)?), // + false
2464 ]
2465 }
2466 });
2467 }
2468 // A FAILURE → stream its reason over the Text port, then trap-halt `reasoned` (see [`FailPlan`]).
2469 if let Some(plan) = failure_plan(i, op, constants, ft)? {
2470 let oc = |name: &str| crate::vm::binisa::opcode_of_name(name).expect("a registered opcode");
2471 let trap = (u32::from(oc("FailWith")) << 24) | 1; // rb = 1: reasoned
2472 let show = |r: Reg| -> Result<u32, String> { Ok(w(SHOWTEXT, 0, reg(r)?, 0)) };
2473 let text_const = |dst: Reg, key: u32| -> Result<[u32; 2], String> {
2474 let (off, len) = pool_of(key)?;
2475 Ok([
2476 ((TEXTCONST as u32) << 24) | (reg(dst)? << 16) | (len & 0xffff),
2477 off,
2478 ])
2479 };
2480 return Ok(match plan {
2481 FailPlan::Text { src } => vec![show(src)?, trap],
2482 FailPlan::Int { src } => vec![
2483 w(oc("FormatValue"), reg(scratch)?, reg(src)?, 0),
2484 show(scratch)?,
2485 trap,
2486 ],
2487 FailPlan::Bool { src, dst } => {
2488 let t = text_const(scratch, POOL_TRUE)?;
2489 let f = text_const(dst, POOL_FALSE)?;
2490 vec![
2491 t[0],
2492 t[1],
2493 f[0],
2494 f[1],
2495 w(oc("Sub"), reg(scratch)?, reg(scratch)?, reg(dst)?), // true − false
2496 w(oc("Mul"), reg(scratch)?, reg(scratch)?, reg(src)?), // × the 0/1 value
2497 w(oc("Add"), reg(scratch)?, reg(scratch)?, reg(dst)?), // + false → the chosen text
2498 show(scratch)?,
2499 trap,
2500 ]
2501 }
2502 FailPlan::Pooled { key } => {
2503 let t = text_const(scratch, key)?;
2504 vec![t[0], t[1], show(scratch)?, trap]
2505 }
2506 });
2507 }
2508 // `Concat` datapath (FormatValue-ing a non-Text operand); a Text `Show` → `ShowText`. The Text plane
2509 // proved the shape; the integer path below is untouched when nothing is Text.
2510 if let Some(t) = text_shape(i, op, constants, ft)? {
2511 let concat_oc =
2512 crate::vm::binisa::opcode_of_name("Concat").expect("Concat is a registered opcode");
2513 let fmt_oc = crate::vm::binisa::opcode_of_name("FormatValue")
2514 .expect("FormatValue is a registered opcode");
2515 return Ok(match t {
2516 TextLower::Const { dst, idx } => {
2517 // TextConst: word0 = [216][dst][len:16], word1 = the pool word-offset (imem index of c0).
2518 let (off, len) = pool_of(idx)?;
2519 if len > 0xffff {
2520 return Err(format!(
2521 "instruction {i}: Text constant length {len} exceeds the 16-bit field"
2522 ));
2523 }
2524 vec![
2525 ((TEXTCONST as u32) << 24) | (reg(dst)? << 16) | (len & 0xffff),
2526 off,
2527 ]
2528 }
2529 TextLower::Concat { dst, lhs, rhs, fmt } => match fmt {
2530 // Text ++ Text — a bare Concat of the two char-lists.
2531 None => vec![w(concat_oc, reg(dst)?, reg(lhs)?, reg(rhs)?)],
2532 // A non-Text operand → FormatValue it into `scratch` (its decimal char-list), then Concat with
2533 // the scratch substituted for that operand. The scratch is a program-wide-unused register, dead
2534 // the instant the Concat consumes it, so this needs no liveness dance.
2535 Some(nt) => {
2536 let (a, b) = if nt == lhs {
2537 (scratch, rhs)
2538 } else {
2539 (lhs, scratch)
2540 };
2541 vec![
2542 w(fmt_oc, reg(scratch)?, reg(nt)?, 0),
2543 w(concat_oc, reg(dst)?, reg(a)?, reg(b)?),
2544 ]
2545 }
2546 },
2547 TextLower::Show { src } => vec![w(SHOWTEXT, 0, reg(src)?, 0)],
2548 TextLower::CharAt {
2549 dst,
2550 collection,
2551 index,
2552 } => {
2553 vec![w(TEXTCHARAT, reg(dst)?, reg(collection)?, reg(index)?)]
2554 }
2555 TextLower::Compare { dst, lhs, rhs, eq } => {
2556 vec![w(
2557 if eq { TEXTEQ } else { TEXTNE },
2558 reg(dst)?,
2559 reg(lhs)?,
2560 reg(rhs)?,
2561 )]
2562 }
2563 // A Text `contains` → the substring char-scanner (op 234): [234][dst][hay=ra][needle=rb].
2564 TextLower::Contains { dst, hay, needle } => {
2565 vec![w(TEXTCONTAINS, reg(dst)?, reg(hay)?, reg(needle)?)]
2566 }
2567 });
2568 }
2569 // A float LITERAL: 3-word FConst carrying the exact f64 bits (the constant kind is ground truth).
2570 if let Op::LoadConst { dst, idx } = op {
2571 if let Some(Constant::Float(f)) = constants.get(*idx as usize) {
2572 let bits = f.to_bits();
2573 return Ok(vec![
2574 w(FCONST, reg(*dst)?, 0, 0),
2575 (bits & 0xffff_ffff) as u32,
2576 (bits >> 32) as u32,
2577 ]);
2578 }
2579 }
2580 let oc = opcode(op);
2581 let three = |dst: Reg, a: Reg, b: Reg| -> Result<Vec<u32>, String> {
2582 Ok(vec![w(oc, reg(dst)?, reg(a)?, reg(b)?)])
2583 };
2584 Ok(match op {
2585 // ── arithmetic / comparison / bitwise — 3-register [op][dst][lhs][rhs] ──
2586 Op::Add { dst, lhs, rhs }
2587 | Op::Sub { dst, lhs, rhs }
2588 | Op::Mul { dst, lhs, rhs }
2589 | Op::Div { dst, lhs, rhs }
2590 | Op::Mod { dst, lhs, rhs }
2591 | Op::ExactDiv { dst, lhs, rhs }
2592 | Op::FloorDiv { dst, lhs, rhs }
2593 | Op::Lt { dst, lhs, rhs }
2594 | Op::Gt { dst, lhs, rhs }
2595 | Op::LtEq { dst, lhs, rhs }
2596 | Op::GtEq { dst, lhs, rhs }
2597 | Op::Eq { dst, lhs, rhs }
2598 | Op::NotEq { dst, lhs, rhs }
2599 | Op::BitXor { dst, lhs, rhs }
2600 | Op::BitAnd { dst, lhs, rhs }
2601 | Op::BitOr { dst, lhs, rhs }
2602 | Op::Shl { dst, lhs, rhs }
2603 | Op::Shr { dst, lhs, rhs } => three(*dst, *lhs, *rhs)?,
2604 Op::Not { dst, src } => three(*dst, *src, 0)?,
2605 // `is approximately` (ApproxEq, opcode 30) — the tolerant numeric compare. On INTEGER operands
2606 // (the value plane) `≈` collapses to exact `==` (any two distinct integers differ by ≥ 1, far
2607 // above the 1e-9 relative floor), which is what the ALU's op-30 arm computes (`a == b`), so a
2608 // pure-int ApproxEq rides the ordinary 3-register ALU word. A FLOAT ApproxEq is DECOMPOSED into the
2609 // primitive float ops by `float_shape` above (the isclose sequence), so it never reaches here.
2610 Op::ApproxEq { dst, lhs, rhs } => three(*dst, *lhs, *rhs)?,
2611 // `Swap a and b` (RevSwap, opcode 4) — the reversible register exchange. The model's op-4 arm
2612 // (`regs.swap(dst, ra)`) swaps the two locals in place; the encoder emits [4][a][b][0].
2613 Op::RevSwap { a, b } => vec![w(oc, reg(*a)?, reg(*b)?, 0)],
2614 // `Set x to x + y` — in-place add lowers onto the Add datapath (lhs = dst).
2615 Op::AddAssign { dst, src } => {
2616 let add = crate::vm::binisa::opcode_of_name("Add").expect("Add opcode");
2617 vec![w(add, reg(*dst)?, reg(*dst)?, reg(*src)?)]
2618 }
2619 Op::Move { dst, src } => three(*dst, *src, 0)?,
2620 // ── constants ──
2621 Op::LoadConst { dst, idx } => {
2622 let c = constants
2623 .get(*idx as usize)
2624 .ok_or_else(|| format!("LoadConst references missing constant #{idx}"))?;
2625 let v = const_i64(c)?;
2626 if !fits_i32(v) {
2627 // A constant wider than i32 (PRNG moduli 2³¹ / 2³², full-range i64 literals) is held in a
2628 // 3-word LoadConstWide64 (op 222) at EVERY width — low word then high word, mirroring FConst's
2629 // two-immediate layout. The op-222 decode retires the full i64 irrespective of datapath width,
2630 // and the value plane auto-promotes to BigInt on overflow, so width 32 reproduces the constant
2631 // EXACTLY (width is a fast-path/area knob, not a correctness switch — BigInt handles exactness).
2632 let bits = v as u64;
2633 vec![
2634 (u32::from(LOADCONSTWIDE64) << 24) | (reg(*dst)? << 16),
2635 (bits & 0xffff_ffff) as u32,
2636 (bits >> 32) as u32,
2637 ]
2638 } else {
2639 // An i32-fitting constant uses the inline forms EXACTLY: narrow (16-bit, opcode 5) or the
2640 // 2-word 32-bit immediate (opcode 200). No truncation happens here — the over-i32 case is
2641 // handled above by op 222 at every width, so the full core never wraps a constant.
2642 let vi = v as i32;
2643 if narrow(vi) {
2644 vec![(oc as u32) << 24 | (reg(*dst)? << 16) | ((vi as u32) & 0xffff)]
2645 } else {
2646 vec![(200u32 << 24) | (reg(*dst)? << 16), vi as u32]
2647 }
2648 }
2649 }
2650 // ── control flow — 16-bit imem targets SPLIT low-byte-first (scalar-compatible) / 32-bit Call entry ──
2651 // Jump: [op][t_lo:8][t_hi:8][0]; JumpIf: [op][t_lo:8][cond:8][t_hi:8].
2652 Op::Jump { target } => {
2653 let t = imem_of(*target)?;
2654 vec![((oc as u32) << 24) | ((t & 0xff) << 16) | (((t >> 8) & 0xff) << 8)]
2655 }
2656 Op::JumpIfFalse { cond, target } | Op::JumpIfTrue { cond, target } => {
2657 let t = imem_of(*target)?;
2658 vec![((oc as u32) << 24) | ((t & 0xff) << 16) | (reg(*cond)? << 8) | ((t >> 8) & 0xff)]
2659 }
2660 Op::Call {
2661 dst,
2662 func,
2663 args_start,
2664 arg_count,
2665 } => {
2666 if *arg_count > 255 {
2667 return Err(format!("Call arg_count {arg_count} exceeds 255"));
2668 }
2669 reg(args_start.saturating_add(arg_count.saturating_sub(1)))?; // the top arg register must fit too
2670 vec![
2671 ((oc as u32) << 24)
2672 | (reg(*dst)? << 16)
2673 | (reg(*args_start)? << 8)
2674 | (*arg_count as u32 & 0xff),
2675 entry_of(*func)?,
2676 ]
2677 }
2678 Op::Return { src } => vec![w(oc, 0, reg(*src)?, 0)],
2679 Op::ReturnNothing => vec![(oc as u32) << 24],
2680 Op::Show { src } => vec![w(oc, 0, reg(*src)?, 0)],
2681 Op::Halt => vec![(oc as u32) << 24],
2682 // `args()` — argv, program name first, on every tier. A chip has no host argv, so its argument
2683 // vector is the program name alone, `["logos"]` — exactly the VM's with no host argv
2684 // (`semantics::argv_with_program_name`). Built from ops the chip already runs: a fresh list, the
2685 // pooled Text `logos` into the scratch register, and one push. So `length of args()` is 1 on chip
2686 // and VM alike.
2687 Op::Args { dst } => {
2688 let new_empty =
2689 crate::vm::binisa::opcode_of_name("NewEmptyList").expect("NewEmptyList opcode");
2690 let push = crate::vm::binisa::opcode_of_name("ListPush").expect("ListPush opcode");
2691 let (off, len) = pool_of(ARGV0_POOL_KEY)?;
2692 vec![
2693 w(new_empty, reg(*dst)?, 0, 0),
2694 ((TEXTCONST as u32) << 24) | (reg(scratch)? << 16) | (len & 0xffff),
2695 off,
2696 w(push, reg(*dst)?, reg(scratch)?, 0),
2697 ]
2698 }
2699 // `FailWith` — the unrecoverable trap (op 91): the model halts (error strobe raised), matching the
2700 // VM's error. It sits at the exhaustive-`Inspect` fall-through (a non-`Otherwise` match emits it after
2701 // the arms), UNREACHABLE for a well-typed scrutinee — but it must lower so the arm chain assembles.
2702 Op::FailWith { .. } => vec![(oc as u32) << 24],
2703 // ── structured heap (lists / tuples / ranges / sets / maps) ──
2704 Op::NewList { dst, start, count } | Op::NewTuple { dst, start, count } => {
2705 if *count > 255 {
2706 return Err(format!("NewList/NewTuple count {count} exceeds 255"));
2707 }
2708 // A `Seq of Float` literal (`[1.5, 2.5, …]`) built by a plain `NewList` would lay its structure on
2709 // the INT heap and leave the parallel f64 payload plane (`fheap`) all-zero — so a later `item i of
2710 // xs` (FIndex) reads 0.0, a SILENT divergence from the VM. Instead build it through the float plane:
2711 // `NewEmptyList` then one `FListPush` per element (each writes the int structure AND `fheap[slot] =
2712 // fregs[value]`), exactly the sequence a float `Push` loop emits (which is already proven ≡ VM). The
2713 // element registers are proven float scalars (`is_float`), so their f64 values live in `fregs`.
2714 if (0..*count).any(|k| ft.is_float(i, start.saturating_add(k))) {
2715 if !(0..*count).all(|k| ft.is_float(i, start.saturating_add(k))) {
2716 return Err(format!(
2717 "instruction {i}: `NewList` with a MIX of float and non-float elements has no chip \
2718 lowering (the float-payload plane is uniform per seq)"
2719 ));
2720 }
2721 reg(start.saturating_add(count.saturating_sub(1)))?;
2722 let new_empty =
2723 crate::vm::binisa::opcode_of_name("NewEmptyList").expect("NewEmptyList opcode");
2724 let mut words = Vec::with_capacity(1 + *count as usize);
2725 words.push(w(new_empty, reg(*dst)?, 0, 0));
2726 for k in 0..*count {
2727 words.push(w(FLIST_PUSH, reg(*dst)?, reg(start.saturating_add(k))?, 0));
2728 }
2729 return Ok(words);
2730 }
2731 reg(start.saturating_add(count.saturating_sub(1)))?;
2732 vec![w(oc, reg(*dst)?, reg(*start)?, *count as u32 & 0xff)]
2733 }
2734 Op::NewEmptyList { dst } | Op::NewEmptyListI32 { dst } | Op::NewEmptySet { dst } => {
2735 vec![w(oc, reg(*dst)?, 0, 0)]
2736 }
2737 // The physical fabric reserves a compile-time-sized collection block. Keep the semantic
2738 // capacity operand in bytecode/traces, but lower allocation to the existing physical
2739 // constructor: runtime-sized reserve has no representable field in this instruction word.
2740 Op::NewEmptyListWithCapacity { dst, .. } => {
2741 let physical =
2742 crate::vm::binisa::opcode_of_name("NewEmptyList").expect("NewEmptyList opcode");
2743 vec![w(physical, reg(*dst)?, 0, 0)]
2744 }
2745 Op::NewEmptyListFixed { dst, .. } => {
2746 let physical =
2747 crate::vm::binisa::opcode_of_name("NewEmptyList").expect("NewEmptyList opcode");
2748 vec![w(physical, reg(*dst)?, 0, 0)]
2749 }
2750 // `a new Map of …` — an empty MAP_TAG-tagged heap block (opcode 52). Its Index/SetIndex lower to the
2751 // INT-KEY key-scan ops (223/224) when the key is a non-Text integer, or stay on the string-key ops
2752 // (57/59, `key_eq`) when the key is a Text handle — the `is_map`/`is_text` routing below.
2753 Op::NewEmptyMap { dst } => vec![w(oc, reg(*dst)?, 0, 0)],
2754 Op::NewEmptyMapWithCapacity { dst, .. } => {
2755 let physical =
2756 crate::vm::binisa::opcode_of_name("NewEmptyMap").expect("NewEmptyMap opcode");
2757 vec![w(physical, reg(*dst)?, 0, 0)]
2758 }
2759 Op::NewRange { dst, start, end } => three(*dst, *start, *end)?,
2760 Op::ListPush { list, value } => three(*list, *value, 0)?,
2761 Op::SetAdd { set, value } => three(*set, *value, 0)?,
2762 Op::ListPop { list, dst } => three(*dst, *list, 0)?,
2763 // `copy of xs` — an independent deep heap copy (opcode 66); the value-semantics fresh binding.
2764 Op::DeepClone { dst, src } => three(*dst, *src, 0)?,
2765 // `a followed by b` — a fresh sequence `lhs ++ rhs` (opcode 33); Text `+` uses Concat (32) via
2766 // `text_shape` above, so this arm is the Seq-of-Int/Seq concatenation path.
2767 Op::SeqConcat { dst, lhs, rhs } => three(*dst, *lhs, *rhs)?,
2768 // `a union b` / `a intersection b` — a FRESH set (opcodes 68/69): dedup'd union / membership
2769 // intersection over the two source blocks. Three-register, single-word (the model builds the
2770 // result block by scanning, byte-identical to the VM's `set_union`/`set_intersection`).
2771 Op::UnionOp { dst, lhs, rhs } | Op::IntersectOp { dst, lhs, rhs } => {
2772 three(*dst, *lhs, *rhs)?
2773 }
2774 // `Remove v from s` (opcode 56) — swap-out by value equality, shifting the tail down. In-place
2775 // mutation of the collection register; the model finds the first `== v` slot and compacts.
2776 Op::RemoveFrom { collection, value } => three(*collection, *value, 0)?,
2777 // `items i through j of xs` (SliceOp, op 65) — a fresh 1-based-inclusive subsequence. The chip's
2778 // slice unit (RTL `soft_core.v` op 65) takes the bounds as IMMEDIATE b-byte nibbles (`start=b[7:4]`,
2779 // `end=b[3:0]`), so both must be provably small integer constants (`1..=15`), recovered by
2780 // `slice_bound_folds`. A runtime / out-of-range bound has no chip form — rejected loudly (a genuine
2781 // hardware limit, honestly declined; the op still lowers for the common constant-bound case).
2782 Op::SliceOp {
2783 dst, collection, ..
2784 } => {
2785 let (s, e) = slice_fold(i).ok_or_else(|| {
2786 format!(
2787 "instruction {i}: `SliceOp` bounds are not provably small integer constants (1..=15) — the \
2788 chip's slice unit takes immediate nibble bounds, so a runtime or large slice bound has no \
2789 full-core chip lowering yet"
2790 )
2791 })?;
2792 vec![w(
2793 oc,
2794 reg(*dst)?,
2795 reg(*collection)?,
2796 (u32::from(s) << 4) | u32::from(e),
2797 )]
2798 }
2799 // `Push x to obj's field` (ListPushField, opcode 47) — append to a struct-field seq in place (COW
2800 // if the field's list is not on top of the heap, then repoint the field). word: [47][obj=dst][
2801 // src=ra][field=rb]. The field name resolves to its usage-derived positional slot via the struct
2802 // oracle, exactly as `StructInsert`/`GetField`.
2803 Op::ListPushField { obj, field, src } => {
2804 let (slot, _n) = resolve_field(i, constants, st, *obj, *field)?;
2805 if slot > 255 {
2806 return Err(format!(
2807 "instruction {i}: `ListPushField` field slot {slot} exceeds the 8-bit field"
2808 ));
2809 }
2810 vec![w(oc, reg(*obj)?, reg(*src)?, slot as u32)]
2811 }
2812 Op::Length { dst, collection } => three(*dst, *collection, 0)?,
2813 Op::Contains {
2814 dst,
2815 collection,
2816 value,
2817 } => three(*dst, *collection, *value)?,
2818 Op::Index {
2819 dst,
2820 collection,
2821 index,
2822 }
2823 | Op::IndexUnchecked {
2824 dst,
2825 collection,
2826 index,
2827 } => {
2828 // A map read (`item k of m`) key-scans: the INT-KEY op (223) when the key is a non-Text integer,
2829 // else the string-key op (59, via `three`, which `key_eq`-scans). A plain LIST index stays positional
2830 // (59). The `is_map`/`is_text` oracle at this code index picks the family — the run loop then reads
2831 // the key cell accordingly.
2832 if ft.is_map(i, *collection) && !ft.is_text(i, *index) {
2833 vec![w(MAPINDEXINT, reg(*dst)?, reg(*collection)?, reg(*index)?)]
2834 } else {
2835 three(*dst, *collection, *index)?
2836 }
2837 }
2838 Op::SetIndex {
2839 collection,
2840 index,
2841 value,
2842 }
2843 | Op::SetIndexUnchecked {
2844 collection,
2845 index,
2846 value,
2847 } => {
2848 // A map write (`Set item k of m to v`) key-scans: the INT-KEY op (224) for a non-Text integer key,
2849 // else the string-key op (57). A plain LIST set stays positional (57).
2850 if ft.is_map(i, *collection) && !ft.is_text(i, *index) {
2851 vec![w(
2852 MAPSETINDEXINT,
2853 reg(*collection)?,
2854 reg(*index)?,
2855 reg(*value)?,
2856 )]
2857 } else {
2858 three(*collection, *index, *value)?
2859 }
2860 }
2861 // ── snapshot iteration (Repeat / for-each) ──
2862 Op::IterPrepare { iterable } => vec![w(oc, 0, reg(*iterable)?, 0)],
2863 // IterNext: [op][dst:8][exit_lo:8][exit_hi:8]. A `Repeat for v in (Seq of Float)` iterates the float
2864 // payload plane — op 231 `FIterNext` reads `fheap[ptr+cursor]` into `fregs[dst]` (so the loop variable
2865 // is a float scalar); an ordinary iteration keeps op 86 reading the int structure into `regs[dst]`.
2866 Op::IterNext { dst, exit } => {
2867 let e = imem_of(*exit)?;
2868 let iter_oc = if is_float_iter(i) { FITER_NEXT } else { oc };
2869 vec![w(iter_oc, reg(*dst)?, e & 0xff, (e >> 8) & 0xff)]
2870 }
2871 Op::IterPop => vec![(oc as u32) << 24],
2872 // ── globals (Main bindings referenced inside functions) ──
2873 Op::GlobalGet { dst, idx } => {
2874 if *idx > 255 {
2875 return Err(format!("GlobalGet idx {idx} exceeds 255"));
2876 }
2877 vec![w(oc, reg(*dst)?, *idx as u32 & 0xff, 0)]
2878 }
2879 Op::GlobalSet { idx, src } => {
2880 if *idx > 255 {
2881 return Err(format!("GlobalSet idx {idx} exceeds 255"));
2882 }
2883 vec![w(oc, *idx as u32 & 0xff, reg(*src)?, 0)]
2884 }
2885 // ── value-transparent metadata — no-ops on the chip (as in run_soft_core / the VM) ──
2886 Op::EnsureOwned { .. } | Op::RegionBoundsGuard { .. } => vec![(oc as u32) << 24],
2887 // A standalone `FormatValue` — the `Show "{x:.N}"` render. An INTEGER operand → op 64 (the decimal
2888 // char-list of regs[src]); a FLOAT operand → op 225 `FormatValueFloat` (fregs[src] to `.N` places, the
2889 // precision in the b-byte), matching the VM's `apply_format_spec` exactly. Both build a Text char-list
2890 // a downstream `ShowText`/`Concat` consumes. `oc` is `FormatValue`'s registered opcode (== 64).
2891 Op::FormatValue {
2892 dst,
2893 src,
2894 spec,
2895 debug_prefix,
2896 } => {
2897 if ft.is_float(i, *src) {
2898 let prec = float_format_precision(*spec, *debug_prefix, constants).ok_or_else(|| {
2899 format!(
2900 "instruction {i}: float `FormatValue` needs a `.N` precision spec — a bare `{{f}}` / \
2901 currency / alignment on a float has no full-core chip lowering yet"
2902 )
2903 })?;
2904 vec![w(
2905 FORMATVALUE_FLOAT,
2906 reg(*dst)?,
2907 reg(*src)?,
2908 u32::from(prec),
2909 )]
2910 } else {
2911 // Integer FormatValue (op 64) — the decimal string of regs[src]. Any spec/debug on an INT is
2912 // dropped (the chip has only the bare-decimal integer render); no benchmark exercises that.
2913 vec![w(oc, reg(*dst)?, reg(*src)?, 0)]
2914 }
2915 }
2916 // ── STRUCTS (model ops 74/75/76) — a heap record `[nfields, f0, …]`, fields addressed POSITIONALLY.
2917 // The bytecode names a field by const; the struct oracle's usage-derived layout maps (struct type,
2918 // field name) → its slot. `NewStruct` needs the struct's field COUNT (the slots to allocate).
2919 Op::NewStruct { dst, type_name } => {
2920 // The struct type is the ground-truth `type_name` const — resolve it directly (the register
2921 // plane types the dst only on the op's EXIT, not at the NewStruct itself). `field_count` is the
2922 // usage-derived slot count for that type.
2923 let name = match constants.get(*type_name as usize) {
2924 Some(Constant::Text(s)) => s.as_str(),
2925 _ => {
2926 return Err(format!(
2927 "instruction {i}: `NewStruct` type name const #{type_name} is not Text"
2928 ))
2929 }
2930 };
2931 let ty = st.index_of_name(name).ok_or_else(|| {
2932 format!("instruction {i}: `NewStruct` of `{name}` — no usage-derived field layout for this struct type")
2933 })?;
2934 let nfields = st.field_count(ty);
2935 if nfields > 255 {
2936 return Err(format!(
2937 "instruction {i}: struct has {nfields} fields (exceeds the 8-bit count)"
2938 ));
2939 }
2940 // op 74: [74][dst][ra unused][num_fields=rb]
2941 vec![w(oc, reg(*dst)?, 0, nfields as u32)]
2942 }
2943 Op::StructInsert { obj, field, value } => {
2944 let (slot, _n) = resolve_field(i, constants, st, *obj, *field)?;
2945 if slot > 255 {
2946 return Err(format!(
2947 "instruction {i}: struct field slot {slot} exceeds the 8-bit field"
2948 ));
2949 }
2950 // op 75: [75][obj=dst][value=ra][field=rb]
2951 vec![w(oc, reg(*obj)?, reg(*value)?, slot as u32)]
2952 }
2953 Op::GetField { dst, obj, field } => {
2954 let (slot, _n) = resolve_field(i, constants, st, *obj, *field)?;
2955 if slot > 255 {
2956 return Err(format!(
2957 "instruction {i}: struct field slot {slot} exceeds the 8-bit field"
2958 ));
2959 }
2960 // op 76: [76][dst][obj=ra][field=rb]
2961 vec![w(oc, reg(*dst)?, reg(*obj)?, slot as u32)]
2962 }
2963 // ── INDUCTIVES + MATCH (model ops 77/78/79) — a heap record `[tag, payload0, …]`. The variant TAG is
2964 // the constructor-name const index (deduped, so `NewInductive`'s ctor and every `TestArm`'s variant on
2965 // that name agree — exactly the VM's name equality); `BindArm` extracts payload position `index`.
2966 Op::NewInductive {
2967 dst,
2968 ctor,
2969 args_start,
2970 count,
2971 ..
2972 } => {
2973 let tag = inductive_tag(i, *ctor)?;
2974 if *count > 15 {
2975 return Err(format!(
2976 "instruction {i}: `NewInductive` payload count {count} exceeds the 4-bit field"
2977 ));
2978 }
2979 let asr = reg(*args_start)?;
2980 if asr > 15 {
2981 return Err(format!(
2982 "instruction {i}: `NewInductive` args_start r{args_start} exceeds the 4-bit b[7:4] field"
2983 ));
2984 }
2985 reg(args_start.saturating_add(count.saturating_sub(1)))?; // the top payload register must fit too
2986 // op 77: [77][dst][ctor=a-byte][b: args_start(hi nibble) | count(lo nibble)]
2987 let b = (asr << 4) | (*count as u32 & 0xf);
2988 vec![w(oc, reg(*dst)?, tag, b)]
2989 }
2990 Op::TestArm {
2991 dst,
2992 target,
2993 variant,
2994 } => {
2995 let tag = inductive_tag(i, *variant)?;
2996 // op 78: [78][dst][target=ra][variant=b-byte]
2997 vec![w(oc, reg(*dst)?, reg(*target)?, tag)]
2998 }
2999 Op::BindArm {
3000 dst, target, index, ..
3001 } => {
3002 if *index > 255 {
3003 return Err(format!(
3004 "instruction {i}: `BindArm` index {index} exceeds the 8-bit field"
3005 ));
3006 }
3007 // op 79: [79][dst][target=ra][index=b-byte]
3008 vec![w(oc, reg(*dst)?, reg(*target)?, *index as u32 & 0xff)]
3009 }
3010 // ── CRDTs, single-replica projection (model ops 80/81/84/82/83/113). A `Counter`/`Tally` lowers to a
3011 // plain struct; `CrdtBump` accumulates a field (`+= amount`, negated for `Decrease`), `CrdtMerge`
3012 // sum-joins field-wise, `CrdtJoin` per-replica MAX-joins, `CrdtResolve` overwrites (LWW). The field
3013 // index is the usage-derived slot; it must fit the 4-bit `b[3:0]` (`CrdtBump`) / 8-bit (`CrdtResolve`).
3014 Op::CrdtBump {
3015 obj,
3016 field,
3017 amount,
3018 negate,
3019 } => {
3020 let (slot, _n) = resolve_field(i, constants, st, *obj, *field)?;
3021 if slot > 15 {
3022 return Err(format!(
3023 "instruction {i}: CRDT field slot {slot} exceeds the 4-bit b[3:0] field of `CrdtBump`"
3024 ));
3025 }
3026 // op 80: [80][obj=dst][amount=ra][b: negate(bit4) | field(bits 3:0)]
3027 let b = ((*negate as u32) << 4) | (slot as u32 & 0xf);
3028 vec![w(oc, reg(*obj)?, reg(*amount)?, b)]
3029 }
3030 // op 81: [81][target=dst][source=ra] — field-wise sum-join.
3031 Op::CrdtMerge { target, source } => vec![w(oc, reg(*target)?, reg(*source)?, 0)],
3032 // op 113: [113][target=dst][source=ra] — per-replica MAX join.
3033 Op::CrdtJoin { target, source } => vec![w(oc, reg(*target)?, reg(*source)?, 0)],
3034 // op 82: [82][dst] — a fresh empty CRDT collection (seq/set, single-replica projection = empty list).
3035 Op::NewCrdt { dst, .. } => vec![w(oc, reg(*dst)?, 0, 0)],
3036 // op 83: [83][seq=dst][value=ra] — RGA append (ordered list append, single-replica projection).
3037 Op::CrdtAppend { seq, value } => vec![w(oc, reg(*seq)?, reg(*value)?, 0)],
3038 Op::CrdtResolve { obj, field, value } => {
3039 let (slot, _n) = resolve_field(i, constants, st, *obj, *field)?;
3040 if slot > 255 {
3041 return Err(format!(
3042 "instruction {i}: CRDT resolve field slot {slot} exceeds the 8-bit field"
3043 ));
3044 }
3045 // op 84: [84][obj=dst][value=ra][field=rb] — register last-write = field overwrite (LWW).
3046 vec![w(oc, reg(*obj)?, reg(*value)?, slot as u32)]
3047 }
3048 // ── NON-CAPTURING CLOSURES (model ops 40/39). `MakeClosure` stores a closure value = `[entry]` on the
3049 // heap; `CallValue` does an indirect Call through it. Only the NON-capturing case lowers: a closure
3050 // that captures locals/globals needs a capture frame the single-word op 40 (`[entry]` only) cannot
3051 // carry, so a capturing closure is rejected loudly (its `captures` list is non-empty).
3052 Op::MakeClosure { dst, func, .. } => {
3053 if captures_nonempty(*func) {
3054 return Err(format!(
3055 "instruction {i}: capturing closure has no full-core chip lowering (the single-word \
3056 MakeClosure carries only the entry, not a capture frame)"
3057 ));
3058 }
3059 let entry = entry_of(*func)?;
3060 if entry > 255 {
3061 return Err(format!(
3062 "instruction {i}: closure entry imem index {entry} exceeds the 8-bit a-byte field of `MakeClosure`"
3063 ));
3064 }
3065 // op 40: [40][dst][func=entry a-byte][0] — non-capturing closure = [entry].
3066 vec![w(oc, reg(*dst)?, entry, 0)]
3067 }
3068 Op::CallValue {
3069 dst,
3070 callee,
3071 args_start,
3072 arg_count,
3073 ..
3074 } => {
3075 // op 39: [39][dst=result][callee=ra][b: args_start(hi nibble) | arg_count(lo nibble)]. The model
3076 // clones the caller frame, window-copies R[args_start..+arg_count] → R[0..] (so a compiled
3077 // closure body reads its param at its base r0), then runs the static body and returns.
3078 let asr = reg(*args_start)?;
3079 if asr > 15 {
3080 return Err(format!(
3081 "instruction {i}: `CallValue` args_start r{args_start} exceeds the 4-bit b[7:4] field"
3082 ));
3083 }
3084 if *arg_count > 15 {
3085 return Err(format!(
3086 "instruction {i}: `CallValue` arg_count {arg_count} exceeds the 4-bit b[3:0] field"
3087 ));
3088 }
3089 reg(args_start.saturating_add(arg_count.saturating_sub(1)))?; // top arg register must fit too
3090 let b = (asr << 4) | (*arg_count as u32 & 0xf);
3091 vec![w(oc, reg(*dst)?, reg(*callee)?, b)]
3092 }
3093 // ── DETERMINISTIC CONCURRENCY (model ops 92/93/94, Spawn 98). The single-node deterministic runtime
3094 // runs Launch SYNCHRONOUSLY and channels FIFO (the offline semantics proven for the WASM AOT backend);
3095 // the model mirrors it. `ChanNew` = a fresh ring channel; `ChanSend`/`ChanRecv` enqueue/dequeue FIFO.
3096 Op::ChanNew { dst, .. } => vec![w(oc, reg(*dst)?, 0, 0)],
3097 // op 93: [93][chan=dst][val=ra] — enqueue.
3098 Op::ChanSend { chan, val } => vec![w(oc, reg(*chan)?, reg(*val)?, 0)],
3099 // op 94: [94][dst][chan=ra] — dequeue (FIFO).
3100 Op::ChanRecv { dst, chan } => vec![w(oc, reg(*dst)?, reg(*chan)?, 0)],
3101 // op 98: [98][entry=dst byte][args_start=ra][arg_count=rb] — deterministic sync launch. The model
3102 // arg-copies R[args_start..]→R[0..] into a fresh frame (like Call) then runs the task to completion,
3103 // returning to Main via the launched body's `ReturnNothing` — so the channel side-effects persist.
3104 Op::Spawn {
3105 func,
3106 args_start,
3107 arg_count,
3108 } => {
3109 if *arg_count > 255 {
3110 return Err(format!(
3111 "instruction {i}: `Spawn` arg_count {arg_count} exceeds 255"
3112 ));
3113 }
3114 reg(args_start.saturating_add(arg_count.saturating_sub(1)))?;
3115 let entry = entry_of(*func)?;
3116 if entry > 255 {
3117 return Err(format!(
3118 "instruction {i}: `Spawn` entry imem index {entry} exceeds the 8-bit entry field"
3119 ));
3120 }
3121 vec![w(oc, entry, reg(*args_start)?, *arg_count as u32 & 0xff)]
3122 }
3123 // ── INTEGER POWER (model + RTL op 18). `a ** b` for a non-negative integer exponent: the chip's
3124 // sequential-multiply power unit (a stall-based FSM in `soft_core.v`, mirroring the shared divider)
3125 // computes `acc = 1; repeat b times: acc *= a`. The model retires it directly (BigInt-exact via the
3126 // auto-promoting value-integer multiply) so it is byte-identical to the VM's `int_power`. op 18:
3127 // [18][dst][lhs=a][rhs=b].
3128 Op::Pow { dst, lhs, rhs } => vec![w(oc, reg(*dst)?, reg(*lhs)?, reg(*rhs)?)],
3129 // ── INTEGER-PLANE NUMERIC BUILTIN COPROCESSOR (op 38). The Word32 rotate/shift builtins and the
3130 // FLOAT-plane numeric builtins were already handled by `word_shape`/`float_shape` above; what reaches
3131 // here is a numeric builtin on the INTEGER value plane — `abs`/`min`/`max`/`count_ones` (ordinals
3132 // 5/7/8/9 from `builtin_chip_ordinal`). op 38: [38][dst][args_start=ra][ordinal=rb]; the model reads
3133 // `regs[args_start]` (and `regs[args_start+1]` for the 2-arg min/max) and computes the value-plane
3134 // result BigInt-exactly, byte-identical to `call_builtin` (Abs's i64::MIN→2^63 promotion, Min/Max's
3135 // `regval_cmp`, CountOnes's `(n as u64).count_ones()`). A builtin with no integer-plane ordinal (host
3136 // parse/format/temporal/uuid/…) falls through to the loud reject — it has no deterministic chip form.
3137 Op::CallBuiltin {
3138 dst,
3139 builtin,
3140 args_start,
3141 arg_count,
3142 } => {
3143 // The top argument register must fit the file (2-arg builtins read args_start+1).
3144 reg(args_start.saturating_add(arg_count.saturating_sub(1)))?;
3145 // FIRST the deterministic string / fill builtins (`chr`=233 / TEXT `contains`=234 / `repeatSeq`
3146 // =235 / `parseInt`=236 / `text_bytes`=237), each a fresh on-heap value or scanned scalar from
3147 // register operands — [op][dst][args_start][args_start+1] (`parseInt` and `text_bytes` read only
3148 // `args_start`, the text handle). The type gate inside `text_builtin_opcode` rejects an
3149 // unsupported operand shape loudly (a non-Text `contains`, a float/reference `repeatSeq` element,
3150 // a non-Text `parseInt` / `text_bytes`) rather than mis-lowering it.
3151 if let Some(t_oc) = text_builtin_opcode(i, *builtin, *args_start, ft)? {
3152 vec![w(
3153 t_oc,
3154 reg(*dst)?,
3155 reg(*args_start)?,
3156 reg(args_start.saturating_add(1))?,
3157 )]
3158 } else {
3159 // ELSE the integer-plane numeric coprocessor (op 38): abs/min/max/count_ones by ordinal. A
3160 // builtin with no chip form at all (host parse/format/temporal/uuid/…) hits the loud reject.
3161 let ord = builtin_chip_ordinal(*builtin).ok_or_else(|| {
3162 format!(
3163 "instruction {i}: builtin `{builtin:?}` has no full-core chip lowering yet — only \
3164 abs/min/max/count_ones (integer plane), floor/ceil/round/abs/min/max (float plane), \
3165 sqrt, chr/contains/repeatSeq/parseInt/text_bytes, and the Word32 vocabulary lower; \
3166 `encode_op` rejects the rest loudly"
3167 )
3168 })?;
3169 vec![w(oc, reg(*dst)?, reg(*args_start)?, u32::from(ord))]
3170 }
3171 }
3172 // ── TUPLE DESTRUCTURE (model + RTL op 71). `Repeat for (k, v) in …` spreads a heap tuple's payload
3173 // into `count` consecutive registers. op 71: [71][start=dst][src=ra][count=rb]; the model scatters
3174 // `heap[src+1+i]` → `regs[start+i]` for `i in 0..count`.
3175 Op::DestructureTuple { src, start, count } => {
3176 if *count > 255 {
3177 return Err(format!(
3178 "instruction {i}: `DestructureTuple` count {count} exceeds the 8-bit field"
3179 ));
3180 }
3181 reg(start.saturating_add(count.saturating_sub(1)))?; // the top spread register must fit too
3182 vec![w(oc, reg(*start)?, reg(*src)?, *count as u32 & 0xff)]
3183 }
3184 // ── DETERMINISTIC SCHEDULER (model + RTL). On the single-node deterministic runtime a `Sleep` is a
3185 // pure no-op (op 88 advances the pc), and the non-blocking channel ops ride the SAME FIFO ring the
3186 // blocking `ChanSend`/`ChanRecv` (93/94) use — `ChanTrySend` (95) reports success in `dst`,
3187 // `ChanTryRecv` (96) delivers the value or `0`, `ChanClose` (97) sets a flag no receive path reads.
3188 Op::Sleep { duration } => vec![w(oc, 0, reg(*duration)?, 0)],
3189 // op 95: [95][success=dst][chan=ra][val=rb].
3190 Op::ChanTrySend { dst, chan, val } => vec![w(oc, reg(*dst)?, reg(*chan)?, reg(*val)?)],
3191 // op 96: [96][value=dst][chan=ra].
3192 Op::ChanTryRecv { dst, chan } => vec![w(oc, reg(*dst)?, reg(*chan)?, 0)],
3193 // op 97: [97][.][chan=ra] — output-transparent close.
3194 Op::ChanClose { chan } => vec![w(oc, 0, reg(*chan)?, 0)],
3195 // ── HANDLE-BOUND SYNC LAUNCH + JOIN (model + RTL). `Let h be Launch a task to f with …` runs `f`
3196 // SYNCHRONOUSLY (deterministic offline Launch) and binds a task handle. op 99: [99][handle=dst][
3197 // entry=a-byte][.] — the model saves the caller frame, sets the handle placeholder, and jumps to
3198 // the callee (sharing the caller's registers, so the task reads the channel handle the caller placed
3199 // at its base register). `TaskAwait` (100) is the instant join and `TaskAbort` (101, `Stop h`) a
3200 // no-op — the deterministic child already ran to completion at the launch point.
3201 Op::SpawnHandle { dst, func, .. } => {
3202 let entry = entry_of(*func)?;
3203 if entry > 255 {
3204 return Err(format!(
3205 "instruction {i}: `SpawnHandle` entry imem index {entry} exceeds the 8-bit a-byte field"
3206 ));
3207 }
3208 vec![w(oc, reg(*dst)?, entry, 0)]
3209 }
3210 // op 100: [100][dst][handle=ra] — the sync child already completed, so the join passes through.
3211 Op::TaskAwait { dst, handle } => vec![w(oc, reg(*dst)?, reg(*handle)?, 0)],
3212 // op 101: [101][.][handle=ra] — abort a task that already ran to completion (a no-op).
3213 Op::TaskAbort { handle } => vec![w(oc, 0, reg(*handle)?, 0)],
3214 // ── SELECT / WAIT-ON-MULTIPLE (model + RTL). `Await the first of:` registers each arm, then waits.
3215 // op 102: [102][chan=dst][var=ra] — a recv arm; op 103: [103][.][ticks=ra] — a timeout arm (the tick
3216 // quantity is never observed: the timeout fires only when NO recv arm is ready); op 104: [104][
3217 // dst_arm] — fire the first ready recv (dequeue FIFO) else the timeout arm, writing the arm index.
3218 Op::SelectArmRecv { chan, var } => vec![w(oc, reg(*chan)?, reg(*var)?, 0)],
3219 Op::SelectArmTimeout { ticks } => vec![w(oc, 0, reg(*ticks)?, 0)],
3220 Op::SelectWait { dst_arm } => vec![w(oc, reg(*dst_arm)?, 0, 0)],
3221 // ── OFFLINE SINGLE-NODE NETWORKING (model + RTL). On the deterministic single-node chip there is no
3222 // peer or wire, so — exactly like the VM's documented offline model (tw == VM-net == AOT) — `Connect`
3223 // (105), `Listen` (106), `Stream` (108) and `Sync` (111) are no-ops, `NetMakePeer` (110) mints a
3224 // fresh loopback peer key, `NetSend` (107) publishes `(peer key, message)` to the loopback inbox, and
3225 // `NetAwait` (109) drains the inbox entry keyed by that peer — a local round-trip to itself.
3226 Op::NetConnect { url } => vec![w(oc, 0, reg(*url)?, 0)],
3227 Op::NetListen { topic } => vec![w(oc, 0, reg(*topic)?, 0)],
3228 // op 107: [107][.][to=ra][msg=rb] — publish to the loopback inbox.
3229 Op::NetSend { to, msg } => vec![w(oc, 0, reg(*to)?, reg(*msg)?)],
3230 // op 108: [108][.][to=ra][values=rb] — offline stream no-op.
3231 Op::NetStream { to, values } => vec![w(oc, 0, reg(*to)?, reg(*values)?)],
3232 // op 109: [109][dst][from=ra] — drain the loopback inbox entry keyed by the peer (a `stream` await
3233 // drains the same inbox, so the flag needs no distinct lowering on the single node).
3234 Op::NetAwait { dst, from, .. } => vec![w(oc, reg(*dst)?, reg(*from)?, 0)],
3235 // op 110: [110][dst][addr=ra] — a fresh offline peer key (the loopback identity).
3236 Op::NetMakePeer { dst, addr } => vec![w(oc, reg(*dst)?, reg(*addr)?, 0)],
3237 // op 111: [111][dst][topic=ra] — offline sync no-op (no peer to merge; the variable is unchanged).
3238 Op::NetSync { dst, topic } => vec![w(oc, reg(*dst)?, reg(*topic)?, 0)],
3239 other => {
3240 return Err(format!(
3241 "`{}` has no full-core chip lowering yet (outside the integer benchmark subset)",
3242 other.name()
3243 ))
3244 }
3245 })
3246}
3247
3248// ── Value-semantics isolation (P1.6) ────────────────────────────────────────────────────────────────
3249// LOGOS collections are value-typed; the VM preserves this with lazy Rc copy-on-write, but the chip's flat
3250// heap has no refcount, so an in-place mutator (`ListPush`/`SetIndex`/…) on a list that is SHARED with
3251// another binding would corrupt the alias (nqueens: `Let mutable next be queens; Push col to next`).
3252// A forward dataflow tracks, per register, whether it PROVABLY holds a uniquely-owned ("Fresh") collection;
3253// any mutator whose target is not provably Fresh gets an explicit `DeepClone` isolation emitted before it.
3254// This is always sound (a mutator target is always a collection, so cloning it can never corrupt a scalar),
3255// keeps list-building loops O(n) (a `new Seq` is Fresh → in-place push, no clone), and clones only true
3256// aliases. Function params seed as shared (arg-copy aliases the caller's handle).
3257
3258#[derive(Clone, Copy, PartialEq, Eq)]
3259enum Origin {
3260 Bottom, // unreached
3261 Fresh, // provably a uniquely-owned collection (safe to mutate in place)
3262 NotFresh, // possibly aliased / scalar / unknown
3263}
3264
3265/// What an op does to the origin lattice.
3266enum Act {
3267 MakeFresh(u16), // allocates a fresh collection into dst
3268 Alias(u16, u16), // dst := src (both handles now shared)
3269 Mutate(u16), // in-place mutation of a collection register (isolate if not Fresh)
3270 Write(u16), // writes dst with a scalar / possibly-shared value → NotFresh
3271 Ctrl, // no register write we track
3272}
3273
3274fn classify(op: &Op) -> Act {
3275 use Op::*;
3276 match op {
3277 NewList { dst, .. } | NewTuple { dst, .. } | NewEmptyList { dst } | NewEmptyListI32 { dst }
3278 | NewEmptyListWithCapacity { dst, .. } | NewEmptyListFixed { dst, .. }
3279 | NewEmptySet { dst } | NewEmptyMap { dst }
3280 | NewEmptyMapWithCapacity { dst, .. } | NewRange { dst, .. } | DeepClone { dst, .. }
3281 | SliceOp { dst, .. } | Concat { dst, .. } | SeqConcat { dst, .. } | UnionOp { dst, .. }
3282 | IntersectOp { dst, .. } | FormatValue { dst, .. } | Args { dst }
3283 // A freshly built struct / inductive / CRDT record is uniquely owned — so its OWN construction
3284 // inserts (`StructInsert`/`CrdtBump`) mutate in place with no spurious clone, and a `Let q be p`
3285 // aliases it (below), forcing an isolating clone only when an ALIAS is then mutated.
3286 | NewStruct { dst, .. } | NewInductive { dst, .. } | NewCrdt { dst, .. } => Act::MakeFresh(*dst),
3287 Move { dst, src } => Act::Alias(*dst, *src),
3288 ListPush { list, .. } => Act::Mutate(*list),
3289 SetAdd { set, .. } => Act::Mutate(*set),
3290 SetIndex { collection, .. } | SetIndexUnchecked { collection, .. } => Act::Mutate(*collection),
3291 RemoveFrom { collection, .. } => Act::Mutate(*collection),
3292 ListPop { list, .. } => Act::Mutate(*list),
3293 StructInsert { obj, .. } => Act::Mutate(*obj),
3294 // `Push x to obj's field` mutates the FIELD's LIST, not the struct record itself — and the model's
3295 // op-47 already COPY-ON-WRITEs that field list internally (repointing the field when it is shared),
3296 // so the correct value semantics hold WITHOUT isolating the struct. Isolating the struct here would
3297 // be WRONG: it would DeepClone the (compiler-Move-aliased) record and repoint the CLONE's field, so
3298 // the push would never reach the real `b` (a length-0 read). It is therefore control-transparent to
3299 // the struct-freshness lattice — the field-list COW is the whole isolation this op needs.
3300 ListPushField { .. } => Act::Ctrl,
3301 // Struct/CRDT field mutators: an in-place field write on a (possibly aliased) record.
3302 CrdtBump { obj, .. } | CrdtResolve { obj, .. } => Act::Mutate(*obj),
3303 CrdtMerge { target, .. } | CrdtJoin { target, .. } => Act::Mutate(*target),
3304 CrdtAppend { seq, .. } => Act::Mutate(*seq),
3305 // dst-writing ops whose result may be a shared nested collection (or a scalar) → NotFresh.
3306 Add { dst, .. } | Sub { dst, .. } | Mul { dst, .. } | Div { dst, .. } | Mod { dst, .. }
3307 | ExactDiv { dst, .. } | FloorDiv { dst, .. } | AddAssign { dst, .. }
3308 | Lt { dst, .. } | Gt { dst, .. } | LtEq { dst, .. } | GtEq { dst, .. } | Eq { dst, .. } | NotEq { dst, .. }
3309 | BitXor { dst, .. } | BitAnd { dst, .. } | BitOr { dst, .. } | Shl { dst, .. } | Shr { dst, .. } | Not { dst, .. }
3310 | LoadConst { dst, .. } | Length { dst, .. } | Contains { dst, .. }
3311 | Index { dst, .. } | IndexUnchecked { dst, .. } | IterNext { dst, .. } | GlobalGet { dst, .. } => Act::Write(*dst),
3312 _ => Act::Ctrl,
3313 }
3314}
3315
3316/// Code-index successors in the linear bytecode (Main then function bodies). A `Call` returns to `i+1`;
3317/// the callee body is reached from its own seeded entry, so it is a separate dataflow root.
3318fn successors(i: usize, op: &Op, n: usize, imem_target: impl Fn(usize) -> usize) -> Vec<usize> {
3319 let next = if i + 1 < n { vec![i + 1] } else { vec![] };
3320 match op {
3321 Op::Jump { target } => vec![imem_target(*target)],
3322 Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
3323 let mut s = vec![imem_target(*target)];
3324 s.extend(next);
3325 s
3326 }
3327 Op::IterNext { exit, .. } => {
3328 let mut s = vec![imem_target(*exit)];
3329 s.extend(next);
3330 s
3331 }
3332 Op::Return { .. } | Op::ReturnNothing | Op::Halt | Op::FailWith { .. } => vec![],
3333 _ => next,
3334 }
3335}
3336
3337/// The set of mutator code indices whose target is NOT provably uniquely-owned, each mapped to the
3338/// register that must be `DeepClone`-isolated before the mutation. Fixpoint forward dataflow.
3339fn isolation_targets(program: &CompiledProgram) -> std::collections::HashMap<usize, u16> {
3340 let n = program.code.len();
3341 const R: usize = 256;
3342 // Targets already live in CODE-index space (Jump targets, entry_pc). Successors use them directly.
3343 let tgt = |ci: usize| ci;
3344
3345 let mut entry: Vec<[Origin; R]> = vec![[Origin::Bottom; R]; n];
3346 let mut work: std::collections::VecDeque<usize> = Default::default();
3347 let seed = |entry: &mut Vec<[Origin; R]>,
3348 work: &mut std::collections::VecDeque<usize>,
3349 pc: usize,
3350 params: u16| {
3351 if pc >= entry.len() {
3352 return;
3353 }
3354 let mut changed = false;
3355 for r in 0..(params as usize).min(R) {
3356 if entry[pc][r] != Origin::NotFresh {
3357 entry[pc][r] = Origin::NotFresh; // a param aliases the caller's handle
3358 changed = true;
3359 }
3360 }
3361 if entry[pc].iter().all(|&o| o == Origin::Bottom) || changed || params == 0 {
3362 work.push_back(pc);
3363 }
3364 };
3365 seed(&mut entry, &mut work, 0, 0); // Main
3366 for f in &program.functions {
3367 seed(&mut entry, &mut work, f.entry_pc, f.param_count);
3368 }
3369
3370 let merge = |a: Origin, b: Origin| match (a, b) {
3371 (Origin::Bottom, x) | (x, Origin::Bottom) => x,
3372 (Origin::Fresh, Origin::Fresh) => Origin::Fresh,
3373 _ => Origin::NotFresh,
3374 };
3375 while let Some(i) = work.pop_front() {
3376 let mut out = entry[i];
3377 match classify(&program.code[i]) {
3378 Act::MakeFresh(d) => out[d as usize & 255] = Origin::Fresh,
3379 Act::Alias(d, s) => {
3380 out[d as usize & 255] = Origin::NotFresh;
3381 out[s as usize & 255] = Origin::NotFresh;
3382 }
3383 Act::Mutate(t) => out[t as usize & 255] = Origin::Fresh, // unique after this op (isolated or already unique)
3384 Act::Write(d) => out[d as usize & 255] = Origin::NotFresh,
3385 Act::Ctrl => {}
3386 }
3387 for s in successors(i, &program.code[i], n, tgt) {
3388 if s >= n {
3389 continue;
3390 }
3391 let mut changed = false;
3392 for r in 0..R {
3393 let m = merge(entry[s][r], out[r]);
3394 if m != entry[s][r] {
3395 entry[s][r] = m;
3396 changed = true;
3397 }
3398 }
3399 if changed {
3400 work.push_back(s);
3401 }
3402 }
3403 }
3404
3405 // Final decision: a mutator whose target is not provably Fresh on entry needs isolation.
3406 let mut iso = std::collections::HashMap::new();
3407 for (i, op) in program.code.iter().enumerate() {
3408 if let Act::Mutate(t) = classify(op) {
3409 if entry[i][t as usize & 255] != Origin::Fresh {
3410 iso.insert(i, t);
3411 }
3412 }
3413 }
3414 iso
3415}
3416
3417/// The provably-constant integer value a register holds ENTERING code index `use_idx`, recovered by a
3418/// backward STRAIGHT-LINE scan: walk `use_idx-1 .. 0` for the first op that WRITES `r`. If it is a
3419/// `LoadConst { dst: r }` of an integer AND no code index strictly between that def and the use is a branch
3420/// TARGET (so no other value can flow in on a merged path), the constant is proven; a `Move`-through follows
3421/// the source register. Any other writer, a hit on a branch target, or a non-constant source → `None`
3422/// (conservatively unknown — the caller then rejects). Sound: it only trusts a value on a single-entry run.
3423fn const_reg_value(
3424 code: &[Op],
3425 constants: &[Constant],
3426 is_target: &dyn Fn(usize) -> bool,
3427 use_idx: usize,
3428 r: Reg,
3429 depth: u8,
3430) -> Option<i64> {
3431 if depth > 8 {
3432 return None; // bounded Move-chain following
3433 }
3434 let mut j = use_idx;
3435 while j > 0 {
3436 j -= 1;
3437 // A branch target between the def and the use means a different value could reach here — unknown.
3438 if is_target(j + 1) && (j + 1) < use_idx {
3439 return None;
3440 }
3441 if writes_reg(&code[j], r) {
3442 return match &code[j] {
3443 Op::LoadConst { dst, idx } if *dst == r => match constants.get(*idx as usize) {
3444 Some(Constant::Int(v)) => Some(*v),
3445 Some(Constant::Bool(b)) => Some(i64::from(*b)),
3446 _ => None,
3447 },
3448 Op::Move { dst, src } if *dst == r => {
3449 const_reg_value(code, constants, is_target, j, *src, depth + 1)
3450 }
3451 _ => None, // some other op defines `r` — not a known constant
3452 };
3453 }
3454 }
3455 None
3456}
3457
3458/// Does `op` WRITE register `r` (a conservative over-approximation — a windowed writer like
3459/// `DestructureTuple` counts if `r` is in its written range)? Used by the straight-line const scan to stop at
3460/// the nearest definition of a register.
3461fn writes_reg(op: &Op, r: Reg) -> bool {
3462 op_registers_written(op).contains(&r)
3463}
3464
3465/// The register(s) an op writes (its destinations). Conservative — used only by the SliceOp const-fold's
3466/// backward scan, so an over-broad set only makes the fold DECLINE (reject), never mis-fold.
3467fn op_registers_written(op: &Op) -> Vec<Reg> {
3468 use Op::*;
3469 match op {
3470 LoadConst { dst, .. } | Move { dst, .. } | Not { dst, .. } => vec![*dst],
3471 Add { dst, .. }
3472 | Sub { dst, .. }
3473 | Mul { dst, .. }
3474 | Div { dst, .. }
3475 | Mod { dst, .. }
3476 | ExactDiv { dst, .. }
3477 | FloorDiv { dst, .. }
3478 | Pow { dst, .. }
3479 | Lt { dst, .. }
3480 | Gt { dst, .. }
3481 | LtEq { dst, .. }
3482 | GtEq { dst, .. }
3483 | Eq { dst, .. }
3484 | NotEq { dst, .. }
3485 | ApproxEq { dst, .. }
3486 | BitXor { dst, .. }
3487 | BitAnd { dst, .. }
3488 | BitOr { dst, .. }
3489 | Shl { dst, .. }
3490 | Shr { dst, .. }
3491 | Index { dst, .. }
3492 | IndexUnchecked { dst, .. }
3493 | Length { dst, .. }
3494 | Contains { dst, .. }
3495 | GetField { dst, .. }
3496 | GlobalGet { dst, .. }
3497 | SliceOp { dst, .. }
3498 | UnionOp { dst, .. }
3499 | IntersectOp { dst, .. }
3500 | CallBuiltin { dst, .. } => vec![*dst],
3501 AddAssign { dst, .. } => vec![*dst],
3502 DestructureTuple { start, count, .. } => {
3503 (0..*count).map(|k| start.saturating_add(k)).collect()
3504 }
3505 Call { dst, .. } | CallValue { dst, .. } => vec![*dst],
3506 RevSwap { a, b } => vec![*a, *b],
3507 // Everything else either writes a collection register we don't const-track or writes nothing an
3508 // integer bound would read — return empty so the scan continues past it (never a mis-fold).
3509 _ => Vec::new(),
3510 }
3511}
3512
3513/// The set of code indices that are BRANCH TARGETS (Jump/branch destinations, IterNext exit targets, and
3514/// function entry points) — a code index a value can reach on a merged path. The SliceOp const-fold uses
3515/// this to refuse a fold across a control-flow merge (soundness). Targets are in CODE-index space.
3516fn branch_targets(program: &CompiledProgram) -> std::collections::HashSet<usize> {
3517 let mut t = std::collections::HashSet::new();
3518 for op in &program.code {
3519 match op {
3520 Op::Jump { target }
3521 | Op::JumpIfFalse { target, .. }
3522 | Op::JumpIfTrue { target, .. } => {
3523 t.insert(*target as usize);
3524 }
3525 Op::IterNext { exit, .. } => {
3526 t.insert(*exit as usize);
3527 }
3528 _ => {}
3529 }
3530 }
3531 for f in &program.functions {
3532 t.insert(f.entry_pc);
3533 }
3534 t
3535}
3536
3537/// For each `SliceOp` code index, the folded 1-based-inclusive bounds `(start, end)` IFF both bounds are
3538/// provably small integer constants (`1..=15`, fitting the chip word's b-byte nibbles) — the immediate form
3539/// the RTL slice unit (`soft_core.v` op 65) decodes. A runtime / out-of-nibble-range bound is ABSENT, so the
3540/// encoder rejects it loudly (a genuine chip limitation, honestly declined — the op still lowers for the
3541/// common `items 2 through 5 of xs` constant-bound case, exactly like `FormatValue`'s `.N`-only float form).
3542fn slice_bound_folds(program: &CompiledProgram) -> std::collections::HashMap<usize, (u8, u8)> {
3543 let targets = branch_targets(program);
3544 let is_target = |ci: usize| targets.contains(&ci);
3545 let mut folds = std::collections::HashMap::new();
3546 for (i, op) in program.code.iter().enumerate() {
3547 if let Op::SliceOp { start, end, .. } = op {
3548 let s = const_reg_value(&program.code, &program.constants, &is_target, i, *start, 0);
3549 let e = const_reg_value(&program.code, &program.constants, &is_target, i, *end, 0);
3550 if let (Some(s), Some(e)) = (s, e) {
3551 if (1..=15).contains(&s) && (1..=15).contains(&e) {
3552 folds.insert(i, (s as u8, e as u8));
3553 }
3554 }
3555 }
3556 }
3557 folds
3558}
3559
3560/// Assemble a whole compiled program (from [`crate::vm::Compiler::compile_for_chip`]) into full soft-core
3561/// imem words. TWO-PASS: pass 1 lays out Main then every function body, computing each code index's imem
3562/// word offset (a `Call` and a wide `LoadConst` occupy two words); pass 2 emits the words, resolving every
3563/// Jump/branch/iter target and callee entry through that pc→imem map. `Err` if the program uses an op with
3564/// no chip lowering (floats, Text, closures, CRDTs, channels, …) or overflows an operand field.
3565///
3566/// The full-core image (`width` = 32). Because the value datapath auto-promotes to BigInt on overflow and
3567/// over-i32 constants are carried in full by op 222 at every width, the emitted words are IDENTICAL at 32 and
3568/// 64 — the width no longer changes the encoding. Pass an explicit width to [`encode_soft_core_full_w`].
3569pub fn encode_soft_core_full(program: &CompiledProgram) -> Result<Vec<u32>, String> {
3570 encode_soft_core_full_w(program, 32)
3571}
3572
3573/// The width-parametric form of [`encode_soft_core_full`]. `width` is retained as an API/experimental knob
3574/// describing the runtime datapath's fast-path size, but it NO LONGER changes the emitted image: every
3575/// over-i32 constant is a 3-word `LoadConstWide64` (op 222) carrying the full `i64` at every width, and the
3576/// value plane auto-promotes to BigInt on overflow exactly like the VM's unbounded `Int`. So the full core
3577/// reproduces the VM EXACTLY — every constant and every arithmetic result, into the BigInt tail — and does so
3578/// identically at width 32 and 64 (BigInt handles exactness; the width is a perf/area knob, not a correctness
3579/// switch). Runs on [`crate::codegen_sva::soft_core_model::run_soft_core_w`] at the matching width. (The
3580/// distinct 32-bit tape-out core in `scalar_asm.rs` genuinely wraps at 2³² — that is the small-area chip.)
3581pub fn encode_soft_core_full_w(program: &CompiledProgram, width: u32) -> Result<Vec<u32>, String> {
3582 // Value-semantics isolation set: mutator code indices → the collection register to DeepClone-isolate
3583 // first (one extra word each). See `isolation_targets`.
3584 let iso = isolation_targets(program);
3585 let deep_clone =
3586 crate::vm::binisa::opcode_of_name("DeepClone").expect("DeepClone is a registered opcode");
3587
3588 // The STRUCT-TYPE ORACLE: which registers hold which struct type at each code index, plus the
3589 // usage-derived positional field layout — drives the (field name → slot) lowering of every struct /
3590 // CRDT field op (`NewStruct`/`StructInsert`/`GetField`/`CrdtBump`/`CrdtResolve`).
3591 let st = StructTyping::analyze(program);
3592
3593 // The TYPE ORACLE: which registers hold f64 / a `Seq of Float` handle / a `Text` handle at each code
3594 // index — drives the Int-vs-Float-vs-Text lowering of every arithmetic/compare/Show/const op (the integer
3595 // path is untouched when nothing is float or text). A field read takes the field's declared type, which
3596 // the struct oracle names.
3597 let ft = FloatTyping::analyze(program, &st);
3598
3599 // The SLICE-BOUND FOLDS: for each `SliceOp`, its 1-based-inclusive bounds IFF both are provably small
3600 // integer constants (1..=15, the chip's immediate nibble form). A runtime/large bound is absent → the
3601 // encoder rejects it (a genuine hardware limit). Computed ONCE so pass 1 (`imem_width`) and pass 2
3602 // (`encode_op`) agree on which slices lower and their word count.
3603 let folds = slice_bound_folds(program);
3604 let slice_fold = |ci: usize| folds.get(&ci).copied();
3605
3606 // A non-capturing-closure predicate for `MakeClosure`: only a closure whose `captures` list is EMPTY
3607 // lowers (the single-word op 40 carries only the entry, no capture frame).
3608 let captures_nonempty = |func: u16| -> bool {
3609 program
3610 .functions
3611 .get(func as usize)
3612 .map(|f| !f.captures.is_empty())
3613 .unwrap_or(false)
3614 };
3615
3616 // ── STRING-CONSTANT POOL — the char data of every DISTINCT Text constant, appended AFTER the code as one
3617 // shared block per string (compact: an N-char string is N imem words, not ~2N NewEmptyList+push ops).
3618 // A `TextConst` copies its block into a fresh heap char-list. Distinct strings are deduped by content
3619 // (two `""`/two `" "` constants share one block). The pool base (imem index of the first block) is the
3620 // code word count `total`, resolved after pass 1 sizes the code. `pool_index[const_idx]` maps a Text
3621 // constant to its distinct-block ordinal; `pool_blocks[ordinal]` is that block's char codepoints.
3622 let mut pool_blocks: Vec<Vec<i32>> = Vec::new();
3623 let mut pool_by_content: std::collections::HashMap<&str, usize> =
3624 std::collections::HashMap::new();
3625 let mut pool_index: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
3626 for (ci, c) in program.constants.iter().enumerate() {
3627 if let Constant::Text(s) = c {
3628 let ord = *pool_by_content.entry(s.as_str()).or_insert_with(|| {
3629 let ord = pool_blocks.len();
3630 // The char-list stores i32 CODE POINTS (one per `char`), matching the model + the VM's char
3631 // granularity for `item i of` / `length` (ASCII: codepoint == byte, char count == byte length).
3632 pool_blocks.push(s.chars().map(|ch| ch as i32).collect());
3633 ord
3634 });
3635 pool_index.insert(ci as u32, ord);
3636 }
3637 }
3638 // A failure may need Text no constant spells: the Bool renderings and the refusals.
3639 if program.code.iter().any(|op| {
3640 matches!(
3641 op,
3642 Op::CallBuiltin {
3643 builtin: crate::semantics::builtins::BuiltinId::Fail
3644 | crate::semantics::builtins::BuiltinId::FailReason
3645 | crate::semantics::builtins::BuiltinId::Format,
3646 ..
3647 }
3648 )
3649 }) {
3650 for &(key, text) in FAILURE_POOL {
3651 let ord = *pool_by_content.entry(text).or_insert_with(|| {
3652 pool_blocks.push(text.chars().map(|ch| ch as i32).collect());
3653 pool_blocks.len() - 1
3654 });
3655 pool_index.insert(key, ord);
3656 }
3657 }
3658 // `args()` needs the program name `logos` in the pool even when no Text constant spells it.
3659 if program.code.iter().any(|op| matches!(op, Op::Args { .. })) {
3660 let ord = *pool_by_content
3661 .entry(crate::semantics::PROGRAM_NAME)
3662 .or_insert_with(|| {
3663 pool_blocks.push(
3664 crate::semantics::PROGRAM_NAME
3665 .chars()
3666 .map(|ch| ch as i32)
3667 .collect(),
3668 );
3669 pool_blocks.len() - 1
3670 });
3671 pool_index.insert(ARGV0_POOL_KEY, ord);
3672 }
3673
3674 // A SCRATCH register for the FormatValue-then-Concat lowering of a `Text + Int` interpolation: a
3675 // program-wide-UNUSED register (max index any op touches + 1), so writing it can never clobber a live
3676 // value. It is dead the instant the Concat consumes it, so no liveness tracking is needed. Only computed
3677 // (and required to exist) when a Text interpolation is actually emitted.
3678 let scratch: Reg = {
3679 let mut hi = 0u16;
3680 for op in &program.code {
3681 for r in op_registers(op) {
3682 hi = hi.max(r);
3683 }
3684 }
3685 for f in &program.functions {
3686 hi = hi.max((f.register_count.min(256).saturating_sub(1)) as u16);
3687 }
3688 hi
3689 };
3690 let scratch = scratch
3691 .checked_add(1)
3692 .filter(|&s| s <= 255)
3693 .ok_or_else(|| {
3694 "no free scratch register for a Text interpolation (the program uses all 256 registers)"
3695 .to_string()
3696 })?;
3697
3698 // Pass 1 — pc (code index) → imem word index.
3699 let mut pc_to_imem: Vec<usize> = Vec::with_capacity(program.code.len());
3700 let mut cursor = 0usize;
3701 for (i, op) in program.code.iter().enumerate() {
3702 pc_to_imem.push(cursor);
3703 cursor += imem_width(i, op, &program.constants, &ft, &slice_fold, scratch)
3704 .map_err(|e| format!("instruction {i}: {e}"))?
3705 + usize::from(iso.contains_key(&i));
3706 }
3707 let total = cursor; // the pool base: string blocks are laid out starting here, after the code.
3708
3709 // Function entry (FuncIdx → imem word index) and branch-target (code index → imem word index) resolvers.
3710 let entry_of = |func: u16| -> Result<u32, String> {
3711 let f = program
3712 .functions
3713 .get(func as usize)
3714 .ok_or_else(|| format!("Call references missing function #{func}"))?;
3715 let imem = *pc_to_imem
3716 .get(f.entry_pc)
3717 .ok_or_else(|| format!("function #{func} entry_pc {} out of range", f.entry_pc))?;
3718 Ok(imem as u32)
3719 };
3720 let imem_of = |code_idx: usize| -> Result<u32, String> {
3721 let imem = *pc_to_imem
3722 .get(code_idx)
3723 .ok_or_else(|| format!("branch target code index {code_idx} out of range"))?;
3724 if imem > 0xffff {
3725 return Err(format!(
3726 "branch target imem index {imem} exceeds the 16-bit target field"
3727 ));
3728 }
3729 Ok(imem as u32)
3730 };
3731 // Each distinct string block's (imem word offset of its first char, char length), laid out consecutively
3732 // after the code. Resolves a Text constant (`TextConst`'s pool operand) to where its chars live in imem.
3733 let mut block_offset: Vec<u32> = Vec::with_capacity(pool_blocks.len());
3734 {
3735 let mut at = total;
3736 for b in &pool_blocks {
3737 block_offset.push(at as u32);
3738 at += b.len();
3739 }
3740 }
3741 let pool_of = |const_idx: u32| -> Result<(u32, u32), String> {
3742 let ord = *pool_index
3743 .get(&const_idx)
3744 .ok_or_else(|| format!("Text constant #{const_idx} missing from the string pool"))?;
3745 let off = block_offset[ord];
3746 let len = pool_blocks[ord].len() as u32;
3747 Ok((off, len))
3748 };
3749
3750 // Pass 2 — emit the code.
3751 let mut imem: Vec<u32> =
3752 Vec::with_capacity(total + pool_blocks.iter().map(|b| b.len()).sum::<usize>());
3753 for (i, op) in program.code.iter().enumerate() {
3754 // Value-semantics barrier: isolate an aliased collection into a uniquely-owned copy before the
3755 // in-place mutation, so the chip's flat heap preserves LOGOS's value semantics (the VM's Rc-COW).
3756 if let Some(&target) = iso.get(&i) {
3757 imem.push(w(deep_clone, reg(target)?, reg(target)?, 0)); // DeepClone(target, target) — isolate in place
3758 }
3759 let is_float_iter = |ci: usize| is_float_iternext(ci, program, &ft);
3760 let words = encode_op(
3761 i,
3762 op,
3763 &program.constants,
3764 &ft,
3765 &st,
3766 &entry_of,
3767 &imem_of,
3768 &pool_of,
3769 &captures_nonempty,
3770 &is_float_iter,
3771 &slice_fold,
3772 scratch,
3773 )
3774 .map_err(|e| format!("instruction {i}: {e}"))?;
3775 debug_assert_eq!(
3776 words.len() + usize::from(iso.contains_key(&i)),
3777 imem_width(i, op, &program.constants, &ft, &slice_fold, scratch).unwrap()
3778 + usize::from(iso.contains_key(&i)),
3779 "imem_width disagrees with encode_op for {}",
3780 op.name()
3781 );
3782 imem.extend(words);
3783 }
3784 debug_assert_eq!(
3785 imem.len(),
3786 total,
3787 "pass-2 code size disagrees with pass-1 layout"
3788 );
3789 // Append the string-constant pool AFTER the code — one word per char codepoint, blocks in the same order
3790 // `block_offset` recorded. `TextConst` copies from `imem[offset..offset+len]`.
3791 for b in &pool_blocks {
3792 for &ch in b {
3793 imem.push(ch as u32);
3794 }
3795 }
3796 Ok(imem)
3797}
3798
3799/// The register indices an op reads or writes — a conservative upper set used only to pick a program-wide
3800/// unused SCRATCH register for the Text-interpolation FormatValue temp. Covers the operand + windowed forms
3801/// (a `Call`/`NewList` touches its whole `[start .. start+count)` window).
3802fn op_registers(op: &Op) -> Vec<u16> {
3803 use Op::*;
3804 let mut v = Vec::new();
3805 let mut push = |r: u16| v.push(r);
3806 match op {
3807 Add { dst, lhs, rhs }
3808 | Sub { dst, lhs, rhs }
3809 | Mul { dst, lhs, rhs }
3810 | Div { dst, lhs, rhs }
3811 | Mod { dst, lhs, rhs }
3812 | ExactDiv { dst, lhs, rhs }
3813 | FloorDiv { dst, lhs, rhs }
3814 | Lt { dst, lhs, rhs }
3815 | Gt { dst, lhs, rhs }
3816 | LtEq { dst, lhs, rhs }
3817 | GtEq { dst, lhs, rhs }
3818 | Eq { dst, lhs, rhs }
3819 | NotEq { dst, lhs, rhs }
3820 | ApproxEq { dst, lhs, rhs }
3821 | BitXor { dst, lhs, rhs }
3822 | BitAnd { dst, lhs, rhs }
3823 | BitOr { dst, lhs, rhs }
3824 | Shl { dst, lhs, rhs }
3825 | Shr { dst, lhs, rhs } => {
3826 push(*dst);
3827 push(*lhs);
3828 push(*rhs);
3829 }
3830 Index {
3831 dst,
3832 collection,
3833 index,
3834 }
3835 | IndexUnchecked {
3836 dst,
3837 collection,
3838 index,
3839 } => {
3840 push(*dst);
3841 push(*collection);
3842 push(*index);
3843 }
3844 SetIndex {
3845 collection,
3846 index,
3847 value,
3848 }
3849 | SetIndexUnchecked {
3850 collection,
3851 index,
3852 value,
3853 } => {
3854 push(*collection);
3855 push(*index);
3856 push(*value);
3857 }
3858 NewRange { dst, start, end } => {
3859 push(*dst);
3860 push(*start);
3861 push(*end);
3862 }
3863 Not { dst, src }
3864 | Move { dst, src }
3865 | Length {
3866 dst,
3867 collection: src,
3868 }
3869 | ListPop { list: src, dst }
3870 | AddAssign { dst, src } => {
3871 push(*dst);
3872 push(*src);
3873 }
3874 Show { src } => push(*src),
3875 Contains {
3876 dst,
3877 collection,
3878 value,
3879 } => {
3880 push(*dst);
3881 push(*collection);
3882 push(*value);
3883 }
3884 SeqConcat { dst, lhs, rhs } => {
3885 push(*dst);
3886 push(*lhs);
3887 push(*rhs);
3888 }
3889 DeepClone { dst, src } => {
3890 push(*dst);
3891 push(*src);
3892 }
3893 ListPush { list, value } | SetAdd { set: list, value } => {
3894 push(*list);
3895 push(*value);
3896 }
3897 LoadConst { dst, .. }
3898 | NewEmptyList { dst }
3899 | NewEmptyListFixed { dst, .. }
3900 | NewEmptyListI32 { dst }
3901 | NewEmptySet { dst }
3902 | NewEmptyMap { dst }
3903 | GlobalGet { dst, .. }
3904 | IterNext { dst, .. } => push(*dst),
3905 NewEmptyListWithCapacity { dst, capacity } | NewEmptyMapWithCapacity { dst, capacity } => {
3906 push(*dst);
3907 push(*capacity);
3908 }
3909 GlobalSet { src, .. } | IterPrepare { iterable: src } | Return { src } => push(*src),
3910 Call {
3911 dst,
3912 args_start,
3913 arg_count,
3914 ..
3915 } => {
3916 push(*dst);
3917 for k in 0..*arg_count {
3918 push(args_start.saturating_add(k));
3919 }
3920 }
3921 NewList { dst, start, count } | NewTuple { dst, start, count } => {
3922 push(*dst);
3923 for k in 0..*count {
3924 push(start.saturating_add(k));
3925 }
3926 }
3927 CallBuiltin {
3928 dst,
3929 args_start,
3930 arg_count,
3931 ..
3932 } => {
3933 push(*dst);
3934 for k in 0..*arg_count {
3935 push(args_start.saturating_add(k));
3936 }
3937 }
3938 JumpIfFalse { cond, .. } | JumpIfTrue { cond, .. } => push(*cond),
3939 _ => {}
3940 }
3941 v
3942}
3943
3944/// Render imem words as a `.hex` file body (one 8-hex-digit word per line).
3945pub fn to_hex(words: &[u32]) -> String {
3946 let mut s = String::with_capacity(words.len() * 9);
3947 for x in words {
3948 s.push_str(&format!("{x:08x}\n"));
3949 }
3950 s
3951}
3952
3953/// Compile a `.lg` source all the way to full soft-core imem words, through the chip compile path
3954/// (`compile_for_chip` — plain Div/Mod, no magic reciprocals). `Err` if it doesn't parse/compile or
3955/// uses anything outside the chip's integer subset. Targets the 32-bit datapath (the tape-out core); for
3956/// a 64-bit datapath (VM parity on 64-bit-range arithmetic) use [`compile_to_full_words_w`].
3957pub fn compile_to_full_words(source: &str) -> Result<Vec<u32>, String> {
3958 compile_to_full_words_w(source, 32)
3959}
3960
3961/// The WIDTH-PARAMETRIC form of [`compile_to_full_words`]: compile `.lg` source to full soft-core imem
3962/// words for a datapath of `width` bits (`32` = the tape-out core, byte-identical to the historical output;
3963/// `64` = full-i64 VM parity — a constant that exceeds i32, like the benchmark PRNGs' `% 2147483648` /
3964/// `% 4294967296` moduli, is emitted as a 3-word `LoadConstWide64` instead of being truncated). Pair with
3965/// [`crate::codegen_sva::soft_core_model::run_soft_core_w`] at the SAME width.
3966pub fn compile_to_full_words_w(source: &str, width: u32) -> Result<Vec<u32>, String> {
3967 if let Some(refusal) = crate::compile::exact_refusal(source, "soft core", "the soft core") {
3968 return Err(refusal);
3969 }
3970 // PRUNE the auto-imported stdlib prelude's DEAD functions before the chip encode: `apply_prelude` splices
3971 // a WHOLE module when the program mentions ANY of its names, but the chip encoder — like the flat C
3972 // namespace — must LOWER every op in the image, so an UNCALLED prelude helper that reaches a host builtin
3973 // (`parseFloat`, `format`, …) would make the whole program reject even though it never runs. `prune_
3974 // unreachable_prelude` drops exactly those dead PRELUDE helpers (never a user function, so LOGOS's
3975 // non-`name(` call forms like `Launch a task to worker` are always kept), so the encoder sees only the ops
3976 // the program executes — output-identical to the VM (the pruned functions were dead). `## NoPrelude` stops
3977 // `parse_seam` re-splicing the full module.
3978 let spliced = crate::loader::apply_prelude(source);
3979 let pruned = crate::loader::prune_unreachable_prelude_from(source, spliced.as_ref());
3980 let shaken = format!("## NoPrelude\n{pruned}");
3981 crate::ui_bridge::with_parsed_program(&shaken, |parsed, interner| {
3982 let (stmts, types, _policies) = parsed.map_err(|advice| format!("{advice}"))?;
3983 let oracle = crate::optimize::oracle_analyze_with(stmts, interner);
3984 let program =
3985 crate::vm::Compiler::compile_for_chip(stmts, interner, Some(types), Some(oracle))?;
3986 crate::compile::refuse_unlinkable(&program, interner, crate::compile::Backend::SoftCore)?;
3987 encode_soft_core_full_w(&program, width)
3988 })
3989}