1use 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
14pub fn normalize_meta_builtins(program: &Program) -> Result<Program> {
20 normalize_meta_builtins_owned(program.clone())
21}
22
23pub 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 pred_columns: HashMap<String, Vec<TypeRef>>,
61 term_valued_preds: HashSet<String>,
65 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 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 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
1126pub(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
1243fn 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}