pub mod event;
use rune_ast::Noun;
use cyber_hemera as hemera;
const GOLDILOCKS_PRIME: u64 = 0xFFFF_FFFF_0000_0001u64;
#[derive(Debug, Clone, PartialEq)]
pub struct InterpError {
pub message: String,
}
impl std::fmt::Display for InterpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
pub trait Host {
fn perform(&mut self, act: u64, args: &Noun, caps: &Noun) -> Result<Noun, InterpError>;
}
pub struct DenyHost;
impl Host for DenyHost {
fn perform(&mut self, _act: u64, _args: &Noun, _caps: &Noun) -> Result<Noun, InterpError> {
Ok(Noun::Atom(0))
}
}
pub fn eval(subject: &Noun, formula: &Noun) -> Result<Noun, InterpError> {
eval_with_host(subject, formula, &mut DenyHost)
}
pub fn eval_with_host(subject: &Noun, formula: &Noun, host: &mut dyn Host) -> Result<Noun, InterpError> {
let mut subj = subject.clone();
let mut form = formula.clone();
loop {
match form {
Noun::Cell(ref fh, ref ft) if matches!(fh.as_ref(), Noun::Cell(..)) => {
let h = eval_with_host(&subj, fh, host)?;
let t = eval_with_host(&subj, ft, host)?;
return Ok(Noun::cell(h, t));
}
Noun::Cell(ref op, ref rest) => match op.as_ref() {
Noun::Atom(0) => return eval_axis(&subj, rest),
Noun::Atom(1) => return Ok(*rest.clone()),
Noun::Atom(2) => {
let (a, b) = pair(rest)?;
let new_subj = eval_with_host(&subj, &a, host)?;
let new_form = eval_with_host(&subj, &b, host)?;
subj = new_subj;
form = new_form;
continue;
}
Noun::Atom(3) => {
let (a, b) = pair(rest)?;
let ha = eval_with_host(&subj, &a, host)?;
let ta = eval_with_host(&subj, &b, host)?;
return Ok(Noun::cell(ha, ta));
}
Noun::Atom(4) => {
let (test_f, ynb) = pair(rest)?;
let (yes_f, no_f) = pair(&ynb)?;
let test_val = eval_with_host(&subj, &test_f, host)?;
let test_n = atom_u64(&test_val, "nox-4 branch: test must be atom")?;
form = if test_n == 0 { yes_f } else { no_f };
continue;
}
Noun::Atom(5) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-5 add")?;
let vb = eval_atom_h(&subj, &b, host, "nox-5 add")?;
return Ok(Noun::Atom(add_field(va, vb)));
}
Noun::Atom(6) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-6 sub")?;
let vb = eval_atom_h(&subj, &b, host, "nox-6 sub")?;
return Ok(Noun::Atom(sub_field(va, vb)));
}
Noun::Atom(7) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-7 mul")?;
let vb = eval_atom_h(&subj, &b, host, "nox-7 mul")?;
return Ok(Noun::Atom(mul_field(va, vb)));
}
Noun::Atom(8) => {
let va = eval_atom_h(&subj, rest, host, "nox-8 inv")?;
return Ok(Noun::Atom(inv_field(va)));
}
Noun::Atom(9) => {
let (a, b) = pair(rest)?;
let ra = eval_with_host(&subj, &a, host)?;
let rb = eval_with_host(&subj, &b, host)?;
return Ok(Noun::Atom(if ra == rb { 0 } else { 1 }));
}
Noun::Atom(10) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-10 lt")?;
let vb = eval_atom_h(&subj, &b, host, "nox-10 lt")?;
return Ok(Noun::Atom(if va < vb { 0 } else { 1 }));
}
Noun::Atom(11) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-11 xor")?;
let vb = eval_atom_h(&subj, &b, host, "nox-11 xor")?;
return Ok(Noun::Atom(va ^ vb));
}
Noun::Atom(12) => {
let (a, b) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-12 and")?;
let vb = eval_atom_h(&subj, &b, host, "nox-12 and")?;
return Ok(Noun::Atom(va & vb));
}
Noun::Atom(13) => {
let va = eval_atom_h(&subj, rest, host, "nox-13 not")?;
return Ok(Noun::Atom(!va));
}
Noun::Atom(14) => {
let (a, n) = pair(rest)?;
let va = eval_atom_h(&subj, &a, host, "nox-14 shl")?;
let vn = eval_atom_h(&subj, &n, host, "nox-14 shl")?;
return Ok(Noun::Atom(va << (vn & 63)));
}
Noun::Atom(15) => {
let r = eval_with_host(&subj, rest, host)?;
return Ok(Noun::Atom(hash_noun(&r)));
}
Noun::Atom(16) => {
let (meta, cont) = pair(rest)?;
let (tag_noun, arg_f) = pair(&meta)?;
if let Noun::Atom(t) = tag_noun {
if rune_ast::act::is_act(t) {
let args = eval_with_host(&subj, &arg_f, host)?;
let caps = axis(&subj, rune_ast::act::CAPS_AXIS)
.unwrap_or(Noun::Atom(0));
let result = host.perform(t, &args, &caps)?;
subj = Noun::cell(result, subj);
form = cont;
continue;
}
}
form = cont;
continue;
}
Noun::Atom(17) => {
let (path_form, world_form) = pair(rest)?;
let path = eval_with_host(&subj, &path_form, host)?;
let world = eval_with_host(&subj, &world_form, host)?;
return Ok(scry_world(&path, &world));
}
_ => return Err(err(&format!("nox: unrecognized opcode {:?}", op))),
},
Noun::Atom(_) => return Err(err("nox: atom is not a valid formula")),
}
}
}
pub fn axis(noun: &Noun, addr: u64) -> Result<Noun, InterpError> {
match addr {
0 => match noun {
Noun::Cell(..) => Ok(Noun::Atom(0)),
Noun::Atom(v) => Ok(Noun::Atom(*v)),
},
1 => Ok(noun.clone()),
2 => match noun {
Noun::Cell(h, _) => Ok(*h.clone()),
_ => Err(err("nox-0 axis: /2 on atom")),
},
3 => match noun {
Noun::Cell(_, t) => Ok(*t.clone()),
_ => Err(err("nox-0 axis: /3 on atom")),
},
n => {
let parent = axis(noun, n / 2)?;
axis(&parent, 2 + (n % 2))
}
}
}
fn scry_world(path: &Noun, world: &Noun) -> Noun {
match world {
Noun::Atom(_) => Noun::Atom(0),
Noun::Cell(entry, rest) => {
match entry.as_ref() {
Noun::Cell(key, val) if key.as_ref() == path => *val.clone(),
_ => scry_world(path, rest),
}
}
}
}
fn eval_axis(subj: &Noun, addr_noun: &Noun) -> Result<Noun, InterpError> {
let n = match addr_noun {
Noun::Atom(n) => *n,
_ => return Err(err("nox-0 axis: address must be an atom")),
};
axis(subj, n)
}
fn pair(noun: &Noun) -> Result<(Noun, Noun), InterpError> {
match noun {
Noun::Cell(h, t) => Ok((*h.clone(), *t.clone())),
_ => Err(err("nox: expected cell")),
}
}
fn eval_atom_h(subj: &Noun, formula: &Noun, host: &mut dyn Host, ctx: &str) -> Result<u64, InterpError> {
let r = eval_with_host(subj, formula, host)?;
atom_u64(&r, ctx)
}
fn atom_u64(noun: &Noun, ctx: &str) -> Result<u64, InterpError> {
match noun {
Noun::Atom(n) => Ok(*n),
_ => Err(err(&format!("{}: expected atom, got cell", ctx))),
}
}
fn add_field(a: u64, b: u64) -> u64 {
((a as u128 + b as u128) % GOLDILOCKS_PRIME as u128) as u64
}
fn sub_field(a: u64, b: u64) -> u64 {
((a as u128 + GOLDILOCKS_PRIME as u128 - b as u128) % GOLDILOCKS_PRIME as u128) as u64
}
fn mul_field(a: u64, b: u64) -> u64 {
((a as u128 * b as u128) % GOLDILOCKS_PRIME as u128) as u64
}
fn inv_field(a: u64) -> u64 {
if a == 0 {
return 0;
}
let p = GOLDILOCKS_PRIME;
let exp = p - 2;
let mut base = a as u128;
let mut result: u128 = 1;
let mut e = exp;
let m = p as u128;
while e > 0 {
if e & 1 == 1 {
result = result * base % m;
}
base = base * base % m;
e >>= 1;
}
result as u64
}
fn hash_noun(noun: &Noun) -> u64 {
let digest = hash_noun_bytes(noun);
u64::from_le_bytes(digest[..8].try_into().unwrap())
}
fn hash_noun_bytes(noun: &Noun) -> [u8; 32] {
match noun {
Noun::Atom(n) => *hemera::hash(&n.to_le_bytes()).as_bytes(),
Noun::Cell(h, t) => {
let hh = hash_noun_bytes(h);
let ht = hash_noun_bytes(t);
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(&hh);
buf[32..].copy_from_slice(&ht);
*hemera::hash(&buf).as_bytes()
}
}
}
fn err(msg: &str) -> InterpError {
InterpError { message: msg.to_string() }
}
#[cfg(test)]
mod tests {
use super::*;
fn a(n: u64) -> Noun { Noun::Atom(n) }
fn c(h: Noun, t: Noun) -> Noun { Noun::cell(h, t) }
#[test]
fn axis_identity() {
assert_eq!(eval(&a(42), &c(a(0), a(1))).unwrap(), a(42));
}
#[test]
fn axis_head() {
let s = c(a(1), a(2));
assert_eq!(eval(&s, &c(a(0), a(2))).unwrap(), a(1));
}
#[test]
fn axis_tail() {
let s = c(a(1), a(2));
assert_eq!(eval(&s, &c(a(0), a(3))).unwrap(), a(2));
}
#[test]
fn axis_deep_6() {
let s = c(c(a(1), a(2)), c(a(3), a(4)));
assert_eq!(eval(&s, &c(a(0), a(6))).unwrap(), a(3));
}
#[test]
fn axis_deep_7() {
let s = c(c(a(1), a(2)), c(a(3), a(4)));
assert_eq!(eval(&s, &c(a(0), a(7))).unwrap(), a(4));
}
#[test]
fn axis_deep_14() {
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))));
assert_eq!(eval(&s, &c(a(0), a(14))).unwrap(), a(7));
}
#[test]
fn axis_zero_hash_cell_stub() {
let s = c(a(10), a(20));
assert_eq!(eval(&s, &c(a(0), a(0))).unwrap(), a(0));
}
#[test]
fn axis_zero_hash_atom_stub() {
assert_eq!(eval(&a(99), &c(a(0), a(0))).unwrap(), a(99));
}
#[test]
fn quote_atom() {
assert_eq!(eval(&a(0), &c(a(1), a(42))).unwrap(), a(42));
}
#[test]
fn quote_cell() {
let v = c(a(1), a(2));
assert_eq!(eval(&a(0), &c(a(1), v.clone())).unwrap(), v);
}
#[test]
fn compose_identity_chain() {
let s = c(a(99), a(0));
let id = c(a(0), a(1));
let quote_head = c(a(1), c(a(0), a(2)));
let formula = c(a(2), c(id, quote_head));
assert_eq!(eval(&s, &formula).unwrap(), a(99));
}
#[test]
fn compose_quote_then_identity() {
let s = a(5);
let id = c(a(0), a(1));
let quote_id = c(a(1), id.clone());
let formula = c(a(2), c(id, quote_id));
assert_eq!(eval(&s, &formula).unwrap(), s);
}
#[test]
fn cons_two_literals() {
let formula = c(a(3), c(c(a(1), a(1)), c(a(1), a(2))));
assert_eq!(eval(&a(0), &formula).unwrap(), c(a(1), a(2)));
}
#[test]
fn branch_zero_takes_yes() {
let formula = c(a(4), c(c(a(1), a(0)), c(c(a(1), a(99)), c(a(1), a(0)))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
}
#[test]
fn branch_nonzero_takes_no() {
let formula = c(a(4), c(c(a(1), a(1)), c(c(a(1), a(99)), c(a(1), a(77)))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(77));
}
#[test]
fn add_basic() {
let formula = c(a(5), c(c(a(1), a(5)), c(a(1), a(3))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(8));
}
#[test]
fn add_wraps_goldilocks() {
let p_minus_1 = GOLDILOCKS_PRIME - 1;
let formula = c(a(5), c(c(a(1), a(p_minus_1)), c(a(1), a(1))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn sub_basic() {
let formula = c(a(6), c(c(a(1), a(10)), c(a(1), a(3))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(7));
}
#[test]
fn sub_wraps_around() {
let formula = c(a(6), c(c(a(1), a(0)), c(a(1), a(1))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(GOLDILOCKS_PRIME - 1));
}
#[test]
fn mul_basic() {
let formula = c(a(7), c(c(a(1), a(6)), c(a(1), a(7))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(42));
}
#[test]
fn inv_nonzero() {
let formula = c(a(8), c(a(1), a(2)));
let result = eval(&a(0), &formula).unwrap();
let Noun::Atom(r) = result else { panic!("expected atom") };
assert_eq!(mul_field(2, r), 1);
}
#[test]
fn inv_zero() {
let formula = c(a(8), c(a(1), a(0)));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn eq_atoms_equal() {
let formula = c(a(9), c(c(a(1), a(42)), c(a(1), a(42))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn eq_atoms_not_equal() {
let formula = c(a(9), c(c(a(1), a(1)), c(a(1), a(2))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
}
#[test]
fn eq_cells_equal() {
let cell_val = c(a(1), a(2));
let formula = c(a(9), c(c(a(1), cell_val.clone()), c(a(1), cell_val)));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn eq_cells_not_equal() {
let formula = c(a(9), c(c(a(1), c(a(1), a(2))), c(a(1), c(a(1), a(3)))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
}
#[test]
fn lt_true() {
let formula = c(a(10), c(c(a(1), a(3)), c(a(1), a(7))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn lt_false_equal() {
let formula = c(a(10), c(c(a(1), a(7)), c(a(1), a(7))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
}
#[test]
fn lt_false_greater() {
let formula = c(a(10), c(c(a(1), a(9)), c(a(1), a(3))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(1));
}
#[test]
fn xor_basic() {
let formula = c(a(11), c(c(a(1), a(0b1010)), c(a(1), a(0b1100))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0b0110));
}
#[test]
fn and_basic() {
let formula = c(a(12), c(c(a(1), a(0b1100)), c(a(1), a(0b1010))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0b1000));
}
#[test]
fn not_basic() {
let formula = c(a(13), c(a(1), a(0)));
assert_eq!(eval(&a(0), &formula).unwrap(), a(!0u64));
}
#[test]
fn shl_basic() {
let formula = c(a(14), c(c(a(1), a(1)), c(a(1), a(3))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(8));
}
#[test]
fn hash_atom_returns_atom() {
let formula = c(a(15), c(a(1), a(55)));
let result = eval(&a(0), &formula).unwrap();
assert!(matches!(result, Noun::Atom(_)));
}
#[test]
fn hash_atom_is_deterministic() {
let formula = c(a(15), c(a(1), a(42)));
let r1 = eval(&a(0), &formula).unwrap();
let r2 = eval(&a(0), &formula).unwrap();
assert_eq!(r1, r2);
}
#[test]
fn hash_atom_differs_by_value() {
let f1 = c(a(15), c(a(1), a(1)));
let f2 = c(a(15), c(a(1), a(2)));
let r1 = eval(&a(0), &f1).unwrap();
let r2 = eval(&a(0), &f2).unwrap();
assert_ne!(r1, r2);
}
#[test]
fn hash_cell_returns_atom() {
let formula = c(a(15), c(a(3), c(c(a(1), a(1)), c(a(1), a(2)))));
let result = eval(&a(0), &formula).unwrap();
assert!(matches!(result, Noun::Atom(_)));
}
#[test]
fn hash_cell_differs_from_atom() {
let atom_f = c(a(15), c(a(1), a(1)));
let cell_f = c(a(15), c(a(3), c(c(a(1), a(1)), c(a(1), a(2)))));
let ra = eval(&a(0), &atom_f).unwrap();
let rc = eval(&a(0), &cell_f).unwrap();
assert_ne!(ra, rc);
}
#[test]
fn hint_evaluates_body() {
let tag = c(a(1), a(42));
let selector = c(a(1), a(0));
let hint_meta = c(tag, selector);
let body = c(a(1), a(99));
let formula = c(a(16), c(hint_meta, body));
assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
}
#[test]
fn hint_evaluates_arithmetic_body() {
let hint_meta = c(c(a(1), a(0)), c(a(1), a(0)));
let body = c(a(5), c(c(a(1), a(3)), c(a(1), a(4))));
let formula = c(a(16), c(hint_meta, body));
assert_eq!(eval(&a(0), &formula).unwrap(), a(7));
}
#[test]
fn host_call_returns_zero() {
let hint_meta = c(c(a(1), a(99)), c(a(1), a(0)));
let body = c(a(1), a(0)); let formula = c(a(16), c(hint_meta, body));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn look_stub_returns_zero() {
let formula = c(a(17), c(c(a(1), a(42)), c(a(1), a(0))));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn look_finds_entry_in_world() {
let world = c(c(a(42), a(99)), a(0));
let formula = c(a(17), c(c(a(1), a(42)), c(a(1), world)));
assert_eq!(eval(&a(0), &formula).unwrap(), a(99));
}
#[test]
fn look_misses_entry_returns_zero() {
let world = c(c(a(42), a(99)), a(0));
let formula = c(a(17), c(c(a(1), a(7)), c(a(1), world)));
assert_eq!(eval(&a(0), &formula).unwrap(), a(0));
}
#[test]
fn distribution_cell_formula() {
let formula = c(c(a(1), a(10)), c(a(1), a(20)));
assert_eq!(eval(&a(0), &formula).unwrap(), c(a(10), a(20)));
}
#[test]
fn distribution_nested() {
let formula = c(c(a(1), a(1)), c(a(1), a(2)));
assert_eq!(eval(&a(0), &formula).unwrap(), c(a(1), a(2)));
}
#[test]
fn compose_chaining_via_quote() {
let s = a(42);
let formula = c(a(2), c(c(a(0), a(1)), c(a(1), c(a(0), a(1)))));
assert_eq!(eval(&s, &formula).unwrap(), a(42));
}
#[test]
fn pub_axis_fn() {
let s = c(c(a(1), a(2)), c(a(3), a(4)));
assert_eq!(axis(&s, 1).unwrap(), s);
assert_eq!(axis(&s, 2).unwrap(), c(a(1), a(2)));
assert_eq!(axis(&s, 3).unwrap(), c(a(3), a(4)));
assert_eq!(axis(&s, 6).unwrap(), a(3));
assert_eq!(axis(&s, 7).unwrap(), a(4));
}
#[test]
fn field_add_sub_inverse() {
let x = 12345678u64;
let y = 87654321u64;
assert_eq!(sub_field(add_field(x, y), y), x);
}
#[test]
fn field_mul_inv() {
let x = 7u64;
let ix = inv_field(x);
assert_eq!(mul_field(x, ix), 1);
}
use rune_ast::act;
struct Recorder { acts: Vec<(u64, Noun)>, caps: Noun, reply: Noun }
impl Host for Recorder {
fn perform(&mut self, act: u64, args: &Noun, caps: &Noun) -> Result<Noun, InterpError> {
self.acts.push((act, args.clone()));
self.caps = caps.clone();
Ok(self.reply.clone())
}
}
fn emit_act(n: u64) -> Noun {
c(a(16), c(c(a(act::EMIT), c(a(1), a(n))), c(a(0), a(2))))
}
#[test]
fn act_performs_and_splices_result() {
let mut h = Recorder { acts: vec![], caps: a(0), reply: a(55) };
let r = eval_with_host(&a(0), &emit_act(7), &mut h).unwrap();
assert_eq!(h.acts.len(), 1);
assert_eq!(h.acts[0].0, act::EMIT);
assert_eq!(h.acts[0].1, a(7)); assert_eq!(r, a(55)); }
#[test]
fn acts_compose_via_cons() {
let formula = c(a(3), c(emit_act(1), emit_act(2)));
let mut h = Recorder { acts: vec![], caps: a(0), reply: a(0) };
eval_with_host(&a(0), &formula, &mut h).unwrap();
let args: Vec<Noun> = h.acts.iter().map(|(_, n)| n.clone()).collect();
assert_eq!(args, vec![a(1), a(2)]);
}
#[test]
fn act_reads_caps_from_axis_30() {
let subj = c(a(0), c(a(1), c(a(2), c(a(123), a(0)))));
let mut h = Recorder { acts: vec![], caps: a(0), reply: a(0) };
eval_with_host(&subj, &emit_act(7), &mut h).unwrap();
assert_eq!(h.caps, a(123));
}
#[test]
fn pure_eval_noops_acts() {
assert_eq!(eval(&a(0), &emit_act(7)).unwrap(), a(0));
}
}