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            if let Some(result) = extern_call(name, values) {
82                return result;
83            }
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    let initially_bound: BTreeSet<Var> = frames
192        .first()
193        .map(|frame| frame.keys().cloned().collect())
194        .unwrap_or_default();
195    let order = order_clauses(clauses, &initially_bound, ctx);
196    let mut frames = frames;
197    for index in order {
198        frames = eval_clause(ctx, &clauses[index], frames, stack)?;
199        // Set semantics: identical frames carry no extra information.
200        let set: BTreeSet<Frame> = frames.into_iter().collect();
201        frames = set.into_iter().collect();
202        if frames.is_empty() {
203            return Ok(frames);
204        }
205    }
206    Ok(frames)
207}
208
209fn eval_clause(
210    ctx: &ExecCtx<'_>,
211    clause: &Clause,
212    frames: Vec<Frame>,
213    stack: &mut Vec<RuleKey>,
214) -> Result<Vec<Frame>, QueryError> {
215    match clause {
216        Clause::Pattern(pattern) => eval_pattern(ctx, pattern, frames),
217        Clause::Pred { name, args } => eval_pred(ctx, name, args, frames),
218        Clause::Fn {
219            name,
220            args,
221            binding,
222        } => eval_fn(ctx, name, args, binding, frames),
223        Clause::Not {
224            src: _,
225            vars,
226            clauses,
227        } => {
228            let mut kept = Vec::new();
229            for frame in frames {
230                let seed = match vars {
231                    Some(vars) => restrict(&frame, vars),
232                    None => frame.clone(),
233                };
234                let sub = eval_clauses(ctx, clauses, vec![seed], stack)?;
235                if sub.is_empty() {
236                    kept.push(frame);
237                }
238            }
239            Ok(kept)
240        }
241        Clause::Or {
242            src: _,
243            vars,
244            branches,
245        } => {
246            let mut out = Vec::new();
247            for frame in frames {
248                for branch in branches {
249                    let seed = match vars {
250                        Some(vars) => restrict(&frame, vars),
251                        None => frame.clone(),
252                    };
253                    for result in eval_clauses(ctx, branch, vec![seed], stack)? {
254                        let mut merged = frame.clone();
255                        let mut consistent = true;
256                        let exported: Vec<&Var> = match vars {
257                            Some(vars) => vars.iter().collect(),
258                            None => result.keys().collect(),
259                        };
260                        for var in exported {
261                            if let Some(value) = result.get(var) {
262                                match merged.get(var) {
263                                    Some(existing) if !value_match(existing, value) => {
264                                        consistent = false;
265                                        break;
266                                    }
267                                    Some(_) => {}
268                                    None => {
269                                        merged.insert(var.clone(), value.clone());
270                                    }
271                                }
272                            }
273                        }
274                        if consistent {
275                            out.push(merged);
276                        }
277                    }
278                }
279            }
280            Ok(out)
281        }
282        Clause::RuleCall { name, args } => eval_rule_call(ctx, name, args, frames, stack),
283    }
284}
285
286fn restrict(frame: &Frame, vars: &[Var]) -> Frame {
287    vars.iter()
288        .filter_map(|var| frame.get(var).map(|value| (var.clone(), value.clone())))
289        .collect()
290}
291
292/// A resolved pattern position: unbound, a concrete value, or provably
293/// unmatchable (e.g. a lookup ref that resolves to nothing).
294enum Spec {
295    Free(Option<Var>),
296    Bound(Value),
297    NoMatch,
298}
299
300fn resolve_term(
301    ctx: &ExecCtx<'_>,
302    db: &Db,
303    frame: &Frame,
304    term: &Term,
305    position: Position,
306    attr: Option<AttrId>,
307) -> Result<Spec, QueryError> {
308    let coerce = |value: Value| -> Value {
309        match (position, attr) {
310            (Position::V, Some(a)) => db.schema().get(a).map_or_else(
311                || value.clone(),
312                |meta| coerce_for_type(value.clone(), meta.value_type),
313            ),
314            _ => value,
315        }
316    };
317    match term {
318        Term::Blank => Ok(Spec::Free(None)),
319        Term::Var(var) => Ok(frame
320            .get(var)
321            .map_or(Spec::Free(Some(var.clone())), |value| {
322                Spec::Bound(coerce(value.clone()))
323            })),
324        Term::Const(form) => match position {
325            Position::E | Position::Tx => match entity_const(db, form)? {
326                Some(e) => Ok(Spec::Bound(Value::Ref(e))),
327                None => Ok(Spec::NoMatch),
328            },
329            Position::A => match ctx.attr_of(db, form) {
330                Some(a) => Ok(Spec::Bound(Value::Ref(a))),
331                None => match form {
332                    Edn::Keyword(k) => Err(QueryError::UnknownIdent(k.clone())),
333                    _ => Ok(Spec::NoMatch),
334                },
335            },
336            Position::Added => match form {
337                Edn::Bool(b) => Ok(Spec::Bound(Value::Bool(*b))),
338                _ => Ok(Spec::NoMatch),
339            },
340            Position::V => match const_value(db, form) {
341                Some(value) => Ok(Spec::Bound(coerce(value))),
342                None => match entity_const(db, form)? {
343                    Some(e) => Ok(Spec::Bound(Value::Ref(e))),
344                    None => Ok(Spec::NoMatch),
345                },
346            },
347        },
348    }
349}
350
351/// Resolves an entity-position constant: id, tagged id, ident keyword, or
352/// lookup ref `[attr value]`.
353fn entity_const(db: &Db, form: &Edn) -> Result<Option<EntityId>, QueryError> {
354    match form {
355        Edn::Long(_) | Edn::Tagged(_, _) => match const_value(db, form) {
356            Some(Value::Ref(e)) => Ok(Some(e)),
357            Some(Value::Long(n)) => Ok(u64::try_from(n).ok().map(EntityId::from_raw)),
358            _ => Ok(None),
359        },
360        Edn::Keyword(k) => Ok(db.idents().entid(k)),
361        Edn::Vector(items) => {
362            let [attr_form, value_form] = items.as_slice() else {
363                return Ok(None);
364            };
365            let Some(attr_kw) = attr_form.as_keyword() else {
366                return Ok(None);
367            };
368            let attr = db
369                .idents()
370                .entid(attr_kw)
371                .ok_or_else(|| QueryError::UnknownIdent(attr_kw.clone()))?;
372            let Some(value) = const_value(db, value_form) else {
373                return Ok(None);
374            };
375            let value = db.schema().get(attr).map_or(value.clone(), |meta| {
376                coerce_for_type(value, meta.value_type)
377            });
378            Ok(db.lookup(attr, &value))
379        }
380        _ => Ok(None),
381    }
382}
383
384#[derive(Clone, Copy, Eq, PartialEq)]
385enum Position {
386    E,
387    A,
388    V,
389    Tx,
390    Added,
391}
392
393#[allow(clippy::too_many_lines)]
394fn eval_pattern(
395    ctx: &ExecCtx<'_>,
396    pattern: &Pattern,
397    frames: Vec<Frame>,
398) -> Result<Vec<Frame>, QueryError> {
399    let db = ctx
400        .db(&pattern.src)
401        .ok_or_else(|| QueryError::UnknownSource(pattern.src.clone()))?;
402    let mut out = Vec::new();
403    for frame in frames {
404        // Attribute first: value coercion and index choice depend on it.
405        let a_spec = resolve_term(ctx, db, &frame, &pattern.a, Position::A, None)?;
406        let attr = match &a_spec {
407            Spec::Bound(Value::Ref(a)) => Some(*a),
408            Spec::Bound(other) => to_entity(other),
409            _ => None,
410        };
411        let e_spec = resolve_term(ctx, db, &frame, &pattern.e, Position::E, None)?;
412        let v_spec = resolve_term(ctx, db, &frame, &pattern.v, Position::V, attr)?;
413        let tx_spec = resolve_term(ctx, db, &frame, &pattern.tx, Position::Tx, None)?;
414        let added_spec = resolve_term(ctx, db, &frame, &pattern.added, Position::Added, None)?;
415        if [&a_spec, &e_spec, &v_spec, &tx_spec, &added_spec]
416            .iter()
417            .any(|spec| matches!(spec, Spec::NoMatch))
418        {
419            continue;
420        }
421        let e = match &e_spec {
422            Spec::Bound(value) => to_entity(value),
423            _ => None,
424        };
425        let v = match &v_spec {
426            Spec::Bound(value) => Some(value.clone()),
427            _ => None,
428        };
429        let choice: ScanChoice = choose_index(
430            e.is_some(),
431            attr.is_some(),
432            v.is_some(),
433            attr.is_some_and(|a| avet_covered(db.schema(), a)),
434            matches!(v, Some(Value::Ref(_))),
435        );
436        let prefix = match choice.order {
437            IndexOrder::Eavt => key_prefix(
438                IndexOrder::Eavt,
439                e,
440                if choice.prefix_len >= 2 { attr } else { None },
441                if choice.prefix_len >= 3 {
442                    v.as_ref()
443                } else {
444                    None
445                },
446            ),
447            IndexOrder::Aevt => key_prefix(IndexOrder::Aevt, None, attr, None),
448            IndexOrder::Avet => key_prefix(IndexOrder::Avet, None, attr, v.as_ref()),
449            IndexOrder::Vaet => key_prefix(
450                IndexOrder::Vaet,
451                None,
452                if choice.prefix_len >= 2 { attr } else { None },
453                v.as_ref(),
454            ),
455        };
456        for datom in db.datoms_prefix(choice.order, &prefix) {
457            ctx.spend(1)?;
458            ctx.scanned.set(ctx.scanned.get() + 1);
459            let fields = [
460                (Position::E, Value::Ref(datom.e)),
461                (Position::A, Value::Ref(datom.a)),
462                (Position::V, datom.v.clone()),
463                (Position::Tx, Value::Ref(datom.tx)),
464                (Position::Added, Value::Bool(datom.added)),
465            ];
466            let specs = [&e_spec, &a_spec, &v_spec, &tx_spec, &added_spec];
467            let mut extended = frame.clone();
468            let mut matched = true;
469            for ((_, actual), spec) in fields.into_iter().zip(specs) {
470                match spec {
471                    Spec::Bound(expected) => {
472                        if !value_match(&actual, expected) {
473                            matched = false;
474                            break;
475                        }
476                    }
477                    Spec::Free(Some(var)) => match extended.get(var) {
478                        Some(existing) => {
479                            if !value_match(&actual, existing) {
480                                matched = false;
481                                break;
482                            }
483                        }
484                        None => {
485                            extended.insert(var.clone(), actual);
486                        }
487                    },
488                    Spec::Free(None) | Spec::NoMatch => {}
489                }
490            }
491            if matched {
492                out.push(extended);
493            }
494        }
495    }
496    Ok(out)
497}
498
499/// Resolves call arguments to values; `Err` inside the option marks an
500/// argument that is a variable still unbound.
501fn call_args(ctx: &ExecCtx<'_>, frame: &Frame, args: &[Term]) -> Result<Vec<Value>, QueryError> {
502    let db = ctx.default_db();
503    args.iter()
504        .map(|term| match term {
505            Term::Var(var) => frame
506                .get(var)
507                .cloned()
508                .ok_or_else(|| QueryError::Unbound(var.clone())),
509            Term::Blank => Err(QueryError::Parse("call arguments cannot be _".into())),
510            Term::Const(form) => db
511                .and_then(|db| const_value(db, form))
512                .ok_or_else(|| QueryError::Type(format!("bad call argument {form}"))),
513        })
514        .collect()
515}
516
517fn eval_pred(
518    ctx: &ExecCtx<'_>,
519    name: &str,
520    args: &[Term],
521    frames: Vec<Frame>,
522) -> Result<Vec<Frame>, QueryError> {
523    if name == "missing?" {
524        return eval_missing(ctx, args, frames);
525    }
526    let mut out = Vec::new();
527    for frame in frames {
528        let values = call_args(ctx, &frame, args)?;
529        match ctx.call(name, &values)? {
530            CallResult::Test(true) | CallResult::Scalar(Value::Bool(true)) => out.push(frame),
531            CallResult::Test(false) | CallResult::Scalar(Value::Bool(false)) => {}
532            _ => {
533                return Err(QueryError::Type(format!("{name} is not a predicate")));
534            }
535        }
536    }
537    Ok(out)
538}
539
540fn eval_fn(
541    ctx: &ExecCtx<'_>,
542    name: &str,
543    args: &[Term],
544    binding: &Binding,
545    frames: Vec<Frame>,
546) -> Result<Vec<Frame>, QueryError> {
547    if name == "ground" {
548        return eval_ground(ctx, args, binding, frames);
549    }
550    if name == "get-else" {
551        return eval_get_else(ctx, args, binding, frames);
552    }
553    let mut out = Vec::new();
554    for frame in frames {
555        let values = call_args(ctx, &frame, args)?;
556        let result = match ctx.call(name, &values)? {
557            CallResult::Test(truth) => CallResult::Scalar(Value::Bool(truth)),
558            other => other,
559        };
560        bind_result(&frame, binding, result, &mut out)?;
561    }
562    Ok(out)
563}
564
565fn bind_result(
566    frame: &Frame,
567    binding: &Binding,
568    result: CallResult,
569    out: &mut Vec<Frame>,
570) -> Result<(), QueryError> {
571    let scalar = |value: &Value, var: &Var, frame: &Frame| -> Option<Frame> {
572        let mut next = frame.clone();
573        match next.get(var) {
574            Some(existing) if !value_match(existing, value) => None,
575            Some(_) => Some(next),
576            None => {
577                next.insert(var.clone(), value.clone());
578                Some(next)
579            }
580        }
581    };
582    let bind_tuple = |values: &[Value], targets: &[BindTarget], frame: &Frame| -> Option<Frame> {
583        if values.len() != targets.len() {
584            return None;
585        }
586        let mut next = frame.clone();
587        for (value, target) in values.iter().zip(targets) {
588            match target {
589                BindTarget::Blank => {}
590                BindTarget::Var(var) => match next.get(var) {
591                    Some(existing) if !value_match(existing, value) => return None,
592                    Some(_) => {}
593                    None => {
594                        next.insert(var.clone(), value.clone());
595                    }
596                },
597            }
598        }
599        Some(next)
600    };
601    match (binding, result) {
602        (Binding::Scalar(var), CallResult::Scalar(value)) => {
603            out.extend(scalar(&value, var, frame));
604        }
605        (Binding::Scalar(var), CallResult::Test(flag)) => {
606            out.extend(scalar(&Value::Bool(flag), var, frame));
607        }
608        (Binding::Coll(target), CallResult::Coll(values)) => match target {
609            BindTarget::Blank => {
610                if !values.is_empty() {
611                    out.push(frame.clone());
612                }
613            }
614            BindTarget::Var(var) => {
615                for value in values {
616                    out.extend(scalar(&value, var, frame));
617                }
618            }
619        },
620        (Binding::Tuple(targets), CallResult::Coll(values)) => {
621            out.extend(bind_tuple(&values, targets, frame));
622        }
623        (Binding::Rel(targets), CallResult::Rel(rows)) => {
624            for row in rows {
625                out.extend(bind_tuple(&row, targets, frame));
626            }
627        }
628        (_, result) => {
629            return Err(QueryError::Type(format!(
630                "binding form does not fit result {result:?}"
631            )));
632        }
633    }
634    Ok(())
635}
636
637/// `[(ground <const>) binding]`: converts a constant EDN form per binding shape.
638fn eval_ground(
639    ctx: &ExecCtx<'_>,
640    args: &[Term],
641    binding: &Binding,
642    frames: Vec<Frame>,
643) -> Result<Vec<Frame>, QueryError> {
644    let db = ctx
645        .default_db()
646        .ok_or_else(|| QueryError::UnknownSource("$".into()))?;
647    let [Term::Const(form)] = args else {
648        return Err(QueryError::Arity("ground takes one constant".into()));
649    };
650    let value_of = |form: &Edn| -> Result<Value, QueryError> {
651        const_value(db, form).ok_or_else(|| QueryError::Type(format!("cannot ground {form}")))
652    };
653    let result = match (binding, form) {
654        (Binding::Coll(_) | Binding::Tuple(_), Edn::Vector(items) | Edn::List(items)) => {
655            CallResult::Coll(items.iter().map(value_of).collect::<Result<_, _>>()?)
656        }
657        (Binding::Rel(_), Edn::Vector(rows) | Edn::List(rows)) => CallResult::Rel(
658            rows.iter()
659                .map(|row| match row {
660                    Edn::Vector(items) | Edn::List(items) => {
661                        items.iter().map(value_of).collect::<Result<Vec<_>, _>>()
662                    }
663                    _ => Err(QueryError::Type("ground relation requires tuples".into())),
664                })
665                .collect::<Result<_, _>>()?,
666        ),
667        (Binding::Scalar(_), form) => CallResult::Scalar(value_of(form)?),
668        _ => return Err(QueryError::Type(format!("cannot ground {form}"))),
669    };
670    let mut out = Vec::new();
671    for frame in frames {
672        bind_result(&frame, binding, result.clone(), &mut out)?;
673    }
674    Ok(out)
675}
676
677/// `[(get-else $src ?e attr default) ?v]`.
678fn eval_get_else(
679    ctx: &ExecCtx<'_>,
680    args: &[Term],
681    binding: &Binding,
682    frames: Vec<Frame>,
683) -> Result<Vec<Frame>, QueryError> {
684    let (db, rest) = db_context_args(ctx, args)?;
685    let [entity_term, attr_term, default_term] = rest else {
686        return Err(QueryError::Arity(
687            "get-else takes a source, entity, attribute, and default".into(),
688        ));
689    };
690    let attr = attr_const(ctx, db, attr_term)?;
691    let value_type = db.schema().get(attr).map(|meta| meta.value_type);
692    let mut out = Vec::new();
693    for frame in frames {
694        let entity = entity_arg(db, &frame, entity_term)?;
695        let value = entity
696            .and_then(|e| db.values(e, attr).into_iter().next())
697            .map_or_else(
698                || match default_term {
699                    Term::Const(form) => {
700                        let value = const_value(db, form).ok_or_else(|| {
701                            QueryError::Type(format!("bad get-else default {form}"))
702                        })?;
703                        Ok(match value_type {
704                            Some(t) => coerce_for_type(value, t),
705                            None => value,
706                        })
707                    }
708                    Term::Var(var) => frame
709                        .get(var)
710                        .cloned()
711                        .ok_or_else(|| QueryError::Unbound(var.clone())),
712                    Term::Blank => Err(QueryError::Parse("get-else default cannot be _".into())),
713                },
714                Ok,
715            )?;
716        bind_result(&frame, binding, CallResult::Scalar(value), &mut out)?;
717    }
718    Ok(out)
719}
720
721/// `[(missing? $src ?e attr)]`.
722fn eval_missing(
723    ctx: &ExecCtx<'_>,
724    args: &[Term],
725    frames: Vec<Frame>,
726) -> Result<Vec<Frame>, QueryError> {
727    let (db, rest) = db_context_args(ctx, args)?;
728    let [entity_term, attr_term] = rest else {
729        return Err(QueryError::Arity(
730            "missing? takes a source, entity, and attribute".into(),
731        ));
732    };
733    let attr = attr_const(ctx, db, attr_term)?;
734    let mut out = Vec::new();
735    for frame in frames {
736        let entity = entity_arg(db, &frame, entity_term)?;
737        let missing = entity.is_none_or(|e| db.values(e, attr).is_empty());
738        if missing {
739            out.push(frame);
740        }
741    }
742    Ok(out)
743}
744
745fn db_context_args<'a, 'b>(
746    ctx: &'b ExecCtx<'a>,
747    args: &'b [Term],
748) -> Result<(&'a Db, &'b [Term]), QueryError> {
749    match args.split_first() {
750        Some((Term::Const(Edn::Symbol(src)), rest)) if src.starts_with('$') => {
751            let db = ctx
752                .db(src)
753                .ok_or_else(|| QueryError::UnknownSource(src.clone()))?;
754            Ok((db, rest))
755        }
756        _ => {
757            let db = ctx
758                .default_db()
759                .ok_or_else(|| QueryError::UnknownSource("$".into()))?;
760            Ok((db, args))
761        }
762    }
763}
764
765fn attr_const(ctx: &ExecCtx<'_>, db: &Db, term: &Term) -> Result<AttrId, QueryError> {
766    match term {
767        Term::Const(form) => ctx.attr_of(db, form).ok_or_else(|| match form {
768            Edn::Keyword(k) => QueryError::UnknownIdent(k.clone()),
769            _ => QueryError::Type(format!("bad attribute argument {form}")),
770        }),
771        _ => Err(QueryError::Type(
772            "attribute argument must be a constant".into(),
773        )),
774    }
775}
776
777fn entity_arg(db: &Db, frame: &Frame, term: &Term) -> Result<Option<EntityId>, QueryError> {
778    match term {
779        Term::Var(var) => {
780            let value = frame
781                .get(var)
782                .ok_or_else(|| QueryError::Unbound(var.clone()))?;
783            Ok(to_entity(value))
784        }
785        Term::Const(form) => entity_const(db, form),
786        Term::Blank => Err(QueryError::Parse("entity argument cannot be _".into())),
787    }
788}
789
790fn eval_rule_call(
791    ctx: &ExecCtx<'_>,
792    name: &str,
793    args: &[Term],
794    frames: Vec<Frame>,
795    stack: &mut Vec<RuleKey>,
796) -> Result<Vec<Frame>, QueryError> {
797    let db = ctx.default_db();
798    let mut out = Vec::new();
799    for frame in frames {
800        let mut key_args: Vec<Option<Value>> = Vec::with_capacity(args.len());
801        for term in args {
802            key_args.push(match term {
803                Term::Var(var) => frame.get(var).cloned(),
804                Term::Blank => None,
805                Term::Const(form) => Some(
806                    db.and_then(|db| const_value(db, form))
807                        .ok_or_else(|| QueryError::Type(format!("bad rule argument {form}")))?,
808                ),
809            });
810        }
811        let tuples = rule_tuples(ctx, name, key_args, stack)?;
812        for tuple in tuples {
813            let mut extended = frame.clone();
814            let mut consistent = true;
815            for (term, value) in args.iter().zip(&tuple) {
816                match term {
817                    // Blanks bind nothing; constants were seeded into the key.
818                    Term::Blank | Term::Const(_) => {}
819                    Term::Var(var) => match extended.get(var) {
820                        Some(existing) if !value_match(existing, value) => {
821                            consistent = false;
822                            break;
823                        }
824                        Some(_) => {}
825                        None => {
826                            extended.insert(var.clone(), value.clone());
827                        }
828                    },
829                }
830            }
831            if consistent {
832                out.push(extended);
833            }
834        }
835    }
836    Ok(out)
837}
838
839/// Solves a rule invocation to its tuple set.
840///
841/// Bottom-up evaluation with per-invocation memo tables: tables grow
842/// monotonically; a top-level invocation iterates to a global fixpoint,
843/// while invocations nested inside rule bodies read the tables as they
844/// stand (the enclosing loop drives them to closure).
845fn rule_tuples(
846    ctx: &ExecCtx<'_>,
847    name: &str,
848    key_args: Vec<Option<Value>>,
849    stack: &mut Vec<RuleKey>,
850) -> Result<BTreeSet<Vec<Value>>, QueryError> {
851    let key: RuleKey = (name.to_owned(), key_args);
852    if stack.is_empty() {
853        loop {
854            ctx.rule_state.borrow_mut().changed = false;
855            eval_rule_once(ctx, &key, stack)?;
856            if !ctx.rule_state.borrow().changed {
857                break;
858            }
859            ctx.spend(1)?;
860        }
861    } else {
862        eval_rule_once(ctx, &key, stack)?;
863    }
864    Ok(ctx
865        .rule_state
866        .borrow()
867        .partial
868        .get(&key)
869        .cloned()
870        .unwrap_or_default())
871}
872
873fn eval_rule_once(
874    ctx: &ExecCtx<'_>,
875    key: &RuleKey,
876    stack: &mut Vec<RuleKey>,
877) -> Result<(), QueryError> {
878    if stack.contains(key) {
879        return Ok(());
880    }
881    let defs = ctx
882        .rules
883        .get(&key.0)
884        .ok_or_else(|| QueryError::Unsupported(format!("unknown rule {}", key.0)))?
885        .clone();
886    stack.push(key.clone());
887    let result = (|| -> Result<(), QueryError> {
888        for def in &defs {
889            let head: Vec<&Var> = def.head_vars();
890            if head.len() != key.1.len() {
891                return Err(QueryError::Arity(format!(
892                    "rule {} expects {} arguments",
893                    key.0,
894                    head.len()
895                )));
896            }
897            for (index, _) in def.required.iter().enumerate() {
898                if key.1[index].is_none() {
899                    return Err(QueryError::Unbound(format!(
900                        "rule {} requires its first {} arguments bound",
901                        key.0,
902                        def.required.len()
903                    )));
904                }
905            }
906            let mut seed = Frame::new();
907            for (var, value) in head.iter().zip(&key.1) {
908                if let Some(value) = value {
909                    seed.insert((*var).clone(), value.clone());
910                }
911            }
912            let frames = eval_clauses(ctx, &def.clauses, vec![seed], stack)?;
913            for frame in frames {
914                let tuple = head
915                    .iter()
916                    .map(|var| {
917                        frame
918                            .get(*var)
919                            .cloned()
920                            .ok_or_else(|| QueryError::Unbound((*var).clone()))
921                    })
922                    .collect::<Result<Vec<_>, _>>()?;
923                let mut state = ctx.rule_state.borrow_mut();
924                if state.partial.entry(key.clone()).or_default().insert(tuple) {
925                    state.changed = true;
926                }
927            }
928        }
929        Ok(())
930    })();
931    stack.pop();
932    result
933}