Skip to main content

logicaffeine_compile/semantics/
builtins.rs

1//! Built-in functions over already-evaluated values.
2//!
3//! `show` is NOT here — output is an engine concern. Arity is checked by the
4//! caller BEFORE evaluating arguments (via [`check_arity`]) to preserve the
5//! tree-walker's error ordering: a wrong-arity call reports the arity error
6//! even when an argument expression would itself error.
7
8use std::cell::RefCell;
9use std::rc::Rc;
10
11use serde::{Deserialize, Serialize};
12
13use crate::interpreter::{ListRepr, RuntimeValue};
14use logicaffeine_base::{Decimal, LanesVal, Word16, Word32, Word64, WordVal};
15
16/// Read a `Seq of Int` as raw bytes (each element masked to 0–255) — the byte-array convention the
17/// UUID/hash builtins share with `uuid.lg` so the version constructors can be written in LOGOS.
18fn byte_seq(v: &RuntimeValue) -> Result<Vec<u8>, String> {
19    match v {
20        RuntimeValue::List(l) => {
21            let l = l.borrow();
22            let mut out = Vec::with_capacity(l.len());
23            for i in 0..l.len() {
24                match l.get(i) {
25                    Some(RuntimeValue::Int(n)) => out.push((n & 0xff) as u8),
26                    _ => return Err(format!("expected a Seq of Int (bytes); element {} is not an Int", i + 1)),
27                }
28            }
29            Ok(out)
30        }
31        _ => Err(format!("expected a Seq of Int (bytes), got {}", v.type_name())),
32    }
33}
34
35/// Build a `Seq of Int` from raw bytes (the packed-`i64` list repr).
36fn bytes_to_seq(bytes: &[u8]) -> RuntimeValue {
37    RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(
38        bytes.iter().map(|&b| b as i64).collect(),
39    ))))
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub enum BuiltinId {
44    Length,
45    Format,
46    ParseInt,
47    ParseFloat,
48    Chr,
49    Abs,
50    Sqrt,
51    Min,
52    Max,
53    Floor,
54    Ceil,
55    Round,
56    Pow,
57    /// `decimal(text)` / `decimal(int)` — construct an exact base-10 fixed-point (money)
58    /// from its literal text (`decimal("19.99")`) or an integer. The non-breaking entry
59    /// into the `Decimal` tower (it does NOT change how `19.99` literals parse).
60    Decimal,
61    /// `complex(re, im)` — construct an exact complex number from two exact reals.
62    /// `complex(0, 1)` is the imaginary unit `i`; `complex(0,1) * complex(0,1) = −1`.
63    Complex,
64    /// `modular(value, modulus)` — construct an element of ℤ/nℤ (the value reduced mod n).
65    Modular,
66    /// `quantity(value, "unit")` — construct a dimensioned physical quantity (`quantity(2, "inch")`).
67    /// The magnitude rides the exact rational tower; the unit name resolves through the catalog.
68    Quantity,
69    /// `money(amount, "USD")` — construct an exact monetary amount, quantised to the currency's
70    /// minor unit. The amount rides the Decimal tower (never float-drifts); the code resolves through
71    /// the ISO-4217 catalog.
72    Money,
73    /// `set_rate("EUR", 1.10)` — install/replace one exchange rate (vs the reference) in the ambient
74    /// rate context that `<money> in <currency>` reads. A side effect; returns nothing.
75    SetRate,
76    /// `to_currency(money, "EUR")` — convert money to another currency via the ambient rate context
77    /// (exact). Errors if no rates are in scope or the currency is unknown. Surface: `<money> in EUR`.
78    ToCurrency,
79    /// `set_rates(map)` — bulk-install a whole exchange-rate table from a `Map of Text to <number>`
80    /// (code → rate vs the reference) into the ambient rate context. The bridge a literal,
81    /// network-synced, or fetched rate table feeds. A side effect; returns nothing.
82    SetRates,
83    /// `uuid("550e8400-…")` — parse a UUID from text (canonical/simple/braced/urn); error on a bad id.
84    Uuid,
85    /// `uuid_nil()` / `uuid_max()` — the two special ids.
86    UuidNil,
87    UuidMax,
88    /// `uuid_version(u)` — the version nibble (0 nil, 1–8, 15 max).
89    UuidVersion,
90    /// The well-known namespace ids: `uuid_dns()`, `uuid_url()`, `uuid_oid()`, `uuid_x500()`.
91    UuidDns,
92    UuidUrl,
93    UuidOid,
94    UuidX500,
95    /// Byte-level primitives that let the UUID version constructors be written IN LOGOS (uuid.lg):
96    /// `text_bytes` is a Text's UTF-8 bytes; `uuid_bytes`/`uuid_from_bytes` convert a Uuid to/from its
97    /// 16 bytes. (`md5`/`sha1`/`uuid_v3`/`uuid_v5` are Logos stdlib functions now, not builtins.)
98    TextBytes,
99    /// `text_from_bytes(seq)` — the exact inverse of `text_bytes`: rebuild a Text from its UTF-8
100    /// `Seq of Int`. The wire medium for the Core-IR codec (`Seq of Int`) carries Text this way.
101    TextFromBytes,
102    /// `wireBytes(value)` — the value's plain wire form (peer codec `encode_value_raw`) as a
103    /// `Seq of Int`. Lets the host serialize a program AST for a compile-once native PE, byte-
104    /// identical to what the native binary's generated `wire_decode` reads.
105    WireBytes,
106    /// `readWireProgram()` — read ONE length-framed `CProgram` from stdin (`u32` LE length, then
107    /// that many wire bytes, decoded via the generated `wire_decode`) and return it; on EOF the
108    /// process exits cleanly (the resident-server loop terminator). AOT-only (returns the generated
109    /// `CProgram`); the interpreter variant rebuilds a `RuntimeValue` for parity.
110    ReadWireProgram,
111    /// `writeWireResidual(text)` — write `text` back as a length-framed residual (`u32` LE length,
112    /// then the UTF-8 bytes) to stdout and flush, returning the byte count. The response half of the
113    /// resident PE server's request/response protocol.
114    WriteWireResidual,
115    UuidBytes,
116    UuidFromBytes,
117    /// `convert(quantity, "unit")` — re-express a quantity in another unit of the SAME dimension
118    /// (`convert(q, "foot")`); a different dimension is a clean error (the forbidden cast).
119    Convert,
120    /// `parse_timestamp("2024-03-10T07:30:00Z")` — parse an RFC 3339 / ISO 8601 timestamp into a
121    /// `Moment` (nanoseconds since the epoch). The wire-first "the timestamp is its own type" entry.
122    ParseTimestamp,
123    /// `format_timestamp(moment)` — render a `Moment` as an RFC 3339 / ISO 8601 UTC string.
124    FormatTimestamp,
125    /// Calendar component extractors on a `Moment` (UTC), each returning an `Int`:
126    /// `year_of` / `month_of` (1–12) / `day_of` (1–31) / `weekday_of` (0 = Sunday … 6 = Saturday).
127    YearOf,
128    MonthOf,
129    DayOf,
130    WeekdayOf,
131    HourOf,
132    MinuteOf,
133    SecondOf,
134    /// `week_of(moment)` — the ISO-8601 week number (1..=53) of the Moment/Date (UTC).
135    WeekOf,
136    /// `quarter_of(moment)` — the calendar quarter (1..=4) of the Moment/Date (UTC).
137    QuarterOf,
138    /// `date_of(moment)` — the calendar day (a `Date`) the Moment falls on (UTC); identity on a Date.
139    DateOf,
140    /// `time_of(moment)` — the wall-clock time-of-day (a `Time`) of the Moment (UTC).
141    TimeOf,
142    /// `local_instant(moment, "zone")` — the local-as-UTC instant in a named zone (the lowering
143    /// target for `the <comp> of <m> in "<zone>"`, so every extractor reads the LOCAL component).
144    LocalInstant,
145    /// `seconds_between(a, b)` — whole seconds from Moment `a` to Moment `b` (signed `Int`).
146    SecondsBetween,
147    /// `months_between(a, b)` / `years_between(a, b)` — complete calendar months/years (signed `Int`).
148    MonthsBetween,
149    YearsBetween,
150    /// `add_seconds(moment, n)` — the Moment `n` seconds after `moment`.
151    AddSeconds,
152    /// `in_zone(moment, "America/New_York")` — the local wall-clock time (with offset) of a Moment
153    /// in a named IANA zone, as Text. The natural surface is `<moment> in "<zone>"`.
154    InZone,
155    Copy,
156    CountOnes,
157    RunAccepted,
158    /// `word32(n)` / `word64(n)` — construct a fixed-width wrapping integer from an `Int`.
159    Word32,
160    Word64,
161    /// `rotl(w, n)` / `rotr(w, n)` — rotate a word left/right by `n` bits.
162    Rotl,
163    Rotr,
164    /// `word_and(a, b)` / `word_or(a, b)` / `word_not(a)` — bitwise ops on the Word ring, CONSISTENT
165    /// across every tier (the `and`/`or` keywords are logical short-circuit on the VM, so word crypto
166    /// written in Logos — MD5/SHA-1 round functions — uses these instead).
167    Wand,
168    Wor,
169    Wnot,
170    /// The SHA-1 SHA-NI lane vocabulary: `lanes4Word32(s)`/`seqOfLanes4W32(v)` pack/unpack a
171    /// `Lanes4Word32` (128-bit), and `sha1rnds4`/`sha1msg1`/`sha1msg2`/`sha1nexte` are the four SHA-1
172    /// operations — the primitives a SHA-1 written IN LOGOS is built from (AOT → hardware `sha1rnds4`).
173    Lanes4Word32Make,
174    /// Pack four `Word32`s straight into a lane register — the alloc-free constructor (no `Seq`).
175    Lanes4Of,
176    SeqOfLanes4W32,
177    Sha1Rnds4,
178    Sha1Msg1,
179    Sha1Msg2,
180    Sha1Nexte,
181    /// Byte-shuffle lane (`Lanes16Word8` = one `__m128i`): pack/unpack + `shuffle` (pshufb), per-byte
182    /// shift, and the two interleaves — the vocabulary a SIMD hex codec WRITTEN in Logos is built from.
183    Lanes16Word8Make,
184    SeqOfLanes16W8,
185    Splat16Word8,
186    Shuffle16,
187    ShrBytes16,
188    InterleaveLo16,
189    InterleaveHi16,
190    ByteAdd16,
191    Maddubs16,
192    Packus16,
193    /// `lanes8Word32(s)` — pack the first 8 `Word32`s of a Seq into a SIMD lane vector.
194    Lanes8Word32,
195    /// `seqOfLanes8(v)` — unpack a `Lanes8Word32` back into a Seq of 8 `Word32`.
196    SeqOfLanes8,
197    /// `splat8Word32(x)` — broadcast a `Word32` into all 8 lanes.
198    Splat8Word32,
199    /// `intOfWord32(w)` — the unsigned value of a `Word32` as an `Int` (for byte serialization).
200    IntOfWord32,
201    /// `intOfWord64(w)` — the value of a `Word64` as an `Int` (byte-masked lanes in Keccak squeeze).
202    IntOfWord64,
203    /// `word64Shl(w, n)` — logical shift-left of a `Word64` (Keccak lane byte-packing).
204    Word64Shl,
205    /// `word64Shr(w, n)` — logical shift-right of a `Word64` (Keccak squeeze byte-extract).
206    Word64Shr,
207    /// `word64And(a, b)` — bitwise AND of two `Word64`s (Keccak χ's `¬b ∧ c`).
208    Word64And,
209    /// `word32Shr(w, n)` — logical shift-right of a `Word32` (SHA-256's `σ0`/`σ1`, a non-rotating
210    /// shift where the vacated high bits are zero).
211    Word32Shr,
212    /// `word16(n)` — the low 16 bits of an `Int` as a `Word16` (ℤ/2¹⁶ coefficient).
213    Word16Make,
214    /// `intOfWord16(w)` — the unsigned value of a `Word16` as an `Int` (0..2¹⁶−1).
215    IntOfWord16,
216    /// `lanes4Word64(s)` — pack the first 4 `Word64`/`Int`s of a Seq into a `Lanes4Word64`.
217    Lanes4Word64,
218    /// `seqOfLanes4(v)` — unpack a `Lanes4Word64` into a Seq of 4 `Int` lanes.
219    SeqOfLanes4,
220    /// `mul32x32to64(a, b)` — lane-wise widening multiply of the low 32 bits (`vpmuludq`).
221    Mul32x32To64,
222    /// `hsumLanes4(v)` — the horizontal sum of a lane vector's lanes as an `Int`.
223    HsumLanes4,
224    /// `splat4Word64(x)` — broadcast a `Word64` into all 4 Keccak lanes (ι constant, χ all-ones).
225    Splat4Word64,
226    /// `andNot4(a, b)` — 4-way Keccak χ's `(¬a) ∧ b` in one lane op (`vpandn`).
227    AndNot4,
228    /// `lanes16Word16(s)` — pack the first 16 `Word16`/`Int`s of a Seq into a `Lanes16Word16`.
229    Lanes16Word16,
230    /// `seqOfLanes16(v)` — unpack a `Lanes16Word16` into a Seq of 16 `Int` lanes.
231    SeqOfLanes16,
232    /// `splat16Word16(x)` — broadcast a `Word16`/`Int` into all 16 lanes.
233    Splat16Word16,
234    /// `mulhi16(a, b)` — lane-wise SIGNED high-16 multiply (`vpmulhw`, the Montgomery `mulhi`).
235    Mulhi16,
236    /// `montmul32(a, b, q, qinv)` — the signed i32 Montgomery multiply over 8 lanes (`vpmuldq`), the
237    /// ML-DSA (Dilithium) NTT butterfly multiply (`q`/`qinv` broadcast).
238    Montmul32,
239    /// `nttBcastLo(v, h)` — broadcast each `2h`-block's low `h` lanes into both halves (the
240    /// within-vector NTT source-low duplication; `vperm2i128`/`vpshufd` by stride).
241    NttBcastLo,
242    /// `nttBcastHi(v, h)` — broadcast each `2h`-block's high `h` lanes into both halves.
243    NttBcastHi,
244    /// `nttBlend(a, b, h)` — each `2h`-block's low `h` from `a`, high `h` from `b` (the butterfly
245    /// half-recombine; `vperm2i128`/`vpblendd` by stride).
246    NttBlend,
247    /// `mapOf(k1, v1, k2, v2, …)` — construct a Map from flat key/value pairs
248    /// in INSERTION order (the `{k: v, …}` literal's lowering). A duplicate
249    /// key keeps its first position with the last value, like repeated
250    /// `Set item k of m` writes.
251    MapOf,
252    /// `setOf(a, b, …)` — construct a Set from elements in insertion order,
253    /// deduplicating by value equality (the `{a, b, …}` literal's lowering).
254    SetOf,
255    /// `repeatSeq(x, n)` — a fresh sequence of `n` slots, each an INDEPENDENT
256    /// deep copy of `x` (the `n copies of x` / `[x] * n` fill; a repeated
257    /// inner collection is n rows, never n aliases). `n ≤ 0` is empty.
258    RepeatSeq,
259}
260
261/// Resolve a function name to a builtin, if it is one.
262pub fn builtin_from_name(name: &str) -> Option<BuiltinId> {
263    Some(match name {
264        "length" => BuiltinId::Length,
265        "format" => BuiltinId::Format,
266        "mapOf" => BuiltinId::MapOf,
267        "setOf" => BuiltinId::SetOf,
268        "repeatSeq" => BuiltinId::RepeatSeq,
269        "parseInt" => BuiltinId::ParseInt,
270        "parseFloat" => BuiltinId::ParseFloat,
271        "chr" => BuiltinId::Chr,
272        "abs" => BuiltinId::Abs,
273        "sqrt" => BuiltinId::Sqrt,
274        "min" => BuiltinId::Min,
275        "max" => BuiltinId::Max,
276        "floor" => BuiltinId::Floor,
277        "ceil" => BuiltinId::Ceil,
278        "round" => BuiltinId::Round,
279        "pow" => BuiltinId::Pow,
280        "decimal" => BuiltinId::Decimal,
281        "complex" => BuiltinId::Complex,
282        "modular" => BuiltinId::Modular,
283        "quantity" => BuiltinId::Quantity,
284        "money" => BuiltinId::Money,
285        "set_rate" => BuiltinId::SetRate,
286        "set_rates" => BuiltinId::SetRates,
287        "to_currency" => BuiltinId::ToCurrency,
288        "uuid" => BuiltinId::Uuid,
289        "uuid_nil" => BuiltinId::UuidNil,
290        "uuid_max" => BuiltinId::UuidMax,
291        "uuid_version" => BuiltinId::UuidVersion,
292        "uuid_dns" => BuiltinId::UuidDns,
293        "uuid_url" => BuiltinId::UuidUrl,
294        "uuid_oid" => BuiltinId::UuidOid,
295        "uuid_x500" => BuiltinId::UuidX500,
296        "text_bytes" => BuiltinId::TextBytes,
297        "text_from_bytes" => BuiltinId::TextFromBytes,
298        "wireBytes" => BuiltinId::WireBytes,
299        "readWireProgram" => BuiltinId::ReadWireProgram,
300        "writeWireResidual" => BuiltinId::WriteWireResidual,
301        "uuid_bytes" => BuiltinId::UuidBytes,
302        "uuid_from_bytes" => BuiltinId::UuidFromBytes,
303        "convert" => BuiltinId::Convert,
304        "parse_timestamp" => BuiltinId::ParseTimestamp,
305        "format_timestamp" => BuiltinId::FormatTimestamp,
306        "year_of" => BuiltinId::YearOf,
307        "month_of" => BuiltinId::MonthOf,
308        "day_of" => BuiltinId::DayOf,
309        "weekday_of" => BuiltinId::WeekdayOf,
310        "hour_of" => BuiltinId::HourOf,
311        "minute_of" => BuiltinId::MinuteOf,
312        "second_of" => BuiltinId::SecondOf,
313        "week_of" => BuiltinId::WeekOf,
314        "quarter_of" => BuiltinId::QuarterOf,
315        "date_of" => BuiltinId::DateOf,
316        "time_of" => BuiltinId::TimeOf,
317        "local_instant" => BuiltinId::LocalInstant,
318        "seconds_between" => BuiltinId::SecondsBetween,
319        "months_between" => BuiltinId::MonthsBetween,
320        "years_between" => BuiltinId::YearsBetween,
321        "add_seconds" => BuiltinId::AddSeconds,
322        "in_zone" => BuiltinId::InZone,
323        "copy" => BuiltinId::Copy,
324        "count_ones" => BuiltinId::CountOnes,
325        "run_accepted" => BuiltinId::RunAccepted,
326        "word32" => BuiltinId::Word32,
327        "word64" => BuiltinId::Word64,
328        "lanes8Word32" => BuiltinId::Lanes8Word32,
329        "seqOfLanes8" => BuiltinId::SeqOfLanes8,
330        "splat8Word32" => BuiltinId::Splat8Word32,
331        "intOfWord32" => BuiltinId::IntOfWord32,
332        "intOfWord64" => BuiltinId::IntOfWord64,
333        "word64Shl" => BuiltinId::Word64Shl,
334        "word64Shr" => BuiltinId::Word64Shr,
335        "word32Shr" => BuiltinId::Word32Shr,
336        "word64And" => BuiltinId::Word64And,
337        "word16" => BuiltinId::Word16Make,
338        "intOfWord16" => BuiltinId::IntOfWord16,
339        "lanes4Word64" => BuiltinId::Lanes4Word64,
340        "seqOfLanes4" => BuiltinId::SeqOfLanes4,
341        "mul32x32to64" => BuiltinId::Mul32x32To64,
342        "hsumLanes4" => BuiltinId::HsumLanes4,
343        "splat4Word64" => BuiltinId::Splat4Word64,
344        "andNot4" => BuiltinId::AndNot4,
345        "lanes16Word16" => BuiltinId::Lanes16Word16,
346        "seqOfLanes16" => BuiltinId::SeqOfLanes16,
347        "splat16Word16" => BuiltinId::Splat16Word16,
348        "mulhi16" => BuiltinId::Mulhi16,
349        "montmul32" => BuiltinId::Montmul32,
350        "nttBcastLo" => BuiltinId::NttBcastLo,
351        "nttBcastHi" => BuiltinId::NttBcastHi,
352        "nttBlend" => BuiltinId::NttBlend,
353        "rotl" => BuiltinId::Rotl,
354        "word_and" => BuiltinId::Wand,
355        "word_or" => BuiltinId::Wor,
356        "word_not" => BuiltinId::Wnot,
357        "lanes4Word32" => BuiltinId::Lanes4Word32Make,
358        "lanes4Of" => BuiltinId::Lanes4Of,
359        "seqOfLanes4W32" => BuiltinId::SeqOfLanes4W32,
360        "sha1rnds4" => BuiltinId::Sha1Rnds4,
361        "sha1msg1" => BuiltinId::Sha1Msg1,
362        "sha1msg2" => BuiltinId::Sha1Msg2,
363        "sha1nexte" => BuiltinId::Sha1Nexte,
364        "lanes16Word8" => BuiltinId::Lanes16Word8Make,
365        "seqOfLanes16W8" => BuiltinId::SeqOfLanes16W8,
366        "splat16Word8" => BuiltinId::Splat16Word8,
367        "shuffle16" => BuiltinId::Shuffle16,
368        "shrBytes16" => BuiltinId::ShrBytes16,
369        "interleaveLo16" => BuiltinId::InterleaveLo16,
370        "interleaveHi16" => BuiltinId::InterleaveHi16,
371        "byteAdd16" => BuiltinId::ByteAdd16,
372        "maddubs16" => BuiltinId::Maddubs16,
373        "packus16" => BuiltinId::Packus16,
374        "rotr" => BuiltinId::Rotr,
375        _ => return None,
376    })
377}
378
379/// Check the call's arity BEFORE evaluating arguments. `format` accepts any
380/// arity (it reads only its first argument, or none).
381pub fn check_arity(id: BuiltinId, n: usize) -> Result<(), String> {
382    let expected: usize = match id {
383        BuiltinId::Format => return Ok(()),
384        BuiltinId::MapOf => {
385            if n == 0 || n % 2 != 0 {
386                return Err(format!(
387                    "mapOf takes flat key/value pairs (an even, nonzero number of arguments), got {}",
388                    n
389                ));
390            }
391            return Ok(());
392        }
393        BuiltinId::SetOf => {
394            if n == 0 {
395                return Err("setOf takes at least one element (an empty set is `{} of T`)".to_string());
396            }
397            return Ok(());
398        }
399        BuiltinId::Min | BuiltinId::Max | BuiltinId::Pow => 2,
400        BuiltinId::RepeatSeq => 2,
401        BuiltinId::Complex => 2,
402        BuiltinId::Modular => 2,
403        BuiltinId::Quantity | BuiltinId::Convert | BuiltinId::Money => 2,
404        BuiltinId::SetRate | BuiltinId::ToCurrency => 2,
405        BuiltinId::SetRates => 1,
406        BuiltinId::UuidNil
407        | BuiltinId::UuidMax
408        | BuiltinId::UuidDns
409        | BuiltinId::UuidUrl
410        | BuiltinId::UuidOid
411        | BuiltinId::ReadWireProgram
412        | BuiltinId::UuidX500 => 0,
413        BuiltinId::Uuid | BuiltinId::UuidVersion => 1,
414        BuiltinId::TextBytes | BuiltinId::TextFromBytes | BuiltinId::WireBytes | BuiltinId::WriteWireResidual | BuiltinId::UuidBytes | BuiltinId::UuidFromBytes => 1,
415        BuiltinId::SecondsBetween | BuiltinId::AddSeconds | BuiltinId::InZone => 2,
416        BuiltinId::MonthsBetween | BuiltinId::YearsBetween => 2,
417        BuiltinId::LocalInstant => 2,
418        BuiltinId::Rotl | BuiltinId::Rotr => 2,
419        BuiltinId::Wand | BuiltinId::Wor => 2,
420        BuiltinId::Wnot => 1,
421        BuiltinId::Lanes4Word32Make | BuiltinId::SeqOfLanes4W32 => 1,
422        BuiltinId::AndNot4 => 2,
423        BuiltinId::Lanes4Of => 4,
424        BuiltinId::Sha1Rnds4 => 3,
425        BuiltinId::Sha1Msg1 | BuiltinId::Sha1Msg2 | BuiltinId::Sha1Nexte => 2,
426        BuiltinId::Lanes16Word8Make | BuiltinId::SeqOfLanes16W8 | BuiltinId::Splat16Word8 => 1,
427        BuiltinId::Shuffle16 | BuiltinId::ShrBytes16 => 2,
428        BuiltinId::InterleaveLo16 | BuiltinId::InterleaveHi16 => 2,
429        BuiltinId::ByteAdd16 | BuiltinId::Maddubs16 | BuiltinId::Packus16 => 2,
430        BuiltinId::Mul32x32To64 => 2,
431        BuiltinId::Mulhi16 => 2,
432        BuiltinId::Montmul32 => 4,
433        BuiltinId::Word64Shl | BuiltinId::Word64Shr | BuiltinId::Word64And => 2,
434        BuiltinId::Word32Shr => 2,
435        BuiltinId::NttBcastLo | BuiltinId::NttBcastHi => 2,
436        BuiltinId::NttBlend => 3,
437        // run_accepted(fn, arg, lo, hi): the shipped computation + the argument + the
438        // inclusive bounds of the acceptance contract.
439        BuiltinId::RunAccepted => 4,
440        _ => 1,
441    };
442    if n != expected {
443        let name = match id {
444            BuiltinId::Length => "length",
445            BuiltinId::Format => unreachable!(),
446            // MapOf/SetOf return early above (variadic with their own errors).
447            BuiltinId::MapOf | BuiltinId::SetOf => unreachable!(),
448            BuiltinId::RepeatSeq => "repeatSeq",
449            BuiltinId::ParseInt => "parseInt",
450            BuiltinId::ParseFloat => "parseFloat",
451            BuiltinId::Chr => "chr",
452            BuiltinId::Abs => "abs",
453            BuiltinId::Sqrt => "sqrt",
454            BuiltinId::Min => "min",
455            BuiltinId::Max => "max",
456            BuiltinId::Floor => "floor",
457            BuiltinId::Ceil => "ceil",
458            BuiltinId::Round => "round",
459            BuiltinId::Pow => "pow",
460            BuiltinId::Decimal => "decimal",
461            BuiltinId::Complex => "complex",
462            BuiltinId::Modular => "modular",
463            BuiltinId::Quantity => "quantity",
464            BuiltinId::Money => "money",
465            BuiltinId::SetRate => "set_rate",
466            BuiltinId::SetRates => "set_rates",
467            BuiltinId::ToCurrency => "to_currency",
468            BuiltinId::Uuid => "uuid",
469            BuiltinId::UuidNil => "uuid_nil",
470            BuiltinId::UuidMax => "uuid_max",
471            BuiltinId::UuidVersion => "uuid_version",
472            BuiltinId::UuidDns => "uuid_dns",
473            BuiltinId::UuidUrl => "uuid_url",
474            BuiltinId::UuidOid => "uuid_oid",
475            BuiltinId::UuidX500 => "uuid_x500",
476            BuiltinId::TextBytes => "text_bytes",
477            BuiltinId::TextFromBytes => "text_from_bytes",
478            BuiltinId::WireBytes => "wireBytes",
479            BuiltinId::ReadWireProgram => "readWireProgram",
480            BuiltinId::WriteWireResidual => "writeWireResidual",
481            BuiltinId::UuidBytes => "uuid_bytes",
482            BuiltinId::UuidFromBytes => "uuid_from_bytes",
483            BuiltinId::Convert => "convert",
484            BuiltinId::ParseTimestamp => "parse_timestamp",
485            BuiltinId::FormatTimestamp => "format_timestamp",
486            BuiltinId::YearOf => "year_of",
487            BuiltinId::MonthOf => "month_of",
488            BuiltinId::DayOf => "day_of",
489            BuiltinId::WeekdayOf => "weekday_of",
490            BuiltinId::HourOf => "hour_of",
491            BuiltinId::MinuteOf => "minute_of",
492            BuiltinId::SecondOf => "second_of",
493            BuiltinId::WeekOf => "week_of",
494            BuiltinId::QuarterOf => "quarter_of",
495            BuiltinId::DateOf => "date_of",
496            BuiltinId::TimeOf => "time_of",
497            BuiltinId::LocalInstant => "local_instant",
498            BuiltinId::SecondsBetween => "seconds_between",
499            BuiltinId::MonthsBetween => "months_between",
500            BuiltinId::YearsBetween => "years_between",
501            BuiltinId::AddSeconds => "add_seconds",
502            BuiltinId::InZone => "in_zone",
503            BuiltinId::Copy => "copy",
504            BuiltinId::CountOnes => "count_ones",
505            BuiltinId::RunAccepted => "run_accepted",
506            BuiltinId::Word32 => "word32",
507            BuiltinId::Word64 => "word64",
508            BuiltinId::Lanes8Word32 => "lanes8Word32",
509            BuiltinId::SeqOfLanes8 => "seqOfLanes8",
510            BuiltinId::Splat8Word32 => "splat8Word32",
511            BuiltinId::IntOfWord32 => "intOfWord32",
512            BuiltinId::IntOfWord64 => "intOfWord64",
513            BuiltinId::Word64Shl => "word64Shl",
514            BuiltinId::Word64Shr => "word64Shr",
515            BuiltinId::Word32Shr => "word32Shr",
516            BuiltinId::Word64And => "word64And",
517            BuiltinId::Word16Make => "word16",
518            BuiltinId::IntOfWord16 => "intOfWord16",
519            BuiltinId::Lanes4Word64 => "lanes4Word64",
520            BuiltinId::SeqOfLanes4 => "seqOfLanes4",
521            BuiltinId::Mul32x32To64 => "mul32x32to64",
522            BuiltinId::HsumLanes4 => "hsumLanes4",
523            BuiltinId::Splat4Word64 => "splat4Word64",
524            BuiltinId::AndNot4 => "andNot4",
525            BuiltinId::Lanes16Word16 => "lanes16Word16",
526            BuiltinId::SeqOfLanes16 => "seqOfLanes16",
527            BuiltinId::Splat16Word16 => "splat16Word16",
528            BuiltinId::Mulhi16 => "mulhi16",
529            BuiltinId::Montmul32 => "montmul32",
530            BuiltinId::NttBcastLo => "nttBcastLo",
531            BuiltinId::NttBcastHi => "nttBcastHi",
532            BuiltinId::NttBlend => "nttBlend",
533            BuiltinId::Rotl => "rotl",
534            BuiltinId::Wand => "word_and",
535            BuiltinId::Wor => "word_or",
536            BuiltinId::Wnot => "word_not",
537            BuiltinId::Lanes4Word32Make => "lanes4Word32",
538            BuiltinId::Lanes4Of => "lanes4Of",
539            BuiltinId::SeqOfLanes4W32 => "seqOfLanes4W32",
540            BuiltinId::Sha1Rnds4 => "sha1rnds4",
541            BuiltinId::Sha1Msg1 => "sha1msg1",
542            BuiltinId::Sha1Msg2 => "sha1msg2",
543            BuiltinId::Sha1Nexte => "sha1nexte",
544            BuiltinId::Lanes16Word8Make => "lanes16Word8",
545            BuiltinId::SeqOfLanes16W8 => "seqOfLanes16W8",
546            BuiltinId::Splat16Word8 => "splat16Word8",
547            BuiltinId::Shuffle16 => "shuffle16",
548            BuiltinId::ShrBytes16 => "shrBytes16",
549            BuiltinId::InterleaveLo16 => "interleaveLo16",
550            BuiltinId::InterleaveHi16 => "interleaveHi16",
551            BuiltinId::ByteAdd16 => "byteAdd16",
552            BuiltinId::Maddubs16 => "maddubs16",
553            BuiltinId::Packus16 => "packus16",
554            BuiltinId::Rotr => "rotr",
555        };
556        return Err(format!(
557            "{}() takes exactly {} argument{}",
558            name,
559            expected,
560            if expected == 1 { "" } else { "s" }
561        ));
562    }
563    Ok(())
564}
565
566/// Apply a builtin to already-evaluated arguments. The caller has already
567/// validated arity with [`check_arity`].
568/// Validate a within-vector NTT stride argument: an `Int` that is one of the supported half-widths
569/// 8/4/2 (the AVX2 shuffle path is defined only at these granularities).
570fn ntt_stride(v: RuntimeValue, name: &str) -> Result<usize, String> {
571    match v {
572        // i16 within-vector strides are 8/4/2; the i32 (Lanes8Word32) ones are 4/2/1.
573        RuntimeValue::Int(n @ (8 | 4 | 2 | 1)) => Ok(n as usize),
574        RuntimeValue::Int(n) => Err(format!("{name} stride must be 8, 4, 2, or 1, got {n}")),
575        other => Err(format!("{name} stride must be an Int, got {}", other.type_name())),
576    }
577}
578
579pub fn call_builtin(id: BuiltinId, args: Vec<RuntimeValue>) -> Result<RuntimeValue, String> {
580    let mut args = args;
581    match id {
582        BuiltinId::MapOf => {
583            // Flat key/value pairs, insertion-ordered; a duplicate key keeps
584            // its first position with the last value (IndexMap `insert`).
585            let mut m = crate::interpreter::MapStorage::default();
586            let mut it = args.into_iter();
587            while let (Some(k), Some(v)) = (it.next(), it.next()) {
588                crate::semantics::collections::assert_hashable_key(&k)?;
589                m.insert(k, v);
590            }
591            Ok(RuntimeValue::Map(Rc::new(RefCell::new(m))))
592        }
593        BuiltinId::SetOf => {
594            // Elements in insertion order, deduplicated by value equality —
595            // the same semantics as consecutive `Add x to s` statements.
596            let set = RuntimeValue::Set(Rc::new(RefCell::new(Vec::new())));
597            for v in args {
598                crate::semantics::collections::set_add(&set, v)?;
599            }
600            Ok(set)
601        }
602        BuiltinId::RepeatSeq => {
603            // The element evaluates ONCE; each slot deep-copies it, so a
604            // repeated inner collection is n independent rows.
605            let count = args.remove(1);
606            let element = args.remove(0);
607            let n = match count {
608                RuntimeValue::Int(n) => n.max(0) as usize,
609                other => return Err(format!("repeatSeq count must be an Int, got {}", other.type_name())),
610            };
611            let slots: Vec<RuntimeValue> = (0..n).map(|_| element.deep_clone()).collect();
612            Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(slots)))))
613        }
614        BuiltinId::Length => {
615            let val = args.remove(0);
616            match &val {
617                RuntimeValue::List(items) => Ok(RuntimeValue::Int(items.borrow().len() as i64)),
618                RuntimeValue::Text(s) => Ok(RuntimeValue::Int(s.len() as i64)),
619                RuntimeValue::Map(map) => Ok(RuntimeValue::Int(map.borrow().len() as i64)),
620                _ => Err(format!("Cannot get length of {}", val.type_name())),
621            }
622        }
623        BuiltinId::Format => {
624            if args.is_empty() {
625                return Ok(RuntimeValue::Text(Rc::new(String::new())));
626            }
627            let val = args.remove(0);
628            Ok(RuntimeValue::Text(Rc::new(val.to_display_string())))
629        }
630        BuiltinId::ParseInt => {
631            let val = args.remove(0);
632            if let RuntimeValue::Text(s) = &val {
633                Ok(RuntimeValue::Int(
634                    s.trim()
635                        .parse::<i64>()
636                        .map_err(|_| format!("Cannot parse '{}' as Int", s))?,
637                ))
638            } else {
639                Err("parseInt requires a Text argument".to_string())
640            }
641        }
642        BuiltinId::ParseFloat => {
643            let val = args.remove(0);
644            if let RuntimeValue::Text(s) = &val {
645                Ok(RuntimeValue::Float(
646                    s.trim()
647                        .parse::<f64>()
648                        .map_err(|_| format!("Cannot parse '{}' as Float", s))?,
649                ))
650            } else {
651                Err("parseFloat requires a Text argument".to_string())
652            }
653        }
654        BuiltinId::Chr => {
655            let val = args.remove(0);
656            if let RuntimeValue::Int(code) = val {
657                match char::from_u32(code as u32) {
658                    Some(c) => Ok(RuntimeValue::Text(Rc::new(c.to_string()))),
659                    None => Err(format!("Invalid character code: {}", code)),
660                }
661            } else {
662                Err("chr() requires an Int argument".to_string())
663            }
664        }
665        BuiltinId::Abs => {
666            let val = args.remove(0);
667            match val {
668                // |i64::MIN| = 2^63 does not fit i64, so abs is EXACT via promotion
669                // (wrapping_abs would have returned i64::MIN — a sign bug).
670                RuntimeValue::Int(n) => Ok(match n.checked_abs() {
671                    Some(a) => RuntimeValue::Int(a),
672                    None => RuntimeValue::from_bigint(logicaffeine_base::BigInt::from_i64(n).abs()),
673                }),
674                RuntimeValue::BigInt(b) => Ok(RuntimeValue::from_bigint(b.abs())),
675                // |·| of a rational stays a rational (`|-7/2| = 7/2`), downsized if whole.
676                RuntimeValue::Rational(r) => Ok(RuntimeValue::from_rational(r.abs())),
677                // |·| of a decimal stays a decimal, scale preserved (`|-0.05| = 0.05`).
678                RuntimeValue::Decimal(d) => Ok(RuntimeValue::Decimal(Rc::new(d.abs()))),
679                // |z| of a complex is its modulus √(re²+im²) — generally irrational, a Float view.
680                RuntimeValue::Complex(c) => Ok(RuntimeValue::Float(c.abs_f64())),
681                RuntimeValue::Float(f) => Ok(RuntimeValue::Float(f.abs())),
682                _ => Err(format!("abs() requires a number, got {}", val.type_name())),
683            }
684        }
685        BuiltinId::Sqrt => {
686            let val = args.remove(0);
687            match val {
688                RuntimeValue::Float(f) => Ok(RuntimeValue::Float(f.sqrt())),
689                RuntimeValue::Int(n) => Ok(RuntimeValue::Float((n as f64).sqrt())),
690                RuntimeValue::BigInt(b) => Ok(RuntimeValue::Float(b.to_f64().sqrt())),
691                _ => Err(format!("sqrt() requires a number, got {}", val.type_name())),
692            }
693        }
694        BuiltinId::Min => {
695            let b = args.remove(1);
696            let a = args.remove(0);
697            match (&a, &b) {
698                (RuntimeValue::Int(x), RuntimeValue::Int(y)) => Ok(RuntimeValue::Int(*x.min(y))),
699                (RuntimeValue::Float(x), RuntimeValue::Float(y)) => {
700                    Ok(RuntimeValue::Float(x.min(*y)))
701                }
702                (RuntimeValue::Int(x), RuntimeValue::Float(y)) => {
703                    Ok(RuntimeValue::Float((*x as f64).min(*y)))
704                }
705                (RuntimeValue::Float(x), RuntimeValue::Int(y)) => {
706                    Ok(RuntimeValue::Float(x.min(*y as f64)))
707                }
708                // Exact decimal min (value-based, scale of the chosen operand preserved).
709                (RuntimeValue::Decimal(x), RuntimeValue::Decimal(y)) => {
710                    Ok(RuntimeValue::Decimal(if x <= y { x.clone() } else { y.clone() }))
711                }
712                _ => Err("min() requires numbers".to_string()),
713            }
714        }
715        BuiltinId::Max => {
716            let b = args.remove(1);
717            let a = args.remove(0);
718            match (&a, &b) {
719                (RuntimeValue::Int(x), RuntimeValue::Int(y)) => Ok(RuntimeValue::Int(*x.max(y))),
720                (RuntimeValue::Float(x), RuntimeValue::Float(y)) => {
721                    Ok(RuntimeValue::Float(x.max(*y)))
722                }
723                (RuntimeValue::Int(x), RuntimeValue::Float(y)) => {
724                    Ok(RuntimeValue::Float((*x as f64).max(*y)))
725                }
726                (RuntimeValue::Float(x), RuntimeValue::Int(y)) => {
727                    Ok(RuntimeValue::Float(x.max(*y as f64)))
728                }
729                // Exact decimal max (value-based, scale of the chosen operand preserved).
730                (RuntimeValue::Decimal(x), RuntimeValue::Decimal(y)) => {
731                    Ok(RuntimeValue::Decimal(if x >= y { x.clone() } else { y.clone() }))
732                }
733                _ => Err("max() requires numbers".to_string()),
734            }
735        }
736        BuiltinId::Floor => {
737            let val = args.remove(0);
738            // An exact integer (Int/BigInt) is already whole; a Rational rounds toward
739            // −∞ to an exact integer — the explicit floor of `floor(7 / 2) → 3`.
740            match &val {
741                RuntimeValue::Float(f) => Ok(RuntimeValue::Int(f.floor() as i64)),
742                RuntimeValue::Int(_) | RuntimeValue::BigInt(_) => Ok(val.clone()),
743                RuntimeValue::Rational(r) => Ok(RuntimeValue::from_bigint(r.floor())),
744                RuntimeValue::Decimal(d) => Ok(RuntimeValue::from_bigint(d.to_rational().floor())),
745                _ => Err(format!("floor() requires a number, got {}", val.type_name())),
746            }
747        }
748        BuiltinId::Ceil => {
749            let val = args.remove(0);
750            match &val {
751                RuntimeValue::Float(f) => Ok(RuntimeValue::Int(f.ceil() as i64)),
752                RuntimeValue::Int(_) | RuntimeValue::BigInt(_) => Ok(val.clone()),
753                RuntimeValue::Rational(r) => Ok(RuntimeValue::from_bigint(r.ceil())),
754                RuntimeValue::Decimal(d) => Ok(RuntimeValue::from_bigint(d.to_rational().ceil())),
755                _ => Err(format!("ceil() requires a number, got {}", val.type_name())),
756            }
757        }
758        BuiltinId::Round => {
759            let val = args.remove(0);
760            match &val {
761                RuntimeValue::Float(f) => Ok(RuntimeValue::Int(f.round() as i64)),
762                RuntimeValue::Int(_) | RuntimeValue::BigInt(_) => Ok(val.clone()),
763                RuntimeValue::Rational(r) => Ok(RuntimeValue::from_bigint(r.round())),
764                RuntimeValue::Decimal(d) => Ok(RuntimeValue::from_bigint(d.to_rational().round())),
765                _ => Err(format!("round() requires a number, got {}", val.type_name())),
766            }
767        }
768        BuiltinId::Pow => {
769            let exp = args.remove(1);
770            let base = args.remove(0);
771            match (&base, &exp) {
772                // EXACT integer power: on i64 overflow, promote to BigInt rather than
773                // wrapping (e.g. 2^63 is the value, not i64::MIN). A negative exponent
774                // is a fractional power, so it falls to f64 as before.
775                (RuntimeValue::Int(b), RuntimeValue::Int(e)) => {
776                    if *e >= 0 {
777                        Ok(match b.checked_pow(*e as u32) {
778                            Some(p) => RuntimeValue::Int(p),
779                            None => RuntimeValue::from_bigint(logicaffeine_base::BigInt::from_i64(*b).pow(*e as u32)),
780                        })
781                    } else {
782                        Ok(RuntimeValue::Float((*b as f64).powi(*e as i32)))
783                    }
784                }
785                (RuntimeValue::BigInt(b), RuntimeValue::Int(e)) => {
786                    if *e >= 0 {
787                        Ok(RuntimeValue::from_bigint(b.pow(*e as u32)))
788                    } else {
789                        Ok(RuntimeValue::Float(b.to_f64().powi(*e as i32)))
790                    }
791                }
792                (RuntimeValue::Float(b), RuntimeValue::Int(e)) => {
793                    Ok(RuntimeValue::Float(b.powi(*e as i32)))
794                }
795                (RuntimeValue::Float(b), RuntimeValue::Float(e)) => {
796                    Ok(RuntimeValue::Float(b.powf(*e)))
797                }
798                (RuntimeValue::Int(b), RuntimeValue::Float(e)) => {
799                    Ok(RuntimeValue::Float((*b as f64).powf(*e)))
800                }
801                // Modular exponentiation: x^e in ℤ/nℤ (fast square-and-multiply). Exponent ≥ 0.
802                (RuntimeValue::Modular(b), RuntimeValue::Int(e)) if *e >= 0 => {
803                    Ok(RuntimeValue::Modular(Rc::new(b.pow(*e as u64))))
804                }
805                _ => Err("pow() requires numbers".to_string()),
806            }
807        }
808        BuiltinId::Decimal => {
809            let val = args.remove(0);
810            match &val {
811                // Parse the exact base-10 value from its literal text (`decimal("19.99")`).
812                RuntimeValue::Text(s) => Decimal::parse(s.trim())
813                    .map(|d| RuntimeValue::Decimal(Rc::new(d)))
814                    .ok_or_else(|| format!("Cannot parse '{}' as Decimal", s)),
815                // An integer is already exact — widen it to a scale-0 Decimal.
816                RuntimeValue::Int(n) => Ok(RuntimeValue::Decimal(Rc::new(Decimal::from_i64(*n)))),
817                // Already a Decimal — identity.
818                RuntimeValue::Decimal(_) => Ok(val.clone()),
819                _ => Err(format!("decimal() requires a Text or Int, got {}", val.type_name())),
820            }
821        }
822        BuiltinId::Complex => {
823            let im = args.remove(1);
824            let re = args.remove(0);
825            // Each part must be an EXACT real (Int/BigInt/Rational/Decimal); a Float would
826            // make the complex inexact, so it is refused rather than silently coerced.
827            let to_rat = |v: &RuntimeValue| -> Option<logicaffeine_base::Rational> {
828                match v {
829                    RuntimeValue::Int(n) => Some(logicaffeine_base::Rational::from_i64(*n)),
830                    RuntimeValue::BigInt(b) => Some(logicaffeine_base::Rational::from_bigint((**b).clone())),
831                    RuntimeValue::Rational(r) => Some((**r).clone()),
832                    RuntimeValue::Decimal(d) => Some(d.to_rational()),
833                    _ => None,
834                }
835            };
836            match (to_rat(&re), to_rat(&im)) {
837                (Some(re_r), Some(im_r)) => Ok(RuntimeValue::Complex(Rc::new(
838                    logicaffeine_base::Complex::new(re_r, im_r),
839                ))),
840                _ => Err(format!(
841                    "complex() requires two exact numbers, got {} and {}",
842                    re.type_name(),
843                    im.type_name()
844                )),
845            }
846        }
847        BuiltinId::Modular => {
848            let modulus = args.remove(1);
849            let value = args.remove(0);
850            let to_int = |v: &RuntimeValue| -> Option<logicaffeine_base::BigInt> {
851                match v {
852                    RuntimeValue::Int(n) => Some(logicaffeine_base::BigInt::from_i64(*n)),
853                    RuntimeValue::BigInt(b) => Some((**b).clone()),
854                    _ => None,
855                }
856            };
857            match (to_int(&value), to_int(&modulus)) {
858                (Some(v), Some(n)) => match logicaffeine_base::Modular::new(v, n) {
859                    Some(m) => Ok(RuntimeValue::Modular(Rc::new(m))),
860                    None => Err("modular() requires a positive modulus".to_string()),
861                },
862                _ => Err(format!(
863                    "modular() requires two integers, got {} and {}",
864                    value.type_name(),
865                    modulus.type_name()
866                )),
867            }
868        }
869        BuiltinId::Quantity => {
870            let unit_arg = args.remove(1);
871            let value = args.remove(0);
872            // The magnitude must be an EXACT number (Int/BigInt/Rational/Decimal) — a Float would
873            // make the quantity inexact, defeating lossless conversion, so it is refused.
874            let magnitude = match &value {
875                RuntimeValue::Int(n) => logicaffeine_base::Rational::from_i64(*n),
876                RuntimeValue::BigInt(b) => logicaffeine_base::Rational::from_bigint((**b).clone()),
877                RuntimeValue::Rational(r) => (**r).clone(),
878                RuntimeValue::Decimal(d) => d.to_rational(),
879                _ => return Err(format!("quantity() requires an exact number, got {}", value.type_name())),
880            };
881            let unit = match &unit_arg {
882                RuntimeValue::Text(s) => logicaffeine_base::quantity::units::by_name(s)
883                    .ok_or_else(|| format!("Unknown unit '{}'", s))?,
884                _ => return Err(format!("quantity() requires a unit name (Text), got {}", unit_arg.type_name())),
885            };
886            let q = logicaffeine_base::Quantity::of(magnitude, &unit);
887            Ok(RuntimeValue::Quantity(Rc::new(crate::interpreter::QuantityValue { q, unit })))
888        }
889        BuiltinId::Money => {
890            let code_arg = args.remove(1);
891            let value = args.remove(0);
892            // The amount must be EXACT base-10 (Int or Decimal); a Float or non-terminating Rational
893            // would not be representable money, so it is refused.
894            let amount = match &value {
895                RuntimeValue::Int(n) => logicaffeine_base::Decimal::from_i64(*n),
896                RuntimeValue::Decimal(d) => (**d).clone(),
897                _ => return Err(format!("money() requires an exact base-10 amount (Int or Decimal), got {}", value.type_name())),
898            };
899            let currency = match &code_arg {
900                RuntimeValue::Text(s) => logicaffeine_base::money::currency::by_code(s)
901                    .ok_or_else(|| format!("Unknown currency '{}'", s))?,
902                _ => return Err(format!("money() requires a currency code (Text), got {}", code_arg.type_name())),
903            };
904            Ok(RuntimeValue::Money(Rc::new(logicaffeine_base::Money::of(amount, currency))))
905        }
906        BuiltinId::SetRate => {
907            let rate_arg = args.remove(1);
908            let code_arg = args.remove(0);
909            let code = match &code_arg {
910                RuntimeValue::Text(s) => s.to_string(),
911                _ => return Err(format!("set_rate() requires a currency code (Text), got {}", code_arg.type_name())),
912            };
913            let rate = match &rate_arg {
914                RuntimeValue::Int(n) => logicaffeine_base::Rational::from_i64(*n),
915                RuntimeValue::Decimal(d) => d.to_rational(),
916                RuntimeValue::Rational(r) => (**r).clone(),
917                _ => return Err(format!("set_rate() requires an exact rate (Int/Decimal), got {}", rate_arg.type_name())),
918            };
919            logicaffeine_base::money::set_ambient_rate(&code, rate);
920            Ok(RuntimeValue::Nothing)
921        }
922        BuiltinId::SetRates => {
923            let table = args.remove(0);
924            let map = match &table {
925                RuntimeValue::Map(m) => m,
926                _ => {
927                    return Err(format!(
928                        "set_rates() requires a Map of currency code to rate, got {}",
929                        table.type_name()
930                    ))
931                }
932            };
933            for (key, value) in map.borrow().iter() {
934                let code = match key {
935                    RuntimeValue::Text(s) => s.to_string(),
936                    _ => return Err(format!("set_rates() keys must be currency codes (Text), got {}", key.type_name())),
937                };
938                let rate = match value {
939                    RuntimeValue::Int(n) => logicaffeine_base::Rational::from_i64(*n),
940                    RuntimeValue::Decimal(d) => d.to_rational(),
941                    RuntimeValue::Rational(r) => (**r).clone(),
942                    _ => return Err(format!("set_rates() values must be exact rates (Int/Decimal), got {}", value.type_name())),
943                };
944                logicaffeine_base::money::set_ambient_rate(&code, rate);
945            }
946            Ok(RuntimeValue::Nothing)
947        }
948        BuiltinId::Uuid => {
949            let arg = args.remove(0);
950            match &arg {
951                RuntimeValue::Text(s) => logicaffeine_base::Uuid::parse(s)
952                    .map(|u| RuntimeValue::Uuid(Rc::new(u)))
953                    .ok_or_else(|| format!("invalid UUID '{}'", s)),
954                _ => Err(format!("uuid() requires text, got {}", arg.type_name())),
955            }
956        }
957        BuiltinId::UuidNil => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::NIL))),
958        BuiltinId::UuidMax => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::MAX))),
959        BuiltinId::UuidDns => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::NAMESPACE_DNS))),
960        BuiltinId::UuidUrl => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::NAMESPACE_URL))),
961        BuiltinId::UuidOid => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::NAMESPACE_OID))),
962        BuiltinId::UuidX500 => Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::NAMESPACE_X500))),
963        BuiltinId::UuidVersion => {
964            let arg = args.remove(0);
965            match &arg {
966                RuntimeValue::Uuid(u) => Ok(RuntimeValue::Int(u.version() as i64)),
967                _ => Err(format!("uuid_version() requires a Uuid, got {}", arg.type_name())),
968            }
969        }
970        BuiltinId::TextBytes => {
971            let arg = args.remove(0);
972            match &arg {
973                RuntimeValue::Text(s) => Ok(bytes_to_seq(s.as_bytes())),
974                _ => Err(format!("text_bytes() requires text, got {}", arg.type_name())),
975            }
976        }
977        BuiltinId::TextFromBytes => {
978            let bytes = byte_seq(&args.remove(0))?;
979            match String::from_utf8(bytes) {
980                Ok(s) => Ok(RuntimeValue::Text(Rc::new(s))),
981                Err(e) => Err(format!("text_from_bytes(): invalid UTF-8: {}", e)),
982            }
983        }
984        BuiltinId::WireBytes => {
985            let arg = args.remove(0);
986            match crate::concurrency::marshal::encode_value_raw(&arg) {
987                Ok(bytes) => Ok(bytes_to_seq(&bytes)),
988                Err(e) => Err(format!("wireBytes(): {}", e)),
989            }
990        }
991        BuiltinId::ReadWireProgram => {
992            use std::io::Read;
993            let mut len = [0u8; 4];
994            if std::io::stdin().read_exact(&mut len).is_err() {
995                std::process::exit(0);
996            }
997            let n = u32::from_le_bytes(len) as usize;
998            let mut buf = vec![0u8; n];
999            std::io::stdin()
1000                .read_exact(&mut buf)
1001                .map_err(|e| format!("readWireProgram(): frame read failed: {e}"))?;
1002            crate::concurrency::marshal::decode_value_raw(&buf)
1003                .ok_or_else(|| "readWireProgram(): malformed wire program".to_string())
1004        }
1005        BuiltinId::WriteWireResidual => {
1006            use std::io::Write;
1007            let arg = args.remove(0);
1008            let s = match &arg {
1009                RuntimeValue::Text(t) => (**t).clone(),
1010                _ => return Err("writeWireResidual() expects text".to_string()),
1011            };
1012            let b = s.as_bytes();
1013            let out = std::io::stdout();
1014            let mut h = out.lock();
1015            h.write_all(&(b.len() as u32).to_le_bytes())
1016                .and_then(|_| h.write_all(b))
1017                .and_then(|_| h.flush())
1018                .map_err(|e| format!("writeWireResidual(): {e}"))?;
1019            Ok(RuntimeValue::Int(b.len() as i64))
1020        }
1021        BuiltinId::UuidBytes => {
1022            let arg = args.remove(0);
1023            match &arg {
1024                RuntimeValue::Uuid(u) => Ok(bytes_to_seq(u.as_bytes())),
1025                _ => Err(format!("uuid_bytes() requires a Uuid, got {}", arg.type_name())),
1026            }
1027        }
1028        BuiltinId::UuidFromBytes => {
1029            let bytes = byte_seq(&args.remove(0))?;
1030            if bytes.len() < 16 {
1031                return Err(format!("uuid_from_bytes() needs 16 bytes, got {}", bytes.len()));
1032            }
1033            let mut b = [0u8; 16];
1034            b.copy_from_slice(&bytes[..16]);
1035            Ok(RuntimeValue::Uuid(Rc::new(logicaffeine_base::Uuid::from_bytes(b))))
1036        }
1037        BuiltinId::ToCurrency => {
1038            let code_arg = args.remove(1);
1039            let money = args.remove(0);
1040            match (&money, &code_arg) {
1041                (RuntimeValue::Money(m), RuntimeValue::Text(code)) => {
1042                    let to = logicaffeine_base::money::currency::by_code(code)
1043                        .ok_or_else(|| format!("Unknown currency '{}'", code))?;
1044                    logicaffeine_base::money::ambient_convert(m, to)
1045                        .map(|c| RuntimeValue::Money(Rc::new(c)))
1046                        .ok_or_else(|| {
1047                            if logicaffeine_base::money::has_ambient_rates() {
1048                                format!("no exchange rate for {} or {}", m.currency.code, to.code)
1049                            } else {
1050                                "no exchange rates in scope (set a rate first)".to_string()
1051                            }
1052                        })
1053                }
1054                _ => Err(format!(
1055                    "to_currency() requires money and a currency code, got {} and {}",
1056                    money.type_name(),
1057                    code_arg.type_name()
1058                )),
1059            }
1060        }
1061        BuiltinId::Convert => {
1062            let unit_arg = args.remove(1);
1063            let value = args.remove(0);
1064            let unit = match &unit_arg {
1065                RuntimeValue::Text(s) => logicaffeine_base::quantity::units::by_name(s)
1066                    .ok_or_else(|| format!("Unknown unit '{}'", s))?,
1067                _ => return Err(format!("convert() requires a unit name (Text), got {}", unit_arg.type_name())),
1068            };
1069            match &value {
1070                // Same dimension → re-express (the SI magnitude is unchanged, only the display unit);
1071                // a different dimension is the forbidden cast.
1072                RuntimeValue::Quantity(qv) => {
1073                    if qv.q.dimension() != unit.dimension {
1074                        return Err(format!(
1075                            "cannot convert a {} quantity to '{}' — different dimension",
1076                            qv.unit.symbol, unit.symbol
1077                        ));
1078                    }
1079                    Ok(RuntimeValue::Quantity(Rc::new(crate::interpreter::QuantityValue {
1080                        q: qv.q.clone(),
1081                        unit,
1082                    })))
1083                }
1084                _ => Err(format!("convert() requires a quantity, got {}", value.type_name())),
1085            }
1086        }
1087        BuiltinId::ParseTimestamp => {
1088            let val = args.remove(0);
1089            match &val {
1090                RuntimeValue::Text(s) => logicaffeine_base::temporal::parse_rfc3339(s.trim())
1091                    .map(RuntimeValue::Moment)
1092                    .ok_or_else(|| format!("Cannot parse '{}' as an RFC 3339 timestamp", s)),
1093                _ => Err(format!("parse_timestamp() requires a Text, got {}", val.type_name())),
1094            }
1095        }
1096        BuiltinId::FormatTimestamp => {
1097            let val = args.remove(0);
1098            match &val {
1099                RuntimeValue::Moment(nanos) => {
1100                    Ok(RuntimeValue::Text(Rc::new(logicaffeine_base::temporal::format_rfc3339(*nanos))))
1101                }
1102                _ => Err(format!("format_timestamp() requires a Moment, got {}", val.type_name())),
1103            }
1104        }
1105        BuiltinId::YearOf
1106        | BuiltinId::MonthOf
1107        | BuiltinId::DayOf
1108        | BuiltinId::WeekdayOf
1109        | BuiltinId::WeekOf
1110        | BuiltinId::QuarterOf
1111        | BuiltinId::HourOf
1112        | BuiltinId::MinuteOf
1113        | BuiltinId::SecondOf => {
1114            let val = args.remove(0);
1115            match &val {
1116                RuntimeValue::Moment(nanos) => {
1117                    use logicaffeine_base::temporal;
1118                    let civil = temporal::civil_from_unix_nanos(*nanos);
1119                    let component = match id {
1120                        BuiltinId::YearOf => civil.year,
1121                        BuiltinId::MonthOf => civil.month as i64,
1122                        BuiltinId::DayOf => civil.day as i64,
1123                        BuiltinId::HourOf => civil.hour as i64,
1124                        BuiltinId::MinuteOf => civil.minute as i64,
1125                        BuiltinId::SecondOf => civil.second as i64,
1126                        BuiltinId::WeekdayOf => {
1127                            temporal::weekday_from_days(nanos.div_euclid(temporal::NANOS_PER_DAY)) as i64
1128                        }
1129                        BuiltinId::WeekOf => {
1130                            temporal::iso_week_from_days(nanos.div_euclid(temporal::NANOS_PER_DAY)).1 as i64
1131                        }
1132                        BuiltinId::QuarterOf => (civil.month as i64 - 1) / 3 + 1,
1133                        _ => unreachable!(),
1134                    };
1135                    Ok(RuntimeValue::Int(component))
1136                }
1137                // The same accessors on a calendar Date (no time-of-day, so hour/minute/second
1138                // don't apply) — `the year of 2024-03-10`.
1139                RuntimeValue::Date(days) => {
1140                    use logicaffeine_base::temporal;
1141                    let (y, m, d) = temporal::civil_from_days(*days as i64);
1142                    let component = match id {
1143                        BuiltinId::YearOf => y,
1144                        BuiltinId::MonthOf => m as i64,
1145                        BuiltinId::DayOf => d as i64,
1146                        BuiltinId::WeekdayOf => temporal::weekday_from_days(*days as i64) as i64,
1147                        BuiltinId::WeekOf => temporal::iso_week_from_days(*days as i64).1 as i64,
1148                        BuiltinId::QuarterOf => (m as i64 - 1) / 3 + 1,
1149                        BuiltinId::HourOf | BuiltinId::MinuteOf | BuiltinId::SecondOf => {
1150                            return Err("a Date has no time-of-day — use a timestamp/Moment".to_string());
1151                        }
1152                        _ => unreachable!(),
1153                    };
1154                    Ok(RuntimeValue::Int(component))
1155                }
1156                _ => Err(format!("a date component extractor requires a Moment or Date, got {}", val.type_name())),
1157            }
1158        }
1159        BuiltinId::DateOf => {
1160            let val = args.remove(0);
1161            match &val {
1162                // The calendar day a Moment falls on (UTC); a Date is already a date (identity).
1163                RuntimeValue::Moment(nanos) => Ok(RuntimeValue::Date(
1164                    nanos.div_euclid(logicaffeine_base::temporal::NANOS_PER_DAY) as i32,
1165                )),
1166                RuntimeValue::Date(days) => Ok(RuntimeValue::Date(*days)),
1167                _ => Err(format!("date_of() requires a Moment or Date, got {}", val.type_name())),
1168            }
1169        }
1170        BuiltinId::TimeOf => {
1171            let val = args.remove(0);
1172            match &val {
1173                // The wall-clock time-of-day a Moment falls on (UTC). A Date has no time-of-day.
1174                RuntimeValue::Moment(nanos) => Ok(RuntimeValue::Time(
1175                    nanos.rem_euclid(logicaffeine_base::temporal::NANOS_PER_DAY),
1176                )),
1177                RuntimeValue::Date(_) => {
1178                    Err("a Date has no time-of-day — use a timestamp/Moment".to_string())
1179                }
1180                _ => Err(format!("time_of() requires a Moment, got {}", val.type_name())),
1181            }
1182        }
1183        BuiltinId::SecondsBetween => {
1184            let b = args.remove(1);
1185            let a = args.remove(0);
1186            match (&a, &b) {
1187                (RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => {
1188                    Ok(RuntimeValue::Int((b - a) / 1_000_000_000))
1189                }
1190                _ => Err(format!(
1191                    "seconds_between() requires two Moments, got {} and {}",
1192                    a.type_name(),
1193                    b.type_name()
1194                )),
1195            }
1196        }
1197        BuiltinId::MonthsBetween | BuiltinId::YearsBetween => {
1198            let b = args.remove(1);
1199            let a = args.remove(0);
1200            match (&a, &b) {
1201                (RuntimeValue::Moment(a), RuntimeValue::Moment(b)) => {
1202                    let n = if matches!(id, BuiltinId::MonthsBetween) {
1203                        logicaffeine_base::temporal::months_between(*a, *b)
1204                    } else {
1205                        logicaffeine_base::temporal::years_between(*a, *b)
1206                    };
1207                    Ok(RuntimeValue::Int(n))
1208                }
1209                _ => {
1210                    let name = if matches!(id, BuiltinId::MonthsBetween) {
1211                        "months_between"
1212                    } else {
1213                        "years_between"
1214                    };
1215                    Err(format!(
1216                        "{name}() requires two Moments, got {} and {}",
1217                        a.type_name(),
1218                        b.type_name()
1219                    ))
1220                }
1221            }
1222        }
1223        BuiltinId::AddSeconds => {
1224            let secs = args.remove(1);
1225            let moment = args.remove(0);
1226            match (&moment, &secs) {
1227                (RuntimeValue::Moment(nanos), RuntimeValue::Int(n)) => {
1228                    Ok(RuntimeValue::Moment(nanos + n * 1_000_000_000))
1229                }
1230                _ => Err(format!(
1231                    "add_seconds() requires a Moment and an Int, got {} and {}",
1232                    moment.type_name(),
1233                    secs.type_name()
1234                )),
1235            }
1236        }
1237        BuiltinId::InZone => {
1238            let zone = args.remove(1);
1239            let moment = args.remove(0);
1240            match (&moment, &zone) {
1241                (RuntimeValue::Moment(nanos), RuntimeValue::Text(z)) => {
1242                    logicaffeine_base::temporal::format_zoned(*nanos, z)
1243                        .map(|s| RuntimeValue::Text(Rc::new(s)))
1244                        .ok_or_else(|| format!("Unknown time zone '{}'", z))
1245                }
1246                _ => Err(format!(
1247                    "in_zone() requires a Moment and a zone name (Text), got {} and {}",
1248                    moment.type_name(),
1249                    zone.type_name()
1250                )),
1251            }
1252        }
1253        BuiltinId::LocalInstant => {
1254            let zone = args.remove(1);
1255            let moment = args.remove(0);
1256            match (&moment, &zone) {
1257                (RuntimeValue::Moment(nanos), RuntimeValue::Text(z)) => {
1258                    logicaffeine_base::temporal::local_instant_nanos(*nanos, z)
1259                        .map(RuntimeValue::Moment)
1260                        .ok_or_else(|| format!("Unknown time zone '{}'", z))
1261                }
1262                _ => Err(format!(
1263                    "local_instant() requires a Moment and a zone name (Text), got {} and {}",
1264                    moment.type_name(),
1265                    zone.type_name()
1266                )),
1267            }
1268        }
1269        BuiltinId::Copy => {
1270            let val = args.remove(0);
1271            Ok(val.deep_clone())
1272        }
1273        BuiltinId::CountOnes => {
1274            let val = args.remove(0);
1275            match val {
1276                // Two's-complement faithful: count set bits over the full 64-bit
1277                // pattern (matches codegen's `(x as u64).count_ones()`).
1278                RuntimeValue::Int(n) => Ok(RuntimeValue::Int((n as u64).count_ones() as i64)),
1279                _ => Err(format!(
1280                    "count_ones() requires an Int, got {}",
1281                    val.type_name()
1282                )),
1283            }
1284        }
1285        BuiltinId::Word32 => {
1286            let n = args.remove(0);
1287            match n {
1288                RuntimeValue::Int(v) => Ok(RuntimeValue::Word(WordVal::W32(Word32(v as u32)))),
1289                RuntimeValue::Word(w) => Ok(RuntimeValue::Word(WordVal::W32(Word32(w.to_u64() as u32)))),
1290                _ => Err(format!("word32() requires an Int, got {}", n.type_name())),
1291            }
1292        }
1293        BuiltinId::Word64 => {
1294            let n = args.remove(0);
1295            match n {
1296                RuntimeValue::Int(v) => Ok(RuntimeValue::Word(WordVal::W64(Word64(v as u64)))),
1297                RuntimeValue::Word(w) => Ok(RuntimeValue::Word(WordVal::W64(Word64(w.to_u64())))),
1298                _ => Err(format!("word64() requires an Int, got {}", n.type_name())),
1299            }
1300        }
1301        BuiltinId::Rotl | BuiltinId::Rotr => {
1302            let w = args.remove(0);
1303            let n = args.remove(0);
1304            let count = match n {
1305                RuntimeValue::Int(c) => c as u32,
1306                RuntimeValue::Word(c) => c.to_u64() as u32,
1307                _ => return Err(format!("rotate count must be an Int, got {}", n.type_name())),
1308            };
1309            match w {
1310                RuntimeValue::Word(word) => {
1311                    let r = if matches!(id, BuiltinId::Rotl) { word.rotl(count) } else { word.rotr(count) };
1312                    Ok(RuntimeValue::Word(r))
1313                }
1314                // A SIMD lane vector rotates lane-wise (the ChaCha diffusion op). Only left rotation
1315                // is part of the lane vocabulary so far.
1316                RuntimeValue::Lanes(lanes) if matches!(id, BuiltinId::Rotl) => {
1317                    match lanes.rotl(count) {
1318                        Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1319                        None => Err(format!("rotl is not defined for {}", lanes.type_name())),
1320                    }
1321                }
1322                _ => Err(format!("rotate requires a Word, got {}", w.type_name())),
1323            }
1324        }
1325        BuiltinId::Wand | BuiltinId::Wor => {
1326            let b = args.remove(1);
1327            let a = args.remove(0);
1328            match (&a, &b) {
1329                (RuntimeValue::Word(x), RuntimeValue::Word(y)) => {
1330                    let r = if id == BuiltinId::Wand { x.bitand(*y) } else { x.bitor(*y) };
1331                    match r {
1332                        Some(w) => Ok(RuntimeValue::Word(w)),
1333                        None => Err(format!("word_and/or width mismatch: {} vs {}", a.type_name(), b.type_name())),
1334                    }
1335                }
1336                // Lane-vector bitwise mixing (the MD5 F/G functions written over `Lanes8Word32`).
1337                (RuntimeValue::Lanes(x), RuntimeValue::Lanes(y)) => {
1338                    let r = if id == BuiltinId::Wand { (**x).bitand(**y) } else { (**x).bitor(**y) };
1339                    match r {
1340                        Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1341                        None => Err(format!(
1342                            "word_and/or lane-config mismatch: {} vs {}",
1343                            a.type_name(),
1344                            b.type_name()
1345                        )),
1346                    }
1347                }
1348                _ => Err(format!("word_and/or requires two Words, got {} and {}", a.type_name(), b.type_name())),
1349            }
1350        }
1351        BuiltinId::Wnot => {
1352            let a = args.remove(0);
1353            match &a {
1354                RuntimeValue::Word(x) => Ok(RuntimeValue::Word(x.not())),
1355                RuntimeValue::Lanes(x) => match (**x).lane_not() {
1356                    Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1357                    None => Err(format!("word_not: lane config has no complement: {}", a.type_name())),
1358                },
1359                _ => Err(format!("word_not requires a Word, got {}", a.type_name())),
1360            }
1361        }
1362        BuiltinId::Lanes4Word32Make => {
1363            // Pack a Seq of (up to) 4 Word32 into the 128-bit SHA-1 lane register.
1364            let s = args.remove(0);
1365            match s {
1366                RuntimeValue::List(items) => {
1367                    let vals = items.borrow().to_values();
1368                    let mut words = Vec::with_capacity(vals.len());
1369                    for v in &vals {
1370                        match v {
1371                            RuntimeValue::Word(WordVal::W32(w)) => words.push(*w),
1372                            RuntimeValue::Int(n) => words.push(Word32(*n as u32)),
1373                            other => {
1374                                return Err(format!(
1375                                    "lanes4Word32() needs a Seq of Word32, found {}",
1376                                    other.type_name()
1377                                ))
1378                            }
1379                        }
1380                    }
1381                    Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L4W32(
1382                        logicaffeine_base::Lanes4Word32::from_words(&words),
1383                    ))))
1384                }
1385                other => Err(format!("lanes4Word32() requires a Seq of Word32, got {}", other.type_name())),
1386            }
1387        }
1388        BuiltinId::Lanes4Of => {
1389            // Pack four Word32 values straight into the lane register — no Seq, no heap. This is the
1390            // per-round constructor the Logos SHA-1 uses, so it must not allocate.
1391            let mut w = [Word32(0); 4];
1392            for slot in w.iter_mut() {
1393                *slot = match args.remove(0) {
1394                    RuntimeValue::Word(WordVal::W32(x)) => x,
1395                    RuntimeValue::Int(n) => Word32(n as u32),
1396                    other => {
1397                        return Err(format!("lanes4Of() needs four Word32, found {}", other.type_name()))
1398                    }
1399                };
1400            }
1401            Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L4W32(
1402                logicaffeine_base::Lanes4Word32::from_words(&w),
1403            ))))
1404        }
1405        BuiltinId::SeqOfLanes4W32 => {
1406            let v = args.remove(0);
1407            match v {
1408                RuntimeValue::Lanes(lanes) => match *lanes {
1409                    LanesVal::L4W32(lv) => {
1410                        let vals: Vec<RuntimeValue> = lv
1411                            .to_words()
1412                            .iter()
1413                            .map(|w| RuntimeValue::Word(WordVal::W32(*w)))
1414                            .collect();
1415                        Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(
1416                            crate::interpreter::ListRepr::from_values(vals),
1417                        ))))
1418                    }
1419                    other => Err(format!("seqOfLanes4W32() requires a Lanes4Word32, got {}", other.type_name())),
1420                },
1421                other => Err(format!("seqOfLanes4W32() requires a Lanes4Word32, got {}", other.type_name())),
1422            }
1423        }
1424        BuiltinId::Sha1Rnds4 => {
1425            let func = args.remove(2);
1426            let msg = args.remove(1);
1427            let abcd = args.remove(0);
1428            let f = match &func {
1429                RuntimeValue::Int(n) => *n as u32,
1430                _ => return Err(format!("sha1rnds4() func must be an Int, got {}", func.type_name())),
1431            };
1432            match (&abcd, &msg) {
1433                (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) => (**a)
1434                    .sha1rnds4(**b, f)
1435                    .map(|r| RuntimeValue::Lanes(Rc::new(r)))
1436                    .ok_or_else(|| "sha1rnds4() requires two Lanes4Word32".to_string()),
1437                _ => Err(format!(
1438                    "sha1rnds4() requires two Lanes4Word32, got {} and {}",
1439                    abcd.type_name(),
1440                    msg.type_name()
1441                )),
1442            }
1443        }
1444        BuiltinId::Sha1Msg1 | BuiltinId::Sha1Msg2 | BuiltinId::Sha1Nexte => {
1445            let b = args.remove(1);
1446            let a = args.remove(0);
1447            match (&a, &b) {
1448                (RuntimeValue::Lanes(x), RuntimeValue::Lanes(y)) => {
1449                    let r = match id {
1450                        BuiltinId::Sha1Msg1 => (**x).sha1msg1(**y),
1451                        BuiltinId::Sha1Msg2 => (**x).sha1msg2(**y),
1452                        _ => (**x).sha1nexte(**y),
1453                    };
1454                    r.map(|v| RuntimeValue::Lanes(Rc::new(v)))
1455                        .ok_or_else(|| "sha1msg/nexte requires two Lanes4Word32".to_string())
1456                }
1457                _ => Err(format!(
1458                    "sha1 message op requires two Lanes4Word32, got {} and {}",
1459                    a.type_name(),
1460                    b.type_name()
1461                )),
1462            }
1463        }
1464        BuiltinId::Lanes16Word8Make => {
1465            // Pack a Seq of Int (bytes) into one byte-shuffle register — the SIMD hex codec loads its
1466            // 16 working bytes this way.
1467            let bytes = byte_seq(&args.remove(0))?;
1468            Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L16W8(
1469                logicaffeine_base::Lanes16Word8::from_bytes(&bytes),
1470            ))))
1471        }
1472        BuiltinId::SeqOfLanes16W8 => {
1473            let v = args.remove(0);
1474            match v {
1475                RuntimeValue::Lanes(lanes) => match *lanes {
1476                    LanesVal::L16W8(lv) => Ok(bytes_to_seq(&lv.to_bytes())),
1477                    other => Err(format!(
1478                        "seqOfLanes16W8() requires a Lanes16Word8, got {}",
1479                        other.type_name()
1480                    )),
1481                },
1482                other => Err(format!(
1483                    "seqOfLanes16W8() requires a Lanes16Word8, got {}",
1484                    other.type_name()
1485                )),
1486            }
1487        }
1488        BuiltinId::Splat16Word8 => match args.remove(0) {
1489            RuntimeValue::Int(n) => Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L16W8(
1490                logicaffeine_base::Lanes16Word8::splat(n as u8),
1491            )))),
1492            other => Err(format!("splat16Word8() requires an Int, got {}", other.type_name())),
1493        },
1494        BuiltinId::Shuffle16
1495        | BuiltinId::InterleaveLo16
1496        | BuiltinId::InterleaveHi16
1497        | BuiltinId::ByteAdd16
1498        | BuiltinId::Maddubs16
1499        | BuiltinId::Packus16 => {
1500            let b = args.remove(1);
1501            let a = args.remove(0);
1502            match (&a, &b) {
1503                (RuntimeValue::Lanes(x), RuntimeValue::Lanes(y)) => {
1504                    let r = match id {
1505                        BuiltinId::Shuffle16 => (**x).shuffle(**y),
1506                        BuiltinId::InterleaveLo16 => (**x).interleave_lo(**y),
1507                        BuiltinId::InterleaveHi16 => (**x).interleave_hi(**y),
1508                        BuiltinId::ByteAdd16 => (**x).byte_add(**y),
1509                        BuiltinId::Maddubs16 => (**x).maddubs_bytes(**y),
1510                        _ => (**x).packus_bytes(**y),
1511                    };
1512                    r.map(|v| RuntimeValue::Lanes(Rc::new(v)))
1513                        .ok_or_else(|| "byte-lane op requires two Lanes16Word8".to_string())
1514                }
1515                _ => Err(format!(
1516                    "byte-lane op requires two Lanes16Word8, got {} and {}",
1517                    a.type_name(),
1518                    b.type_name()
1519                )),
1520            }
1521        }
1522        BuiltinId::ShrBytes16 => {
1523            let n = args.remove(1);
1524            let v = args.remove(0);
1525            match (&v, &n) {
1526                (RuntimeValue::Lanes(x), RuntimeValue::Int(k)) => (**x)
1527                    .shr_bytes(*k as u32)
1528                    .map(|r| RuntimeValue::Lanes(Rc::new(r)))
1529                    .ok_or_else(|| "shrBytes16() requires a Lanes16Word8".to_string()),
1530                _ => Err(format!(
1531                    "shrBytes16() requires a Lanes16Word8 and an Int, got {} and {}",
1532                    v.type_name(),
1533                    n.type_name()
1534                )),
1535            }
1536        }
1537        BuiltinId::Lanes8Word32 => {
1538            // Pack a Seq of Word32 (or Int) into one 8-lane SIMD vector — the constructor a Logos
1539            // lane kernel uses to load its working state.
1540            let s = args.remove(0);
1541            match s {
1542                RuntimeValue::List(items) => {
1543                    let vals = items.borrow().to_values();
1544                    let mut words = Vec::with_capacity(vals.len());
1545                    for v in &vals {
1546                        match v {
1547                            RuntimeValue::Word(WordVal::W32(w)) => words.push(*w),
1548                            RuntimeValue::Int(n) => words.push(Word32(*n as u32)),
1549                            other => {
1550                                return Err(format!(
1551                                    "lanes8Word32() needs a Seq of Word32, found {}",
1552                                    other.type_name()
1553                                ))
1554                            }
1555                        }
1556                    }
1557                    Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L8W32(
1558                        logicaffeine_base::Lanes8Word32::from_words(&words),
1559                    ))))
1560                }
1561                other => Err(format!(
1562                    "lanes8Word32() requires a Seq of Word32, got {}",
1563                    other.type_name()
1564                )),
1565            }
1566        }
1567        BuiltinId::IntOfWord32 => {
1568            // The unsigned value of a Word32 as an Int (0..2³²−1) — for byte serialization.
1569            let x = args.remove(0);
1570            match x {
1571                RuntimeValue::Word(w) => Ok(RuntimeValue::Int(w.to_u64() as i64)),
1572                RuntimeValue::Int(n) => Ok(RuntimeValue::Int(n)),
1573                other => Err(format!("intOfWord32() requires a Word32, got {}", other.type_name())),
1574            }
1575        }
1576        BuiltinId::IntOfWord64 => {
1577            // The value of a Word64 as an Int (Keccak byte-masked lanes).
1578            let x = args.remove(0);
1579            match x {
1580                RuntimeValue::Word(w) => Ok(RuntimeValue::Int(w.to_u64() as i64)),
1581                RuntimeValue::Int(n) => Ok(RuntimeValue::Int(n)),
1582                other => Err(format!("intOfWord64() requires a Word64, got {}", other.type_name())),
1583            }
1584        }
1585        BuiltinId::Word64Shl | BuiltinId::Word64Shr => {
1586            let is_shl = matches!(id, BuiltinId::Word64Shl);
1587            let w = args.remove(0);
1588            let n = args.remove(0);
1589            let wv = match w {
1590                RuntimeValue::Word(w) => w.to_u64(),
1591                RuntimeValue::Int(v) => v as u64,
1592                other => return Err(format!("word64 shift requires a Word64, got {}", other.type_name())),
1593            };
1594            let nv = match n {
1595                RuntimeValue::Int(v) => v as u32,
1596                other => return Err(format!("word64 shift amount requires an Int, got {}", other.type_name())),
1597            };
1598            let r = if is_shl { wv.wrapping_shl(nv) } else { wv.wrapping_shr(nv) };
1599            Ok(RuntimeValue::Word(WordVal::W64(Word64(r))))
1600        }
1601        BuiltinId::Word64And => {
1602            let a = args.remove(0);
1603            let b = args.remove(0);
1604            let av = match a {
1605                RuntimeValue::Word(w) => w.to_u64(),
1606                RuntimeValue::Int(v) => v as u64,
1607                other => return Err(format!("word64And requires a Word64, got {}", other.type_name())),
1608            };
1609            let bv = match b {
1610                RuntimeValue::Word(w) => w.to_u64(),
1611                RuntimeValue::Int(v) => v as u64,
1612                other => return Err(format!("word64And requires a Word64, got {}", other.type_name())),
1613            };
1614            Ok(RuntimeValue::Word(WordVal::W64(Word64(av & bv))))
1615        }
1616        BuiltinId::Word32Shr => {
1617            let w = args.remove(0);
1618            let n = args.remove(0);
1619            let wv = match w {
1620                RuntimeValue::Word(word) => word.to_u64() as u32,
1621                RuntimeValue::Int(v) => v as u32,
1622                other => return Err(format!("word32Shr requires a Word32, got {}", other.type_name())),
1623            };
1624            let nv = match n {
1625                RuntimeValue::Int(v) => v as u32,
1626                other => return Err(format!("word32Shr amount requires an Int, got {}", other.type_name())),
1627            };
1628            Ok(RuntimeValue::Word(WordVal::W32(Word32(wv.wrapping_shr(nv)))))
1629        }
1630        BuiltinId::Word16Make => {
1631            // Low 16 bits of an Int as a Word16 (ℤ/2¹⁶). No distinct W16 runtime carrier, so it is
1632            // held in a Word32 with the value in [0, 2¹⁶) — exactly how lanes16Word16/intOfWord16 read it.
1633            let n = args.remove(0);
1634            match n {
1635                RuntimeValue::Int(v) => Ok(RuntimeValue::Word(WordVal::W32(Word32(v as u16 as u32)))),
1636                RuntimeValue::Word(w) => Ok(RuntimeValue::Word(WordVal::W32(Word32(w.to_u64() as u16 as u32)))),
1637                other => Err(format!("word16() requires an Int, got {}", other.type_name())),
1638            }
1639        }
1640        BuiltinId::IntOfWord16 => {
1641            // The unsigned value of a Word16 as an Int (0..2¹⁶−1).
1642            let x = args.remove(0);
1643            match x {
1644                RuntimeValue::Word(w) => Ok(RuntimeValue::Int(w.to_u64() as u16 as i64)),
1645                RuntimeValue::Int(n) => Ok(RuntimeValue::Int(n as u16 as i64)),
1646                other => Err(format!("intOfWord16() requires a Word16, got {}", other.type_name())),
1647            }
1648        }
1649        BuiltinId::Lanes4Word64 => {
1650            // Pack a Seq of Word64/Int into one 4-lane u64 vector (the Poly1305 limb working set).
1651            let s = args.remove(0);
1652            match s {
1653                RuntimeValue::List(items) => {
1654                    let vals = items.borrow().to_values();
1655                    let mut words = Vec::with_capacity(vals.len());
1656                    for v in &vals {
1657                        match v {
1658                            RuntimeValue::Word(WordVal::W64(w)) => words.push(*w),
1659                            RuntimeValue::Int(n) => words.push(Word64(*n as u64)),
1660                            other => {
1661                                return Err(format!(
1662                                    "lanes4Word64() needs a Seq of Word64/Int, found {}",
1663                                    other.type_name()
1664                                ))
1665                            }
1666                        }
1667                    }
1668                    Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L4W64(
1669                        logicaffeine_base::Lanes4Word64::from_words(&words),
1670                    ))))
1671                }
1672                other => Err(format!("lanes4Word64() requires a Seq, got {}", other.type_name())),
1673            }
1674        }
1675        BuiltinId::SeqOfLanes4 => {
1676            // Unpack a lane vector into a Seq of its lanes as Int.
1677            let v = args.remove(0);
1678            match v {
1679                RuntimeValue::Lanes(lanes) => {
1680                    let vals: Vec<RuntimeValue> = (0..lanes.lanes())
1681                        .map(|i| RuntimeValue::Int(lanes.lane(i) as i64))
1682                        .collect();
1683                    Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(
1684                        crate::interpreter::ListRepr::from_values(vals),
1685                    ))))
1686                }
1687                other => Err(format!("seqOfLanes4() requires a lane vector, got {}", other.type_name())),
1688            }
1689        }
1690        BuiltinId::Mul32x32To64 => {
1691            // Lane-wise widening multiply of the low 32 bits (vpmuludq) — the Poly1305 limb product.
1692            let a = args.remove(0);
1693            let b = args.remove(0);
1694            match (a, b) {
1695                (RuntimeValue::Lanes(la), RuntimeValue::Lanes(lb)) => match la.mul_lo32_wide(*lb) {
1696                    Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1697                    None => Err(format!(
1698                        "mul32x32to64 requires two Lanes4Word64, got {} and {}",
1699                        la.type_name(),
1700                        lb.type_name()
1701                    )),
1702                },
1703                (a, b) => Err(format!(
1704                    "mul32x32to64 requires two lane vectors, got {} and {}",
1705                    a.type_name(),
1706                    b.type_name()
1707                )),
1708            }
1709        }
1710        BuiltinId::HsumLanes4 => {
1711            // The horizontal sum of a lane vector's lanes, as an Int.
1712            let v = args.remove(0);
1713            match v {
1714                RuntimeValue::Lanes(lanes) => Ok(RuntimeValue::Int(lanes.hsum())),
1715                other => Err(format!("hsumLanes4 requires a lane vector, got {}", other.type_name())),
1716            }
1717        }
1718        BuiltinId::Splat4Word64 => {
1719            // Broadcast a Word64 into all 4 Keccak lanes. The 4-way lane Keccak is an AOT-only speed
1720            // path (the tree-walker/VM run the scalar `keccakF` over Word64 instead).
1721            Err("splat4Word64 compiles to an AVX2 lane broadcast — AOT only, not the interpreter".to_string())
1722        }
1723        BuiltinId::AndNot4 => {
1724            Err("andNot4 compiles to an AVX2 lane vpandn — AOT only, not the interpreter".to_string())
1725        }
1726        BuiltinId::Lanes16Word16 => {
1727            // Pack a Seq of Word16/Int into one 16-lane NTT coefficient vector.
1728            let s = args.remove(0);
1729            match s {
1730                RuntimeValue::List(items) => {
1731                    let vals = items.borrow().to_values();
1732                    let mut words = Vec::with_capacity(vals.len());
1733                    for v in &vals {
1734                        match v {
1735                            RuntimeValue::Word(WordVal::W32(w)) => words.push(Word16(w.0 as u16)),
1736                            RuntimeValue::Int(n) => words.push(Word16(*n as u16)),
1737                            other => {
1738                                return Err(format!(
1739                                    "lanes16Word16() needs a Seq of Word16/Int, found {}",
1740                                    other.type_name()
1741                                ))
1742                            }
1743                        }
1744                    }
1745                    Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L16W16(
1746                        logicaffeine_base::Lanes16Word16::from_words(&words),
1747                    ))))
1748                }
1749                other => Err(format!("lanes16Word16() requires a Seq, got {}", other.type_name())),
1750            }
1751        }
1752        BuiltinId::SeqOfLanes16 => {
1753            // Unpack a lane vector into a Seq of its lanes as Int (same as seqOfLanes4, any config).
1754            let v = args.remove(0);
1755            match v {
1756                RuntimeValue::Lanes(lanes) => {
1757                    let vals: Vec<RuntimeValue> = (0..lanes.lanes())
1758                        .map(|i| RuntimeValue::Int(lanes.lane(i) as i64))
1759                        .collect();
1760                    Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(
1761                        crate::interpreter::ListRepr::from_values(vals),
1762                    ))))
1763                }
1764                other => Err(format!("seqOfLanes16() requires a lane vector, got {}", other.type_name())),
1765            }
1766        }
1767        BuiltinId::Splat16Word16 => {
1768            // Broadcast a Word16/Int into all 16 lanes (the NTT loads a shared zeta/constant).
1769            let x = args.remove(0);
1770            let w = match x {
1771                RuntimeValue::Word(WordVal::W32(w)) => w.0 as u16,
1772                RuntimeValue::Int(n) => n as u16,
1773                other => {
1774                    return Err(format!("splat16Word16() requires a Word16/Int, got {}", other.type_name()))
1775                }
1776            };
1777            Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L16W16(
1778                logicaffeine_base::Lanes16Word16::splat(w),
1779            ))))
1780        }
1781        BuiltinId::Mulhi16 => {
1782            // Lane-wise SIGNED high-16 multiply (vpmulhw) — the Montgomery butterfly's mulhi.
1783            let a = args.remove(0);
1784            let b = args.remove(0);
1785            match (a, b) {
1786                (RuntimeValue::Lanes(la), RuntimeValue::Lanes(lb)) => match la.mulhi(*lb) {
1787                    Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1788                    None => Err(format!(
1789                        "mulhi16 requires two Lanes16Word16, got {} and {}",
1790                        la.type_name(),
1791                        lb.type_name()
1792                    )),
1793                },
1794                (a, b) => Err(format!(
1795                    "mulhi16 requires two lane vectors, got {} and {}",
1796                    a.type_name(),
1797                    b.type_name()
1798                )),
1799            }
1800        }
1801        BuiltinId::Montmul32 => {
1802            // The signed i32 Montgomery multiply (ML-DSA butterfly) — montmul32(a, b, q, qinv).
1803            let a = args.remove(0);
1804            let b = args.remove(0);
1805            let q = args.remove(0);
1806            let qi = args.remove(0);
1807            match (a, b, q, qi) {
1808                (
1809                    RuntimeValue::Lanes(la),
1810                    RuntimeValue::Lanes(lb),
1811                    RuntimeValue::Lanes(lq),
1812                    RuntimeValue::Lanes(lqi),
1813                ) => match la.montmul32(*lb, *lq, *lqi) {
1814                    Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1815                    None => Err(format!(
1816                        "montmul32 requires four Lanes8Word32, got {}, {}, {}, {}",
1817                        la.type_name(),
1818                        lb.type_name(),
1819                        lq.type_name(),
1820                        lqi.type_name()
1821                    )),
1822                },
1823                (a, b, q, qi) => Err(format!(
1824                    "montmul32 requires four lane vectors, got {}, {}, {}, {}",
1825                    a.type_name(),
1826                    b.type_name(),
1827                    q.type_name(),
1828                    qi.type_name()
1829                )),
1830            }
1831        }
1832        BuiltinId::NttBcastLo | BuiltinId::NttBcastHi => {
1833            // The within-vector NTT source duplications, at stride h (vperm2i128/vpshufd).
1834            let is_low = matches!(id, BuiltinId::NttBcastLo);
1835            let name = if is_low { "nttBcastLo" } else { "nttBcastHi" };
1836            let v = args.remove(0);
1837            let h = ntt_stride(args.remove(0), name)?;
1838            match v {
1839                RuntimeValue::Lanes(lv) => {
1840                    let r = if is_low { lv.ntt_bcast_lo(h) } else { lv.ntt_bcast_hi(h) };
1841                    match r {
1842                        Some(out) => Ok(RuntimeValue::Lanes(Rc::new(out))),
1843                        None => Err(format!("{} requires a Lanes16Word16, got {}", name, lv.type_name())),
1844                    }
1845                }
1846                other => Err(format!("{} requires a lane vector, got {}", name, other.type_name())),
1847            }
1848        }
1849        BuiltinId::NttBlend => {
1850            // Recombine the +/− halves of the within-vector butterfly, at stride h (vperm2i128/vpblendd).
1851            let a = args.remove(0);
1852            let b = args.remove(0);
1853            let h = ntt_stride(args.remove(0), "nttBlend")?;
1854            match (a, b) {
1855                (RuntimeValue::Lanes(la), RuntimeValue::Lanes(lb)) => match la.ntt_blend(*lb, h) {
1856                    Some(v) => Ok(RuntimeValue::Lanes(Rc::new(v))),
1857                    None => Err(format!(
1858                        "nttBlend requires two Lanes16Word16, got {} and {}",
1859                        la.type_name(),
1860                        lb.type_name()
1861                    )),
1862                },
1863                (a, b) => Err(format!(
1864                    "nttBlend requires two lane vectors, got {} and {}",
1865                    a.type_name(),
1866                    b.type_name()
1867                )),
1868            }
1869        }
1870        BuiltinId::Splat8Word32 => {
1871            // Broadcast a Word32 (or Int) into all 8 lanes.
1872            let x = args.remove(0);
1873            let w = match x {
1874                RuntimeValue::Word(WordVal::W32(w)) => w,
1875                RuntimeValue::Int(n) => Word32(n as u32),
1876                other => {
1877                    return Err(format!("splat8Word32() requires a Word32, got {}", other.type_name()))
1878                }
1879            };
1880            Ok(RuntimeValue::Lanes(Rc::new(LanesVal::L8W32(
1881                logicaffeine_base::Lanes8Word32::splat(w.0),
1882            ))))
1883        }
1884        BuiltinId::SeqOfLanes8 => {
1885            // Unpack a lane vector back into a Seq of 8 Word32 — read its lanes out.
1886            let v = args.remove(0);
1887            match v {
1888                RuntimeValue::Lanes(lanes) => match *lanes {
1889                    LanesVal::L8W32(lv) => {
1890                        let vals: Vec<RuntimeValue> = lv
1891                            .to_words()
1892                            .iter()
1893                            .map(|w| RuntimeValue::Word(WordVal::W32(*w)))
1894                            .collect();
1895                        Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(
1896                            crate::interpreter::ListRepr::from_values(vals),
1897                        ))))
1898                    }
1899                    other => Err(format!(
1900                        "seqOfLanes8() requires a Lanes8Word32, got {}",
1901                        other.type_name()
1902                    )),
1903                },
1904                other => Err(format!(
1905                    "seqOfLanes8() requires a Lanes8Word32, got {}",
1906                    other.type_name()
1907                )),
1908            }
1909        }
1910        BuiltinId::RunAccepted => {
1911            // run_accepted(fn, arg, lo, hi): the receiver's typed, bounded acceptance
1912            // contract for a SHIPPED computation — validate the function's shape and the
1913            // argument's range, then evaluate in the sandbox. Out-of-range is REFUSED, never
1914            // clamped; an ordinary (non-shipped) closure is refused at the signature check.
1915            let int_arg = |v: &RuntimeValue, what: &str| -> Result<i64, String> {
1916                match v {
1917                    RuntimeValue::Int(n) => Ok(*n),
1918                    other => Err(format!("run_accepted: {what} must be an Int, got {}", other.type_name())),
1919                }
1920            };
1921            let arg = int_arg(&args[1], "the argument")?;
1922            let lo = int_arg(&args[2], "the lower bound")?;
1923            let hi = int_arg(&args[3], "the upper bound")?;
1924            let contract = crate::semantics::acceptance::AcceptanceContract::new(lo, hi);
1925            contract.apply(&args[0], arg).map(RuntimeValue::Int)
1926        }
1927    }
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932    use super::*;
1933
1934    /// Every temporal extractor, on a Moment AND a Date, plus every error path — the interpreter
1935    /// dispatch shared by the tree-walker and the bytecode VM. If any accessor silently returns the
1936    /// wrong number or fails to reject a bad input, this fails.
1937    #[test]
1938    fn temporal_extractors_cover_every_component_and_reject_bad_inputs() {
1939        use logicaffeine_base::temporal;
1940        let call = |id, v: &RuntimeValue| call_builtin(id, vec![v.clone()]);
1941        let int = |id, v: &RuntimeValue| match call(id, v) {
1942            Ok(RuntimeValue::Int(n)) => n,
1943            other => panic!("{id:?} on {:?} -> {other:?}", v.type_name()),
1944        };
1945
1946        // 2024-03-10T07:30:45Z — a Sunday (weekday 0), ISO week 10, Q1.
1947        let m = RuntimeValue::Moment(temporal::parse_rfc3339("2024-03-10T07:30:45Z").unwrap());
1948        assert_eq!(int(BuiltinId::YearOf, &m), 2024);
1949        assert_eq!(int(BuiltinId::MonthOf, &m), 3);
1950        assert_eq!(int(BuiltinId::DayOf, &m), 10);
1951        assert_eq!(int(BuiltinId::WeekdayOf, &m), 0);
1952        assert_eq!(int(BuiltinId::HourOf, &m), 7);
1953        assert_eq!(int(BuiltinId::MinuteOf, &m), 30);
1954        assert_eq!(int(BuiltinId::SecondOf, &m), 45);
1955        assert_eq!(int(BuiltinId::WeekOf, &m), 10);
1956        assert_eq!(int(BuiltinId::QuarterOf, &m), 1);
1957        // date_of / time_of project onto the right runtime types.
1958        assert!(matches!(call(BuiltinId::DateOf, &m), Ok(RuntimeValue::Date(_))));
1959        assert!(matches!(call(BuiltinId::TimeOf, &m), Ok(RuntimeValue::Time(_))));
1960
1961        // The calendar accessors work identically on a bare Date...
1962        let days = (temporal::parse_rfc3339("2024-03-10T07:30:45Z").unwrap())
1963            .div_euclid(temporal::NANOS_PER_DAY) as i32;
1964        let d = RuntimeValue::Date(days);
1965        assert_eq!(int(BuiltinId::YearOf, &d), 2024);
1966        assert_eq!(int(BuiltinId::MonthOf, &d), 3);
1967        assert_eq!(int(BuiltinId::DayOf, &d), 10);
1968        assert_eq!(int(BuiltinId::WeekdayOf, &d), 0);
1969        assert_eq!(int(BuiltinId::WeekOf, &d), 10);
1970        assert_eq!(int(BuiltinId::QuarterOf, &d), 1);
1971        assert!(matches!(call(BuiltinId::DateOf, &d), Ok(RuntimeValue::Date(x)) if x == days));
1972
1973        // ...but a Date has NO time-of-day: clock accessors must ERROR, not silently return 0.
1974        for id in [BuiltinId::HourOf, BuiltinId::MinuteOf, BuiltinId::SecondOf, BuiltinId::TimeOf] {
1975            assert!(call(id, &d).unwrap_err().contains("no time-of-day"), "{id:?} should reject a Date");
1976        }
1977
1978        // Non-temporal inputs are rejected with a typed error — never a bogus number.
1979        let bogus = RuntimeValue::Int(5);
1980        for id in [BuiltinId::YearOf, BuiltinId::WeekOf, BuiltinId::QuarterOf, BuiltinId::DateOf, BuiltinId::TimeOf] {
1981            assert!(call(id, &bogus).is_err(), "{id:?} should reject a non-temporal value");
1982        }
1983
1984        // seconds_between requires two Moments, in either position.
1985        assert!(call_builtin(BuiltinId::SecondsBetween, vec![m.clone(), bogus.clone()]).is_err());
1986        assert!(call_builtin(BuiltinId::SecondsBetween, vec![bogus.clone(), m.clone()]).is_err());
1987
1988        // Pre-epoch (negative) Moments decompose by FLOOR division — no negative hour/second.
1989        let pre = RuntimeValue::Moment(temporal::parse_rfc3339("1969-12-31T23:59:58Z").unwrap());
1990        assert_eq!(int(BuiltinId::YearOf, &pre), 1969);
1991        assert_eq!(int(BuiltinId::MonthOf, &pre), 12);
1992        assert_eq!(int(BuiltinId::DayOf, &pre), 31);
1993        assert_eq!(int(BuiltinId::HourOf, &pre), 23);
1994        assert_eq!(int(BuiltinId::MinuteOf, &pre), 59);
1995        assert_eq!(int(BuiltinId::SecondOf, &pre), 58);
1996    }
1997
1998    /// `date_of` is the inverse-projection partner of the component accessors: extracting the date
1999    /// then re-reading its components must agree with reading them off the Moment directly.
2000    #[test]
2001    fn date_of_then_components_agree_with_the_moment() {
2002        use logicaffeine_base::temporal;
2003        for ts in ["2024-03-10T07:30:45Z", "2000-02-29T00:00:00Z", "1969-12-31T23:59:58Z"] {
2004            let m = RuntimeValue::Moment(temporal::parse_rfc3339(ts).unwrap());
2005            let d = call_builtin(BuiltinId::DateOf, vec![m.clone()]).unwrap();
2006            for id in [BuiltinId::YearOf, BuiltinId::MonthOf, BuiltinId::DayOf, BuiltinId::WeekdayOf, BuiltinId::WeekOf, BuiltinId::QuarterOf] {
2007                let from_moment = call_builtin(id, vec![m.clone()]).unwrap();
2008                let from_date = call_builtin(id, vec![d.clone()]).unwrap();
2009                assert_eq!(from_moment, from_date, "{id:?} disagrees for {ts}");
2010            }
2011        }
2012    }
2013
2014    #[test]
2015    fn run_accepted_validates_then_runs_a_shipped_computation() {
2016        use crate::concurrency::marshal::GenExpr;
2017        use crate::interpreter::ClosureValue;
2018        use std::collections::HashMap;
2019        use std::rc::Rc;
2020        // A shipped `3·x + 1` computation (what a peer ships via `Send computed`).
2021        let mk = || {
2022            let gen = GenExpr::Add(
2023                Box::new(GenExpr::Mul(Box::new(GenExpr::Index), Box::new(GenExpr::Const(3)))),
2024                Box::new(GenExpr::Const(1)),
2025            );
2026            RuntimeValue::Function(Box::new(ClosureValue {
2027                body_index: usize::MAX,
2028                captured_env: HashMap::default(),
2029                param_names: vec![logicaffeine_base::Symbol::from_index(0)],
2030                generated: Some(Rc::new(gen)),
2031            }))
2032        };
2033        // In-bounds (5 ∈ [0,1000]) → 3·5 + 1 = 16, run in the sandbox.
2034        match call_builtin(
2035            BuiltinId::RunAccepted,
2036            vec![mk(), RuntimeValue::Int(5), RuntimeValue::Int(0), RuntimeValue::Int(1000)],
2037        ) {
2038            Ok(RuntimeValue::Int(n)) => assert_eq!(n, 16),
2039            other => panic!("in-bounds run_accepted should return Int(16), got {other:?}"),
2040        }
2041        // Out-of-bounds (5000 ∉ [0,1000]) → refused, not clamped.
2042        assert!(
2043            call_builtin(
2044                BuiltinId::RunAccepted,
2045                vec![mk(), RuntimeValue::Int(5000), RuntimeValue::Int(0), RuntimeValue::Int(1000)],
2046            )
2047            .is_err(),
2048            "an out-of-range argument must be refused at the contract"
2049        );
2050    }
2051
2052    #[test]
2053    fn arity_messages_match_treewalker() {
2054        assert_eq!(
2055            check_arity(BuiltinId::Length, 2).unwrap_err(),
2056            "length() takes exactly 1 argument"
2057        );
2058        assert_eq!(
2059            check_arity(BuiltinId::Min, 1).unwrap_err(),
2060            "min() takes exactly 2 arguments"
2061        );
2062        assert!(check_arity(BuiltinId::Format, 0).is_ok());
2063        assert!(check_arity(BuiltinId::Format, 5).is_ok());
2064    }
2065
2066    #[test]
2067    fn parse_and_chr_messages() {
2068        let e = call_builtin(
2069            BuiltinId::ParseInt,
2070            vec![RuntimeValue::Text(Rc::new("zz".to_string()))],
2071        )
2072        .unwrap_err();
2073        assert_eq!(e, "Cannot parse 'zz' as Int");
2074        let e = call_builtin(BuiltinId::Chr, vec![RuntimeValue::Int(-1)]).unwrap_err();
2075        assert_eq!(e, "Invalid character code: -1");
2076        let r = call_builtin(BuiltinId::Chr, vec![RuntimeValue::Int(65)]).unwrap();
2077        assert!(matches!(&r, RuntimeValue::Text(s) if **s == "A"));
2078    }
2079
2080    #[test]
2081    fn numeric_builtins_coerce_like_treewalker() {
2082        assert!(matches!(
2083            call_builtin(BuiltinId::Sqrt, vec![RuntimeValue::Int(9)]).unwrap(),
2084            RuntimeValue::Float(f) if f == 3.0
2085        ));
2086        assert!(matches!(
2087            call_builtin(BuiltinId::Min, vec![RuntimeValue::Int(3), RuntimeValue::Float(2.5)]).unwrap(),
2088            RuntimeValue::Float(f) if f == 2.5
2089        ));
2090        assert!(matches!(
2091            call_builtin(BuiltinId::Floor, vec![RuntimeValue::Float(2.9)]).unwrap(),
2092            RuntimeValue::Int(2)
2093        ));
2094        assert!(matches!(
2095            call_builtin(BuiltinId::Pow, vec![RuntimeValue::Int(2), RuntimeValue::Int(10)]).unwrap(),
2096            RuntimeValue::Int(1024)
2097        ));
2098        // Negative Int exponent goes to float.
2099        assert!(matches!(
2100            call_builtin(BuiltinId::Pow, vec![RuntimeValue::Int(2), RuntimeValue::Int(-1)]).unwrap(),
2101            RuntimeValue::Float(f) if f == 0.5
2102        ));
2103    }
2104
2105    #[test]
2106    fn decimal_builtin_constructs_exact_money() {
2107        // decimal("19.99") is an exact, scale-preserving Decimal — the non-breaking entry
2108        // into the money tower.
2109        let d = call_builtin(BuiltinId::Decimal, vec![RuntimeValue::Text(Rc::new("19.99".into()))]).unwrap();
2110        assert!(matches!(&d, RuntimeValue::Decimal(_)));
2111        assert_eq!(d.to_display_string(), "19.99");
2112        // An Int widens to a scale-0 Decimal.
2113        let i = call_builtin(BuiltinId::Decimal, vec![RuntimeValue::Int(5)]).unwrap();
2114        assert!(matches!(&i, RuntimeValue::Decimal(_)));
2115        assert_eq!(i.to_display_string(), "5");
2116        // Garbage text is a clean error, never a panic.
2117        let e = call_builtin(BuiltinId::Decimal, vec![RuntimeValue::Text(Rc::new("zz".into()))]).unwrap_err();
2118        assert_eq!(e, "Cannot parse 'zz' as Decimal");
2119        // Arity is exactly one.
2120        assert_eq!(check_arity(BuiltinId::Decimal, 2).unwrap_err(), "decimal() takes exactly 1 argument");
2121    }
2122
2123    #[test]
2124    fn complex_builtin_constructs_exact_complex_numbers() {
2125        // complex(0, 1) is the imaginary unit i.
2126        let i = call_builtin(BuiltinId::Complex, vec![RuntimeValue::Int(0), RuntimeValue::Int(1)]).unwrap();
2127        assert!(matches!(&i, RuntimeValue::Complex(_)));
2128        assert_eq!(i.to_display_string(), "i");
2129        // complex(3, 4) → 3+4i; complex(0, -1) → -i.
2130        let z = call_builtin(BuiltinId::Complex, vec![RuntimeValue::Int(3), RuntimeValue::Int(4)]).unwrap();
2131        assert_eq!(z.to_display_string(), "3+4i");
2132        let neg_i = call_builtin(BuiltinId::Complex, vec![RuntimeValue::Int(0), RuntimeValue::Int(-1)]).unwrap();
2133        assert_eq!(neg_i.to_display_string(), "-i");
2134        let _ = i; // the imaginary unit; its i·i = −1 is covered in the arith tests.
2135        // An inexact Float part is refused — exactness preserved.
2136        assert!(call_builtin(BuiltinId::Complex, vec![RuntimeValue::Float(1.0), RuntimeValue::Int(1)]).is_err());
2137        // Arity is exactly two.
2138        assert_eq!(check_arity(BuiltinId::Complex, 1).unwrap_err(), "complex() takes exactly 2 arguments");
2139    }
2140
2141    #[test]
2142    fn decimal_and_complex_pass_through_the_numeric_builtins() {
2143        let d = |s: &str| RuntimeValue::Decimal(Rc::new(Decimal::parse(s).unwrap()));
2144        // abs preserves Decimal + scale; floor/ceil/round give the exact Int; min/max value-based.
2145        assert_eq!(call_builtin(BuiltinId::Abs, vec![d("-0.05")]).unwrap().to_display_string(), "0.05");
2146        assert_eq!(call_builtin(BuiltinId::Floor, vec![d("19.99")]).unwrap().to_display_string(), "19");
2147        assert_eq!(call_builtin(BuiltinId::Ceil, vec![d("19.01")]).unwrap().to_display_string(), "20");
2148        assert_eq!(call_builtin(BuiltinId::Round, vec![d("2.5")]).unwrap().to_display_string(), "3");
2149        assert_eq!(call_builtin(BuiltinId::Round, vec![d("-19.99")]).unwrap().to_display_string(), "-20");
2150        assert_eq!(call_builtin(BuiltinId::Min, vec![d("0.10"), d("0.2")]).unwrap().to_display_string(), "0.10");
2151        assert_eq!(call_builtin(BuiltinId::Max, vec![d("0.10"), d("0.2")]).unwrap().to_display_string(), "0.2");
2152        // |3+4i| = 5 (the modulus, a Float view of a generally-irrational magnitude).
2153        let z = RuntimeValue::Complex(Rc::new(logicaffeine_base::Complex::new(
2154            logicaffeine_base::Rational::from_i64(3),
2155            logicaffeine_base::Rational::from_i64(4),
2156        )));
2157        match call_builtin(BuiltinId::Abs, vec![z]).unwrap() {
2158            RuntimeValue::Float(f) => assert!((f - 5.0).abs() < 1e-12),
2159            other => panic!("expected a Float magnitude, got {other:?}"),
2160        }
2161    }
2162
2163    #[test]
2164    fn modular_builtin_constructs_and_exponentiates_in_the_ring() {
2165        // modular(10, 7) reduces to 3 (mod 7).
2166        let x = call_builtin(BuiltinId::Modular, vec![RuntimeValue::Int(10), RuntimeValue::Int(7)]).unwrap();
2167        assert!(matches!(&x, RuntimeValue::Modular(_)));
2168        assert_eq!(x.to_display_string(), "3 (mod 7)");
2169        // pow(modular(3,5), 4) = 81 ≡ 1 (mod 5) — fast modular exponentiation via the builtin.
2170        let base = call_builtin(BuiltinId::Modular, vec![RuntimeValue::Int(3), RuntimeValue::Int(5)]).unwrap();
2171        let p = call_builtin(BuiltinId::Pow, vec![base, RuntimeValue::Int(4)]).unwrap();
2172        assert_eq!(p.to_display_string(), "1 (mod 5)");
2173        // A non-positive modulus is refused; arity is exactly two.
2174        assert!(call_builtin(BuiltinId::Modular, vec![RuntimeValue::Int(3), RuntimeValue::Int(0)]).is_err());
2175        assert_eq!(check_arity(BuiltinId::Modular, 1).unwrap_err(), "modular() takes exactly 2 arguments");
2176    }
2177
2178    #[test]
2179    fn quantity_builtins_construct_convert_and_compute_exactly() {
2180        use crate::semantics::arith::{add, divide, multiply, subtract};
2181        let q = |v: i64, u: &str| {
2182            call_builtin(BuiltinId::Quantity, vec![RuntimeValue::Int(v), RuntimeValue::Text(Rc::new(u.into()))]).unwrap()
2183        };
2184        let conv = |x: RuntimeValue, u: &str| {
2185            call_builtin(BuiltinId::Convert, vec![x, RuntimeValue::Text(Rc::new(u.into()))])
2186        };
2187        // Construction carries the display unit.
2188        assert!(matches!(&q(2, "inch"), RuntimeValue::Quantity(_)));
2189        assert_eq!(q(2, "inch").to_display_string(), "2 in");
2190        assert_eq!(q(20, "celsius").to_display_string(), "20 °C");
2191        // THE GOLDEN: 2 inches + 5 centimeters, in feet, is EXACTLY 42/127 ft — no float.
2192        let sum = add(q(2, "inch"), q(5, "centimeter")).unwrap();
2193        assert_eq!(conv(sum, "foot").unwrap().to_display_string(), "42/127 ft");
2194        // Same-dimension subtraction keeps the left operand's unit; the magnitude is exact.
2195        assert_eq!(subtract(q(1, "meter"), q(50, "centimeter")).unwrap().to_display_string(), "1/2 m");
2196        // Scaling by a dimensionless number preserves the unit (× and ÷).
2197        assert_eq!(multiply(q(2, "inch"), RuntimeValue::Int(3)).unwrap().to_display_string(), "6 in");
2198        assert_eq!(multiply(RuntimeValue::Int(3), q(2, "inch")).unwrap().to_display_string(), "6 in");
2199        assert_eq!(divide(q(6, "inch"), RuntimeValue::Int(2)).unwrap().to_display_string(), "3 in");
2200        // Length × Length = Area, shown in dimension form until a named compound unit exists.
2201        assert_eq!(multiply(q(3, "meter"), q(4, "meter")).unwrap().to_display_string(), "12 L^2");
2202        // Length ÷ Time = Speed.
2203        assert_eq!(divide(q(100, "meter"), q(10, "second")).unwrap().to_display_string(), "10 L·T^-1");
2204        // Quantity equality is PHYSICAL: 100 cm == 1 m (display unit is presentation only).
2205        assert_eq!(q(100, "centimeter"), q(1, "meter"));
2206        // Dimension mismatch on + is a clean typed error (Length + Mass), never a silent coercion.
2207        assert!(add(q(1, "meter"), q(1, "kilogram")).is_err());
2208        // Converting across dimensions is the forbidden cast.
2209        assert!(conv(q(1, "meter"), "kilogram").is_err());
2210        // Unknown unit names and Float magnitudes are clean errors (exactness preserved).
2211        assert!(call_builtin(BuiltinId::Quantity,
2212            vec![RuntimeValue::Int(1), RuntimeValue::Text(Rc::new("zorgle".into()))]).is_err());
2213        assert!(call_builtin(BuiltinId::Quantity,
2214            vec![RuntimeValue::Float(1.5), RuntimeValue::Text(Rc::new("meter".into()))]).is_err());
2215        // Arity is exactly two for both builtins.
2216        assert_eq!(check_arity(BuiltinId::Quantity, 1).unwrap_err(), "quantity() takes exactly 2 arguments");
2217        assert_eq!(check_arity(BuiltinId::Convert, 3).unwrap_err(), "convert() takes exactly 2 arguments");
2218    }
2219
2220    #[test]
2221    fn abs_and_pow_are_exact_promoting_past_i64() {
2222        // Arrange / Act / Assert: |i64::MIN| = 2^63 has no i64 representation, so abs
2223        // promotes to the EXACT BigInt rather than wrapping back to i64::MIN.
2224        let abs_min = call_builtin(BuiltinId::Abs, vec![RuntimeValue::Int(i64::MIN)]).unwrap();
2225        assert_eq!(abs_min.to_display_string(), "9223372036854775808");
2226        assert_eq!(abs_min.type_name(), "Int", "a BigInt is still an integer type");
2227
2228        // 2^63 overflows i64 → exact BigInt (not the wrapped i64::MIN).
2229        let two_pow_63 = call_builtin(BuiltinId::Pow, vec![RuntimeValue::Int(2), RuntimeValue::Int(63)]).unwrap();
2230        assert_eq!(two_pow_63.to_display_string(), "9223372036854775808");
2231
2232        // A far-larger power stays exact (2^100 = 31 digits).
2233        let two_pow_100 = call_builtin(BuiltinId::Pow, vec![RuntimeValue::Int(2), RuntimeValue::Int(100)]).unwrap();
2234        assert_eq!(two_pow_100.to_display_string(), "1267650600228229401496703205376");
2235
2236        // In-range results stay narrow `Int` (downsizing is automatic).
2237        assert!(matches!(
2238            call_builtin(BuiltinId::Pow, vec![RuntimeValue::Int(3), RuntimeValue::Int(4)]).unwrap(),
2239            RuntimeValue::Int(81)
2240        ));
2241        // abs of an ordinary negative is unchanged.
2242        assert!(matches!(
2243            call_builtin(BuiltinId::Abs, vec![RuntimeValue::Int(-5)]).unwrap(),
2244            RuntimeValue::Int(5)
2245        ));
2246    }
2247
2248    #[test]
2249    fn copy_is_deep() {
2250        use std::cell::RefCell;
2251        let inner = std::rc::Rc::new(RefCell::new(
2252            crate::interpreter::ListRepr::from_values(vec![RuntimeValue::Int(1)]),
2253        ));
2254        let original = RuntimeValue::List(inner.clone());
2255        let copied = call_builtin(BuiltinId::Copy, vec![original]).unwrap();
2256        if let RuntimeValue::List(copied_items) = &copied {
2257            inner.borrow_mut().push(RuntimeValue::Int(2));
2258            assert_eq!(copied_items.borrow().len(), 1, "copy must not share the allocation");
2259        } else {
2260            panic!("copy changed the type");
2261        }
2262    }
2263}