use inf_ast::{Call, Term};
use inf_source::{LocalSource, RelationSource};
use inf_value::Value;
use nebu::Goldilocks;
use nox::{reduce, Order, Reduction, Outcome};
use std::collections::BTreeMap;
const N: usize = 8192;
const BUDGET: u64 = 1_000_000;
pub type Binding = BTreeMap<String, Value>;
#[derive(Debug, PartialEq)]
pub enum LowerError {
Unsupported(String),
Arena,
Runtime(String),
}
fn quote(o: &mut Reduction<N>, v: u64) -> Result<Order, LowerError> {
let one = o.atom(Goldilocks::new(1)).ok_or(LowerError::Arena)?;
let val = o.atom(Goldilocks::new(v)).ok_or(LowerError::Arena)?;
o.pair(one, val).ok_or(LowerError::Arena)
}
fn binary(o: &mut Reduction<N>, op: u64, a: Order, b: Order) -> Result<Order, LowerError> {
let body = o.pair(a, b).ok_or(LowerError::Arena)?;
let tag = o.atom(Goldilocks::new(op)).ok_or(LowerError::Arena)?;
o.pair(tag, body).ok_or(LowerError::Arena)
}
fn require2(c: &Call) -> Result<(), LowerError> {
if c.args.len() == 2 { Ok(()) }
else { Err(LowerError::Unsupported(format!("`{}` arity", c.func))) }
}
fn require1(c: &Call) -> Result<(), LowerError> {
if c.args.len() == 1 { Ok(()) }
else { Err(LowerError::Unsupported(format!("`{}` arity", c.func))) }
}
fn lower_term(o: &mut Reduction<N>, t: &Term, cols: &[String]) -> Result<Order, LowerError> {
match t {
Term::Int(i) if *i >= 0 => quote(o, *i as u64),
Term::Field(u) => quote(o, *u),
Term::Bool(b) => quote(o, if *b { 0 } else { 1 }),
Term::Var(v) => {
let idx = cols
.iter()
.position(|c| c == v)
.ok_or_else(|| LowerError::Unsupported(format!("variable `{v}`")))?;
let axis = (1u64 << (idx + 2)) - 2;
let zero = o.atom(Goldilocks::new(0)).ok_or(LowerError::Arena)?;
let ax = o.atom(Goldilocks::new(axis)).ok_or(LowerError::Arena)?;
o.pair(zero, ax).ok_or(LowerError::Arena)
}
Term::Call(c) if c.sibling.is_none() => lower_call(o, c, cols),
other => Err(LowerError::Unsupported(format!("{other:?}"))),
}
}
fn lower_call(o: &mut Reduction<N>, c: &Call, cols: &[String]) -> Result<Order, LowerError> {
match c.func.as_str() {
"add" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 5, a, b) }
"sub" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 6, a, b) }
"mul" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 7, a, b) }
"eq" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 9, a, b) }
"ne" | "neq" => {
require2(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let b = lower_term(o, &c.args[1], cols)?;
let eq_ab = binary(o, 9, a, b)?;
let zero = quote(o, 0)?;
binary(o, 10, zero, eq_ab)
}
"lt" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 10, a, b) }
"gt" => { require2(c)?; let a = lower_term(o, &c.args[0], cols)?; let b = lower_term(o, &c.args[1], cols)?; binary(o, 10, b, a) }
"ge" | "gte" => {
require2(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let b = lower_term(o, &c.args[1], cols)?;
let lt_ab = binary(o, 10, a, b)?;
let one = quote(o, 1)?;
binary(o, 9, lt_ab, one)
}
"le" | "lte" => {
require2(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let b = lower_term(o, &c.args[1], cols)?;
let lt_ba = binary(o, 10, b, a)?;
let one = quote(o, 1)?;
binary(o, 9, lt_ba, one)
}
"and" => {
require2(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let b = lower_term(o, &c.args[1], cols)?;
binary(o, 5, a, b)
}
"or" => {
require2(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let b = lower_term(o, &c.args[1], cols)?;
binary(o, 7, a, b)
}
"not" => {
require1(c)?;
let a = lower_term(o, &c.args[0], cols)?;
let one = quote(o, 1)?;
binary(o, 9, a, one)
}
_ => Err(LowerError::Unsupported(format!("operator `{}`", c.func))),
}
}
fn tuple_data(o: &mut Reduction<N>, tuple: &[i64]) -> Result<Order, LowerError> {
let mut acc = o.atom(Goldilocks::new(0)).ok_or(LowerError::Arena)?;
for &v in tuple.iter().rev() {
let val = o.atom(Goldilocks::new(v as u64)).ok_or(LowerError::Arena)?;
acc = o.pair(val, acc).ok_or(LowerError::Arena)?;
}
Ok(acc)
}
fn run(o: &mut Reduction<N>, subject: Order, formula: Order) -> Result<u64, LowerError> {
let calls = nox::NullCalls;
let mut tracer = nox::NoTrace;
match reduce(o, subject, formula, BUDGET, &calls, &mut tracer) {
Outcome::Ok(id, _) => {
let g = o.atom_value(id).ok_or(LowerError::Runtime("non-atom result".into()))?;
Ok(g.as_u64())
}
Outcome::Halt(_) => Err(LowerError::Runtime("halted".into())),
Outcome::Error(e) => Err(LowerError::Runtime(format!("{e:?}"))),
}
}
pub fn lower_and_run(t: &Term) -> Result<u64, LowerError> {
let mut o: Reduction<N> = Reduction::new();
let formula = lower_term(&mut o, t, &[])?;
let subject = o.atom(Goldilocks::new(0)).ok_or(LowerError::Arena)?;
run(&mut o, subject, formula)
}
pub fn lower_and_run_tuple(t: &Term, cols: &[String], tuple: &[i64]) -> Result<u64, LowerError> {
let mut o: Reduction<N> = Reduction::new();
let formula = lower_term(&mut o, t, cols)?;
let subject = tuple_data(&mut o, tuple)?;
run(&mut o, subject, formula)
}
fn collect_vars(t: &Term, out: &mut Vec<String>) {
match t {
Term::Var(v) => {
if !out.contains(v) {
out.push(v.clone());
}
}
Term::Call(c) => {
for arg in &c.args {
collect_vars(arg, out);
}
}
Term::List(items) => {
for item in items {
collect_vars(item, out);
}
}
_ => {}
}
}
pub fn eval_cond_via_nox(call: &Call, binding: &Binding) -> Option<bool> {
if call.sibling.is_some() {
return None; }
let t = Term::Call(Box::new(call.clone()));
let mut var_names: Vec<String> = Vec::new();
collect_vars(&t, &mut var_names);
let mut vals: Vec<i64> = Vec::new();
for v in &var_names {
match binding.get(v)? {
Value::Int(i) => vals.push(*i),
Value::Field(f) => vals.push(f.val() as i64),
Value::Bool(b) => vals.push(if *b { 0 } else { 1 }),
_ => return None,
}
}
let mut o: Reduction<N> = Reduction::new();
let formula = lower_term(&mut o, &t, &var_names).ok()?;
let subject = tuple_data(&mut o, &vals).ok()?;
match run(&mut o, subject, formula) {
Ok(v) => Some(v == 0), Err(_) => None,
}
}
pub fn make_nox_ctx() -> inf_eval::Ctx {
inf_eval::Ctx {
self_neuron: inf_value::tag_hash("@me"),
host: None,
nox_cond: Some(Box::new(|call, binding| eval_cond_via_nox(call, binding))),
}
}
#[cfg(feature = "prove")]
pub fn prove_expr(t: &Term) -> Result<(u64, zheng::TraceProof), LowerError> {
use nox::VecTrace;
let mut o: Reduction<N> = Reduction::new();
let formula = lower_term(&mut o, t, &[])?;
let subject = o.atom(Goldilocks::new(0)).ok_or(LowerError::Arena)?;
let calls = nox::NullCalls;
let mut tracer = VecTrace::default();
let result_id = match reduce(&mut o, subject, formula, BUDGET, &calls, &mut tracer) {
Outcome::Ok(id, _) => id,
Outcome::Halt(_) => return Err(LowerError::Runtime("halted".into())),
Outcome::Error(e) => return Err(LowerError::Runtime(format!("{e:?}"))),
};
let g = o.atom_value(result_id).ok_or(LowerError::Runtime("non-atom result".into()))?;
let result = g.as_u64();
let statement = zheng::Statement {
program_hash: [0u8; 32],
input_hash: [0u8; 32],
output_hash: [0u8; 32],
focus_bound: tracer.0.len() as u64,
};
let params = zheng::ProofParams::default();
let proof = zheng::commit(&tracer, &[], &[], &[], &statement, ¶ms)
.map_err(|e| LowerError::Runtime(format!("zheng commit: {e:?}")))?;
Ok((result, proof))
}
#[cfg(feature = "prove")]
pub fn verify_expr_proof(proof: &zheng::TraceProof) -> Result<(), LowerError> {
let statement = zheng::Statement {
program_hash: [0u8; 32],
input_hash: [0u8; 32],
output_hash: [0u8; 32],
focus_bound: u64::MAX,
};
let params = zheng::ProofParams::default();
zheng::verify(proof, &statement, ¶ms)
.map_err(|e| LowerError::Runtime(format!("zheng verify: {e:?}")))
}
#[cfg(feature = "prove")]
pub fn prove_cond(
cond: &inf_ast::Call,
col_names: &[String],
tuple: &[i64],
) -> Result<(bool, zheng::TraceProof), LowerError> {
use nox::VecTrace;
let t = Term::Call(Box::new(cond.clone()));
let mut o: Reduction<N> = Reduction::new();
let formula = lower_term(&mut o, &t, col_names)?;
let subject = tuple_data(&mut o, tuple)?;
let calls = nox::NullCalls;
let mut tracer = VecTrace::default();
let result_id = match reduce(&mut o, subject, formula, BUDGET, &calls, &mut tracer) {
Outcome::Ok(id, _) => id,
Outcome::Halt(_) => return Err(LowerError::Runtime("halted".into())),
Outcome::Error(e) => return Err(LowerError::Runtime(format!("{e:?}"))),
};
let g = o.atom_value(result_id).ok_or(LowerError::Runtime("non-atom result".into()))?;
let holds = g.as_u64() == 0;
let statement = zheng::Statement {
program_hash: [0u8; 32],
input_hash: [0u8; 32],
output_hash: [0u8; 32],
focus_bound: tracer.0.len() as u64,
};
let params = zheng::ProofParams::default();
let proof = zheng::commit(&tracer, &[], &[], &[], &statement, ¶ms)
.map_err(|e| LowerError::Runtime(format!("zheng commit: {e:?}")))?;
Ok((holds, proof))
}
pub const MAX_ARITY: u64 = 16;
fn val_to_u64(v: &Value) -> u64 {
match v {
Value::Int(i) => *i as u64,
Value::Field(f) => f.val(),
Value::Bool(b) => *b as u64,
_ => 0,
}
}
pub struct LocalLookProvider {
ns_rows: Vec<Vec<Vec<u64>>>, }
impl LocalLookProvider {
pub fn new(src: &LocalSource, rel_order: &[&str]) -> Self {
let ns_rows = rel_order.iter()
.map(|rel| src.scan(rel).map(|row| row.iter().map(val_to_u64).collect()).collect())
.collect();
LocalLookProvider { ns_rows }
}
}
impl nox::LookProvider for LocalLookProvider {
fn look(&self, _ct: Goldilocks, ns: Goldilocks, key: Goldilocks) -> Option<Goldilocks> {
let k = key.as_u64();
let v = self.ns_rows
.get(ns.as_u64() as usize)?
.get((k / MAX_ARITY) as usize)?
.get((k % MAX_ARITY) as usize)?;
Some(Goldilocks::new(*v))
}
}
impl<const NN: usize> nox::CallProvider<NN> for LocalLookProvider {
fn provide(&self, _o: &mut Reduction<NN>, _tag: Goldilocks, _obj: Order) -> Option<Order> {
None
}
}
fn bbg_obj(o: &mut Reduction<N>) -> Result<Order, LowerError> {
let l0 = o.atom(Goldilocks::ZERO).ok_or(LowerError::Arena)?;
let l1 = o.atom(Goldilocks::ZERO).ok_or(LowerError::Arena)?;
let l2 = o.atom(Goldilocks::ZERO).ok_or(LowerError::Arena)?;
let l3 = o.atom(Goldilocks::ZERO).ok_or(LowerError::Arena)?;
let inner = o.pair(l2, l3).ok_or(LowerError::Arena)?;
let mid = o.pair(l1, inner).ok_or(LowerError::Arena)?;
let root = o.pair(l0, mid).ok_or(LowerError::Arena)?;
let rest = o.atom(Goldilocks::ZERO).ok_or(LowerError::Arena)?;
o.pair(root, rest).ok_or(LowerError::Arena)
}
fn lf(o: &mut Reduction<N>, ns: u64, key: u64) -> Result<Order, LowerError> {
let op = o.atom(Goldilocks::new(17)).ok_or(LowerError::Arena)?;
let nsf = quote(o, ns)?;
let kf = quote(o, key)?;
let bdy = o.pair(nsf, kf).ok_or(LowerError::Arena)?;
o.pair(op, bdy).ok_or(LowerError::Arena)
}
fn cf(o: &mut Reduction<N>, a: Order, b: Order) -> Result<Order, LowerError> {
let op = o.atom(Goldilocks::new(3)).ok_or(LowerError::Arena)?;
let bdy = o.pair(a, b).ok_or(LowerError::Arena)?;
o.pair(op, bdy).ok_or(LowerError::Arena)
}
fn brf(o: &mut Reduction<N>, cond: Order, yes: Order, no: Order) -> Result<Order, LowerError> {
let op = o.atom(Goldilocks::new(4)).ok_or(LowerError::Arena)?;
let arms = o.pair(yes, no).ok_or(LowerError::Arena)?;
let bdy = o.pair(cond, arms).ok_or(LowerError::Arena)?;
o.pair(op, bdy).ok_or(LowerError::Arena)
}
fn tuple_f(o: &mut Reduction<N>, ns: u64, row: usize, cols: &[usize]) -> Result<Order, LowerError> {
let mut acc = quote(o, 0)?; for &col in cols.iter().rev() {
let lk = lf(o, ns, row as u64 * MAX_ARITY + col as u64)?;
acc = cf(o, lk, acc)?;
}
Ok(acc)
}
fn decode_results(o: &Reduction<N>, id: Order) -> Vec<Vec<u64>> {
let mut rows = Vec::new();
let mut cur = id;
while o.is_pair(cur) {
let head = o.head(cur).unwrap();
cur = o.tail(cur).unwrap();
rows.push(decode_tuple(o, head));
}
rows
}
fn decode_tuple(o: &Reduction<N>, id: Order) -> Vec<u64> {
let mut cols = Vec::new();
let mut cur = id;
while o.is_pair(cur) {
let head = o.head(cur).unwrap();
cur = o.tail(cur).unwrap();
if let Some(g) = o.atom_value(head) {
cols.push(g.as_u64());
}
}
cols
}
fn run_rel(o: &mut Reduction<N>, formula: Order, prov: &LocalLookProvider) -> Result<Order, LowerError> {
let subj = bbg_obj(o)?;
let mut tracer = nox::NoTrace;
match reduce(o, subj, formula, BUDGET, prov, &mut tracer) {
Outcome::Ok(id, _) => Ok(id),
Outcome::Halt(_) => Err(LowerError::Runtime("halted".into())),
Outcome::Error(e) => Err(LowerError::Runtime(format!("look-scan: {e:?}"))),
}
}
pub fn nox_vm_scan(src: &LocalSource, rel: &str, out_cols: &[usize]) -> Result<Vec<Vec<u64>>, LowerError> {
let prov = LocalLookProvider::new(src, &[rel]);
let n_rows = src.scan(rel).count();
let mut o: Reduction<N> = Reduction::new();
let mut acc = quote(&mut o, 0)?; for row in (0..n_rows).rev() {
let tup = tuple_f(&mut o, 0, row, out_cols)?;
acc = cf(&mut o, tup, acc)?;
}
let result = run_rel(&mut o, acc, &prov)?;
let mut rows = decode_results(&o, result);
rows.sort();
Ok(rows)
}
pub fn nox_vm_scan_gt(
src: &LocalSource, rel: &str,
filter_col: usize, threshold: u64,
out_cols: &[usize],
) -> Result<Vec<Vec<u64>>, LowerError> {
let prov = LocalLookProvider::new(src, &[rel]);
let n_rows = src.scan(rel).count();
let mut o: Reduction<N> = Reduction::new();
let mut acc = quote(&mut o, 0)?;
for row in (0..n_rows).rev() {
let lk = lf(&mut o, 0, row as u64 * MAX_ARITY + filter_col as u64)?;
let thresh = quote(&mut o, threshold)?;
let cond = binary(&mut o, 10, thresh, lk)?;
let tup = tuple_f(&mut o, 0, row, out_cols)?;
let inc = cf(&mut o, tup, acc)?;
acc = brf(&mut o, cond, inc, acc)?;
}
let result = run_rel(&mut o, acc, &prov)?;
let mut rows = decode_results(&o, result);
rows.sort();
Ok(rows)
}
pub fn nox_vm_equijoin(
src: &LocalSource,
rel1: &str, jcol1: usize,
rel2: &str, jcol2: usize,
out1: &[usize], out2: &[usize],
extra_gt: Option<(usize, u64)>, ) -> Result<Vec<Vec<u64>>, LowerError> {
let prov = LocalLookProvider::new(src, &[rel1, rel2]);
let n1 = src.scan(rel1).count();
let n2 = src.scan(rel2).count();
let mut o: Reduction<N> = Reduction::new();
let (ns1, ns2) = (0u64, 1u64);
let mut acc = quote(&mut o, 0)?;
for r1 in (0..n1).rev() {
for r2 in (0..n2).rev() {
let lk1 = lf(&mut o, ns1, r1 as u64 * MAX_ARITY + jcol1 as u64)?;
let lk2 = lf(&mut o, ns2, r2 as u64 * MAX_ARITY + jcol2 as u64)?;
let jcond = binary(&mut o, 9, lk1, lk2)?;
let cond = if let Some((fc, thr)) = extra_gt {
let lkfc = lf(&mut o, ns2, r2 as u64 * MAX_ARITY + fc as u64)?;
let thrq = quote(&mut o, thr)?;
let fcond = binary(&mut o, 10, thrq, lkfc)?; binary(&mut o, 5, jcond, fcond)? } else {
jcond
};
let mut parts = Vec::new();
for &c in out1 { parts.push(lf(&mut o, ns1, r1 as u64 * MAX_ARITY + c as u64)?); }
for &c in out2 { parts.push(lf(&mut o, ns2, r2 as u64 * MAX_ARITY + c as u64)?); }
let mut tup = quote(&mut o, 0)?;
for &p in parts.iter().rev() { tup = cf(&mut o, p, tup)?; }
let inc = cf(&mut o, tup, acc)?;
acc = brf(&mut o, cond, inc, acc)?;
}
}
let result = run_rel(&mut o, acc, &prov)?;
let mut rows = decode_results(&o, result);
rows.sort();
Ok(rows)
}
fn decode_atom_list(o: &Reduction<N>, id: Order) -> Vec<u64> {
let mut vals = Vec::new();
let mut cur = id;
while o.is_pair(cur) {
let head = o.head(cur).unwrap();
cur = o.tail(cur).unwrap();
if let Some(g) = o.atom_value(head) {
vals.push(g.as_u64());
}
}
vals
}
pub fn nox_vm_bounded_reach(
src: &LocalSource,
edge_rel: &str,
from_col: usize,
to_col: usize,
seed: u64,
extra_steps: usize,
) -> Result<Vec<u64>, LowerError> {
let prov = LocalLookProvider::new(src, &[edge_rel]);
let n_edges = src.scan(edge_rel).count();
let one_step = |frontier: &[u64]| -> Result<Vec<u64>, LowerError> {
let mut o: Reduction<N> = Reduction::new();
let mut acc = quote(&mut o, 0)?;
for r in (0..n_edges).rev() {
let lk_from = lf(&mut o, 0, r as u64 * MAX_ARITY + from_col as u64)?;
let lk_to = lf(&mut o, 0, r as u64 * MAX_ARITY + to_col as u64)?;
let mut cond = quote(&mut o, 1)?; for &f in frontier {
let fq = quote(&mut o, f)?;
let eq_f = binary(&mut o, 9, lk_from, fq)?;
cond = binary(&mut o, 7, cond, eq_f)?; }
let inc = cf(&mut o, lk_to, acc)?;
acc = brf(&mut o, cond, inc, acc)?;
}
let result = run_rel(&mut o, acc, &prov)?;
Ok(decode_atom_list(&o, result))
};
let base = one_step(&[seed])?;
let mut reached: Vec<u64> = base.clone();
let mut frontier: Vec<u64> = base;
reached.sort(); reached.dedup();
for _ in 0..extra_steps {
if frontier.is_empty() { break; }
let next = one_step(&frontier)?;
let mut new_frontier = Vec::new();
for n in next {
if !reached.contains(&n) {
reached.push(n);
new_frontier.push(n);
}
}
frontier = new_frontier;
}
reached.sort();
Ok(reached)
}
#[cfg(test)]
mod tests {
use super::*;
use inf_ast::{Call, Term};
use inf_eval::eval;
use inf_source::LocalSource;
use inf_value::Value;
fn call(func: &str, a: Term, b: Term) -> Term {
Term::Call(Box::new(Call { sibling: None, func: func.into(), args: vec![a, b] }))
}
fn int(i: i64) -> Term {
Term::Int(i)
}
fn var(v: &str) -> Term {
Term::Var(v.into())
}
fn assert_numeric_match(t: &Term) {
let nox_val = lower_and_run(t).expect("lower+run");
let reference = inf_eval::eval_const_term(t).expect("eval");
let want = match reference {
Value::Int(i) => (i as u128 % inf_value::P as u128) as u64,
Value::Field(f) => f.val(),
other => panic!("non-numeric reference {other:?}"),
};
assert_eq!(nox_val, want, "nox vs reference mismatch for {t:?}");
}
#[test]
fn arithmetic_lowers_and_matches_reference() {
assert_numeric_match(&call("add", int(2), int(3)));
assert_numeric_match(&call("mul", int(6), int(7)));
assert_numeric_match(&call("sub", int(10), int(3)));
assert_numeric_match(&call("add", call("mul", int(2), int(3)), int(4)));
}
#[test]
fn tuple_columns_lower_and_run_on_nox() {
fn check(t: &Term, cols: &[&str], tuple: &[i64]) {
let cs: Vec<String> = cols.iter().map(|s| s.to_string()).collect();
let nox_val = lower_and_run_tuple(t, &cs, tuple).expect("lower+run");
let reference = inf_eval::eval_term_in(t, &cs, tuple).expect("eval");
let want = match reference {
Value::Int(i) => (i as u128 % inf_value::P as u128) as u64,
Value::Field(f) => f.val(),
other => panic!("non-numeric reference {other:?}"),
};
assert_eq!(nox_val, want, "nox vs reference for {t:?}");
}
check(&call("add", call("mul", var("a"), var("b")), var("c")), &["a", "b", "c"], &[10, 3, 7]);
check(&call("mul", call("add", var("x"), var("y")), var("x")), &["x", "y"], &[5, 2]);
}
#[test]
fn runs_on_the_real_nox_vm() {
assert_eq!(lower_and_run(&call("add", int(40), int(2))).unwrap(), 42);
}
#[test]
fn unsupported_is_reported_not_faked() {
let r = lower_and_run(&Term::Str("x".into()));
assert!(matches!(r, Err(LowerError::Unsupported(_))));
}
fn cond_call(func: &str, a: Term, b: Term) -> Call {
Call { sibling: None, func: func.into(), args: vec![a, b] }
}
fn binding_from(pairs: &[(&str, i64)]) -> Binding {
pairs.iter().map(|(k, v)| (k.to_string(), Value::Int(*v))).collect()
}
#[test]
fn nox_cond_lt() {
let c = cond_call("lt", var("d"), int(4));
let b3 = binding_from(&[("d", 3)]);
let b5 = binding_from(&[("d", 5)]);
assert_eq!(eval_cond_via_nox(&c, &b3), Some(true), "3 < 4 should pass");
assert_eq!(eval_cond_via_nox(&c, &b5), Some(false), "5 < 4 should fail");
}
#[test]
fn nox_cond_gt() {
let c = cond_call("gt", var("x"), int(0));
let b1 = binding_from(&[("x", 1)]);
let b0 = binding_from(&[("x", 0)]);
assert_eq!(eval_cond_via_nox(&c, &b1), Some(true));
assert_eq!(eval_cond_via_nox(&c, &b0), Some(false));
}
#[test]
fn nox_cond_eq_and_ne() {
let ceq = cond_call("eq", var("a"), var("b"));
let cne = cond_call("ne", var("a"), var("b"));
let same = binding_from(&[("a", 7), ("b", 7)]);
let diff = binding_from(&[("a", 7), ("b", 8)]);
assert_eq!(eval_cond_via_nox(&ceq, &same), Some(true));
assert_eq!(eval_cond_via_nox(&ceq, &diff), Some(false));
assert_eq!(eval_cond_via_nox(&cne, &same), Some(false));
assert_eq!(eval_cond_via_nox(&cne, &diff), Some(true));
}
#[test]
fn nox_cond_ge_le() {
let cge = cond_call("ge", var("x"), var("y"));
let cle = cond_call("le", var("x"), var("y"));
let b55 = binding_from(&[("x", 5), ("y", 5)]);
let b35 = binding_from(&[("x", 3), ("y", 5)]);
assert_eq!(eval_cond_via_nox(&cge, &b55), Some(true), "5>=5");
assert_eq!(eval_cond_via_nox(&cge, &b35), Some(false), "3>=5");
assert_eq!(eval_cond_via_nox(&cle, &b55), Some(true), "5<=5");
assert_eq!(eval_cond_via_nox(&cle, &b35), Some(true), "3<=5");
}
#[test]
fn nox_cond_and_or() {
let a5 = var("a"); let b3 = var("b");
let lt_a = Call { sibling: None, func: "lt".into(), args: vec![a5.clone(), int(10)] };
let gt_b = Call { sibling: None, func: "gt".into(), args: vec![b3.clone(), int(0)] };
let cand = Call {
sibling: None,
func: "and".into(),
args: vec![Term::Call(Box::new(lt_a.clone())), Term::Call(Box::new(gt_b.clone()))],
};
let cor = Call {
sibling: None,
func: "or".into(),
args: vec![Term::Call(Box::new(lt_a.clone())), Term::Call(Box::new(gt_b.clone()))],
};
let both = binding_from(&[("a", 5), ("b", 3)]);
let fail_b = binding_from(&[("a", 5), ("b", 0)]);
assert_eq!(eval_cond_via_nox(&cand, &both), Some(true), "and: both pass");
assert_eq!(eval_cond_via_nox(&cand, &fail_b), Some(false), "and: b fails");
assert_eq!(eval_cond_via_nox(&cor, &both), Some(true), "or: both pass");
assert_eq!(eval_cond_via_nox(&cor, &fail_b), Some(true), "or: a still passes");
}
#[test]
fn nox_cond_returns_none_for_hash_vars() {
let c = cond_call("eq", var("x"), int(0));
let b_hash: Binding = std::iter::once(("x".to_string(), Value::Hash([0u8; 32]))).collect();
assert_eq!(eval_cond_via_nox(&c, &b_hash), None, "hash var โ None (fallback to ref)");
}
fn fixture() -> LocalSource {
use inf_value::Value::Int;
let mut src = LocalSource::new();
src.add("edge", &["from", "to"], vec![
vec![Int(1), Int(2)],
vec![Int(2), Int(3)],
vec![Int(3), Int(4)],
vec![Int(4), Int(5)],
]);
src.add("node", &["id", "weight"], vec![
vec![Int(1), Int(10)],
vec![Int(2), Int(20)],
vec![Int(3), Int(5)],
vec![Int(4), Int(15)],
vec![Int(5), Int(8)],
]);
src
}
fn parse_and_plan(prog: &str) -> inf_ast::IrProgram {
let ast = inf_parse::parse(prog).expect("parse");
inf_plan::plan(&ast).expect("plan")
}
fn assert_nox_matches_reference(prog: &str, src: &LocalSource) {
let ir = parse_and_plan(prog);
let ref_ctx = inf_eval::Ctx::default();
let nox_ctx = make_nox_ctx();
let ref_out = eval(&ir, src, &ref_ctx).expect("reference eval");
let nox_out = eval(&ir, src, &nox_ctx).expect("nox eval");
assert_eq!(ref_out.rows, nox_out.rows,
"nox vs reference mismatch\nreference: {:?}\nnox: {:?}", ref_out.rows, nox_out.rows);
}
#[test]
fn whole_query_filter_gt_matches_reference() {
let src = fixture();
assert_nox_matches_reference(
"?[id, w] := node{id: id, weight: w}, gt(w, 10)",
&src,
);
}
#[test]
fn whole_query_filter_lt_matches_reference() {
let src = fixture();
assert_nox_matches_reference(
"?[id] := node{id: id, weight: w}, lt(w, 15)",
&src,
);
}
#[test]
fn whole_query_join_with_filter_matches_reference() {
let src = fixture();
assert_nox_matches_reference(
"?[f, t] := edge{from: f, to: t}, node{id: f, weight: w}, gt(w, 10)",
&src,
);
}
#[test]
fn whole_query_bounded_reachability_matches_reference() {
let src = fixture();
assert_nox_matches_reference(
"reach[n] := edge{from: 1, to: n}\n\
reach[n] := reach[m], edge{from: m, to: n}\n\
:bounded 2\n\
?[n] := reach[n]",
&src,
);
}
#[test]
fn whole_query_eq_condition_matches_reference() {
let src = fixture();
assert_nox_matches_reference(
"?[f, t] := edge{from: f, to: t}, eq(f, 2)",
&src,
);
}
fn fixture_src() -> LocalSource {
use inf_value::Value::Int;
let mut src = LocalSource::new();
src.add("node", &["id", "weight"], vec![
vec![Int(1), Int(10)],
vec![Int(2), Int(20)],
vec![Int(3), Int(5)],
vec![Int(4), Int(15)],
]);
src.add("edge", &["from", "to"], vec![
vec![Int(1), Int(2)],
vec![Int(2), Int(3)],
vec![Int(3), Int(4)],
]);
src
}
fn ref_scan(src: &LocalSource, rel: &str, out_cols: &[usize]) -> Vec<Vec<u64>> {
let mut rows: Vec<Vec<u64>> = src.scan(rel)
.map(|row| out_cols.iter().map(|&c| val_to_u64(&row[c])).collect())
.collect();
rows.sort();
rows
}
#[test]
fn nox_scan_all_rows_matches_reference() {
let src = fixture_src();
let nox_rows = nox_vm_scan(&src, "node", &[0, 1]).unwrap();
let ref_rows = ref_scan(&src, "node", &[0, 1]);
assert_eq!(nox_rows, ref_rows, "nox scan all rows vs reference");
}
#[test]
fn nox_scan_single_column() {
let src = fixture_src();
let nox_rows = nox_vm_scan(&src, "node", &[0]).unwrap();
let ref_rows = ref_scan(&src, "node", &[0]);
assert_eq!(nox_rows, ref_rows, "nox scan id column only");
}
#[test]
fn nox_scan_gt_filter_matches_reference() {
let src = fixture_src();
let nox_rows = nox_vm_scan_gt(&src, "node", 1, 10, &[0, 1]).unwrap();
let ref_rows: Vec<Vec<u64>> = {
let mut r: Vec<Vec<u64>> = src.scan("node")
.filter(|row| val_to_u64(&row[1]) > 10)
.map(|row| vec![val_to_u64(&row[0]), val_to_u64(&row[1])])
.collect();
r.sort(); r
};
assert_eq!(nox_rows, ref_rows, "nox scan+gt filter vs reference");
}
#[test]
fn nox_equijoin_matches_reference() {
let src = fixture_src();
let nox_rows = nox_vm_equijoin(&src,
"edge", 0, "node", 0, &[0, 1], &[], None, ).unwrap();
let ref_rows: Vec<Vec<u64>> = {
let edges: Vec<_> = src.scan("edge").collect();
let nodes: Vec<_> = src.scan("node").collect();
let mut r = Vec::new();
for e in &edges {
for n in &nodes {
if val_to_u64(&e[0]) == val_to_u64(&n[0]) {
r.push(vec![val_to_u64(&e[0]), val_to_u64(&e[1])]);
}
}
}
r.sort(); r
};
assert_eq!(nox_rows, ref_rows, "nox equijoin vs reference");
}
#[test]
fn nox_equijoin_with_filter_matches_reference() {
let src = fixture_src();
let nox_rows = nox_vm_equijoin(&src,
"edge", 0,
"node", 0,
&[0, 1],
&[],
Some((1, 10)), ).unwrap();
let ref_rows: Vec<Vec<u64>> = {
let edges: Vec<_> = src.scan("edge").collect();
let nodes: Vec<_> = src.scan("node").collect();
let mut r = Vec::new();
for e in &edges {
for n in &nodes {
if val_to_u64(&e[0]) == val_to_u64(&n[0]) && val_to_u64(&n[1]) > 10 {
r.push(vec![val_to_u64(&e[0]), val_to_u64(&e[1])]);
}
}
}
r.sort(); r
};
assert_eq!(nox_rows, ref_rows, "nox equijoin+filter vs reference");
}
#[test]
fn nox_bounded_reach_1hop_matches_reference() {
let src = fixture_src();
let nox_reached = nox_vm_bounded_reach(&src, "edge", 0, 1, 1, 1).unwrap();
let ir = parse_and_plan(
"reach[n] := edge{from: 1, to: n}\n\
reach[n] := reach[m], edge{from: m, to: n}\n\
:bounded 2\n\
?[n] := reach[n]",
);
let ref_out = eval(&ir, &src, &inf_eval::Ctx::default()).expect("ref eval");
let mut ref_nodes: Vec<u64> = ref_out.rows.iter().map(|r| val_to_u64(&r[0])).collect();
ref_nodes.sort();
assert_eq!(nox_reached, ref_nodes, "nox 1-hop reach vs reference :bounded 2");
}
#[test]
fn nox_bounded_reach_2hops_matches_reference() {
let src = fixture_src();
let nox_reached = nox_vm_bounded_reach(&src, "edge", 0, 1, 1, 2).unwrap();
let ir = parse_and_plan(
"reach[n] := edge{from: 1, to: n}\n\
reach[n] := reach[m], edge{from: m, to: n}\n\
:bounded 3\n\
?[n] := reach[n]",
);
let ref_out = eval(&ir, &src, &inf_eval::Ctx::default()).expect("ref eval");
let mut ref_nodes: Vec<u64> = ref_out.rows.iter().map(|r| val_to_u64(&r[0])).collect();
ref_nodes.sort();
assert_eq!(nox_reached, ref_nodes, "nox 2-hop reach vs reference :bounded 3");
}
#[cfg(feature = "prove")]
#[test]
fn prove_and_verify_arithmetic_expr() {
let t = call("mul", int(6), int(7));
let (result, proof) = prove_expr(&t).expect("prove");
assert_eq!(result, 42, "nox computes 6*7=42");
verify_expr_proof(&proof).expect("verify");
}
#[cfg(feature = "prove")]
#[test]
fn prove_and_verify_condition() {
let c = inf_ast::Call { sibling: None, func: "lt".into(), args: vec![int(3), int(5)] };
let (holds, proof) = prove_cond(&c, &[], &[]).expect("prove");
assert!(holds, "3 < 5 should hold");
verify_expr_proof(&proof).expect("verify");
}
#[cfg(feature = "prove")]
#[test]
fn prove_and_verify_tuple_condition() {
let weight = Term::Var("weight".into());
let c = inf_ast::Call {
sibling: None,
func: "gt".into(),
args: vec![weight, int(10)],
};
let cols = vec!["weight".to_string()];
let (holds, proof) = prove_cond(&c, &cols, &[15]).expect("prove");
assert!(holds, "15 > 10 should hold");
verify_expr_proof(&proof).expect("verify");
}
}
#[cfg(all(test, feature = "bbg"))]
mod bbg_diff_tests {
use bbg::state::BbgState;
use bbg::types::NeuronRecord;
use inf_eval::eval;
use inf_source::{BbgSource, RelationSource};
fn parse_and_plan(prog: &str) -> inf_ast::IrProgram {
let ast = inf_parse::parse(prog).expect("parse");
inf_plan::plan(&ast).expect("plan")
}
fn nid(n: u8) -> [u8; 32] {
[n; 32]
}
fn seeded_state() -> BbgState {
let mut st = BbgState::new();
st.neurons.insert(nid(1), NeuronRecord { focus: 100, karma: 1, stake: 0 });
st.neurons.insert(nid(2), NeuronRecord { focus: 5_000, karma: 2, stake: 0 });
st.neurons.insert(nid(3), NeuronRecord { focus: 9_000, karma: 3, stake: 0 });
st
}
fn mirror(bbg_src: &BbgSource, rels: &[&str]) -> inf_source::LocalSource {
let mut local = inf_source::LocalSource::new();
for &rel in rels {
let schema = bbg_src.schema(rel).expect("schema");
let cols: Vec<&str> = schema.columns.iter().map(|c| c.as_str()).collect();
let rows = bbg_src.scan(rel).collect::<Vec<_>>();
local.add(rel, &cols, rows);
}
local
}
fn assert_parity(prog: &str, st: &BbgState, rels: &[&str]) {
let bbg_src = BbgSource::new(st);
let local = mirror(&bbg_src, rels);
let ir = parse_and_plan(prog);
let ctx = inf_eval::Ctx::default();
let bbg_out = eval(&ir, &bbg_src, &ctx).expect("bbg eval");
let local_out = eval(&ir, &local, &ctx).expect("local eval");
assert_eq!(
bbg_out.rows, local_out.rows,
"BbgSource vs LocalSource mismatch\nbbg: {:?}\nlocal: {:?}",
bbg_out.rows, local_out.rows
);
assert!(!bbg_out.rows.is_empty(), "query should return rows");
}
#[test]
fn scan_projection_parity() {
let st = seeded_state();
assert_parity("?[id, focus] := neurons{id: id, focus: focus}", &st, &["neurons"]);
}
#[test]
fn filter_gt_parity() {
let st = seeded_state();
assert_parity(
"?[id, focus] := neurons{id: id, focus: focus}, gt(focus, 1000)",
&st,
&["neurons"],
);
}
#[test]
fn multi_column_filter_parity() {
let st = seeded_state();
assert_parity(
"?[id] := neurons{id: id, karma: k}, gt(k, 1)",
&st,
&["neurons"],
);
}
}