Skip to main content

xlog_logic/
meta_normalize.rs

1//! Safe meta-predicate normalization for the language-completeness surface.
2
3use std::collections::{BTreeMap, HashMap, HashSet};
4
5use xlog_core::{symbol, Result, ScalarType, XlogError};
6
7use crate::ast::{
8    Atom, BodyLiteral, Comparison, Constraint, DomainDecl, PredColumn, PredDecl, Program, Query,
9    Rule, Term, TypeRef, Univ,
10};
11
12const TERM_ID_TYPE: ScalarType = ScalarType::U64;
13
14/// Normalize safe meta-predicates into finite helper relations.
15///
16/// Accepted forms are static and finite. Runtime-variable predicate names,
17/// dynamic database mutation, unrestricted calls, and non-finite collection are
18/// rejected before lowering.
19pub fn normalize_meta_builtins(program: &Program) -> Result<Program> {
20    normalize_meta_builtins_owned(program.clone())
21}
22
23/// [`normalize_meta_builtins`] taking the program by value.
24///
25/// Avoids cloning the whole AST: facts whose predicate has no term-valued
26/// columns are moved through untouched (the pass is the identity on them, see
27/// `MetaNormalizer::fact_is_identity`), everything else is rebuilt as before.
28pub fn normalize_meta_builtins_owned(program: Program) -> Result<Program> {
29    let mut normalizer = MetaNormalizer::new(&program)?;
30    normalizer.normalize_program(program)
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34enum ExpectedTerm {
35    Any,
36    Compound,
37    PredRef,
38}
39
40#[derive(Debug, Clone)]
41enum RegisteredTerm {
42    Scalar,
43    PredRef,
44    Compound {
45        functor: String,
46        args: Vec<u64>,
47        parts: Vec<u64>,
48    },
49}
50
51#[derive(Debug, Clone)]
52struct TermRecord {
53    id: u64,
54    kind: RegisteredTerm,
55}
56
57struct MetaNormalizer {
58    domains: HashMap<String, ScalarType>,
59    /// Declared column types per predicate (index = column position).
60    pred_columns: HashMap<String, Vec<TypeRef>>,
61    /// Predicates with at least one term-valued (`term`/`compound`/`predref`)
62    /// or `list<T>` column: only their atoms can change under value
63    /// normalization.
64    term_valued_preds: HashSet<String>,
65    /// Source facts by predicate — consulted by `findall`/`maplist`
66    /// expansion only, so it is built only when a meta predicate occurs.
67    source_facts: HashMap<String, Vec<Atom>>,
68    derived_predicates: HashSet<String>,
69    term_ids: HashMap<String, u64>,
70    term_records: Vec<TermRecord>,
71    next_term_id: u64,
72    helper_decls: BTreeMap<String, Vec<PredColumn>>,
73    helper_facts: Vec<Rule>,
74    helper_fact_keys: HashSet<String>,
75    findall_counter: usize,
76    maplist_counter: usize,
77}
78
79impl MetaNormalizer {
80    fn new(program: &Program) -> Result<Self> {
81        let domains: HashMap<String, ScalarType> = program
82            .domains
83            .iter()
84            .map(|DomainDecl { name, typ }| (name.clone(), *typ))
85            .collect();
86
87        let mut pred_columns: HashMap<String, Vec<TypeRef>> = HashMap::new();
88        let mut term_valued_preds = HashSet::new();
89        for pred in &program.predicates {
90            let columns: Vec<TypeRef> = pred
91                .schema_columns()
92                .into_iter()
93                .map(|col| col.typ)
94                .collect();
95            if columns.iter().any(|typ| {
96                matches!(
97                    typ,
98                    TypeRef::Term | TypeRef::Compound | TypeRef::PredRef | TypeRef::List(_)
99                )
100            }) {
101                term_valued_preds.insert(pred.name.clone());
102            }
103            pred_columns.insert(pred.name.clone(), columns);
104        }
105
106        let needs_source_facts = program_uses_meta_predicates(program);
107        let mut source_facts: HashMap<String, Vec<Atom>> = HashMap::new();
108        let mut derived_predicates = HashSet::new();
109        for rule in &program.rules {
110            if rule.is_fact() {
111                if needs_source_facts {
112                    source_facts
113                        .entry(rule.head.predicate.clone())
114                        .or_default()
115                        .push(rule.head.clone());
116                }
117            } else {
118                derived_predicates.insert(rule.head.predicate.clone());
119            }
120        }
121
122        Ok(Self {
123            domains,
124            pred_columns,
125            term_valued_preds,
126            source_facts,
127            derived_predicates,
128            term_ids: HashMap::new(),
129            term_records: Vec::new(),
130            next_term_id: 1,
131            helper_decls: BTreeMap::new(),
132            helper_facts: Vec::new(),
133            helper_fact_keys: HashSet::new(),
134            findall_counter: 0,
135            maplist_counter: 0,
136        })
137    }
138
139    /// Declared column type of `pred` at position `idx`, if any.
140    fn column_type(&self, pred: &str, idx: usize) -> Option<&TypeRef> {
141        self.pred_columns.get(pred).and_then(|cols| cols.get(idx))
142    }
143
144    /// True when value normalization is the identity on this fact: its
145    /// predicate has no term-valued/list column, so every term goes through
146    /// `normalize_untyped_value`, which rebuilds the term unchanged. Such facts
147    /// are moved through without being rebuilt (they dominate ILP/EDB inputs).
148    fn fact_is_identity(&self, rule: &Rule) -> bool {
149        rule.body.is_empty() && !self.term_valued_preds.contains(&rule.head.predicate)
150    }
151
152    fn normalize_program(&mut self, program: Program) -> Result<Program> {
153        let mut out = program;
154        out.rules = std::mem::take(&mut out.rules)
155            .into_iter()
156            .map(|rule| {
157                if self.fact_is_identity(&rule) {
158                    Ok(rule)
159                } else {
160                    self.normalize_rule(&rule)
161                }
162            })
163            .collect::<Result<Vec<_>>>()?;
164        out.constraints = std::mem::take(&mut out.constraints)
165            .into_iter()
166            .map(|constraint| {
167                let mut bound = HashMap::new();
168                Ok(Constraint {
169                    authored_index: constraint.authored_index,
170                    body: self.normalize_body(&constraint.body, &mut bound)?,
171                })
172            })
173            .collect::<Result<Vec<_>>>()?;
174        out.queries = std::mem::take(&mut out.queries)
175            .into_iter()
176            .map(|query| {
177                Ok(Query {
178                    atom: self.normalize_atom_values(&query.atom)?,
179                })
180            })
181            .collect::<Result<Vec<_>>>()?;
182
183        out.prob_facts = std::mem::take(&mut out.prob_facts)
184            .into_iter()
185            .map(|mut pf| {
186                pf.atom = self.normalize_atom_values(&pf.atom)?;
187                Ok(pf)
188            })
189            .collect::<Result<Vec<_>>>()?;
190        out.annotated_disjunctions = std::mem::take(&mut out.annotated_disjunctions)
191            .into_iter()
192            .map(|mut ad| {
193                for choice in &mut ad.choices {
194                    choice.atom = self.normalize_atom_values(&choice.atom)?;
195                }
196                Ok(ad)
197            })
198            .collect::<Result<Vec<_>>>()?;
199        out.evidence = std::mem::take(&mut out.evidence)
200            .into_iter()
201            .map(|mut evidence| {
202                evidence.atom = self.normalize_atom_values(&evidence.atom)?;
203                Ok(evidence)
204            })
205            .collect::<Result<Vec<_>>>()?;
206        out.prob_queries = std::mem::take(&mut out.prob_queries)
207            .into_iter()
208            .map(|mut query| {
209                query.atom = self.normalize_atom_values(&query.atom)?;
210                Ok(query)
211            })
212            .collect::<Result<Vec<_>>>()?;
213        out.neural_predicates = std::mem::take(&mut out.neural_predicates)
214            .into_iter()
215            .map(|mut neural| {
216                neural.predicate = self.normalize_atom_values(&neural.predicate)?;
217                Ok(neural)
218            })
219            .collect::<Result<Vec<_>>>()?;
220        out.learnable_rules = std::mem::take(&mut out.learnable_rules)
221            .into_iter()
222            .map(|mut learnable| {
223                let mut bound = HashMap::new();
224                learnable.head = self.normalize_atom_values(&learnable.head)?;
225                learnable.body = self.normalize_body(&learnable.body, &mut bound)?;
226                Ok(learnable)
227            })
228            .collect::<Result<Vec<_>>>()?;
229
230        self.append_helpers(&mut out);
231        lower_meta_type_refs_in_predicates(&mut out);
232        Ok(out)
233    }
234
235    fn normalize_rule(&mut self, rule: &Rule) -> Result<Rule> {
236        let head = self.normalize_atom_values(&rule.head)?;
237        let mut bound = HashMap::new();
238        let body = self.normalize_body(&rule.body, &mut bound)?;
239        Ok(Rule { head, body })
240    }
241
242    fn normalize_body(
243        &mut self,
244        body: &[BodyLiteral],
245        bound: &mut HashMap<String, ScalarType>,
246    ) -> Result<Vec<BodyLiteral>> {
247        let mut out = Vec::new();
248        for lit in body {
249            match lit {
250                BodyLiteral::Positive(atom) => {
251                    let expanded = self.normalize_positive_atom(atom, bound)?;
252                    for lit in expanded {
253                        self.record_bound_from_literal(&lit, bound);
254                        out.push(lit);
255                    }
256                }
257                BodyLiteral::Negated(atom) => {
258                    if is_meta_predicate(&atom.predicate)
259                        || is_blocked_dynamic_predicate(&atom.predicate)
260                    {
261                        return Err(meta_error(
262                            "safe meta-predicates are not supported under negation in the finite meta normalization subset",
263                        ));
264                    }
265                    out.push(BodyLiteral::Negated(self.normalize_atom_values(atom)?));
266                }
267                BodyLiteral::Comparison(cmp) => out.push(BodyLiteral::Comparison(Comparison {
268                    op: cmp.op,
269                    left: self.normalize_untyped_value(&cmp.left)?,
270                    right: self.normalize_untyped_value(&cmp.right)?,
271                })),
272                BodyLiteral::Epistemic(lit) => out.push(BodyLiteral::Epistemic(lit.clone())),
273                BodyLiteral::IsExpr(is_expr) => out.push(BodyLiteral::IsExpr(is_expr.clone())),
274                BodyLiteral::Univ(univ) => {
275                    let lit = self.expand_univ(univ, bound)?;
276                    self.record_bound_from_literal(&lit, bound);
277                    out.push(lit);
278                }
279            }
280        }
281        Ok(out)
282    }
283
284    fn normalize_positive_atom(
285        &mut self,
286        atom: &Atom,
287        bound: &HashMap<String, ScalarType>,
288    ) -> Result<Vec<BodyLiteral>> {
289        if is_blocked_dynamic_predicate(&atom.predicate) {
290            return Err(dynamic_predicate_error(&atom.predicate));
291        }
292
293        match atom.predicate.as_str() {
294            "ground" => self.expand_ground_like(atom, bound, MetaTruthKind::Ground),
295            "var" => self.expand_ground_like(atom, bound, MetaTruthKind::Var),
296            "nonvar" => self.expand_ground_like(atom, bound, MetaTruthKind::NonVar),
297            "functor" => self.expand_functor(atom, bound),
298            "findall" => self.expand_findall(atom, bound),
299            "maplist" => self.expand_maplist(atom),
300            _ => Ok(vec![BodyLiteral::Positive(
301                self.normalize_atom_values(atom)?,
302            )]),
303        }
304    }
305
306    fn normalize_atom_values(&mut self, atom: &Atom) -> Result<Atom> {
307        let mut terms = Vec::with_capacity(atom.terms.len());
308        for (idx, term) in atom.terms.iter().enumerate() {
309            let expected = self.column_type(&atom.predicate, idx).cloned();
310            terms.push(self.normalize_value_term(term, expected.as_ref())?);
311        }
312        Ok(Atom {
313            predicate: atom.predicate.clone(),
314            terms,
315        })
316    }
317
318    fn normalize_value_term(&mut self, term: &Term, expected: Option<&TypeRef>) -> Result<Term> {
319        match expected {
320            Some(TypeRef::Term) if term.is_any_variable() => Ok(term.clone()),
321            Some(TypeRef::Term) => Ok(Term::Integer(
322                self.register_term(term, ExpectedTerm::Any)? as i64
323            )),
324            Some(TypeRef::Compound) if term.is_any_variable() => Ok(term.clone()),
325            Some(TypeRef::Compound) => Ok(Term::Integer(
326                self.register_term(term, ExpectedTerm::Compound)? as i64,
327            )),
328            Some(TypeRef::PredRef) if term.is_any_variable() => Ok(term.clone()),
329            Some(TypeRef::PredRef) => Ok(Term::Integer(
330                self.register_term(term, ExpectedTerm::PredRef)? as i64,
331            )),
332            Some(TypeRef::List(inner)) => self.normalize_list_value(term, inner),
333            Some(TypeRef::Scalar(_)) | Some(TypeRef::Domain(_)) | None => {
334                self.normalize_untyped_value(term)
335            }
336        }
337    }
338
339    fn normalize_untyped_value(&mut self, term: &Term) -> Result<Term> {
340        match term {
341            Term::List(items) => Ok(Term::List(
342                items
343                    .iter()
344                    .map(|item| self.normalize_untyped_value(item))
345                    .collect::<Result<Vec<_>>>()?,
346            )),
347            Term::Cons { head, tail } => Ok(Term::Cons {
348                head: Box::new(self.normalize_untyped_value(head)?),
349                tail: Box::new(self.normalize_untyped_value(tail)?),
350            }),
351            _ => Ok(term.clone()),
352        }
353    }
354
355    fn normalize_list_value(&mut self, term: &Term, inner: &TypeRef) -> Result<Term> {
356        match term {
357            Term::List(items) => Ok(Term::List(
358                items
359                    .iter()
360                    .map(|item| self.normalize_value_term(item, Some(inner)))
361                    .collect::<Result<Vec<_>>>()?,
362            )),
363            Term::Cons { head, tail } => Ok(Term::Cons {
364                head: Box::new(self.normalize_value_term(head, Some(inner))?),
365                tail: Box::new(self.normalize_list_value(tail, inner)?),
366            }),
367            Term::Variable(_) | Term::Anonymous | Term::Integer(_) => Ok(term.clone()),
368            _ => Err(meta_error(
369                "list<T> column expects a finite list literal, list id, or variable",
370            )),
371        }
372    }
373
374    fn expand_ground_like(
375        &mut self,
376        atom: &Atom,
377        bound: &HashMap<String, ScalarType>,
378        kind: MetaTruthKind,
379    ) -> Result<Vec<BodyLiteral>> {
380        require_arity(atom, 1)?;
381        let truth = match kind {
382            MetaTruthKind::Ground => self.is_ground(&atom.terms[0], bound),
383            MetaTruthKind::Var => self.is_var(&atom.terms[0], bound),
384            MetaTruthKind::NonVar => self.is_nonvar(&atom.terms[0], bound),
385        };
386        if truth {
387            Ok(Vec::new())
388        } else {
389            Ok(vec![BodyLiteral::Positive(self.fail_atom())])
390        }
391    }
392
393    fn expand_functor(
394        &mut self,
395        atom: &Atom,
396        bound: &HashMap<String, ScalarType>,
397    ) -> Result<Vec<BodyLiteral>> {
398        require_arity(atom, 3)?;
399        let term = self.term_id_or_bound_variable(&atom.terms[0], bound, "functor/3")?;
400        let name = self.normalize_symbol_argument(&atom.terms[1], "functor/3 name")?;
401        let arity = self.normalize_u32_argument(&atom.terms[2], "functor/3 arity")?;
402        self.register_helper_decl(
403            functor_pred(),
404            vec![
405                scalar_col("term_id", TERM_ID_TYPE),
406                scalar_col("name", ScalarType::Symbol),
407                scalar_col("arity", ScalarType::U32),
408            ],
409        );
410        Ok(vec![BodyLiteral::Positive(Atom {
411            predicate: functor_pred().to_string(),
412            terms: vec![term, name, arity],
413        })])
414    }
415
416    fn expand_univ(
417        &mut self,
418        univ: &Univ,
419        bound: &HashMap<String, ScalarType>,
420    ) -> Result<BodyLiteral> {
421        let term = self.term_id_or_bound_variable(&univ.term, bound, "univ")?;
422        let parts = self.normalize_univ_parts(&univ.parts)?;
423        self.register_helper_decl(
424            univ_pred(),
425            vec![
426                scalar_col("term_id", TERM_ID_TYPE),
427                PredColumn {
428                    name: Some("parts".to_string()),
429                    typ: TypeRef::List(Box::new(TypeRef::Term)),
430                },
431            ],
432        );
433        Ok(BodyLiteral::Positive(Atom {
434            predicate: univ_pred().to_string(),
435            terms: vec![term, parts],
436        }))
437    }
438
439    fn expand_findall(
440        &mut self,
441        atom: &Atom,
442        bound: &HashMap<String, ScalarType>,
443    ) -> Result<Vec<BodyLiteral>> {
444        require_arity(atom, 3)?;
445        let template = &atom.terms[0];
446        let goal = compound_goal_atom(&atom.terms[1], "findall/3")?;
447        let out_list = atom.terms[2].clone();
448        if self.derived_predicates.contains(&goal.predicate) {
449            return Err(meta_error(
450                "findall/3 is limited to finite source facts; derived goals are reserved for a later aggregate-backed collection path",
451            ));
452        }
453
454        let template_vars: HashSet<String> = template
455            .variables()
456            .into_iter()
457            .map(str::to_string)
458            .collect();
459        let mut group_positions = Vec::new();
460        for (idx, term) in goal.terms.iter().enumerate() {
461            if let Term::Variable(name) = term {
462                if bound.contains_key(name) {
463                    group_positions.push((idx, name.clone()));
464                } else if !template_vars.contains(name) {
465                    return Err(meta_error(
466                        "unsafe findall/3 goal variable is neither bound before findall nor collected by the template",
467                    ));
468                }
469            }
470        }
471
472        let elem_type_ref = self.infer_template_type(template, &goal)?;
473        let helper = format!("__xlog_meta_findall_{}", self.findall_counter);
474        self.findall_counter += 1;
475
476        let mut columns = Vec::new();
477        for (idx, name) in &group_positions {
478            columns.push(PredColumn {
479                name: Some(name.to_ascii_lowercase()),
480                typ: self
481                    .column_type(&goal.predicate, *idx)
482                    .cloned()
483                    .unwrap_or(TypeRef::Scalar(
484                        bound.get(name).copied().unwrap_or(ScalarType::U64),
485                    )),
486            });
487        }
488        columns.push(PredColumn {
489            name: Some("results".to_string()),
490            typ: TypeRef::List(Box::new(elem_type_ref.clone())),
491        });
492        self.register_helper_decl(&helper, columns);
493
494        let mut groups: BTreeMap<String, (Vec<Term>, Vec<Term>)> = BTreeMap::new();
495        for fact in self
496            .source_facts
497            .get(&goal.predicate)
498            .cloned()
499            .unwrap_or_default()
500        {
501            if fact.terms.len() != goal.terms.len() {
502                continue;
503            }
504            let Some(bindings) = self.match_goal_fact(&goal, &fact)? else {
505                continue;
506            };
507            let group_terms: Vec<Term> = group_positions
508                .iter()
509                .map(|(idx, _)| fact.terms[*idx].clone())
510                .collect();
511            let key = group_terms
512                .iter()
513                .map(term_match_key)
514                .collect::<Vec<_>>()
515                .join("|");
516            let template_value = self.substitute_template(template, &bindings, &elem_type_ref)?;
517            groups
518                .entry(key)
519                .or_insert_with(|| (group_terms, Vec::new()))
520                .1
521                .push(template_value);
522        }
523
524        if groups.is_empty() && group_positions.is_empty() {
525            groups.insert(String::new(), (Vec::new(), Vec::new()));
526        }
527
528        for (_, (mut group_terms, values)) in groups {
529            let list = Term::List(values);
530            group_terms.push(list);
531            self.add_helper_fact(&helper, group_terms);
532        }
533
534        let mut terms: Vec<Term> = group_positions
535            .iter()
536            .map(|(_, name)| Term::Variable(name.clone()))
537            .collect();
538        terms.push(out_list);
539        Ok(vec![BodyLiteral::Positive(Atom {
540            predicate: helper,
541            terms,
542        })])
543    }
544
545    fn expand_maplist(&mut self, atom: &Atom) -> Result<Vec<BodyLiteral>> {
546        if atom.terms.len() != 2 && atom.terms.len() != 3 {
547            return Err(meta_error(format!(
548                "maplist expects unary or binary form, got {} arguments",
549                atom.terms.len()
550            )));
551        }
552        let pred = static_pred_name(&atom.terms[0])?;
553        if self.derived_predicates.contains(&pred) {
554            return Err(meta_error(
555                "maplist is limited to finite source facts or literal empty lists",
556            ));
557        }
558        let input_items = finite_list_items(&atom.terms[1], "maplist input")?;
559        let input_type = self
560            .column_type(&pred, 0)
561            .cloned()
562            .unwrap_or_else(|| infer_type_ref_from_terms(input_items));
563        let input_list = Term::List(
564            input_items
565                .iter()
566                .map(|item| self.normalize_value_term(item, Some(&input_type)))
567                .collect::<Result<Vec<_>>>()?,
568        );
569        let helper = format!("__xlog_meta_maplist_{}_{}", pred, self.maplist_counter);
570        self.maplist_counter += 1;
571
572        if atom.terms.len() == 2 {
573            self.register_helper_decl(
574                &helper,
575                vec![PredColumn {
576                    name: Some("input".to_string()),
577                    typ: TypeRef::List(Box::new(input_type)),
578                }],
579            );
580            if self.maplist_unary_holds(&pred, &input_list)? {
581                self.add_helper_fact(&helper, vec![input_list.clone()]);
582            }
583            return Ok(vec![BodyLiteral::Positive(Atom {
584                predicate: helper,
585                terms: vec![atom.terms[1].clone()],
586            })]);
587        }
588
589        let output_type = self
590            .column_type(&pred, 1)
591            .cloned()
592            .unwrap_or(TypeRef::Scalar(ScalarType::U64));
593        self.register_helper_decl(
594            &helper,
595            vec![
596                PredColumn {
597                    name: Some("input".to_string()),
598                    typ: TypeRef::List(Box::new(input_type)),
599                },
600                PredColumn {
601                    name: Some("output".to_string()),
602                    typ: TypeRef::List(Box::new(output_type.clone())),
603                },
604            ],
605        );
606        for output in self.maplist_binary_outputs(&pred, &input_list, &output_type)? {
607            self.add_helper_fact(&helper, vec![input_list.clone(), Term::List(output)]);
608        }
609        Ok(vec![BodyLiteral::Positive(Atom {
610            predicate: helper,
611            terms: vec![atom.terms[1].clone(), atom.terms[2].clone()],
612        })])
613    }
614
615    fn maplist_unary_holds(&self, pred: &str, input: &Term) -> Result<bool> {
616        let Term::List(items) = input else {
617            return Err(meta_error(
618                "maplist input did not normalize to a finite list",
619            ));
620        };
621        let facts = self.source_facts.get(pred).cloned().unwrap_or_default();
622        Ok(items.iter().all(|item| {
623            facts.iter().any(|fact| {
624                fact.terms.len() == 1 && term_match_key(&fact.terms[0]) == term_match_key(item)
625            })
626        }))
627    }
628
629    fn maplist_binary_outputs(
630        &mut self,
631        pred: &str,
632        input: &Term,
633        output_type: &TypeRef,
634    ) -> Result<Vec<Vec<Term>>> {
635        let Term::List(items) = input else {
636            return Err(meta_error(
637                "maplist input did not normalize to a finite list",
638            ));
639        };
640        if items.is_empty() {
641            return Ok(vec![Vec::new()]);
642        }
643        let facts = self.source_facts.get(pred).cloned().unwrap_or_default();
644        let mut choices: Vec<Vec<Term>> = Vec::new();
645        for item in items {
646            let mut outs = Vec::new();
647            for fact in &facts {
648                if fact.terms.len() == 2 && term_match_key(&fact.terms[0]) == term_match_key(item) {
649                    outs.push(self.normalize_value_term(&fact.terms[1], Some(output_type))?);
650                }
651            }
652            if outs.is_empty() {
653                return Ok(Vec::new());
654            }
655            choices.push(outs);
656        }
657        Ok(cartesian_product(&choices))
658    }
659
660    fn match_goal_fact(&self, goal: &Atom, fact: &Atom) -> Result<Option<HashMap<String, Term>>> {
661        let mut bindings = HashMap::new();
662        for (goal_term, fact_term) in goal.terms.iter().zip(&fact.terms) {
663            match goal_term {
664                Term::Variable(name) => {
665                    if let Some(existing) = bindings.get(name) {
666                        if term_match_key(existing) != term_match_key(fact_term) {
667                            return Ok(None);
668                        }
669                    } else {
670                        bindings.insert(name.clone(), fact_term.clone());
671                    }
672                }
673                Term::Anonymous => {}
674                _ => {
675                    if term_match_key(goal_term) != term_match_key(fact_term) {
676                        return Ok(None);
677                    }
678                }
679            }
680        }
681        Ok(Some(bindings))
682    }
683
684    fn substitute_template(
685        &mut self,
686        template: &Term,
687        bindings: &HashMap<String, Term>,
688        elem_type: &TypeRef,
689    ) -> Result<Term> {
690        let value = match template {
691            Term::Variable(name) => bindings.get(name).cloned().ok_or_else(|| {
692                meta_error(format!(
693                    "findall/3 template variable '{}' is not bound by goal",
694                    name
695                ))
696            })?,
697            Term::Anonymous => {
698                return Err(meta_error(
699                    "findall/3 template cannot be the anonymous wildcard",
700                ))
701            }
702            Term::List(items) => Term::List(
703                items
704                    .iter()
705                    .map(|item| self.substitute_template(item, bindings, elem_type))
706                    .collect::<Result<Vec<_>>>()?,
707            ),
708            Term::Compound { functor, args } => Term::Compound {
709                functor: functor.clone(),
710                args: args
711                    .iter()
712                    .map(|arg| self.substitute_template(arg, bindings, elem_type))
713                    .collect::<Result<Vec<_>>>()?,
714            },
715            _ => template.clone(),
716        };
717        self.normalize_value_term(&value, Some(elem_type))
718    }
719
720    fn infer_template_type(&self, template: &Term, goal: &Atom) -> Result<TypeRef> {
721        match template {
722            Term::Variable(name) => {
723                for (idx, term) in goal.terms.iter().enumerate() {
724                    if matches!(term, Term::Variable(var) if var == name) {
725                        return Ok(self
726                            .column_type(&goal.predicate, idx)
727                            .cloned()
728                            .unwrap_or(TypeRef::Scalar(ScalarType::U64)));
729                    }
730                }
731                Err(meta_error(format!(
732                    "findall/3 template variable '{}' is not bound by the goal",
733                    name
734                )))
735            }
736            Term::Integer(_) => Ok(TypeRef::Scalar(ScalarType::U32)),
737            Term::Float(_) => Ok(TypeRef::Scalar(ScalarType::F64)),
738            Term::String(_) | Term::Symbol(_) => Ok(TypeRef::Scalar(ScalarType::Symbol)),
739            Term::Compound { .. } | Term::PredRef(_) | Term::List(_) | Term::Cons { .. } => {
740                Ok(TypeRef::Term)
741            }
742            Term::Anonymous | Term::Aggregate(_) => Err(meta_error(
743                "findall/3 template must be a finite scalar or term expression",
744            )),
745        }
746    }
747
748    fn term_id_or_bound_variable(
749        &mut self,
750        term: &Term,
751        bound: &HashMap<String, ScalarType>,
752        context: &str,
753    ) -> Result<Term> {
754        match term {
755            Term::Variable(name) if bound.contains_key(name) => Ok(term.clone()),
756            Term::Variable(_) | Term::Anonymous => Err(meta_error(format!(
757                "{context} requires a known finite term before runtime inspection"
758            ))),
759            _ => Ok(Term::Integer(
760                self.register_term(term, ExpectedTerm::Any)? as i64
761            )),
762        }
763    }
764
765    fn normalize_univ_parts(&mut self, term: &Term) -> Result<Term> {
766        match term {
767            Term::Variable(_) | Term::Anonymous => Ok(term.clone()),
768            Term::List(items) => Ok(Term::List(
769                items
770                    .iter()
771                    .map(|item| {
772                        Ok(Term::Integer(
773                            self.register_term(item, ExpectedTerm::Any)? as i64
774                        ))
775                    })
776                    .collect::<Result<Vec<_>>>()?,
777            )),
778            _ => Err(meta_error(
779                "univ parts must be a finite list literal or a list variable",
780            )),
781        }
782    }
783
784    fn normalize_symbol_argument(&self, term: &Term, context: &str) -> Result<Term> {
785        match term {
786            Term::Variable(_) | Term::Anonymous => Ok(term.clone()),
787            Term::Symbol(_) | Term::String(_) => Ok(term.clone()),
788            _ => Err(meta_error(format!(
789                "{context} must be a symbol or variable"
790            ))),
791        }
792    }
793
794    fn normalize_u32_argument(&self, term: &Term, context: &str) -> Result<Term> {
795        match term {
796            Term::Variable(_) | Term::Anonymous => Ok(term.clone()),
797            Term::Integer(value) if *value >= 0 && *value <= u32::MAX as i64 => Ok(term.clone()),
798            _ => Err(meta_error(format!(
799                "{context} must be a u32 integer or variable"
800            ))),
801        }
802    }
803
804    fn is_ground(&self, term: &Term, bound: &HashMap<String, ScalarType>) -> bool {
805        match term {
806            Term::Variable(name) => bound.contains_key(name),
807            Term::Anonymous => false,
808            Term::List(items) => items.iter().all(|item| self.is_ground(item, bound)),
809            Term::Cons { head, tail } => self.is_ground(head, bound) && self.is_ground(tail, bound),
810            Term::Compound { args, .. } => args.iter().all(|arg| self.is_ground(arg, bound)),
811            Term::Aggregate(_) => false,
812            Term::Integer(_)
813            | Term::Float(_)
814            | Term::String(_)
815            | Term::Symbol(_)
816            | Term::PredRef(_) => true,
817        }
818    }
819
820    fn is_var(&self, term: &Term, bound: &HashMap<String, ScalarType>) -> bool {
821        match term {
822            Term::Variable(name) => !bound.contains_key(name),
823            Term::Anonymous => true,
824            _ => false,
825        }
826    }
827
828    fn is_nonvar(&self, term: &Term, bound: &HashMap<String, ScalarType>) -> bool {
829        !self.is_var(term, bound)
830    }
831
832    fn record_bound_from_literal(
833        &self,
834        lit: &BodyLiteral,
835        bound: &mut HashMap<String, ScalarType>,
836    ) {
837        let BodyLiteral::Positive(atom) = lit else {
838            return;
839        };
840        for (idx, term) in atom.terms.iter().enumerate() {
841            if let Term::Variable(name) = term {
842                let typ = self
843                    .column_type(&atom.predicate, idx)
844                    .and_then(|typ| self.storage_type_for_type_ref(typ).ok())
845                    .unwrap_or(ScalarType::U64);
846                bound.insert(name.clone(), typ);
847            }
848        }
849    }
850
851    fn register_term(&mut self, term: &Term, expected: ExpectedTerm) -> Result<u64> {
852        let (key, kind) = match term {
853            Term::Integer(value) if expected != ExpectedTerm::PredRef => {
854                (format!("i:{value}"), RegisteredTerm::Scalar)
855            }
856            Term::Float(value) if expected != ExpectedTerm::PredRef => {
857                (format!("f:{}", value.to_bits()), RegisteredTerm::Scalar)
858            }
859            Term::String(value) if expected != ExpectedTerm::PredRef => {
860                (format!("s:{value}"), RegisteredTerm::Scalar)
861            }
862            Term::Symbol(id) if expected == ExpectedTerm::PredRef => {
863                let name = symbol::resolve(*id);
864                (format!("predref:{name}"), RegisteredTerm::PredRef)
865            }
866            Term::String(name) if expected == ExpectedTerm::PredRef => {
867                (format!("predref:{name}"), RegisteredTerm::PredRef)
868            }
869            Term::PredRef(name) => (format!("predref:{name}"), RegisteredTerm::PredRef),
870            Term::Symbol(id) => {
871                let name = symbol::resolve(*id);
872                (format!("sym:{name}"), RegisteredTerm::Scalar)
873            }
874            Term::Compound { functor, args } if expected != ExpectedTerm::PredRef => {
875                let mut arg_ids = Vec::with_capacity(args.len());
876                for arg in args {
877                    arg_ids.push(self.register_term(arg, ExpectedTerm::Any)?);
878                }
879                let functor_id =
880                    self.register_term(&Term::Symbol(symbol::intern(functor)), ExpectedTerm::Any)?;
881                let mut parts = vec![functor_id];
882                parts.extend(arg_ids.iter().copied());
883                (
884                    format!("compound:{functor}({})", join_ids(&arg_ids)),
885                    RegisteredTerm::Compound {
886                        functor: functor.clone(),
887                        args: arg_ids,
888                        parts,
889                    },
890                )
891            }
892            Term::List(_) | Term::Cons { .. } => {
893                return Err(meta_error(
894                    "finite list terms inside term values are reserved for a later term-value encoding path",
895                ))
896            }
897            Term::Variable(_) | Term::Anonymous => {
898                return Err(meta_error(
899                    "finite term registration requires a ground source term",
900                ))
901            }
902            Term::Aggregate(_) => {
903                return Err(meta_error(
904                    "aggregate terms cannot be registered as finite meta terms",
905                ))
906            }
907            Term::Compound { .. } => {
908                return Err(meta_error(
909                    "predref columns require a static predicate reference, not a compound term",
910                ))
911            }
912            Term::Integer(_) | Term::Float(_) | Term::String(_) => {
913                return Err(meta_error(
914                    "predref columns require a static predicate reference",
915                ))
916            }
917        };
918
919        if expected == ExpectedTerm::Compound && !matches!(kind, RegisteredTerm::Compound { .. }) {
920            return Err(meta_error(
921                "compound column requires a finite compound term",
922            ));
923        }
924        if expected == ExpectedTerm::PredRef && !matches!(kind, RegisteredTerm::PredRef) {
925            return Err(meta_error(
926                "predref column requires a static predicate reference",
927            ));
928        }
929
930        if let Some(id) = self.term_ids.get(&key) {
931            return Ok(*id);
932        }
933        let id = self.next_term_id;
934        self.next_term_id += 1;
935        self.term_ids.insert(key, id);
936        self.term_records.push(TermRecord { id, kind });
937        Ok(id)
938    }
939
940    fn append_helpers(&mut self, program: &mut Program) {
941        self.append_term_helper_facts();
942
943        let existing: HashSet<String> = program
944            .predicates
945            .iter()
946            .map(|pred| pred.name.clone())
947            .collect();
948        for (name, columns) in &self.helper_decls {
949            if existing.contains(name) {
950                continue;
951            }
952            program.predicates.push(PredDecl {
953                name: name.clone(),
954                types: columns.iter().map(|col| col.typ.clone()).collect(),
955                columns: columns.clone(),
956                is_private: true,
957            });
958        }
959        program.rules.extend(std::mem::take(&mut self.helper_facts));
960    }
961
962    fn append_term_helper_facts(&mut self) {
963        self.register_helper_decl(
964            functor_pred(),
965            vec![
966                scalar_col("term_id", TERM_ID_TYPE),
967                scalar_col("name", ScalarType::Symbol),
968                scalar_col("arity", ScalarType::U32),
969            ],
970        );
971        self.register_helper_decl(
972            univ_pred(),
973            vec![
974                scalar_col("term_id", TERM_ID_TYPE),
975                PredColumn {
976                    name: Some("parts".to_string()),
977                    typ: TypeRef::List(Box::new(TypeRef::Term)),
978                },
979            ],
980        );
981        self.register_helper_decl(
982            arg_pred(),
983            vec![
984                scalar_col("term_id", TERM_ID_TYPE),
985                scalar_col("idx", ScalarType::U32),
986                scalar_col("arg_id", TERM_ID_TYPE),
987            ],
988        );
989
990        for record in self.term_records.clone() {
991            if let RegisteredTerm::Compound {
992                functor,
993                args,
994                parts,
995            } = record.kind
996            {
997                self.add_helper_fact(
998                    functor_pred(),
999                    vec![
1000                        Term::Integer(record.id as i64),
1001                        Term::Symbol(symbol::intern(&functor)),
1002                        Term::Integer(args.len() as i64),
1003                    ],
1004                );
1005                self.add_helper_fact(
1006                    univ_pred(),
1007                    vec![
1008                        Term::Integer(record.id as i64),
1009                        Term::List(
1010                            parts
1011                                .into_iter()
1012                                .map(|id| Term::Integer(id as i64))
1013                                .collect(),
1014                        ),
1015                    ],
1016                );
1017                for (idx, arg_id) in args.into_iter().enumerate() {
1018                    self.add_helper_fact(
1019                        arg_pred(),
1020                        vec![
1021                            Term::Integer(record.id as i64),
1022                            Term::Integer(idx as i64),
1023                            Term::Integer(arg_id as i64),
1024                        ],
1025                    );
1026                }
1027            }
1028        }
1029    }
1030
1031    fn register_helper_decl(&mut self, name: &str, columns: Vec<PredColumn>) {
1032        self.helper_decls.entry(name.to_string()).or_insert(columns);
1033    }
1034
1035    fn add_helper_fact(&mut self, pred: &str, terms: Vec<Term>) {
1036        let key = format!("{pred}:{:?}", terms);
1037        if !self.helper_fact_keys.insert(key) {
1038            return;
1039        }
1040        self.helper_facts.push(Rule {
1041            head: Atom {
1042                predicate: pred.to_string(),
1043                terms,
1044            },
1045            body: vec![],
1046        });
1047    }
1048
1049    fn fail_atom(&mut self) -> Atom {
1050        self.register_helper_decl(fail_pred(), vec![scalar_col("flag", ScalarType::U32)]);
1051        Atom {
1052            predicate: fail_pred().to_string(),
1053            terms: vec![Term::Integer(1)],
1054        }
1055    }
1056
1057    fn storage_type_for_type_ref(&self, typ: &TypeRef) -> Result<ScalarType> {
1058        match typ {
1059            TypeRef::Scalar(ty) => Ok(*ty),
1060            TypeRef::Domain(name) => self
1061                .domains
1062                .get(name)
1063                .copied()
1064                .ok_or_else(|| meta_error(format!("unknown domain alias '{name}'"))),
1065            TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
1066                Ok(TERM_ID_TYPE)
1067            }
1068        }
1069    }
1070}
1071
1072#[derive(Debug, Clone, Copy)]
1073enum MetaTruthKind {
1074    Ground,
1075    Var,
1076    NonVar,
1077}
1078
1079fn require_arity(atom: &Atom, expected: usize) -> Result<()> {
1080    if atom.terms.len() == expected {
1081        Ok(())
1082    } else {
1083        Err(meta_error(format!(
1084            "{} expects {} arguments, got {}",
1085            atom.predicate,
1086            expected,
1087            atom.terms.len()
1088        )))
1089    }
1090}
1091
1092fn compound_goal_atom(term: &Term, context: &str) -> Result<Atom> {
1093    match term {
1094        Term::Compound { functor, args } => Ok(Atom {
1095            predicate: functor.clone(),
1096            terms: args.clone(),
1097        }),
1098        _ => Err(meta_error(format!(
1099            "{context} goal must be a finite atom expression"
1100        ))),
1101    }
1102}
1103
1104fn finite_list_items<'a>(term: &'a Term, context: &str) -> Result<&'a [Term]> {
1105    match term {
1106        Term::List(items) => Ok(items),
1107        _ => Err(meta_error(format!(
1108            "{context} requires a finite list literal in the safe meta subset"
1109        ))),
1110    }
1111}
1112
1113fn static_pred_name(term: &Term) -> Result<String> {
1114    match term {
1115        Term::Symbol(id) => Ok(symbol::resolve(*id)),
1116        Term::String(name) | Term::PredRef(name) => Ok(name.clone()),
1117        Term::Variable(_) | Term::Anonymous => Err(meta_error(
1118            "maplist requires a static predicate reference; runtime-variable predicate names are rejected",
1119        )),
1120        _ => Err(meta_error(
1121            "maplist predicate argument must be a static predicate reference",
1122        )),
1123    }
1124}
1125
1126/// Return the statically referenced predicate used by a safe meta-predicate.
1127///
1128/// Invalid or dynamic forms are diagnosed by the normalizer itself. This
1129/// helper exposes only references that the accepted finite forms resolve.
1130pub(crate) fn static_meta_predicate_dependency(atom: &Atom) -> Option<String> {
1131    match atom.predicate.as_str() {
1132        "findall" => atom
1133            .terms
1134            .get(1)
1135            .and_then(|term| compound_goal_atom(term, "findall/3").ok())
1136            .map(|goal| goal.predicate),
1137        "maplist" => atom
1138            .terms
1139            .first()
1140            .and_then(|term| static_pred_name(term).ok()),
1141        _ => None,
1142    }
1143}
1144
1145fn infer_type_ref_from_terms(items: &[Term]) -> TypeRef {
1146    if let Some(first) = items.first() {
1147        match first {
1148            Term::Integer(value) if *value >= 0 && *value <= u32::MAX as i64 => {
1149                TypeRef::Scalar(ScalarType::U32)
1150            }
1151            Term::Integer(_) => TypeRef::Scalar(ScalarType::I64),
1152            Term::Float(_) => TypeRef::Scalar(ScalarType::F64),
1153            Term::String(_) | Term::Symbol(_) => TypeRef::Scalar(ScalarType::Symbol),
1154            Term::Compound { .. } | Term::PredRef(_) | Term::List(_) | Term::Cons { .. } => {
1155                TypeRef::Term
1156            }
1157            Term::Variable(_) | Term::Anonymous | Term::Aggregate(_) => {
1158                TypeRef::Scalar(ScalarType::U64)
1159            }
1160        }
1161    } else {
1162        TypeRef::Scalar(ScalarType::U64)
1163    }
1164}
1165
1166fn cartesian_product(choices: &[Vec<Term>]) -> Vec<Vec<Term>> {
1167    let mut rows: Vec<Vec<Term>> = vec![Vec::new()];
1168    for choice in choices {
1169        let mut next = Vec::new();
1170        for row in &rows {
1171            for value in choice {
1172                let mut row = row.clone();
1173                row.push(value.clone());
1174                next.push(row);
1175            }
1176        }
1177        rows = next;
1178    }
1179    rows
1180}
1181
1182fn term_match_key(term: &Term) -> String {
1183    match term {
1184        Term::Variable(name) => format!("var:{name}"),
1185        Term::Anonymous => "_".to_string(),
1186        Term::Integer(value) => format!("i:{value}"),
1187        Term::Float(value) => format!("f:{}", value.to_bits()),
1188        Term::String(value) => format!("s:{value}"),
1189        Term::Symbol(id) => format!("sym:{}", symbol::resolve(*id)),
1190        Term::List(items) => format!(
1191            "list:[{}]",
1192            items
1193                .iter()
1194                .map(term_match_key)
1195                .collect::<Vec<_>>()
1196                .join(",")
1197        ),
1198        Term::Cons { head, tail } => {
1199            format!("cons:{}|{}", term_match_key(head), term_match_key(tail))
1200        }
1201        Term::Compound { functor, args } => format!(
1202            "compound:{functor}({})",
1203            args.iter()
1204                .map(term_match_key)
1205                .collect::<Vec<_>>()
1206                .join(",")
1207        ),
1208        Term::PredRef(name) => format!("predref:{name}"),
1209        Term::Aggregate(agg) => format!("agg:{:?}:{}", agg.op, agg.variable),
1210    }
1211}
1212
1213fn scalar_col(name: &str, typ: ScalarType) -> PredColumn {
1214    PredColumn {
1215        name: Some(name.to_string()),
1216        typ: TypeRef::Scalar(typ),
1217    }
1218}
1219
1220fn lower_meta_type_refs_in_predicates(program: &mut Program) {
1221    for pred in &mut program.predicates {
1222        for typ in &mut pred.types {
1223            *typ = lower_meta_type_ref(typ);
1224        }
1225        for col in &mut pred.columns {
1226            col.typ = lower_meta_type_ref(&col.typ);
1227        }
1228    }
1229}
1230
1231fn lower_meta_type_ref(typ: &TypeRef) -> TypeRef {
1232    match typ {
1233        TypeRef::List(inner) => TypeRef::List(Box::new(lower_meta_type_ref(inner))),
1234        TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => TypeRef::Scalar(TERM_ID_TYPE),
1235        TypeRef::Scalar(_) | TypeRef::Domain(_) => typ.clone(),
1236    }
1237}
1238
1239fn join_ids(ids: &[u64]) -> String {
1240    ids.iter().map(u64::to_string).collect::<Vec<_>>().join(",")
1241}
1242
1243/// Does any body literal name a meta predicate? Only then can `findall`/
1244/// `maplist` expansion consult the source facts.
1245fn program_uses_meta_predicates(program: &Program) -> bool {
1246    let body_uses = |body: &[BodyLiteral]| {
1247        body.iter().any(|lit| match lit {
1248            BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
1249                is_meta_predicate(&atom.predicate)
1250            }
1251            _ => false,
1252        })
1253    };
1254    program.rules.iter().any(|rule| body_uses(&rule.body))
1255        || program
1256            .constraints
1257            .iter()
1258            .any(|constraint| body_uses(&constraint.body))
1259        || program
1260            .learnable_rules
1261            .iter()
1262            .any(|learnable| body_uses(&learnable.body))
1263}
1264
1265fn is_meta_predicate(name: &str) -> bool {
1266    matches!(
1267        name,
1268        "ground" | "var" | "nonvar" | "functor" | "findall" | "maplist"
1269    )
1270}
1271
1272fn is_blocked_dynamic_predicate(name: &str) -> bool {
1273    matches!(name, "call" | "assert" | "asserta" | "assertz" | "retract")
1274}
1275
1276fn dynamic_predicate_error(name: &str) -> XlogError {
1277    match name {
1278        "call" => meta_error("dynamic call/N is outside the safe meta subset"),
1279        "assert" | "asserta" | "assertz" | "retract" => {
1280            meta_error("dynamic database mutation is outside the safe meta subset")
1281        }
1282        _ => meta_error("unsupported dynamic meta predicate"),
1283    }
1284}
1285
1286fn functor_pred() -> &'static str {
1287    "__xlog_meta_functor"
1288}
1289
1290fn univ_pred() -> &'static str {
1291    "__xlog_meta_univ"
1292}
1293
1294fn arg_pred() -> &'static str {
1295    "__xlog_meta_arg"
1296}
1297
1298fn fail_pred() -> &'static str {
1299    "__xlog_meta_fail"
1300}
1301
1302fn meta_error(message: impl Into<String>) -> XlogError {
1303    XlogError::Compilation(format!("meta normalization error: {}", message.into()))
1304}