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