Skip to main content

rune_interp/
lib.rs

1//! Tree-walking Nox interpreter — the instant-start execution path.
2//! Implements all 18 Nox reduction patterns directly over Noun trees.
3//! No external dependencies; no build phase; executes the moment parsing is done.
4//!
5//! Nox reduction rules (patterns 0–17):
6//!
7//! Structural (5):
8//!   [0 n]         axis: n=0 hash-introspect, n=1 identity, n=2 head, n=3 tail,
9//!                       n=2k → head(axis(k)), n=2k+1 → tail(axis(k))
10//!   [1 v]         quote: return v unchanged
11//!   [2 a b]       compose: eval both a,b against subject; reduce(result_a, result_b)
12//!   [3 a b]       cons: Noun::cell(eval(a), eval(b))
13//!   [4 test y n]  branch: eval test; if result==0 → eval y else → eval n
14//!
15//! Field arithmetic over F_p where p = GOLDILOCKS_PRIME (6):
16//!   [5 a b]       add: (eval_a + eval_b) % p
17//!   [6 a b]       sub: (eval_a - eval_b + p) % p
18//!   [7 a b]       mul: (eval_a * eval_b) % p
19//!   [8 a]         inv: modular inverse of eval_a under p (0 maps to 0)
20//!   [9 a b]       eq:  0 if equal, 1 if not (atoms and cells, structural)
21//!   [10 a b]      lt:  0 if eval_a < eval_b, 1 otherwise
22//!
23//! Bitwise over u64 (4):
24//!   [11 a b]      xor: eval_a XOR eval_b
25//!   [12 a b]      and: eval_a AND eval_b
26//!   [13 a]        not: bitwise NOT of eval_a
27//!   [14 a n]      shl: eval_a << eval_n
28//!
29//! Hash + async stubs (3):
30//!   [15 a]              hash: stub — return eval_a (real hemera hash in M2)
31//!   [16 [tag sel] body] hint/call: evaluate body; tag/selector are reactive metadata (M5)
32//!   [17 path]           look: stub for graph scry — return Atom(0) (M2)
33//!
34//! Distribution rule: when formula is [f1 f2] where f1 is itself a cell
35//! (not an atom opcode prefix), eval both sides against subject and return
36//! [result_f1, result_f2].  This is the "auto-cons" / distribute pattern.
37
38pub mod event;
39
40use rune_ast::Noun;
41use cyber_hemera as hemera;
42
43/// Goldilocks prime: 2^64 - 2^32 + 1
44const GOLDILOCKS_PRIME: u64 = 0xFFFF_FFFF_0000_0001u64;
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct InterpError {
48    pub message: String,
49}
50
51impl std::fmt::Display for InterpError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{}", self.message)
54    }
55}
56
57/// A host that performs the acts a runtime requests.
58///
59/// rune is pure: opcodes 0–15/17 cannot touch the world. The only escape is an
60/// **act** — opcode 16 with an act tag (see `rune_ast::act`). When the
61/// interpreter hits one it hands `(act, args, caps)` here; the host performs it
62/// (emit a chunk, query the graph, …) and returns the result noun, which the
63/// interpreter splices into the continuation. This is the seam the ward plugs
64/// into — see `cyb/root/ward.md`.
65pub trait Host {
66    fn perform(&mut self, act: u64, args: &Noun, caps: &Noun) -> Result<Noun, InterpError>;
67}
68
69/// A host that grants nothing — every act no-ops to `Atom(0)`. Used by the pure
70/// `eval` path, where acts cannot be performed.
71pub struct DenyHost;
72impl Host for DenyHost {
73    fn perform(&mut self, _act: u64, _args: &Noun, _caps: &Noun) -> Result<Noun, InterpError> {
74        Ok(Noun::Atom(0))
75    }
76}
77
78/// Evaluate a Nox formula against a subject noun (pure — acts no-op).
79///
80/// Patterns 2 (compose) and 4 (branch) use a tail-call loop to avoid
81/// stack overflow on deeply-nested formulas.
82pub fn eval(subject: &Noun, formula: &Noun) -> Result<Noun, InterpError> {
83    eval_with_host(subject, formula, &mut DenyHost)
84}
85
86/// Evaluate a Nox formula, performing acts through `host`.
87///
88/// Identical to `eval` except opcode-16 act tags are dispatched to the host
89/// (args evaluated, `~caps` read from axis 30, result spliced at the subject
90/// head for the continuation). Non-act opcode-16 tags remain hints (the M5
91/// passthrough: evaluate the body).
92pub fn eval_with_host(subject: &Noun, formula: &Noun, host: &mut dyn Host) -> Result<Noun, InterpError> {
93    let mut subj = subject.clone();
94    let mut form = formula.clone();
95
96    loop {
97        match form {
98            // Distribution rule: formula is a cell whose head is also a cell.
99            Noun::Cell(ref fh, ref ft) if matches!(fh.as_ref(), Noun::Cell(..)) => {
100                let h = eval_with_host(&subj, fh, host)?;
101                let t = eval_with_host(&subj, ft, host)?;
102                return Ok(Noun::cell(h, t));
103            }
104
105            Noun::Cell(ref op, ref rest) => match op.as_ref() {
106
107                // 0 — axis: navigate the noun tree
108                Noun::Atom(0) => return eval_axis(&subj, rest),
109
110                // 1 — quote: return the argument unchanged
111                Noun::Atom(1) => return Ok(*rest.clone()),
112
113                // 2 — compose
114                Noun::Atom(2) => {
115                    let (a, b) = pair(rest)?;
116                    let new_subj = eval_with_host(&subj, &a, host)?;
117                    let new_form = eval_with_host(&subj, &b, host)?;
118                    subj = new_subj;
119                    form = new_form;
120                    continue;
121                }
122
123                // 3 — cons
124                Noun::Atom(3) => {
125                    let (a, b) = pair(rest)?;
126                    let ha = eval_with_host(&subj, &a, host)?;
127                    let ta = eval_with_host(&subj, &b, host)?;
128                    return Ok(Noun::cell(ha, ta));
129                }
130
131                // 4 — branch
132                Noun::Atom(4) => {
133                    let (test_f, ynb) = pair(rest)?;
134                    let (yes_f, no_f) = pair(&ynb)?;
135                    let test_val = eval_with_host(&subj, &test_f, host)?;
136                    let test_n = atom_u64(&test_val, "nox-4 branch: test must be atom")?;
137                    form = if test_n == 0 { yes_f } else { no_f };
138                    continue;
139                }
140
141                // 5 — add
142                Noun::Atom(5) => {
143                    let (a, b) = pair(rest)?;
144                    let va = eval_atom_h(&subj, &a, host, "nox-5 add")?;
145                    let vb = eval_atom_h(&subj, &b, host, "nox-5 add")?;
146                    return Ok(Noun::Atom(add_field(va, vb)));
147                }
148
149                // 6 — sub
150                Noun::Atom(6) => {
151                    let (a, b) = pair(rest)?;
152                    let va = eval_atom_h(&subj, &a, host, "nox-6 sub")?;
153                    let vb = eval_atom_h(&subj, &b, host, "nox-6 sub")?;
154                    return Ok(Noun::Atom(sub_field(va, vb)));
155                }
156
157                // 7 — mul
158                Noun::Atom(7) => {
159                    let (a, b) = pair(rest)?;
160                    let va = eval_atom_h(&subj, &a, host, "nox-7 mul")?;
161                    let vb = eval_atom_h(&subj, &b, host, "nox-7 mul")?;
162                    return Ok(Noun::Atom(mul_field(va, vb)));
163                }
164
165                // 8 — inv
166                Noun::Atom(8) => {
167                    let va = eval_atom_h(&subj, rest, host, "nox-8 inv")?;
168                    return Ok(Noun::Atom(inv_field(va)));
169                }
170
171                // 9 — eq
172                Noun::Atom(9) => {
173                    let (a, b) = pair(rest)?;
174                    let ra = eval_with_host(&subj, &a, host)?;
175                    let rb = eval_with_host(&subj, &b, host)?;
176                    return Ok(Noun::Atom(if ra == rb { 0 } else { 1 }));
177                }
178
179                // 10 — lt
180                Noun::Atom(10) => {
181                    let (a, b) = pair(rest)?;
182                    let va = eval_atom_h(&subj, &a, host, "nox-10 lt")?;
183                    let vb = eval_atom_h(&subj, &b, host, "nox-10 lt")?;
184                    return Ok(Noun::Atom(if va < vb { 0 } else { 1 }));
185                }
186
187                // 11 — xor
188                Noun::Atom(11) => {
189                    let (a, b) = pair(rest)?;
190                    let va = eval_atom_h(&subj, &a, host, "nox-11 xor")?;
191                    let vb = eval_atom_h(&subj, &b, host, "nox-11 xor")?;
192                    return Ok(Noun::Atom(va ^ vb));
193                }
194
195                // 12 — and
196                Noun::Atom(12) => {
197                    let (a, b) = pair(rest)?;
198                    let va = eval_atom_h(&subj, &a, host, "nox-12 and")?;
199                    let vb = eval_atom_h(&subj, &b, host, "nox-12 and")?;
200                    return Ok(Noun::Atom(va & vb));
201                }
202
203                // 13 — not
204                Noun::Atom(13) => {
205                    let va = eval_atom_h(&subj, rest, host, "nox-13 not")?;
206                    return Ok(Noun::Atom(!va));
207                }
208
209                // 14 — shl
210                Noun::Atom(14) => {
211                    let (a, n) = pair(rest)?;
212                    let va = eval_atom_h(&subj, &a, host, "nox-14 shl")?;
213                    let vn = eval_atom_h(&subj, &n, host, "nox-14 shl")?;
214                    return Ok(Noun::Atom(va << (vn & 63)));
215                }
216
217                // 15 — hash
218                Noun::Atom(15) => {
219                    let r = eval_with_host(&subj, rest, host)?;
220                    return Ok(Noun::Atom(hash_noun(&r)));
221                }
222
223                // 16 — act / hint.
224                //   rest = [[tag args-f] cont]
225                // If `tag` is an act (rune_ast::act): evaluate args, read ~caps
226                // (axis 30), perform via host, splice result at subject head,
227                // continue `cont`. Otherwise it is a hint — M5 passthrough
228                // (evaluate the body; true parking lives in `event::eval_step`).
229                Noun::Atom(16) => {
230                    let (meta, cont) = pair(rest)?;
231                    let (tag_noun, arg_f) = pair(&meta)?;
232                    if let Noun::Atom(t) = tag_noun {
233                        if rune_ast::act::is_act(t) {
234                            let args = eval_with_host(&subj, &arg_f, host)?;
235                            let caps = axis(&subj, rune_ast::act::CAPS_AXIS)
236                                .unwrap_or(Noun::Atom(0));
237                            let result = host.perform(t, &args, &caps)?;
238                            subj = Noun::cell(result, subj);
239                            form = cont;
240                            continue;
241                        }
242                    }
243                    form = cont;
244                    continue;
245                }
246
247                // 17 — look: graph scry
248                Noun::Atom(17) => {
249                    let (path_form, world_form) = pair(rest)?;
250                    let path  = eval_with_host(&subj, &path_form, host)?;
251                    let world = eval_with_host(&subj, &world_form, host)?;
252                    return Ok(scry_world(&path, &world));
253                }
254
255                _ => return Err(err(&format!("nox: unrecognized opcode {:?}", op))),
256            },
257
258            Noun::Atom(_) => return Err(err("nox: atom is not a valid formula")),
259        }
260    }
261}
262
263/// Navigate the noun tree by axis address.
264/// addr=0 → hash introspection stub (Atom(0) for cells, atom value for atoms)
265/// addr=1 → identity
266/// addr=2k → head(axis(noun, k))
267/// addr=2k+1 → tail(axis(noun, k))
268pub fn axis(noun: &Noun, addr: u64) -> Result<Noun, InterpError> {
269    match addr {
270        0 => match noun {
271            Noun::Cell(..) => Ok(Noun::Atom(0)),
272            Noun::Atom(v)  => Ok(Noun::Atom(*v)),
273        },
274        1 => Ok(noun.clone()),
275        2 => match noun {
276            Noun::Cell(h, _) => Ok(*h.clone()),
277            _ => Err(err("nox-0 axis: /2 on atom")),
278        },
279        3 => match noun {
280            Noun::Cell(_, t) => Ok(*t.clone()),
281            _ => Err(err("nox-0 axis: /3 on atom")),
282        },
283        n => {
284            let parent = axis(noun, n / 2)?;
285            axis(&parent, 2 + (n % 2))
286        }
287    }
288}
289
290/// Search a world noun (right-nested list of [key value] pairs) for a matching key.
291/// World structure: `[[key1 val1] [[key2 val2] ...]]` or Atom(0) for empty.
292/// Returns the value if found, Atom(0) otherwise.
293fn scry_world(path: &Noun, world: &Noun) -> Noun {
294    match world {
295        Noun::Atom(_) => Noun::Atom(0),
296        Noun::Cell(entry, rest) => {
297            match entry.as_ref() {
298                Noun::Cell(key, val) if key.as_ref() == path => *val.clone(),
299                _ => scry_world(path, rest),
300            }
301        }
302    }
303}
304
305// ── internal helpers ──────────────────────────────────────────────────────────
306
307fn eval_axis(subj: &Noun, addr_noun: &Noun) -> Result<Noun, InterpError> {
308    let n = match addr_noun {
309        Noun::Atom(n) => *n,
310        _ => return Err(err("nox-0 axis: address must be an atom")),
311    };
312    axis(subj, n)
313}
314
315/// Destructure a noun into (head, tail); error if atom.
316fn pair(noun: &Noun) -> Result<(Noun, Noun), InterpError> {
317    match noun {
318        Noun::Cell(h, t) => Ok((*h.clone(), *t.clone())),
319        _ => Err(err("nox: expected cell")),
320    }
321}
322
323/// Eval a sub-formula (through `host`) and extract the u64 atom value.
324fn eval_atom_h(subj: &Noun, formula: &Noun, host: &mut dyn Host, ctx: &str) -> Result<u64, InterpError> {
325    let r = eval_with_host(subj, formula, host)?;
326    atom_u64(&r, ctx)
327}
328
329/// Extract u64 from an atom noun; error on cell.
330fn atom_u64(noun: &Noun, ctx: &str) -> Result<u64, InterpError> {
331    match noun {
332        Noun::Atom(n) => Ok(*n),
333        _ => Err(err(&format!("{}: expected atom, got cell", ctx))),
334    }
335}
336
337// ── Goldilocks field arithmetic ───────────────────────────────────────────────
338
339fn add_field(a: u64, b: u64) -> u64 {
340    // Use u128 to avoid overflow before mod.
341    ((a as u128 + b as u128) % GOLDILOCKS_PRIME as u128) as u64
342}
343
344fn sub_field(a: u64, b: u64) -> u64 {
345    ((a as u128 + GOLDILOCKS_PRIME as u128 - b as u128) % GOLDILOCKS_PRIME as u128) as u64
346}
347
348fn mul_field(a: u64, b: u64) -> u64 {
349    ((a as u128 * b as u128) % GOLDILOCKS_PRIME as u128) as u64
350}
351
352/// Extended Euclidean algorithm for modular inverse.
353/// Returns 0 when a == 0 (no inverse for the additive identity).
354fn inv_field(a: u64) -> u64 {
355    if a == 0 {
356        return 0;
357    }
358    // Fermat: a^(p-2) mod p.  We implement binary exponentiation.
359    let p = GOLDILOCKS_PRIME;
360    let exp = p - 2;
361    let mut base = a as u128;
362    let mut result: u128 = 1;
363    let mut e = exp;
364    let m = p as u128;
365    while e > 0 {
366        if e & 1 == 1 {
367            result = result * base % m;
368        }
369        base = base * base % m;
370        e >>= 1;
371    }
372    result as u64
373}
374
375/// Recursively hash a noun using Poseidon2 (hemera).
376///
377/// Atom(n)   → hemera::hash(&n.to_le_bytes())
378/// Cell(h,t) → hemera::hash(hash(h) ++ hash(t))
379///
380/// The 32-byte digest is truncated to u64 by reading the first 8 bytes
381/// as a little-endian integer.
382fn hash_noun(noun: &Noun) -> u64 {
383    let digest = hash_noun_bytes(noun);
384    u64::from_le_bytes(digest[..8].try_into().unwrap())
385}
386
387fn hash_noun_bytes(noun: &Noun) -> [u8; 32] {
388    match noun {
389        Noun::Atom(n) => *hemera::hash(&n.to_le_bytes()).as_bytes(),
390        Noun::Cell(h, t) => {
391            let hh = hash_noun_bytes(h);
392            let ht = hash_noun_bytes(t);
393            let mut buf = [0u8; 64];
394            buf[..32].copy_from_slice(&hh);
395            buf[32..].copy_from_slice(&ht);
396            *hemera::hash(&buf).as_bytes()
397        }
398    }
399}
400
401fn err(msg: &str) -> InterpError {
402    InterpError { message: msg.to_string() }
403}
404
405// ── unit tests ────────────────────────────────────────────────────────────────
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    fn a(n: u64) -> Noun { Noun::Atom(n) }
412    fn c(h: Noun, t: Noun) -> Noun { Noun::cell(h, t) }
413
414    // ── pattern 0: axis ───────────────────────────────────────────────────────
415
416    #[test]
417    fn axis_identity() {
418        // [0 1] against atom 42 → 42
419        assert_eq!(eval(&a(42), &c(a(0), a(1))).unwrap(), a(42));
420    }
421
422    #[test]
423    fn axis_head() {
424        let s = c(a(1), a(2));
425        assert_eq!(eval(&s, &c(a(0), a(2))).unwrap(), a(1));
426    }
427
428    #[test]
429    fn axis_tail() {
430        let s = c(a(1), a(2));
431        assert_eq!(eval(&s, &c(a(0), a(3))).unwrap(), a(2));
432    }
433
434    #[test]
435    fn axis_deep_6() {
436        // s = [[1 2] [3 4]]  axis 6 = head(tail) = 3
437        let s = c(c(a(1), a(2)), c(a(3), a(4)));
438        assert_eq!(eval(&s, &c(a(0), a(6))).unwrap(), a(3));
439    }
440
441    #[test]
442    fn axis_deep_7() {
443        // axis 7 = tail(tail) = 4
444        let s = c(c(a(1), a(2)), c(a(3), a(4)));
445        assert_eq!(eval(&s, &c(a(0), a(7))).unwrap(), a(4));
446    }
447
448    #[test]
449    fn axis_deep_14() {
450        // s = [[[1 2] [3 4]] [[5 6] [7 8]]]
451        // axis 14 binary = 1110 → strip leading 1 → bits 1,1,0 → tail,tail,head
452        // tail(s) = [[5 6] [7 8]], tail(that) = [7 8], head(that) = 7
453        let s = c(c(c(a(1), a(2)), c(a(3), a(4))), c(c(a(5), a(6)), c(a(7), a(8))));
454        assert_eq!(eval(&s, &c(a(0), a(14))).unwrap(), a(7));
455    }
456
457    #[test]
458    fn axis_zero_hash_cell_stub() {
459        let s = c(a(10), a(20));
460        assert_eq!(eval(&s, &c(a(0), a(0))).unwrap(), a(0));
461    }
462
463    #[test]
464    fn axis_zero_hash_atom_stub() {
465        // atom → returns atom value itself
466        assert_eq!(eval(&a(99), &c(a(0), a(0))).unwrap(), a(99));
467    }
468
469    // ── pattern 1: quote ──────────────────────────────────────────────────────
470
471    #[test]
472    fn quote_atom() {
473        assert_eq!(eval(&a(0), &c(a(1), a(42))).unwrap(), a(42));
474    }
475
476    #[test]
477    fn quote_cell() {
478        let v = c(a(1), a(2));
479        assert_eq!(eval(&a(0), &c(a(1), v.clone())).unwrap(), v);
480    }
481
482    // ── pattern 2: compose ────────────────────────────────────────────────────
483
484    #[test]
485    fn compose_identity_chain() {
486        // [2 a b]: new_subj = eval(s,a), new_form = eval(s,b), then eval(new_subj, new_form)
487        // s = [99 0], a = [0 1] (identity → s), b = [1 [0 2]] (quote of axis-head formula)
488        // new_subj = s = [99 0]
489        // new_form = [0 2]
490        // eval([99 0], [0 2]) = head([99 0]) = 99
491        let s = c(a(99), a(0));
492        let id = c(a(0), a(1));
493        let quote_head = c(a(1), c(a(0), a(2)));
494        let formula = c(a(2), c(id, quote_head));
495        assert_eq!(eval(&s, &formula).unwrap(), a(99));
496    }
497
498    #[test]
499    fn compose_quote_then_identity() {
500        // s=5, formula=[2 [1 [0 1]] [0 1]]
501        // step1: eval [1 [0 1]] against 5 → [0 1] (the literal formula)
502        // step2: eval [0 1] against 5 → 5 (but wait — new_subj = eval(s,[1,[0,1]]) = [0,1])
503        // Actually: compose = reduce(eval(s,a), eval(s,b))
504        // a=[0 1], eval(5,[0 1])=5  →  new_subj=5
505        // b=[1 [0 1]], eval(5,[1 [0 1]])=[0 1]  →  new_form=[0 1]
506        // reduce(5, [0 1]) = 5
507        let s = a(5);
508        let id = c(a(0), a(1));
509        let quote_id = c(a(1), id.clone());
510        let formula = c(a(2), c(id, quote_id));
511        assert_eq!(eval(&s, &formula).unwrap(), s);
512    }
513
514    // ── pattern 3: cons ───────────────────────────────────────────────────────
515
516    #[test]
517    fn cons_two_literals() {
518        // [3 [1 1] [1 2]] against 0 → [1 2]
519        let formula = c(a(3), c(c(a(1), a(1)), c(a(1), a(2))));
520        assert_eq!(eval(&a(0), &formula).unwrap(), c(a(1), a(2)));
521    }
522
523    // ── pattern 4: branch ─────────────────────────────────────────────────────
524
525    #[test]
526    fn branch_zero_takes_yes() {
527        // [4 [1 0] [1 99] [1 0]] against 0 → test=0 → yes → 99
528        let formula = c(a(4), c(c(a(1), a(0)), c(c(a(1), a(99)), c(a(1), a(0)))));
529        assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
530    }
531
532    #[test]
533    fn branch_nonzero_takes_no() {
534        // [4 [1 1] [1 99] [1 77]] against 0 → test=1 (nonzero) → no → 77
535        let formula = c(a(4), c(c(a(1), a(1)), c(c(a(1), a(99)), c(a(1), a(77)))));
536        assert_eq!(eval(&a(0), &formula).unwrap(), a(77));
537    }
538
539    // ── pattern 5: add ────────────────────────────────────────────────────────
540
541    #[test]
542    fn add_basic() {
543        // [5 [1 5] [1 3]] → 8
544        let formula = c(a(5), c(c(a(1), a(5)), c(a(1), a(3))));
545        assert_eq!(eval(&a(0), &formula).unwrap(), a(8));
546    }
547
548    #[test]
549    fn add_wraps_goldilocks() {
550        // (p - 1) + 1 = 0 mod p
551        let p_minus_1 = GOLDILOCKS_PRIME - 1;
552        let formula = c(a(5), c(c(a(1), a(p_minus_1)), c(a(1), a(1))));
553        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
554    }
555
556    // ── pattern 6: sub ────────────────────────────────────────────────────────
557
558    #[test]
559    fn sub_basic() {
560        // [6 [1 10] [1 3]] → 7
561        let formula = c(a(6), c(c(a(1), a(10)), c(a(1), a(3))));
562        assert_eq!(eval(&a(0), &formula).unwrap(), a(7));
563    }
564
565    #[test]
566    fn sub_wraps_around() {
567        // 0 - 1 = p - 1 mod p
568        let formula = c(a(6), c(c(a(1), a(0)), c(a(1), a(1))));
569        assert_eq!(eval(&a(0), &formula).unwrap(), a(GOLDILOCKS_PRIME - 1));
570    }
571
572    // ── pattern 7: mul ────────────────────────────────────────────────────────
573
574    #[test]
575    fn mul_basic() {
576        // [7 [1 6] [1 7]] → 42
577        let formula = c(a(7), c(c(a(1), a(6)), c(a(1), a(7))));
578        assert_eq!(eval(&a(0), &formula).unwrap(), a(42));
579    }
580
581    // ── pattern 8: inv ────────────────────────────────────────────────────────
582
583    #[test]
584    fn inv_nonzero() {
585        // [8 [1 2]] → modular inverse of 2; 2 * inv(2) = 1 mod p
586        let formula = c(a(8), c(a(1), a(2)));
587        let result = eval(&a(0), &formula).unwrap();
588        let Noun::Atom(r) = result else { panic!("expected atom") };
589        assert_eq!(mul_field(2, r), 1);
590    }
591
592    #[test]
593    fn inv_zero() {
594        // [8 [1 0]] → 0 (no inverse for 0)
595        let formula = c(a(8), c(a(1), a(0)));
596        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
597    }
598
599    // ── pattern 9: eq ─────────────────────────────────────────────────────────
600
601    #[test]
602    fn eq_atoms_equal() {
603        // [9 [1 42] [1 42]] → 0
604        let formula = c(a(9), c(c(a(1), a(42)), c(a(1), a(42))));
605        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
606    }
607
608    #[test]
609    fn eq_atoms_not_equal() {
610        // [9 [1 1] [1 2]] → 1
611        let formula = c(a(9), c(c(a(1), a(1)), c(a(1), a(2))));
612        assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
613    }
614
615    #[test]
616    fn eq_cells_equal() {
617        // [9 [1 [1 2]] [1 [1 2]]] → 0
618        let cell_val = c(a(1), a(2));
619        let formula = c(a(9), c(c(a(1), cell_val.clone()), c(a(1), cell_val)));
620        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
621    }
622
623    #[test]
624    fn eq_cells_not_equal() {
625        // [9 [1 [1 2]] [1 [1 3]]] → 1
626        let formula = c(a(9), c(c(a(1), c(a(1), a(2))), c(a(1), c(a(1), a(3)))));
627        assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
628    }
629
630    // ── pattern 10: lt ────────────────────────────────────────────────────────
631
632    #[test]
633    fn lt_true() {
634        // [10 [1 3] [1 7]] → 0 (3 < 7)
635        let formula = c(a(10), c(c(a(1), a(3)), c(a(1), a(7))));
636        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
637    }
638
639    #[test]
640    fn lt_false_equal() {
641        // [10 [1 7] [1 7]] → 1 (not strictly less)
642        let formula = c(a(10), c(c(a(1), a(7)), c(a(1), a(7))));
643        assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
644    }
645
646    #[test]
647    fn lt_false_greater() {
648        // [10 [1 9] [1 3]] → 1
649        let formula = c(a(10), c(c(a(1), a(9)), c(a(1), a(3))));
650        assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
651    }
652
653    // ── pattern 11: xor ───────────────────────────────────────────────────────
654
655    #[test]
656    fn xor_basic() {
657        // [11 [1 0b1010] [1 0b1100]] → 0b0110 = 6
658        let formula = c(a(11), c(c(a(1), a(0b1010)), c(a(1), a(0b1100))));
659        assert_eq!(eval(&a(0), &formula).unwrap(), a(0b0110));
660    }
661
662    // ── pattern 12: and ───────────────────────────────────────────────────────
663
664    #[test]
665    fn and_basic() {
666        let formula = c(a(12), c(c(a(1), a(0b1100)), c(a(1), a(0b1010))));
667        assert_eq!(eval(&a(0), &formula).unwrap(), a(0b1000));
668    }
669
670    // ── pattern 13: not ───────────────────────────────────────────────────────
671
672    #[test]
673    fn not_basic() {
674        let formula = c(a(13), c(a(1), a(0)));
675        assert_eq!(eval(&a(0), &formula).unwrap(), a(!0u64));
676    }
677
678    // ── pattern 14: shl ───────────────────────────────────────────────────────
679
680    #[test]
681    fn shl_basic() {
682        // [14 [1 1] [1 3]] → 1 << 3 = 8
683        let formula = c(a(14), c(c(a(1), a(1)), c(a(1), a(3))));
684        assert_eq!(eval(&a(0), &formula).unwrap(), a(8));
685    }
686
687    // ── pattern 15: hash (hemera Poseidon2) ──────────────────────────────────
688
689    #[test]
690    fn hash_atom_returns_atom() {
691        // [15 [1 55]] evaluates to some atom (u64 truncation of Poseidon2 digest)
692        let formula = c(a(15), c(a(1), a(55)));
693        let result = eval(&a(0), &formula).unwrap();
694        assert!(matches!(result, Noun::Atom(_)));
695    }
696
697    #[test]
698    fn hash_atom_is_deterministic() {
699        let formula = c(a(15), c(a(1), a(42)));
700        let r1 = eval(&a(0), &formula).unwrap();
701        let r2 = eval(&a(0), &formula).unwrap();
702        assert_eq!(r1, r2);
703    }
704
705    #[test]
706    fn hash_atom_differs_by_value() {
707        let f1 = c(a(15), c(a(1), a(1)));
708        let f2 = c(a(15), c(a(1), a(2)));
709        let r1 = eval(&a(0), &f1).unwrap();
710        let r2 = eval(&a(0), &f2).unwrap();
711        assert_ne!(r1, r2);
712    }
713
714    #[test]
715    fn hash_cell_returns_atom() {
716        // [15 [3 [1 1] [1 2]]] → hash the cell [1 2]
717        let formula = c(a(15), c(a(3), c(c(a(1), a(1)), c(a(1), a(2)))));
718        let result = eval(&a(0), &formula).unwrap();
719        assert!(matches!(result, Noun::Atom(_)));
720    }
721
722    #[test]
723    fn hash_cell_differs_from_atom() {
724        // hash([1 2]) ≠ hash(1)
725        let atom_f = c(a(15), c(a(1), a(1)));
726        let cell_f = c(a(15), c(a(3), c(c(a(1), a(1)), c(a(1), a(2)))));
727        let ra = eval(&a(0), &atom_f).unwrap();
728        let rc = eval(&a(0), &cell_f).unwrap();
729        assert_ne!(ra, rc);
730    }
731
732    // ── pattern 16: hint/call ─────────────────────────────────────────────────
733
734    #[test]
735    fn hint_evaluates_body() {
736        // [16 [[1 42] [1 0]] [1 99]] → evaluate body [1 99] → 99
737        // Format: [16 [tag selector] body] where tag=[1 42], selector=[1 0], body=[1 99]
738        let tag = c(a(1), a(42));
739        let selector = c(a(1), a(0));
740        let hint_meta = c(tag, selector);
741        let body = c(a(1), a(99));
742        let formula = c(a(16), c(hint_meta, body));
743        assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
744    }
745
746    #[test]
747    fn hint_evaluates_arithmetic_body() {
748        // [16 [[1 0] [1 0]] [5 [1 3] [1 4]]] → eval [5 [1 3] [1 4]] → add(3,4) = 7
749        let hint_meta = c(c(a(1), a(0)), c(a(1), a(0)));
750        let body = c(a(5), c(c(a(1), a(3)), c(a(1), a(4))));
751        let formula = c(a(16), c(hint_meta, body));
752        assert_eq!(eval(&a(0), &formula).unwrap(), a(7));
753    }
754
755    #[test]
756    fn host_call_returns_zero() {
757        // [16 [[1 host-tag] args] [1 0]] → body = [1 0] → 0
758        let hint_meta = c(c(a(1), a(99)), c(a(1), a(0)));
759        let body = c(a(1), a(0)); // [1 0] = quoted zero
760        let formula = c(a(16), c(hint_meta, body));
761        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
762    }
763
764    // ── pattern 17: look (scry) ───────────────────────────────────────────────
765
766    #[test]
767    fn look_stub_returns_zero() {
768        // [17 [1 42] [1 0]] — path=42, world=Atom(0) (empty) → Atom(0)
769        let formula = c(a(17), c(c(a(1), a(42)), c(a(1), a(0))));
770        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
771    }
772
773    #[test]
774    fn look_finds_entry_in_world() {
775        // world = [[42 99] 0]  (one [key value] pair, nil-terminated)
776        // [17 [1 42] [1 [[42 99] 0]]] — path=42, world contains key=42→val=99
777        let world = c(c(a(42), a(99)), a(0));
778        let formula = c(a(17), c(c(a(1), a(42)), c(a(1), world)));
779        assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
780    }
781
782    #[test]
783    fn look_misses_entry_returns_zero() {
784        // world has key=42→99, but we look for key=7
785        let world = c(c(a(42), a(99)), a(0));
786        let formula = c(a(17), c(c(a(1), a(7)), c(a(1), world)));
787        assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
788    }
789
790    // ── distribution rule ─────────────────────────────────────────────────────
791
792    #[test]
793    fn distribution_cell_formula() {
794        // formula = [[1 10] [1 20]]  both halves are cells (not atom opcodes)
795        // → [eval([1 10]), eval([1 20])] = [10, 20]
796        let formula = c(c(a(1), a(10)), c(a(1), a(20)));
797        assert_eq!(eval(&a(0), &formula).unwrap(), c(a(10), a(20)));
798    }
799
800    #[test]
801    fn distribution_nested() {
802        // formula = [[1 1] [1 2]]  against 0 → [1 2]
803        let formula = c(c(a(1), a(1)), c(a(1), a(2)));
804        assert_eq!(eval(&a(0), &formula).unwrap(), c(a(1), a(2)));
805    }
806
807    // ── compose chaining ──────────────────────────────────────────────────────
808
809    #[test]
810    fn compose_chaining_via_quote() {
811        // s=42, formula=[2 [0 1] [1 [0 1]]]
812        // eval(s, [0 1]) = 42 = new_subj
813        // eval(s, [1 [0 1]]) = [0 1] = new_form
814        // eval(42, [0 1]) = 42
815        let s = a(42);
816        let formula = c(a(2), c(c(a(0), a(1)), c(a(1), c(a(0), a(1)))));
817        assert_eq!(eval(&s, &formula).unwrap(), a(42));
818    }
819
820    // ── axis public function ───────────────────────────────────────────────────
821
822    #[test]
823    fn pub_axis_fn() {
824        let s = c(c(a(1), a(2)), c(a(3), a(4)));
825        assert_eq!(axis(&s, 1).unwrap(), s);
826        assert_eq!(axis(&s, 2).unwrap(), c(a(1), a(2)));
827        assert_eq!(axis(&s, 3).unwrap(), c(a(3), a(4)));
828        assert_eq!(axis(&s, 6).unwrap(), a(3));
829        assert_eq!(axis(&s, 7).unwrap(), a(4));
830    }
831
832    // ── field arithmetic helpers ──────────────────────────────────────────────
833
834    #[test]
835    fn field_add_sub_inverse() {
836        let x = 12345678u64;
837        let y = 87654321u64;
838        assert_eq!(sub_field(add_field(x, y), y), x);
839    }
840
841    #[test]
842    fn field_mul_inv() {
843        let x = 7u64;
844        let ix = inv_field(x);
845        assert_eq!(mul_field(x, ix), 1);
846    }
847
848    // ── acts (eval_with_host) ─────────────────────────────────────────────────
849
850    use rune_ast::act;
851
852    struct Recorder { acts: Vec<(u64, Noun)>, caps: Noun, reply: Noun }
853    impl Host for Recorder {
854        fn perform(&mut self, act: u64, args: &Noun, caps: &Noun) -> Result<Noun, InterpError> {
855            self.acts.push((act, args.clone()));
856            self.caps = caps.clone();
857            Ok(self.reply.clone())
858        }
859    }
860
861    // `emit(n)` lowered shape: [16 [EMIT [1 n]] [0 2]]
862    fn emit_act(n: u64) -> Noun {
863        c(a(16), c(c(a(act::EMIT), c(a(1), a(n))), c(a(0), a(2))))
864    }
865
866    #[test]
867    fn act_performs_and_splices_result() {
868        let mut h = Recorder { acts: vec![], caps: a(0), reply: a(55) };
869        let r = eval_with_host(&a(0), &emit_act(7), &mut h).unwrap();
870        assert_eq!(h.acts.len(), 1);
871        assert_eq!(h.acts[0].0, act::EMIT);
872        assert_eq!(h.acts[0].1, a(7)); // args evaluated before perform
873        assert_eq!(r, a(55));          // host result spliced (axis 2) and returned
874    }
875
876    #[test]
877    fn acts_compose_via_cons() {
878        // [3 emit(1) emit(2)] — the cons performs BOTH acts (nesting-safe)
879        let formula = c(a(3), c(emit_act(1), emit_act(2)));
880        let mut h = Recorder { acts: vec![], caps: a(0), reply: a(0) };
881        eval_with_host(&a(0), &formula, &mut h).unwrap();
882        let args: Vec<Noun> = h.acts.iter().map(|(_, n)| n.clone()).collect();
883        assert_eq!(args, vec![a(1), a(2)]);
884    }
885
886    #[test]
887    fn act_reads_caps_from_axis_30() {
888        // subject = [s0 [s1 [s2 [caps X]]]] → axis 30 = caps = 123
889        let subj = c(a(0), c(a(1), c(a(2), c(a(123), a(0)))));
890        let mut h = Recorder { acts: vec![], caps: a(0), reply: a(0) };
891        eval_with_host(&subj, &emit_act(7), &mut h).unwrap();
892        assert_eq!(h.caps, a(123));
893    }
894
895    #[test]
896    fn pure_eval_noops_acts() {
897        // Same act under pure eval → DenyHost → Atom(0), no panic.
898        assert_eq!(eval(&a(0), &emit_act(7)).unwrap(), a(0));
899    }
900}