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 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}