Skip to main content

corium_query/
exec.rs

1//! Frame-based query executor.
2//!
3//! Execution is a fold of clause evaluations over a set of frames (partial
4//! variable bindings). Pattern clauses pick a covering index per frame from
5//! what is bound at that moment (see [`crate::plan::choose_index`]), so a
6//! bound attribute can never degenerate into a full index scan. Rules are
7//! evaluated bottom-up to a fixpoint with per-invocation memo tables.
8
9use std::cell::{Cell, RefCell};
10use std::collections::{BTreeMap, BTreeSet};
11
12use corium_core::{AttrId, EntityId, IndexOrder, Value, ValueType};
13use corium_db::{Db, avet_covered, key_prefix};
14
15use crate::QueryError;
16use crate::ast::{BindTarget, Binding, Clause, Pattern, RuleDef, Term, Var};
17use crate::builtins::{self, CallResult};
18use crate::edn::Edn;
19use crate::plan::{ScanChoice, choose_index, order_clauses};
20
21/// A partial assignment of variables to values.
22pub type Frame = BTreeMap<Var, Value>;
23
24/// Keyword id used for keyword literals absent from the database's
25/// interner: such literals can never equal a stored keyword value.
26pub const UNKNOWN_KEYWORD: u64 = u64::MAX;
27
28type RuleKey = (String, Vec<Option<Value>>);
29
30#[derive(Default)]
31struct RuleState {
32    partial: BTreeMap<RuleKey, BTreeSet<Vec<Value>>>,
33    changed: bool,
34}
35
36/// Execution context: database sources, rules, and counters.
37pub struct ExecCtx<'a> {
38    dbs: BTreeMap<String, &'a Db>,
39    rules: BTreeMap<String, Vec<RuleDef>>,
40    scanned: Cell<usize>,
41    fuel: Cell<u64>,
42    rule_state: RefCell<RuleState>,
43    extern_call: Option<crate::ExternCall>,
44}
45
46impl<'a> ExecCtx<'a> {
47    /// Creates a context over named database sources and rule definitions.
48    #[must_use]
49    pub fn new(dbs: BTreeMap<String, &'a Db>, rule_defs: Vec<RuleDef>) -> Self {
50        let mut rules: BTreeMap<String, Vec<RuleDef>> = BTreeMap::new();
51        for def in rule_defs {
52            rules.entry(def.name.clone()).or_default().push(def);
53        }
54        Self {
55            dbs,
56            rules,
57            scanned: Cell::new(0),
58            fuel: Cell::new(u64::MAX),
59            rule_state: RefCell::new(RuleState::default()),
60            extern_call: None,
61        }
62    }
63
64    /// Limits the number of datoms the execution may touch.
65    pub fn set_fuel(&self, fuel: u64) {
66        self.fuel.set(fuel);
67    }
68
69    /// Installs the resolver consulted for non-native call clause names.
70    pub fn set_extern_call(&mut self, extern_call: crate::ExternCall) {
71        self.extern_call = Some(extern_call);
72    }
73
74    /// Evaluates a call clause: the native set first, then the extern
75    /// resolver seam for names outside it.
76    fn call(&self, name: &str, values: &[Value]) -> Result<CallResult, QueryError> {
77        if builtins::is_native(name) {
78            return builtins::call(name, values);
79        }
80        if let Some(extern_call) = &self.extern_call
81            && let Some(result) = extern_call(name, values)
82        {
83            return result;
84        }
85        builtins::call(name, values)
86    }
87
88    /// Datoms touched by pattern scans so far.
89    #[must_use]
90    pub fn scanned(&self) -> usize {
91        self.scanned.get()
92    }
93
94    /// Resolves a database source by name.
95    #[must_use]
96    pub fn db(&self, src: &str) -> Option<&'a Db> {
97        self.dbs.get(src).copied()
98    }
99
100    /// The default database source, if bound.
101    #[must_use]
102    pub fn default_db(&self) -> Option<&'a Db> {
103        self.db(crate::ast::DEFAULT_SRC)
104            .or_else(|| self.dbs.values().next().copied())
105    }
106
107    /// Resolves an attribute-position constant against a database.
108    #[must_use]
109    pub fn attr_of(&self, db: &Db, form: &Edn) -> Option<AttrId> {
110        match form {
111            Edn::Keyword(k) => db.idents().entid(k),
112            Edn::Long(n) => u64::try_from(*n).ok().map(EntityId::from_raw),
113            Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
114                Edn::Long(n) => u64::try_from(*n).ok().map(EntityId::from_raw),
115                _ => None,
116            },
117            _ => None,
118        }
119    }
120
121    fn spend(&self, amount: u64) -> Result<(), QueryError> {
122        let fuel = self.fuel.get();
123        if fuel < amount {
124            return Err(QueryError::FuelExhausted);
125        }
126        self.fuel.set(fuel - amount);
127        Ok(())
128    }
129}
130
131/// Converts a constant EDN form to an engine value, without attribute context.
132///
133/// Returns `None` for forms that cannot denote a storable value.
134#[must_use]
135pub fn const_value(db: &Db, form: &Edn) -> Option<Value> {
136    crate::boundary::edn_to_value(Some(db), form)
137}
138
139/// Coerces a value toward an attribute's schema type where representations
140/// legitimately overlap (longs standing for entity ids or instants).
141#[must_use]
142pub fn coerce_for_type(value: Value, value_type: ValueType) -> Value {
143    match (&value, value_type) {
144        (Value::Long(n), ValueType::Ref) => u64::try_from(*n)
145            .map(|n| Value::Ref(EntityId::from_raw(n)))
146            .unwrap_or(value),
147        (Value::Long(n), ValueType::Instant) => Value::Instant(*n),
148        #[allow(clippy::cast_precision_loss)]
149        (Value::Long(n), ValueType::Double) => Value::Double(corium_core::TotalF64(*n as f64)),
150        _ => value,
151    }
152}
153
154/// Interprets a bound value as an entity id.
155#[must_use]
156pub fn to_entity(value: &Value) -> Option<EntityId> {
157    match value {
158        Value::Ref(e) => Some(*e),
159        Value::Long(n) => u64::try_from(*n).ok().map(EntityId::from_raw),
160        _ => None,
161    }
162}
163
164/// Pattern-position match with entity/instant coercion between overlapping
165/// representations. Distinct value types otherwise never match.
166#[must_use]
167pub fn value_match(actual: &Value, expected: &Value) -> bool {
168    if actual == expected {
169        return true;
170    }
171    match (actual, expected) {
172        (Value::Ref(e), Value::Long(n)) | (Value::Long(n), Value::Ref(e)) => {
173            u64::try_from(*n).is_ok_and(|n| e.raw() == n)
174        }
175        (Value::Instant(i), Value::Long(n)) | (Value::Long(n), Value::Instant(i)) => i == n,
176        _ => false,
177    }
178}
179
180/// Evaluates clauses over the given frames, in planner order.
181///
182/// # Errors
183/// Propagates evaluation failures (unknown idents, unbound call arguments,
184/// arity and type errors, fuel exhaustion).
185pub fn eval_clauses(
186    ctx: &ExecCtx<'_>,
187    clauses: &[Clause],
188    frames: Vec<Frame>,
189    stack: &mut Vec<RuleKey>,
190) -> Result<Vec<Frame>, QueryError> {
191    // Clause ordering may only assume variables present in every frame. This
192    // matters after `or`, whose branches can export different bindings.
193    let mut frame_iter = frames.iter();
194    let mut initially_bound: BTreeSet<Var> = frame_iter
195        .next()
196        .map(|frame| frame.keys().cloned().collect())
197        .unwrap_or_default();
198    for frame in frame_iter {
199        initially_bound.retain(|var| frame.contains_key(var));
200    }
201    let order = order_clauses(clauses, &initially_bound, ctx);
202    let mut frames = frames;
203    for index in order {
204        frames = eval_clause(ctx, &clauses[index], frames, stack)?;
205        // Set semantics: identical frames carry no extra information. Sorting
206        // in place avoids allocating an additional BTreeSet after each clause.
207        frames.sort_unstable();
208        frames.dedup();
209        if frames.is_empty() {
210            return Ok(frames);
211        }
212    }
213    Ok(frames)
214}
215
216fn eval_clause(
217    ctx: &ExecCtx<'_>,
218    clause: &Clause,
219    frames: Vec<Frame>,
220    stack: &mut Vec<RuleKey>,
221) -> Result<Vec<Frame>, QueryError> {
222    match clause {
223        Clause::Pattern(pattern) => eval_pattern(ctx, pattern, frames),
224        Clause::Pred { name, args } => eval_pred(ctx, name, args, frames),
225        Clause::Fn {
226            name,
227            args,
228            binding,
229        } => eval_fn(ctx, name, args, binding, frames),
230        Clause::Not {
231            src: _,
232            vars,
233            clauses,
234        } => {
235            let mut kept = Vec::with_capacity(frames.len());
236            for frame in frames {
237                let seed = match vars {
238                    Some(vars) => restrict(&frame, vars),
239                    None => frame.clone(),
240                };
241                let sub = eval_clauses(ctx, clauses, vec![seed], stack)?;
242                if sub.is_empty() {
243                    kept.push(frame);
244                }
245            }
246            Ok(kept)
247        }
248        Clause::Or {
249            src: _,
250            vars,
251            branches,
252        } => {
253            let mut out = Vec::with_capacity(frames.len().saturating_mul(branches.len().max(1)));
254            for frame in frames {
255                for branch in branches {
256                    let seed = match vars {
257                        Some(vars) => restrict(&frame, vars),
258                        None => frame.clone(),
259                    };
260                    for result in eval_clauses(ctx, branch, vec![seed], stack)? {
261                        let mut merged = frame.clone();
262                        let mut consistent = true;
263                        let exported: Vec<&Var> = match vars {
264                            Some(vars) => vars.iter().collect(),
265                            None => result.keys().collect(),
266                        };
267                        for var in exported {
268                            if let Some(value) = result.get(var) {
269                                match merged.get(var) {
270                                    Some(existing) if !value_match(existing, value) => {
271                                        consistent = false;
272                                        break;
273                                    }
274                                    Some(_) => {}
275                                    None => {
276                                        merged.insert(var.clone(), value.clone());
277                                    }
278                                }
279                            }
280                        }
281                        if consistent {
282                            out.push(merged);
283                        }
284                    }
285                }
286            }
287            Ok(out)
288        }
289        Clause::RuleCall { name, args } => eval_rule_call(ctx, name, args, frames, stack),
290    }
291}
292
293fn restrict(frame: &Frame, vars: &[Var]) -> Frame {
294    vars.iter()
295        .filter_map(|var| frame.get(var).map(|value| (var.clone(), value.clone())))
296        .collect()
297}
298
299/// A resolved pattern position: unbound, a concrete value, or provably
300/// unmatchable (e.g. a lookup ref that resolves to nothing).
301enum Spec {
302    Free(Option<Var>),
303    Bound(Value),
304    NoMatch,
305}
306
307fn resolve_term(
308    ctx: &ExecCtx<'_>,
309    db: &Db,
310    frame: &Frame,
311    term: &Term,
312    position: Position,
313    attr: Option<AttrId>,
314) -> Result<Spec, QueryError> {
315    let coerce = |value: Value| -> Value {
316        match (position, attr) {
317            (Position::V, Some(a)) => db.schema().get(a).map_or_else(
318                || value.clone(),
319                |meta| coerce_for_type(value.clone(), meta.value_type),
320            ),
321            _ => value,
322        }
323    };
324    match term {
325        Term::Blank => Ok(Spec::Free(None)),
326        Term::Var(var) => Ok(frame
327            .get(var)
328            .map_or(Spec::Free(Some(var.clone())), |value| {
329                Spec::Bound(coerce(value.clone()))
330            })),
331        Term::Const(form) => match position {
332            Position::E | Position::Tx => match entity_const(db, form)? {
333                Some(e) => Ok(Spec::Bound(Value::Ref(e))),
334                None => Ok(Spec::NoMatch),
335            },
336            Position::A => match ctx.attr_of(db, form) {
337                Some(a) => Ok(Spec::Bound(Value::Ref(a))),
338                None => match form {
339                    Edn::Keyword(k) => Err(QueryError::UnknownIdent(k.clone())),
340                    _ => Ok(Spec::NoMatch),
341                },
342            },
343            Position::Added => match form {
344                Edn::Bool(b) => Ok(Spec::Bound(Value::Bool(*b))),
345                _ => Ok(Spec::NoMatch),
346            },
347            Position::V => match const_value(db, form) {
348                Some(value) => Ok(Spec::Bound(coerce(value))),
349                None => match entity_const(db, form)? {
350                    Some(e) => Ok(Spec::Bound(Value::Ref(e))),
351                    None => Ok(Spec::NoMatch),
352                },
353            },
354        },
355    }
356}
357
358/// Resolves an entity-position constant: id, tagged id, ident keyword, or
359/// lookup ref `[attr value]`.
360fn entity_const(db: &Db, form: &Edn) -> Result<Option<EntityId>, QueryError> {
361    match form {
362        Edn::Long(_) | Edn::Tagged(_, _) => match const_value(db, form) {
363            Some(Value::Ref(e)) => Ok(Some(e)),
364            Some(Value::Long(n)) => Ok(u64::try_from(n).ok().map(EntityId::from_raw)),
365            _ => Ok(None),
366        },
367        Edn::Keyword(k) => Ok(db.idents().entid(k)),
368        Edn::Vector(items) => {
369            let [attr_form, value_form] = items.as_slice() else {
370                return Ok(None);
371            };
372            let Some(attr_kw) = attr_form.as_keyword() else {
373                return Ok(None);
374            };
375            let attr = db
376                .idents()
377                .entid(attr_kw)
378                .ok_or_else(|| QueryError::UnknownIdent(attr_kw.clone()))?;
379            let Some(value) = const_value(db, value_form) else {
380                return Ok(None);
381            };
382            let value = db.schema().get(attr).map_or(value.clone(), |meta| {
383                coerce_for_type(value, meta.value_type)
384            });
385            Ok(db.lookup(attr, &value))
386        }
387        _ => Ok(None),
388    }
389}
390
391#[derive(Clone, Copy, Eq, PartialEq)]
392enum Position {
393    E,
394    A,
395    V,
396    Tx,
397    Added,
398}
399
400#[allow(clippy::too_many_lines)]
401fn eval_pattern(
402    ctx: &ExecCtx<'_>,
403    pattern: &Pattern,
404    frames: Vec<Frame>,
405) -> Result<Vec<Frame>, QueryError> {
406    let db = ctx
407        .db(&pattern.src)
408        .ok_or_else(|| QueryError::UnknownSource(pattern.src.clone()))?;
409    let mut out = Vec::with_capacity(frames.len());
410    for frame in frames {
411        // Attribute first: value coercion and index choice depend on it.
412        let a_spec = resolve_term(ctx, db, &frame, &pattern.a, Position::A, None)?;
413        let attr = match &a_spec {
414            Spec::Bound(Value::Ref(a)) => Some(*a),
415            Spec::Bound(other) => to_entity(other),
416            _ => None,
417        };
418        let e_spec = resolve_term(ctx, db, &frame, &pattern.e, Position::E, None)?;
419        let v_spec = resolve_term(ctx, db, &frame, &pattern.v, Position::V, attr)?;
420        let tx_spec = resolve_term(ctx, db, &frame, &pattern.tx, Position::Tx, None)?;
421        let added_spec = resolve_term(ctx, db, &frame, &pattern.added, Position::Added, None)?;
422        if [&a_spec, &e_spec, &v_spec, &tx_spec, &added_spec]
423            .iter()
424            .any(|spec| matches!(spec, Spec::NoMatch))
425        {
426            continue;
427        }
428        let e = match &e_spec {
429            Spec::Bound(value) => to_entity(value),
430            _ => None,
431        };
432        let v = match &v_spec {
433            Spec::Bound(value) => Some(value.clone()),
434            _ => None,
435        };
436        let choice: ScanChoice = choose_index(
437            e.is_some(),
438            attr.is_some(),
439            v.is_some(),
440            attr.is_some_and(|a| avet_covered(db.schema(), a)),
441            matches!(v, Some(Value::Ref(_))),
442        );
443        let prefix = match choice.order {
444            IndexOrder::Eavt => key_prefix(
445                IndexOrder::Eavt,
446                e,
447                if choice.prefix_len >= 2 { attr } else { None },
448                if choice.prefix_len >= 3 {
449                    v.as_ref()
450                } else {
451                    None
452                },
453            ),
454            IndexOrder::Aevt => key_prefix(IndexOrder::Aevt, None, attr, None),
455            IndexOrder::Avet => key_prefix(IndexOrder::Avet, None, attr, v.as_ref()),
456            IndexOrder::Vaet => key_prefix(
457                IndexOrder::Vaet,
458                None,
459                if choice.prefix_len >= 2 { attr } else { None },
460                v.as_ref(),
461            ),
462        };
463        for datom in db.datoms_prefix(choice.order, &prefix) {
464            ctx.spend(1)?;
465            ctx.scanned.set(ctx.scanned.get() + 1);
466            let fields = [
467                (Position::E, Value::Ref(datom.e)),
468                (Position::A, Value::Ref(datom.a)),
469                (Position::V, datom.v.clone()),
470                (Position::Tx, Value::Ref(datom.tx)),
471                (Position::Added, Value::Bool(datom.added)),
472            ];
473            let specs = [&e_spec, &a_spec, &v_spec, &tx_spec, &added_spec];
474            let mut extended = frame.clone();
475            let mut matched = true;
476            for ((_, actual), spec) in fields.into_iter().zip(specs) {
477                match spec {
478                    Spec::Bound(expected) => {
479                        if !value_match(&actual, expected) {
480                            matched = false;
481                            break;
482                        }
483                    }
484                    Spec::Free(Some(var)) => match extended.get(var) {
485                        Some(existing) => {
486                            if !value_match(&actual, existing) {
487                                matched = false;
488                                break;
489                            }
490                        }
491                        None => {
492                            extended.insert(var.clone(), actual);
493                        }
494                    },
495                    Spec::Free(None) | Spec::NoMatch => {}
496                }
497            }
498            if matched {
499                out.push(extended);
500            }
501        }
502    }
503    Ok(out)
504}
505
506/// Resolves call arguments to values; `Err` inside the option marks an
507/// argument that is a variable still unbound.
508fn call_args(ctx: &ExecCtx<'_>, frame: &Frame, args: &[Term]) -> Result<Vec<Value>, QueryError> {
509    let db = ctx.default_db();
510    args.iter()
511        .map(|term| match term {
512            Term::Var(var) => frame
513                .get(var)
514                .cloned()
515                .ok_or_else(|| QueryError::Unbound(var.clone())),
516            Term::Blank => Err(QueryError::Parse("call arguments cannot be _".into())),
517            Term::Const(form) => db
518                .and_then(|db| const_value(db, form))
519                .ok_or_else(|| QueryError::Type(format!("bad call argument {form}"))),
520        })
521        .collect()
522}
523
524fn eval_pred(
525    ctx: &ExecCtx<'_>,
526    name: &str,
527    args: &[Term],
528    frames: Vec<Frame>,
529) -> Result<Vec<Frame>, QueryError> {
530    if name == "missing?" {
531        return eval_missing(ctx, args, frames);
532    }
533    let mut out = Vec::with_capacity(frames.len());
534    for frame in frames {
535        let values = call_args(ctx, &frame, args)?;
536        match ctx.call(name, &values)? {
537            CallResult::Test(true) | CallResult::Scalar(Value::Bool(true)) => {
538                out.push(frame);
539            }
540            CallResult::Test(false) | CallResult::Scalar(Value::Bool(false)) => {}
541            _ => {
542                return Err(QueryError::Type(format!("{name} is not a predicate")));
543            }
544        }
545    }
546    Ok(out)
547}
548
549fn eval_fn(
550    ctx: &ExecCtx<'_>,
551    name: &str,
552    args: &[Term],
553    binding: &Binding,
554    frames: Vec<Frame>,
555) -> Result<Vec<Frame>, QueryError> {
556    if name == "ground" {
557        return eval_ground(ctx, args, binding, frames);
558    }
559    if name == "get-else" {
560        return eval_get_else(ctx, args, binding, frames);
561    }
562    let mut out = Vec::with_capacity(frames.len());
563    for frame in frames {
564        let values = call_args(ctx, &frame, args)?;
565        let result = match ctx.call(name, &values)? {
566            CallResult::Test(truth) => CallResult::Scalar(Value::Bool(truth)),
567            other => other,
568        };
569        bind_result(&frame, binding, result, &mut out)?;
570    }
571    Ok(out)
572}
573
574fn bind_result(
575    frame: &Frame,
576    binding: &Binding,
577    result: CallResult,
578    out: &mut Vec<Frame>,
579) -> Result<(), QueryError> {
580    let scalar = |value: &Value, var: &Var, frame: &Frame| -> Option<Frame> {
581        let mut next = frame.clone();
582        match next.get(var) {
583            Some(existing) if !value_match(existing, value) => None,
584            Some(_) => Some(next),
585            None => {
586                next.insert(var.clone(), value.clone());
587                Some(next)
588            }
589        }
590    };
591    let bind_tuple = |values: &[Value], targets: &[BindTarget], frame: &Frame| -> Option<Frame> {
592        if values.len() != targets.len() {
593            return None;
594        }
595        let mut next = frame.clone();
596        for (value, target) in values.iter().zip(targets) {
597            match target {
598                BindTarget::Blank => {}
599                BindTarget::Var(var) => match next.get(var) {
600                    Some(existing) if !value_match(existing, value) => return None,
601                    Some(_) => {}
602                    None => {
603                        next.insert(var.clone(), value.clone());
604                    }
605                },
606            }
607        }
608        Some(next)
609    };
610    match (binding, result) {
611        (Binding::Scalar(var), CallResult::Scalar(value)) => {
612            out.extend(scalar(&value, var, frame));
613        }
614        (Binding::Scalar(var), CallResult::Test(flag)) => {
615            out.extend(scalar(&Value::Bool(flag), var, frame));
616        }
617        (Binding::Coll(target), CallResult::Coll(values)) => match target {
618            BindTarget::Blank => {
619                if !values.is_empty() {
620                    out.push(frame.clone());
621                }
622            }
623            BindTarget::Var(var) => {
624                for value in values {
625                    out.extend(scalar(&value, var, frame));
626                }
627            }
628        },
629        (Binding::Tuple(targets), CallResult::Coll(values)) => {
630            out.extend(bind_tuple(&values, targets, frame));
631        }
632        (Binding::Rel(targets), CallResult::Rel(rows)) => {
633            for row in rows {
634                out.extend(bind_tuple(&row, targets, frame));
635            }
636        }
637        (_, result) => {
638            return Err(QueryError::Type(format!(
639                "binding form does not fit result {result:?}"
640            )));
641        }
642    }
643    Ok(())
644}
645
646/// `[(ground <const>) binding]`: converts a constant EDN form per binding shape.
647fn eval_ground(
648    ctx: &ExecCtx<'_>,
649    args: &[Term],
650    binding: &Binding,
651    frames: Vec<Frame>,
652) -> Result<Vec<Frame>, QueryError> {
653    let db = ctx
654        .default_db()
655        .ok_or_else(|| QueryError::UnknownSource("$".into()))?;
656    let [Term::Const(form)] = args else {
657        return Err(QueryError::Arity("ground takes one constant".into()));
658    };
659    let value_of = |form: &Edn| -> Result<Value, QueryError> {
660        const_value(db, form).ok_or_else(|| QueryError::Type(format!("cannot ground {form}")))
661    };
662    let result = match (binding, form) {
663        (Binding::Coll(_) | Binding::Tuple(_), Edn::Vector(items) | Edn::List(items)) => {
664            CallResult::Coll(items.iter().map(value_of).collect::<Result<_, _>>()?)
665        }
666        (Binding::Rel(_), Edn::Vector(rows) | Edn::List(rows)) => CallResult::Rel(
667            rows.iter()
668                .map(|row| match row {
669                    Edn::Vector(items) | Edn::List(items) => {
670                        items.iter().map(value_of).collect::<Result<Vec<_>, _>>()
671                    }
672                    _ => Err(QueryError::Type("ground relation requires tuples".into())),
673                })
674                .collect::<Result<_, _>>()?,
675        ),
676        (Binding::Scalar(_), form) => CallResult::Scalar(value_of(form)?),
677        _ => return Err(QueryError::Type(format!("cannot ground {form}"))),
678    };
679    let mut out = Vec::with_capacity(frames.len());
680    for frame in frames {
681        bind_result(&frame, binding, result.clone(), &mut out)?;
682    }
683    Ok(out)
684}
685
686/// `[(get-else $src ?e attr default) ?v]`.
687fn eval_get_else(
688    ctx: &ExecCtx<'_>,
689    args: &[Term],
690    binding: &Binding,
691    frames: Vec<Frame>,
692) -> Result<Vec<Frame>, QueryError> {
693    let (db, rest) = db_context_args(ctx, args)?;
694    let [entity_term, attr_term, default_term] = rest else {
695        return Err(QueryError::Arity(
696            "get-else takes a source, entity, attribute, and default".into(),
697        ));
698    };
699    let attr = attr_const(ctx, db, attr_term)?;
700    let value_type = db.schema().get(attr).map(|meta| meta.value_type);
701    let mut out = Vec::with_capacity(frames.len());
702    for frame in frames {
703        let entity = entity_arg(db, &frame, entity_term)?;
704        let value = entity
705            .and_then(|e| db.values(e, attr).into_iter().next())
706            .map_or_else(
707                || match default_term {
708                    Term::Const(form) => {
709                        let value = const_value(db, form).ok_or_else(|| {
710                            QueryError::Type(format!("bad get-else default {form}"))
711                        })?;
712                        Ok(match value_type {
713                            Some(t) => coerce_for_type(value, t),
714                            None => value,
715                        })
716                    }
717                    Term::Var(var) => frame
718                        .get(var)
719                        .cloned()
720                        .ok_or_else(|| QueryError::Unbound(var.clone())),
721                    Term::Blank => Err(QueryError::Parse("get-else default cannot be _".into())),
722                },
723                Ok,
724            )?;
725        bind_result(&frame, binding, CallResult::Scalar(value), &mut out)?;
726    }
727    Ok(out)
728}
729
730/// `[(missing? $src ?e attr)]`.
731fn eval_missing(
732    ctx: &ExecCtx<'_>,
733    args: &[Term],
734    frames: Vec<Frame>,
735) -> Result<Vec<Frame>, QueryError> {
736    let (db, rest) = db_context_args(ctx, args)?;
737    let [entity_term, attr_term] = rest else {
738        return Err(QueryError::Arity(
739            "missing? takes a source, entity, and attribute".into(),
740        ));
741    };
742    let attr = attr_const(ctx, db, attr_term)?;
743    let mut out = Vec::with_capacity(frames.len());
744    for frame in frames {
745        let entity = entity_arg(db, &frame, entity_term)?;
746        let missing = entity.is_none_or(|e| db.values(e, attr).is_empty());
747        if missing {
748            out.push(frame);
749        }
750    }
751    Ok(out)
752}
753
754fn db_context_args<'a, 'b>(
755    ctx: &'b ExecCtx<'a>,
756    args: &'b [Term],
757) -> Result<(&'a Db, &'b [Term]), QueryError> {
758    match args.split_first() {
759        Some((Term::Const(Edn::Symbol(src)), rest)) if src.starts_with('$') => {
760            let db = ctx
761                .db(src)
762                .ok_or_else(|| QueryError::UnknownSource(src.clone()))?;
763            Ok((db, rest))
764        }
765        _ => {
766            let db = ctx
767                .default_db()
768                .ok_or_else(|| QueryError::UnknownSource("$".into()))?;
769            Ok((db, args))
770        }
771    }
772}
773
774fn attr_const(ctx: &ExecCtx<'_>, db: &Db, term: &Term) -> Result<AttrId, QueryError> {
775    match term {
776        Term::Const(form) => ctx.attr_of(db, form).ok_or_else(|| match form {
777            Edn::Keyword(k) => QueryError::UnknownIdent(k.clone()),
778            _ => QueryError::Type(format!("bad attribute argument {form}")),
779        }),
780        _ => Err(QueryError::Type(
781            "attribute argument must be a constant".into(),
782        )),
783    }
784}
785
786fn entity_arg(db: &Db, frame: &Frame, term: &Term) -> Result<Option<EntityId>, QueryError> {
787    match term {
788        Term::Var(var) => {
789            let value = frame
790                .get(var)
791                .ok_or_else(|| QueryError::Unbound(var.clone()))?;
792            Ok(to_entity(value))
793        }
794        Term::Const(form) => entity_const(db, form),
795        Term::Blank => Err(QueryError::Parse("entity argument cannot be _".into())),
796    }
797}
798
799fn eval_rule_call(
800    ctx: &ExecCtx<'_>,
801    name: &str,
802    args: &[Term],
803    frames: Vec<Frame>,
804    stack: &mut Vec<RuleKey>,
805) -> Result<Vec<Frame>, QueryError> {
806    let db = ctx.default_db();
807    let mut out = Vec::with_capacity(frames.len());
808    for frame in frames {
809        let mut key_args: Vec<Option<Value>> = Vec::with_capacity(args.len());
810        for term in args {
811            key_args.push(match term {
812                Term::Var(var) => frame.get(var).cloned(),
813                Term::Blank => None,
814                Term::Const(form) => Some(
815                    db.and_then(|db| const_value(db, form))
816                        .ok_or_else(|| QueryError::Type(format!("bad rule argument {form}")))?,
817                ),
818            });
819        }
820        let tuples = rule_tuples(ctx, name, key_args, stack)?;
821        for tuple in tuples {
822            let mut extended = frame.clone();
823            let mut consistent = true;
824            for (term, value) in args.iter().zip(&tuple) {
825                match term {
826                    // Blanks bind nothing; constants were seeded into the key.
827                    Term::Blank | Term::Const(_) => {}
828                    Term::Var(var) => match extended.get(var) {
829                        Some(existing) if !value_match(existing, value) => {
830                            consistent = false;
831                            break;
832                        }
833                        Some(_) => {}
834                        None => {
835                            extended.insert(var.clone(), value.clone());
836                        }
837                    },
838                }
839            }
840            if consistent {
841                out.push(extended);
842            }
843        }
844    }
845    Ok(out)
846}
847
848/// Solves a rule invocation to its tuple set.
849///
850/// Bottom-up evaluation with per-invocation memo tables: tables grow
851/// monotonically; a top-level invocation iterates to a global fixpoint,
852/// while invocations nested inside rule bodies read the tables as they
853/// stand (the enclosing loop drives them to closure).
854fn rule_tuples(
855    ctx: &ExecCtx<'_>,
856    name: &str,
857    key_args: Vec<Option<Value>>,
858    stack: &mut Vec<RuleKey>,
859) -> Result<BTreeSet<Vec<Value>>, QueryError> {
860    let key: RuleKey = (name.to_owned(), key_args);
861    if stack.is_empty() {
862        loop {
863            ctx.rule_state.borrow_mut().changed = false;
864            eval_rule_once(ctx, &key, stack)?;
865            if !ctx.rule_state.borrow().changed {
866                break;
867            }
868            ctx.spend(1)?;
869        }
870    } else {
871        eval_rule_once(ctx, &key, stack)?;
872    }
873    Ok(ctx
874        .rule_state
875        .borrow()
876        .partial
877        .get(&key)
878        .cloned()
879        .unwrap_or_default())
880}
881
882fn eval_rule_once(
883    ctx: &ExecCtx<'_>,
884    key: &RuleKey,
885    stack: &mut Vec<RuleKey>,
886) -> Result<(), QueryError> {
887    if stack.contains(key) {
888        return Ok(());
889    }
890    let defs = ctx
891        .rules
892        .get(&key.0)
893        .ok_or_else(|| QueryError::Unsupported(format!("unknown rule {}", key.0)))?
894        .clone();
895    stack.push(key.clone());
896    let result = (|| -> Result<(), QueryError> {
897        for def in &defs {
898            let head: Vec<&Var> = def.head_vars();
899            if head.len() != key.1.len() {
900                return Err(QueryError::Arity(format!(
901                    "rule {} expects {} arguments",
902                    key.0,
903                    head.len()
904                )));
905            }
906            for (index, _) in def.required.iter().enumerate() {
907                if key.1[index].is_none() {
908                    return Err(QueryError::Unbound(format!(
909                        "rule {} requires its first {} arguments bound",
910                        key.0,
911                        def.required.len()
912                    )));
913                }
914            }
915            let mut seed = Frame::new();
916            for (var, value) in head.iter().zip(&key.1) {
917                if let Some(value) = value {
918                    seed.insert((*var).clone(), value.clone());
919                }
920            }
921            let frames = eval_clauses(ctx, &def.clauses, vec![seed], stack)?;
922            for frame in frames {
923                let tuple = head
924                    .iter()
925                    .map(|var| {
926                        frame
927                            .get(*var)
928                            .cloned()
929                            .ok_or_else(|| QueryError::Unbound((*var).clone()))
930                    })
931                    .collect::<Result<Vec<_>, _>>()?;
932                let mut state = ctx.rule_state.borrow_mut();
933                if state.partial.entry(key.clone()).or_default().insert(tuple) {
934                    state.changed = true;
935                }
936            }
937        }
938        Ok(())
939    })();
940    stack.pop();
941    result
942}