mod agg;
mod expr;
mod fixed;
mod fixed_more;
mod funcs;
pub use expr::Ctx;
use inf_ast::*;
use inf_source::RelationSource;
use inf_value::{Relation, Tuple, Value};
use std::collections::{BTreeMap, HashMap, HashSet};
use expr::{eval_call, eval_term, Binding};
const HARD_CAP: usize = 100_000;
#[derive(Clone, Debug, PartialEq)]
pub struct EvalError {
pub msg: String,
}
fn err<T>(msg: impl Into<String>) -> Result<T, EvalError> {
Err(EvalError { msg: msg.into() })
}
#[derive(Clone, Debug, PartialEq)]
pub enum MutOp {
Link,
Unlink,
Put(String),
Rm(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Output {
pub columns: Vec<String>,
pub rows: Vec<Tuple>,
pub mutation: Option<MutOp>,
}
type Env = BTreeMap<String, (Vec<String>, Relation)>;
fn head_cols(h: &Head) -> Vec<String> {
h.args
.iter()
.map(|a| match a {
HeadArg::Var(v) => v.clone(),
HeadArg::Aggr { var, .. } => var.clone(),
})
.collect()
}
fn head_rel(r: &Rule) -> String {
r.head.name.clone().unwrap_or_else(|| ENTRY.to_string())
}
pub fn eval_const_term(t: &Term) -> Result<Value, EvalError> {
expr::eval_term(t, &Default::default(), &Ctx::default()).map_err(|m| EvalError { msg: m })
}
pub fn eval_term_in(t: &Term, cols: &[String], ints: &[i64]) -> Result<Value, EvalError> {
let mut b = expr::Binding::new();
for (c, v) in cols.iter().zip(ints) {
b.insert(c.clone(), Value::Int(*v));
}
expr::eval_term(t, &b, &Ctx::default()).map_err(|m| EvalError { msg: m })
}
#[derive(Clone, Debug)]
pub struct Event {
pub rel: String,
pub tuple: Tuple,
}
pub fn eval_reactive(
ir: &IrProgram,
mut source: inf_source::LocalSource,
events: &[Event],
ctx: &Ctx,
) -> Result<Vec<Output>, EvalError> {
let mut outs = Vec::new();
for ev in events {
source.insert(&ev.rel, ev.tuple.clone()).map_err(|m| EvalError { msg: m })?;
let trigger = match &ir.subscribe {
None => true,
Some((rel, _)) => &ev.rel == rel,
};
if trigger {
outs.push(eval(ir, &source, ctx)?);
}
}
Ok(outs)
}
pub fn eval(ir: &IrProgram, src: &dyn RelationSource, ctx: &Ctx) -> Result<Output, EvalError> {
let mut env: Env = Env::new();
for stratum in &ir.strata {
eval_stratum(stratum, &mut env, src, ctx)?;
}
let (cols, rel) = env
.get(&ir.entry)
.cloned()
.unwrap_or_else(|| (Vec::new(), Relation::new()));
let mut rows: Vec<Tuple> = rel.into_iter().collect();
apply_opts(&ir.opts, &cols, &mut rows)?;
if let Some(m) = &ir.mutation {
return build_mutation(m, &cols, &rows, ctx);
}
Ok(Output { columns: cols, rows, mutation: None })
}
fn eval_stratum(
stratum: &Stratum,
env: &mut Env,
src: &dyn RelationSource,
ctx: &Ctx,
) -> Result<(), EvalError> {
for r in &stratum.rules {
env.entry(head_rel(r)).or_insert_with(|| (head_cols(&r.head), Relation::new()));
}
eval_stratum_round(stratum, env, src, ctx)?;
if !stratum.recursive {
return Ok(());
}
let in_stratum: HashSet<String> = stratum.rules.iter().map(head_rel).collect();
let mut delta: BTreeMap<String, Relation> = in_stratum
.iter()
.map(|rel| (rel.clone(), env.get(rel).map(|(_, r)| r.clone()).unwrap_or_default()))
.collect();
let mut rounds = 1usize;
loop {
if delta.values().all(|d| d.is_empty()) {
break;
}
if let Some(b) = stratum.bound {
if rounds as u64 >= b {
break;
}
}
if rounds >= HARD_CAP {
return err("evaluation exceeded the hard round cap (non-terminating?)");
}
let mut binds_by_head: BTreeMap<String, (Head, Vec<Binding>)> = BTreeMap::new();
for r in &stratum.rules {
if r.fixed.is_some() {
continue; }
let recursive_positions: Vec<usize> = r
.body
.iter()
.enumerate()
.filter_map(|(i, a)| match a {
Atom::Read { rel, .. } | Atom::Apply { rule: rel, .. }
if in_stratum.contains(rel) =>
{
Some(i)
}
_ => None,
})
.collect();
if recursive_positions.is_empty() {
continue; }
for pos in recursive_positions {
let rel = r.body[pos].rel_name().unwrap().to_string();
let d = match delta.get(&rel) {
Some(d) if !d.is_empty() => d,
_ => continue,
};
let delta_cols = env.get(&rel).map(|(c, _)| c.clone()).unwrap_or_default();
let delta_rows: Vec<Tuple> = d.iter().cloned().collect();
let bs = eval_body_delta_variant(r, env, src, ctx, pos, &delta_cols, &delta_rows)?;
let e = binds_by_head
.entry(head_rel(r))
.or_insert_with(|| (r.head.clone(), Vec::new()));
e.1.extend(bs);
}
}
let mut delta_next: BTreeMap<String, Relation> = BTreeMap::new();
for (rel, (head, binds)) in binds_by_head {
let new_facts: Vec<Tuple> = if head.has_aggr() {
agg::aggregate(&head, &binds).map_err(|m| EvalError { msg: m })?
} else {
binds.iter().map(|b| project(&head, b)).collect::<Result<_, _>>()?
};
let slot = env.get_mut(&rel).unwrap();
let mut nd = Relation::new();
for t in new_facts {
if slot.1.insert(t.clone()) {
nd.insert(t);
}
}
delta_next.insert(rel, nd);
}
delta = delta_next;
rounds += 1;
}
Ok(())
}
fn eval_stratum_round(
stratum: &Stratum,
env: &mut Env,
src: &dyn RelationSource,
ctx: &Ctx,
) -> Result<(), EvalError> {
let mut binds_by_head: BTreeMap<String, (Head, Option<String>, Vec<Binding>)> = BTreeMap::new();
for r in &stratum.rules {
if let Some(f) = &r.fixed {
let edges = rows_of(&f.edges, env, src)?.1;
let tuples = fixed::run(f, &r.head, &edges, ctx).map_err(|m| EvalError { msg: m })?;
let new: Relation = tuples.into_iter().collect();
env.get_mut(&head_rel(r)).unwrap().1 = new;
} else {
let bs = eval_body(r, env, src, ctx)?;
let e = binds_by_head
.entry(head_rel(r))
.or_insert_with(|| (r.head.clone(), r.order.clone(), Vec::new()));
e.2.extend(bs);
}
}
for (rel, (head, order, binds)) in binds_by_head {
if let Some(order_var) = order {
let tuples =
agg::windowed_aggregate(&head, &binds, &order_var).map_err(|m| EvalError { msg: m })?;
env.get_mut(&rel).unwrap().1 = tuples.into_iter().collect();
} else if head.has_aggr() {
let tuples = agg::aggregate(&head, &binds).map_err(|m| EvalError { msg: m })?;
env.get_mut(&rel).unwrap().1 = tuples.into_iter().collect();
} else {
let slot = env.get_mut(&rel).unwrap();
for b in &binds {
let t = project(&head, b)?;
slot.1.insert(t);
}
}
}
Ok(())
}
fn project(head: &Head, b: &Binding) -> Result<Tuple, EvalError> {
let mut t = Vec::new();
for a in &head.args {
match a {
HeadArg::Var(v) => {
t.push(b.get(v).cloned().ok_or_else(|| EvalError {
msg: format!("head variable `{v}` unbound after body evaluation"),
})?);
}
HeadArg::Aggr { .. } => {
return err("aggregation head reached the non-aggregate projection path");
}
}
}
Ok(t)
}
fn eval_body(
r: &Rule,
env: &Env,
src: &dyn RelationSource,
ctx: &Ctx,
) -> Result<Vec<Binding>, EvalError> {
let mut bindings = vec![Binding::new()];
for atom in &r.body {
bindings = step_atom(atom, bindings, env, src, ctx)?;
if bindings.is_empty() {
break;
}
}
Ok(bindings)
}
fn rows_of(
rel: &str,
env: &Env,
src: &dyn RelationSource,
) -> Result<(Vec<String>, Vec<Tuple>), EvalError> {
if let Some((cols, r)) = env.get(rel) {
return Ok((cols.clone(), r.iter().cloned().collect()));
}
if let Some(schema) = src.schema(rel) {
return Ok((schema.columns, src.scan(rel).collect()));
}
err(format!("unknown relation `{rel}`"))
}
fn step_atom(
atom: &Atom,
bindings: Vec<Binding>,
env: &Env,
src: &dyn RelationSource,
ctx: &Ctx,
) -> Result<Vec<Binding>, EvalError> {
match atom {
Atom::Read { rel, binds } => read_join(rel, binds, bindings, env, src, ctx),
Atom::Apply { rule, args } => {
let binds = Binds::Pos(args.clone());
read_join(rule, &binds, bindings, env, src, ctx)
}
Atom::Cond(call) => {
let mut out = Vec::new();
for b in bindings {
let passes = if let Some(nox) = &ctx.nox_cond {
match nox(call, &b) {
Some(r) => r,
None => eval_call(call, &b, ctx)
.map_err(|m| EvalError { msg: m })?
.truthy(),
}
} else {
eval_call(call, &b, ctx).map_err(|m| EvalError { msg: m })?.truthy()
};
if passes {
out.push(b);
}
}
Ok(out)
}
Atom::Bind { var, term } => {
let mut out = Vec::new();
for mut b in bindings {
let v = eval_term(term, &b, ctx).map_err(|m| EvalError { msg: m })?;
match b.get(var) {
Some(existing) if *existing != v => {}
_ => {
b.insert(var.clone(), v);
out.push(b);
}
}
}
Ok(out)
}
Atom::Not(inner) => {
let (rel, binds) = match inner.as_ref() {
Atom::Read { rel, binds } => (rel.clone(), binds.clone()),
Atom::Apply { rule, args } => (rule.clone(), Binds::Pos(args.clone())),
_ => return err("negation must wrap a relation read"),
};
let (cols, rows) = rows_of(&rel, env, src)?;
let mut out = Vec::new();
let idx = bindings.first().and_then(|s| BoundIndex::build(&binds, &cols, &rows, s));
for b in bindings {
let matched = match &idx {
Some(i) => any_match(i.candidates(&b, ctx).iter().copied(), &binds, &cols, &b, ctx)?,
None => any_match(rows.iter(), &binds, &cols, &b, ctx)?,
};
if !matched {
out.push(b);
}
}
Ok(out)
}
}
}
struct BoundIndex<'a> {
key_cols: Vec<(usize, &'a Term)>,
by_key: HashMap<Vec<Value>, Vec<&'a Tuple>>,
}
impl<'a> BoundIndex<'a> {
fn build(binds: &'a Binds, cols: &[String], rows: &'a [Tuple], sample: &Binding) -> Option<Self> {
let key_cols = bound_columns(binds, cols, sample);
if key_cols.is_empty() {
return None;
}
let mut by_key: HashMap<Vec<Value>, Vec<&Tuple>> = HashMap::new();
for row in rows {
let key: Vec<Value> = key_cols.iter().map(|(i, _)| row[*i].clone()).collect();
by_key.entry(key).or_default().push(row);
}
Some(BoundIndex { key_cols, by_key })
}
fn candidates(&self, b: &Binding, ctx: &Ctx) -> &[&'a Tuple] {
let mut key = Vec::with_capacity(self.key_cols.len());
for (_, term) in &self.key_cols {
match index_key_value(term, b, ctx) {
Some(v) => key.push(v),
None => return &[],
}
}
self.by_key.get(&key).map(|v| v.as_slice()).unwrap_or(&[])
}
}
fn bound_columns<'a>(binds: &'a Binds, cols: &[String], sample: &Binding) -> Vec<(usize, &'a Term)> {
let is_bound = |t: &Term| match t {
Term::Var(v) => sample.contains_key(v),
_ => true,
};
match binds {
Binds::Named(pairs) => pairs
.iter()
.filter_map(|(col, term)| {
cols.iter().position(|c| c == col).filter(|_| is_bound(term)).map(|i| (i, term))
})
.collect(),
Binds::Pos(terms) => terms
.iter()
.enumerate()
.filter(|(i, term)| *i < cols.len() && is_bound(term))
.map(|(i, term)| (i, term))
.collect(),
}
}
fn index_key_value(term: &Term, b: &Binding, ctx: &Ctx) -> Option<Value> {
match term {
Term::Var(v) => b.get(v).cloned(),
other => eval_term(other, b, ctx).ok(),
}
}
fn join_matches<'r>(
rows: impl Iterator<Item = &'r Tuple>,
binds: &Binds,
cols: &[String],
b: &Binding,
ctx: &Ctx,
out: &mut Vec<Binding>,
) -> Result<(), EvalError> {
for row in rows {
if let Some(nb) = unify(binds, row, cols, b, ctx)? {
out.push(nb);
}
}
Ok(())
}
fn any_match<'r>(
rows: impl Iterator<Item = &'r Tuple>,
binds: &Binds,
cols: &[String],
b: &Binding,
ctx: &Ctx,
) -> Result<bool, EvalError> {
for row in rows {
if unify(binds, row, cols, b, ctx)?.is_some() {
return Ok(true);
}
}
Ok(false)
}
fn join_over(
cols: &[String],
rows: &[Tuple],
binds: &Binds,
bindings: &[Binding],
ctx: &Ctx,
) -> Result<Vec<Binding>, EvalError> {
let mut out = Vec::new();
if bindings.is_empty() || rows.is_empty() {
return Ok(out);
}
let idx = BoundIndex::build(binds, cols, rows, &bindings[0]);
for b in bindings {
match &idx {
Some(i) => join_matches(i.candidates(b, ctx).iter().copied(), binds, cols, b, ctx, &mut out)?,
None => join_matches(rows.iter(), binds, cols, b, ctx, &mut out)?,
}
}
Ok(out)
}
fn read_join(
rel: &str,
binds: &Binds,
bindings: Vec<Binding>,
env: &Env,
src: &dyn RelationSource,
ctx: &Ctx,
) -> Result<Vec<Binding>, EvalError> {
let (cols, rows) = rows_of(rel, env, src)?;
join_over(&cols, &rows, binds, &bindings, ctx)
}
fn eval_body_delta_variant(
r: &Rule,
env: &Env,
src: &dyn RelationSource,
ctx: &Ctx,
delta_pos: usize,
delta_cols: &[String],
delta_rows: &[Tuple],
) -> Result<Vec<Binding>, EvalError> {
let mut bindings = vec![Binding::new()];
for (i, atom) in r.body.iter().enumerate() {
bindings = if i == delta_pos {
let binds = match atom {
Atom::Read { binds, .. } => binds.clone(),
Atom::Apply { args, .. } => Binds::Pos(args.clone()),
_ => return err("semi-naive delta substitution target must be a relation read"),
};
join_over(delta_cols, delta_rows, &binds, &bindings, ctx)?
} else {
step_atom(atom, bindings, env, src, ctx)?
};
if bindings.is_empty() {
break;
}
}
Ok(bindings)
}
fn unify(
binds: &Binds,
row: &Tuple,
cols: &[String],
base: &Binding,
ctx: &Ctx,
) -> Result<Option<Binding>, EvalError> {
let mut b = base.clone();
match binds {
Binds::Named(pairs) => {
for (col, term) in pairs {
let idx = cols
.iter()
.position(|c| c == col)
.ok_or_else(|| EvalError { msg: format!("unknown column `{col}`") })?;
if !unify_term(term, &row[idx], &mut b, ctx)? {
return Ok(None);
}
}
}
Binds::Pos(terms) => {
if terms.len() > row.len() {
return err("positional read has more columns than the relation");
}
for (i, term) in terms.iter().enumerate() {
if !unify_term(term, &row[i], &mut b, ctx)? {
return Ok(None);
}
}
}
}
Ok(Some(b))
}
fn unify_term(term: &Term, val: &Value, b: &mut Binding, ctx: &Ctx) -> Result<bool, EvalError> {
match term {
Term::Var(v) => match b.get(v) {
Some(existing) => Ok(existing == val),
None => {
b.insert(v.clone(), val.clone());
Ok(true)
}
},
other => {
let tv = eval_term(other, b, ctx).map_err(|m| EvalError { msg: m })?;
Ok(&tv == val)
}
}
}
fn apply_opts(opts: &[Opt], cols: &[String], rows: &mut Vec<Tuple>) -> Result<(), EvalError> {
for o in opts {
match o {
Opt::Sort { col, desc } => {
let idx = cols
.iter()
.position(|c| c == col)
.ok_or_else(|| EvalError { msg: format!("sort: unknown column `{col}`") })?;
rows.sort_by(|a, b| a[idx].cmp(&b[idx]));
if *desc {
rows.reverse();
}
}
Opt::Offset(n) => {
let n = (*n as usize).min(rows.len());
rows.drain(0..n);
}
Opt::Limit(n) => {
rows.truncate(*n as usize);
}
Opt::AssertNone => {
if !rows.is_empty() {
return err(format!(":assert none failed: {} rows", rows.len()));
}
}
Opt::AssertSome => {
if rows.is_empty() {
return err(":assert some failed: no rows");
}
}
}
}
Ok(())
}
fn build_mutation(
m: &Mutation,
entry_cols: &[String],
entry_rows: &[Tuple],
ctx: &Ctx,
) -> Result<Output, EvalError> {
let (op, binds) = match m {
Mutation::Link(b) => (MutOp::Link, b),
Mutation::Unlink(b) => (MutOp::Unlink, b),
Mutation::Put { rel, binds } => (MutOp::Put(rel.clone()), binds),
Mutation::Rm { rel, binds } => (MutOp::Rm(rel.clone()), binds),
};
let pairs = match binds {
Binds::Named(p) => p.clone(),
Binds::Pos(_) => return err("mutation requires named bindings"),
};
let columns: Vec<String> = pairs.iter().map(|(c, _)| c.clone()).collect();
let mut batch: inf_value::Relation = inf_value::Relation::new();
for row in entry_rows {
let mut b = Binding::new();
for (i, c) in entry_cols.iter().enumerate() {
b.insert(c.clone(), row[i].clone());
}
let mut tuple = Vec::new();
for (_, term) in &pairs {
tuple.push(eval_term(term, &b, ctx).map_err(|m| EvalError { msg: m })?);
}
batch.insert(tuple);
}
Ok(Output { columns, rows: batch.into_iter().collect(), mutation: Some(op) })
}