1use std::collections::{HashMap, HashSet};
14
15use xlog_core::{symbol, AggOp as CoreAggOp, RelId, Result, ScalarType, Schema, XlogError};
16use xlog_ir::{
17 CompareOp, CompiledRule, ConstValue, ExecutionPlan, Expr, JoinType, PlanBuilder, ProjectExpr,
18 RirMeta, RirNode, Scc, Stratum as IrStratum,
19};
20
21use crate::ast::{
22 AggOp, ArithExpr, Atom, BodyLiteral, CompOp, Comparison, IsExpr, LearnableRule, Program, Rule,
23 Term, TypeRef,
24};
25use crate::stratify::{build_dependency_graph, find_sccs_for_lowering, DepType};
26
27struct JoinPlan<'a> {
28 node: RirNode,
29 leaf_order: Vec<&'a Atom>,
30 leaf_order_idx: Vec<usize>,
31 var_pos: HashMap<String, usize>,
32 width: usize,
33 est_rows: f64,
34 total_cost: f64,
35}
36
37#[derive(Clone, Copy)]
38enum UserFunctionTypeEvidence {
39 RequireExpansion,
40 Defer,
41}
42
43#[derive(Clone, Copy)]
44enum ArithmeticTypeOperation {
45 Standard,
46 Modulo,
47 MinMax,
48 Power,
49}
50
51enum ArithmeticTypeTask<'a> {
52 Visit(&'a ArithExpr),
53 FinishBinary(ArithmeticTypeOperation),
54 FinishAbs,
55 FinishCast(ScalarType),
56 FinishFunctionArguments(usize),
57 FinishConditional,
58}
59
60#[derive(Clone, Copy)]
61enum ArithmeticExpressionOperation {
62 Add,
63 Sub,
64 Mul,
65 Div,
66 Mod,
67 Min,
68 Max,
69 Pow,
70}
71
72enum ArithmeticExpressionTask<'a> {
73 Visit(&'a ArithExpr),
74 FinishBinary(ArithmeticExpressionOperation),
75 FinishAbs,
76 FinishCast(ScalarType),
77 FinishConditional(CompOp),
78}
79
80fn resolve_pred_column_type(
81 predicate: &str,
82 index: usize,
83 typ: &TypeRef,
84 domains: &HashMap<String, ScalarType>,
85) -> Result<ScalarType> {
86 match typ {
87 TypeRef::Scalar(ty) => Ok(*ty),
88 TypeRef::Domain(name) => domains.get(name).copied().ok_or_else(|| {
89 XlogError::Compilation(format!(
90 "v0.8.5 unknown domain alias '{}' in predicate '{}' column {}",
91 name, predicate, index
92 ))
93 }),
94 TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
95 Ok(ScalarType::U64)
96 }
97 }
98}
99
100fn validate_lowerable_terms(program: &Program) -> Result<()> {
101 for rule in &program.rules {
102 validate_atom_terms(&rule.head, "rule head")?;
103 for lit in &rule.body {
104 match lit {
105 BodyLiteral::Positive(atom) => validate_atom_terms(atom, "positive body atom")?,
106 BodyLiteral::Negated(atom) => validate_atom_terms(atom, "negated body atom")?,
107 BodyLiteral::Epistemic(_) => {}
108 BodyLiteral::Comparison(cmp) => {
109 validate_term_lowerable(&cmp.left, "comparison left operand")?;
110 validate_term_lowerable(&cmp.right, "comparison right operand")?;
111 }
112 BodyLiteral::IsExpr(_) => {}
113 BodyLiteral::Univ(_) => {
114 return Err(XlogError::Compilation(
115 "v0.8.5 meta error: univ literal was not normalized before lowering"
116 .to_string(),
117 ));
118 }
119 }
120 }
121 }
122 for constraint in &program.constraints {
123 for lit in &constraint.body {
124 match lit {
125 BodyLiteral::Positive(atom) => validate_atom_terms(atom, "constraint body atom")?,
126 BodyLiteral::Negated(atom) => {
127 validate_atom_terms(atom, "constraint negated body atom")?
128 }
129 BodyLiteral::Epistemic(_) => {}
130 BodyLiteral::Comparison(cmp) => {
131 validate_term_lowerable(&cmp.left, "constraint comparison left operand")?;
132 validate_term_lowerable(&cmp.right, "constraint comparison right operand")?;
133 }
134 BodyLiteral::IsExpr(_) => {}
135 BodyLiteral::Univ(_) => {
136 return Err(XlogError::Compilation(
137 "v0.8.5 meta error: univ literal was not normalized before lowering"
138 .to_string(),
139 ));
140 }
141 }
142 }
143 }
144 for query in &program.queries {
145 validate_atom_terms(&query.atom, "query atom")?;
146 }
147 for pf in &program.prob_facts {
148 validate_atom_terms(&pf.atom, "probabilistic fact")?;
149 }
150 for ad in &program.annotated_disjunctions {
151 for choice in &ad.choices {
152 validate_atom_terms(&choice.atom, "annotated disjunction choice")?;
153 }
154 }
155 for evidence in &program.evidence {
156 validate_atom_terms(&evidence.atom, "evidence atom")?;
157 }
158 for query in &program.prob_queries {
159 validate_atom_terms(&query.atom, "probabilistic query")?;
160 }
161 for neural in &program.neural_predicates {
162 validate_atom_terms(&neural.predicate, "neural predicate")?;
163 }
164 for learnable in &program.learnable_rules {
165 validate_atom_terms(&learnable.head, "learnable rule head")?;
166 for lit in &learnable.body {
167 if let BodyLiteral::Positive(atom) = lit {
168 validate_atom_terms(atom, "learnable rule body")?;
169 }
170 }
171 }
172 Ok(())
173}
174
175fn validate_atom_terms(atom: &Atom, context: &str) -> Result<()> {
176 for term in &atom.terms {
177 validate_term_lowerable(term, context)?;
178 }
179 Ok(())
180}
181
182fn validate_term_lowerable(term: &Term, context: &str) -> Result<()> {
183 match term {
184 Term::List(_) => Err(term_not_lowerable_error(context, "list")),
185 Term::Cons { .. } => Err(term_not_lowerable_error(context, "cons")),
186 Term::Compound { .. } => Err(term_not_lowerable_error(context, "compound")),
187 Term::PredRef(_) => Err(term_not_lowerable_error(context, "predref")),
188 Term::Variable(_)
189 | Term::Anonymous
190 | Term::Integer(_)
191 | Term::Float(_)
192 | Term::String(_)
193 | Term::Symbol(_)
194 | Term::Aggregate(_) => Ok(()),
195 }
196}
197
198fn term_not_lowerable_error(context: &str, kind: &str) -> XlogError {
199 XlogError::Compilation(format!(
200 "term form '{}' in {} is parsed but not lowerable by this execution path",
201 kind, context
202 ))
203}
204
205fn term_kind_for_lowering_error(term: &Term) -> &'static str {
206 match term {
207 Term::List(_) => "list",
208 Term::Cons { .. } => "cons",
209 Term::Compound { .. } => "compound",
210 Term::PredRef(_) => "predref",
211 Term::Variable(_)
212 | Term::Anonymous
213 | Term::Integer(_)
214 | Term::Float(_)
215 | Term::String(_)
216 | Term::Symbol(_)
217 | Term::Aggregate(_) => "term",
218 }
219}
220
221pub struct Lowerer {
223 schemas: HashMap<String, Schema>,
225 strata: Vec<Vec<String>>,
227 est_cardinality: HashMap<String, u64>,
229 cardinality_hints: HashMap<String, u64>,
231 next_rel_id: u32,
233 rel_ids: HashMap<String, RelId>,
235 sccs: Vec<Scc>,
237 max_active_rules: usize,
239 scc_by_pred: std::collections::HashMap<String, u32>,
243}
244
245impl Default for Lowerer {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251impl Lowerer {
252 pub fn new() -> Self {
254 Self {
255 schemas: HashMap::new(),
256 strata: Vec::new(),
257 est_cardinality: HashMap::new(),
258 cardinality_hints: HashMap::new(),
259 next_rel_id: 0,
260 rel_ids: HashMap::new(),
261 sccs: Vec::new(),
262 max_active_rules: 32,
263 scc_by_pred: std::collections::HashMap::new(),
264 }
265 }
266
267 pub fn set_max_active_rules(&mut self, max: usize) {
269 self.max_active_rules = max;
270 }
271
272 pub(crate) fn set_strata(&mut self, strata: Vec<Vec<String>>) {
274 self.strata = strata;
275 }
276
277 pub(crate) fn set_cardinality_hints(&mut self, hints: HashMap<String, u64>) {
281 self.cardinality_hints = hints;
282 }
283
284 pub fn rel_ids(&self) -> &HashMap<String, RelId> {
286 &self.rel_ids
287 }
288
289 pub fn schemas(&self) -> &HashMap<String, Schema> {
291 &self.schemas
292 }
293
294 pub(crate) fn create_helper_relation(&mut self, schema: Schema) -> (String, RelId) {
295 let name = format!("__kclique_helper_{}", self.next_rel_id);
296 let rel_id = self.get_or_create_rel_id(&name);
297 self.schemas.insert(name.clone(), schema);
298 (name, rel_id)
299 }
300
301 fn get_or_create_rel_id(&mut self, name: &str) -> RelId {
303 if let Some(&id) = self.rel_ids.get(name) {
304 id
305 } else {
306 let id = RelId(self.next_rel_id);
307 self.next_rel_id += 1;
308 self.rel_ids.insert(name.to_string(), id);
309 id
310 }
311 }
312
313 fn validate_rule_types(&self, program: &Program) -> Result<()> {
319 let declared_predicates = program
320 .predicates
321 .iter()
322 .map(|declaration| declaration.name.as_str())
323 .collect::<HashSet<_>>();
324 for rule in &program.rules {
325 let var_types = self.infer_rule_variable_types(rule, |atom, index| {
326 self.schemas
327 .get(&atom.predicate)
328 .and_then(|schema| schema.column_type(index))
329 })?;
330 let Some(head_schema) = self.schemas.get(&rule.head.predicate) else {
331 continue;
332 };
333 for (j, term) in rule.head.terms.iter().enumerate() {
334 let Some((_, head_ty)) = head_schema.columns.get(j) else {
335 continue;
336 };
337 match term {
338 Term::Variable(name) => {
339 let Some((body_ty, source)) = var_types.get(name) else {
340 continue;
341 };
342 if body_ty != head_ty {
343 return Err(XlogError::Compilation(format!(
344 "Type mismatch in rule for '{}': variable {} is {:?} \
345 (from {}) but {} declares {:?} at position {}",
346 rule.head.predicate,
347 name,
348 body_ty,
349 source,
350 rule.head.predicate,
351 head_ty,
352 j
353 )));
354 }
355 }
356 Term::Anonymous => {}
357 Term::Aggregate(aggregate) => {
358 let aggregate_ty =
359 Self::infer_aggregate_result_type(rule, aggregate, &var_types)?
360 .ok_or_else(|| {
361 XlogError::UnsafeVariable(aggregate.variable.clone())
362 })?;
363 if aggregate_ty != *head_ty {
364 return Err(XlogError::Compilation(format!(
365 "Type mismatch in rule for '{}': aggregate {:?} over {} produces \
366 {:?}, but the predicate schema requires {:?} at position {}",
367 rule.head.predicate,
368 aggregate.op,
369 aggregate.variable,
370 aggregate_ty,
371 head_ty,
372 j
373 )));
374 }
375 }
376 Term::Integer(_) | Term::Float(_) | Term::String(_) | Term::Symbol(_) => {
377 if declared_predicates.contains(rule.head.predicate.as_str()) {
378 term_to_typed_const_value(term, *head_ty).map_err(|error| {
379 XlogError::Compilation(format!(
380 "Type mismatch in rule for '{}': head term at position {} is \
381 not compatible with {:?}: {}",
382 rule.head.predicate, j, head_ty, error
383 ))
384 })?;
385 } else if term.inferred_scalar_type() != *head_ty {
386 return Err(XlogError::Compilation(format!(
387 "Type mismatch in rule for '{}': undeclared head term at position \
388 {} has inferred type {:?}, but another clause requires {:?}",
389 rule.head.predicate,
390 j,
391 term.inferred_scalar_type(),
392 head_ty
393 )));
394 }
395 }
396 _ if term.inferred_scalar_type() != *head_ty => {
397 return Err(XlogError::Compilation(format!(
398 "Type mismatch in rule for '{}': head term at position {} has type \
399 {:?}, but the predicate schema requires {:?}",
400 rule.head.predicate,
401 j,
402 term.inferred_scalar_type(),
403 head_ty
404 )));
405 }
406 _ => {}
407 }
408 }
409 }
410 Ok(())
411 }
412
413 fn infer_aggregate_result_type(
414 rule: &Rule,
415 aggregate: &crate::ast::AggExpr,
416 variable_types: &HashMap<String, (ScalarType, String)>,
417 ) -> Result<Option<ScalarType>> {
418 let Some((input_type, source)) = variable_types.get(&aggregate.variable) else {
419 return Ok(aggregate.input_independent_result_type());
420 };
421 aggregate
422 .result_type_for_input(*input_type)
423 .map(Some)
424 .ok_or_else(|| {
425 let required = match aggregate.op {
426 AggOp::Count => "any scalar input",
427 AggOp::Sum | AggOp::Min | AggOp::Max => "U32 or U64 input",
428 AggOp::LogSumExp => "F64 input",
429 };
430 XlogError::Compilation(format!(
431 "Unsupported aggregate input in rule for '{}': {:?}({}) receives {:?} from \
432 {}, but the execution provider requires {}",
433 rule.head.predicate,
434 aggregate.op,
435 aggregate.variable,
436 input_type,
437 source,
438 required
439 ))
440 })
441 }
442
443 fn record_rule_variable_type(
444 rule: &Rule,
445 variable_types: &mut HashMap<String, (ScalarType, String)>,
446 variable: &str,
447 typ: ScalarType,
448 source: String,
449 ) -> Result<()> {
450 match variable_types.get(variable) {
451 Some((existing, existing_source)) if *existing != typ => {
452 Err(XlogError::Compilation(format!(
453 "Type mismatch in rule for '{}': variable {} is {:?} (from {}) but {:?} \
454 is required by {}",
455 rule.head.predicate, variable, existing, existing_source, typ, source
456 )))
457 }
458 Some(_) => Ok(()),
459 None => {
460 variable_types.insert(variable.to_string(), (typ, source));
461 Ok(())
462 }
463 }
464 }
465
466 fn infer_rule_variable_types_with_user_functions<F>(
474 &self,
475 rule: &Rule,
476 mut column_type: F,
477 user_functions: UserFunctionTypeEvidence,
478 ) -> Result<HashMap<String, (ScalarType, String)>>
479 where
480 F: FnMut(&Atom, usize) -> Option<ScalarType>,
481 {
482 let mut variable_types = HashMap::new();
483
484 for literal in &rule.body {
485 let atom = match literal {
486 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => atom,
487 BodyLiteral::Epistemic(_)
488 | BodyLiteral::Comparison(_)
489 | BodyLiteral::IsExpr(_)
490 | BodyLiteral::Univ(_) => continue,
491 };
492 for (index, term) in atom.terms.iter().enumerate() {
493 let Term::Variable(variable) = term else {
494 continue;
495 };
496 let Some(typ) = column_type(atom, index) else {
497 continue;
498 };
499 Self::record_rule_variable_type(
500 rule,
501 &mut variable_types,
502 variable,
503 typ,
504 format!("{} position {}", atom.predicate, index),
505 )?;
506 }
507 }
508
509 for literal in &rule.body {
510 let BodyLiteral::IsExpr(is_expr) = literal else {
511 continue;
512 };
513 let result_type = Self::infer_arith_type_from_known_variables(
514 &is_expr.expr,
515 &|variable| variable_types.get(variable).map(|(typ, _)| *typ),
516 user_functions,
517 )?;
518 if let Some(result_type) = result_type {
519 Self::record_rule_variable_type(
520 rule,
521 &mut variable_types,
522 &is_expr.target,
523 result_type,
524 "an arithmetic binding".to_string(),
525 )?;
526 }
527 }
528
529 Ok(variable_types)
530 }
531
532 pub(crate) fn infer_rule_variable_types<F>(
533 &self,
534 rule: &Rule,
535 column_type: F,
536 ) -> Result<HashMap<String, (ScalarType, String)>>
537 where
538 F: FnMut(&Atom, usize) -> Option<ScalarType>,
539 {
540 self.infer_rule_variable_types_with_user_functions(
541 rule,
542 column_type,
543 UserFunctionTypeEvidence::RequireExpansion,
544 )
545 }
546
547 fn infer_rule_head_column_types_with_user_functions<F>(
548 &self,
549 rule: &Rule,
550 column_type: F,
551 user_functions: UserFunctionTypeEvidence,
552 ) -> Result<Vec<Option<ScalarType>>>
553 where
554 F: FnMut(&Atom, usize) -> Option<ScalarType>,
555 {
556 let variable_types =
557 self.infer_rule_variable_types_with_user_functions(rule, column_type, user_functions)?;
558 rule.head
559 .terms
560 .iter()
561 .map(|term| match term {
562 Term::Variable(name) => Ok(variable_types.get(name).map(|(typ, _)| *typ)),
563 Term::Aggregate(aggregate) => {
564 Self::infer_aggregate_result_type(rule, aggregate, &variable_types)
565 }
566 Term::Anonymous => Ok(None),
567 _ => Ok(Some(term.inferred_scalar_type())),
568 })
569 .collect()
570 }
571
572 pub(crate) fn infer_rule_head_column_types<F>(
578 &self,
579 rule: &Rule,
580 column_type: F,
581 ) -> Result<Vec<Option<ScalarType>>>
582 where
583 F: FnMut(&Atom, usize) -> Option<ScalarType>,
584 {
585 self.infer_rule_head_column_types_with_user_functions(
586 rule,
587 column_type,
588 UserFunctionTypeEvidence::RequireExpansion,
589 )
590 }
591
592 pub(crate) fn infer_rule_head_column_types_before_function_expansion<F>(
596 &self,
597 rule: &Rule,
598 column_type: F,
599 ) -> Result<Vec<Option<ScalarType>>>
600 where
601 F: FnMut(&Atom, usize) -> Option<ScalarType>,
602 {
603 self.infer_rule_head_column_types_with_user_functions(
604 rule,
605 column_type,
606 UserFunctionTypeEvidence::Defer,
607 )
608 }
609
610 pub(crate) fn infer_schemas(&mut self, program: &Program) -> Result<()> {
612 let domains: HashMap<String, ScalarType> = program
613 .domains
614 .iter()
615 .map(|domain| (domain.name.clone(), domain.typ))
616 .collect();
617
618 for pred_decl in &program.predicates {
620 let declared_columns = pred_decl.schema_columns();
621 let columns: Vec<(String, ScalarType)> = declared_columns
622 .iter()
623 .enumerate()
624 .map(|(i, col)| {
625 let name = col.name.clone().unwrap_or_else(|| format!("c{}", i));
626 resolve_pred_column_type(&pred_decl.name, i, &col.typ, &domains)
627 .map(|ty| (name, ty))
628 })
629 .collect::<Result<Vec<_>>>()?;
630 self.schemas
631 .insert(pred_decl.name.clone(), Schema::new(columns));
632 }
633
634 for rule in program.facts() {
636 let pred = &rule.head.predicate;
637 if !self.schemas.contains_key(pred) {
638 let columns: Vec<(String, ScalarType)> = rule
639 .head
640 .terms
641 .iter()
642 .enumerate()
643 .map(|(i, term)| {
644 let ty = term.inferred_scalar_type();
645 (format!("c{}", i), ty)
646 })
647 .collect();
648 self.schemas.insert(pred.clone(), Schema::new(columns));
649 }
650 }
651
652 for pf in &program.prob_facts {
657 let pred = &pf.atom.predicate;
658 if self.schemas.contains_key(pred) {
659 continue;
660 }
661 let columns: Vec<(String, ScalarType)> = pf
662 .atom
663 .terms
664 .iter()
665 .enumerate()
666 .map(|(i, term)| (format!("c{}", i), term.inferred_scalar_type()))
667 .collect();
668 self.schemas.insert(pred.clone(), Schema::new(columns));
669 }
670
671 for ad in &program.annotated_disjunctions {
672 for choice in &ad.choices {
673 let pred = &choice.atom.predicate;
674 if self.schemas.contains_key(pred) {
675 continue;
676 }
677 let columns: Vec<(String, ScalarType)> = choice
678 .atom
679 .terms
680 .iter()
681 .enumerate()
682 .map(|(i, term)| (format!("c{}", i), term.inferred_scalar_type()))
683 .collect();
684 self.schemas.insert(pred.clone(), Schema::new(columns));
685 }
686 }
687
688 let derived_predicates = program
693 .rules
694 .iter()
695 .filter(|rule| !rule.body.is_empty())
696 .map(|rule| rule.head.predicate.as_str())
697 .collect::<HashSet<_>>();
698 for rule in &program.rules {
699 for lit in &rule.body {
700 let atom = match lit {
701 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => atom,
702 BodyLiteral::Epistemic(_)
703 | BodyLiteral::Comparison(_)
704 | BodyLiteral::IsExpr(_)
705 | BodyLiteral::Univ(_) => continue,
706 };
707 let pred = &atom.predicate;
708 if self.schemas.contains_key(pred) || derived_predicates.contains(pred.as_str()) {
709 continue;
710 }
711 let columns: Vec<(String, ScalarType)> = atom
712 .terms
713 .iter()
714 .enumerate()
715 .map(|(i, term)| (format!("c{}", i), term.inferred_scalar_type()))
716 .collect();
717 let schema = Schema::new(columns)
718 .with_sort_labels(sort_labels_from_terms(&atom.terms))
719 .expect("body sort labels match inferred schema arity");
720 self.schemas.insert(pred.clone(), schema);
721 }
722 }
723
724 let mut inferred_rule_columns: HashMap<String, Vec<Option<ScalarType>>> = HashMap::new();
728 for rule in &program.rules {
729 if self.schemas.contains_key(&rule.head.predicate) {
730 continue;
731 }
732 inferred_rule_columns
733 .entry(rule.head.predicate.clone())
734 .or_insert_with(|| vec![None; rule.head.terms.len()]);
735 }
736
737 loop {
738 let mut changed = false;
739 for rule in &program.rules {
740 let pred = &rule.head.predicate;
741 if self.schemas.contains_key(pred) {
742 continue;
743 }
744
745 let Some(current_columns) = inferred_rule_columns.get(pred) else {
746 continue;
747 };
748 if current_columns.len() != rule.head.terms.len() {
749 continue;
750 }
751
752 let resolved_columns = self.infer_rule_head_column_types(rule, |atom, index| {
753 self.schemas
754 .get(&atom.predicate)
755 .and_then(|schema| schema.column_type(index))
756 .or_else(|| {
757 inferred_rule_columns
758 .get(&atom.predicate)
759 .and_then(|columns| columns.get(index))
760 .copied()
761 .flatten()
762 })
763 })?;
764
765 let columns = inferred_rule_columns
766 .get_mut(pred)
767 .expect("rule-head inference entry exists");
768 for (index, (column, resolved)) in
769 columns.iter_mut().zip(resolved_columns).enumerate()
770 {
771 match (*column, resolved) {
772 (None, Some(resolved)) => {
773 *column = Some(resolved);
774 changed = true;
775 }
776 (Some(existing), Some(resolved)) if existing != resolved => {
777 return Err(XlogError::Compilation(format!(
778 "Conflicting inferred schema for predicate '{}': column {} is \
779 {:?} in one rule and {:?} in another",
780 pred,
781 index + 1,
782 existing,
783 resolved
784 )));
785 }
786 (None, None) | (Some(_), None) | (Some(_), Some(_)) => {}
787 }
788 }
789 }
790 if !changed {
791 break;
792 }
793 }
794 for rule in &program.rules {
795 let pred = &rule.head.predicate;
796 if !self.schemas.contains_key(pred) {
797 let inferred_columns = inferred_rule_columns
798 .get(pred)
799 .expect("undeclared rule head has an inference entry");
800 let columns = rule
801 .head
802 .terms
803 .iter()
804 .zip(inferred_columns)
805 .enumerate()
806 .map(|(index, (term, inferred))| {
807 (
808 format!("c{index}"),
809 inferred.unwrap_or_else(|| term.inferred_scalar_type()),
810 )
811 })
812 .collect();
813 let schema = Schema::new(columns)
814 .with_sort_labels(sort_labels_from_terms(&rule.head.terms))
815 .expect("rule head sort labels match inferred schema arity");
816 self.schemas.insert(pred.clone(), schema);
817 }
818 }
819
820 Ok(())
821 }
822
823 pub fn infer_and_validate_schemas(&mut self, program: &Program) -> Result<()> {
830 self.infer_schemas(program)?;
831 self.validate_rule_types(program)
832 }
833
834 fn infer_cardinalities(&mut self, program: &Program) {
835 self.est_cardinality.clear();
836
837 let mut fact_counts: HashMap<String, u64> = HashMap::new();
838 for fact in program.facts() {
839 *fact_counts.entry(fact.head.predicate.clone()).or_insert(0) += 1;
840 }
841
842 for pred in self.schemas.keys() {
843 let est = self
844 .cardinality_hints
845 .get(pred)
846 .copied()
847 .or_else(|| fact_counts.get(pred).copied())
848 .unwrap_or(1000)
849 .max(1);
850 self.est_cardinality.insert(pred.clone(), est);
851 }
852 }
853
854 fn build_sccs(&mut self, program: &Program) {
856 let graph = build_dependency_graph(program);
857 let scc_groups = find_sccs_for_lowering(&graph);
858
859 self.sccs.clear();
860 self.scc_by_pred.clear();
861 for (id, predicates) in scc_groups.iter().enumerate() {
862 let is_recursive = if predicates.len() > 1 {
865 true
866 } else {
867 let pred = &predicates[0];
868 graph
869 .outgoing(pred)
870 .iter()
871 .any(|e| e.to == *pred && e.dep_type == DepType::Positive)
872 };
873
874 for p in predicates {
875 self.scc_by_pred.insert(p.clone(), id as u32);
876 }
877 self.sccs.push(Scc {
878 id: id as u32,
879 predicates: predicates.clone(),
880 is_recursive,
881 });
882 }
883 }
884
885 fn prepare_program_for_lowering(&mut self, program: &Program) -> Result<()> {
886 validate_lowerable_terms(program)?;
887 self.infer_and_validate_schemas(program)?;
888 self.infer_cardinalities(program);
889
890 for pred_decl in &program.predicates {
895 self.get_or_create_rel_id(&pred_decl.name);
896 }
897 for fact in program.facts() {
901 self.get_or_create_rel_id(&fact.head.predicate);
902 }
903
904 Ok(())
905 }
906
907 pub(crate) fn validate_program_without_plan(&mut self, program: &Program) -> Result<()> {
915 self.prepare_program_for_lowering(program)?;
916
917 for rule in program.proper_rules() {
918 self.lower_rule(rule)?;
919 }
920
921 for learnable in &program.learnable_rules {
925 self.get_or_create_rel_id(&learnable.head.predicate);
926 for lit in &learnable.body {
927 if let BodyLiteral::Positive(atom) = lit {
928 self.get_or_create_rel_id(&atom.predicate);
929 }
930 }
931 }
932 for learnable in &program.learnable_rules {
933 self.lower_learnable_rule(learnable)?;
934 }
935
936 Ok(())
937 }
938
939 pub fn lower_program(&mut self, program: &Program) -> Result<ExecutionPlan> {
941 self.prepare_program_for_lowering(program)?;
942
943 self.build_sccs(program);
945
946 let mut builder = PlanBuilder::new();
948
949 for scc in &self.sccs {
951 builder.add_scc(scc.clone());
952 }
953
954 for (id, preds) in self.strata.iter().enumerate() {
956 let pred_set: std::collections::HashSet<&str> =
958 preds.iter().map(|s| s.as_str()).collect();
959 let scc_ids: Vec<u32> = self
960 .sccs
961 .iter()
962 .filter(|scc| scc.predicates.iter().any(|p| pred_set.contains(p.as_str())))
963 .map(|scc| scc.id)
964 .collect();
965
966 if !scc_ids.is_empty() {
967 builder.add_stratum(IrStratum {
968 id: id as u32,
969 sccs: scc_ids,
970 });
971 }
972 }
973
974 let mut rules_by_pred: HashMap<String, Vec<&Rule>> = HashMap::new();
976 for rule in program.proper_rules() {
977 rules_by_pred
978 .entry(rule.head.predicate.clone())
979 .or_default()
980 .push(rule);
981 }
982
983 let mut preds_sorted: Vec<&String> = rules_by_pred.keys().collect();
987 preds_sorted.sort();
988 for pred in preds_sorted {
989 let rules = &rules_by_pred[pred];
990 let scc_id = self.find_scc_for_predicate(pred);
991
992 for rule in rules {
993 let body = self.lower_rule(rule)?;
994 let meta = self.create_meta_for_predicate(pred);
995
996 builder.add_rule(
997 scc_id,
998 CompiledRule {
999 head: pred.clone(),
1000 body,
1001 meta,
1002 },
1003 );
1004 }
1005 }
1006
1007 for learnable in &program.learnable_rules {
1011 self.get_or_create_rel_id(&learnable.head.predicate);
1012 for lit in &learnable.body {
1013 if let BodyLiteral::Positive(atom) = lit {
1014 self.get_or_create_rel_id(&atom.predicate);
1015 }
1016 }
1017 }
1018 for learnable in &program.learnable_rules {
1019 let head_pred = &learnable.head.predicate;
1020 let scc_id = self.find_scc_for_predicate(head_pred);
1021 let body = self.lower_learnable_rule(learnable)?;
1022 let meta = self.create_meta_for_predicate(head_pred);
1023 builder.add_rule(
1024 scc_id,
1025 CompiledRule {
1026 head: head_pred.clone(),
1027 body,
1028 meta,
1029 },
1030 );
1031 }
1032
1033 let mut plan = builder.build();
1034 for rule in program.proper_rules() {
1040 if let Some(&id) = self.rel_ids.get(&rule.head.predicate) {
1041 plan.rel_arities.insert(id, rule.head.terms.len());
1042 }
1043 for lit in &rule.body {
1044 let atom = match lit {
1045 BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => a,
1046 _ => continue,
1047 };
1048 if let Some(&id) = self.rel_ids.get(&atom.predicate) {
1049 plan.rel_arities.insert(id, atom.terms.len());
1050 }
1051 }
1052 }
1053 for fact in program.facts() {
1054 if let Some(&id) = self.rel_ids.get(&fact.head.predicate) {
1055 plan.rel_arities.insert(id, fact.head.terms.len());
1056 }
1057 }
1058 Ok(plan)
1059 }
1060
1061 fn find_scc_for_predicate(&self, pred: &str) -> u32 {
1063 self.scc_by_pred.get(pred).copied().unwrap_or(0)
1064 }
1065
1066 fn create_meta_for_predicate(&self, pred: &str) -> RirMeta {
1068 let schema = self
1069 .schemas
1070 .get(pred)
1071 .cloned()
1072 .unwrap_or_else(|| Schema::new(vec![]));
1073 RirMeta::with_schema(schema)
1074 }
1075
1076 fn lower_learnable_rule(&mut self, rule: &LearnableRule) -> Result<RirNode> {
1081 if rule.body.len() != 2 {
1083 return Err(XlogError::Compilation(format!(
1084 "learnable rule '{}' requires exactly 2 body literals, got {}",
1085 rule.mask_name,
1086 rule.body.len()
1087 )));
1088 }
1089 for (idx, lit) in rule.body.iter().enumerate() {
1090 match lit {
1091 BodyLiteral::Positive(_) => {}
1092 _ => {
1093 return Err(XlogError::Compilation(format!(
1094 "learnable rule '{}' body[{}]: only positive atoms allowed",
1095 rule.mask_name, idx
1096 )));
1097 }
1098 }
1099 }
1100
1101 let mut rel_index: Vec<(RelId, String)> = self
1103 .rel_ids()
1104 .iter()
1105 .map(|(name, id)| (*id, name.clone()))
1106 .collect();
1107 rel_index.sort_by_key(|(id, _)| id.0);
1108 let schema_size = rel_index.len();
1109
1110 let (left_keys, right_keys) =
1111 self.extract_template_join_keys(&rule.body[0], &rule.body[1])?;
1112
1113 let head_rel_name = rule.head.predicate.clone();
1114 let head_rel_id = self.get_or_create_rel_id(&head_rel_name);
1116
1117 let left_atom = rule.body[0].atom().unwrap();
1120 let right_atom = rule.body[1].atom().unwrap();
1121 let left_arity = left_atom.terms.len();
1122
1123 let mut var_to_col: HashMap<String, usize> = HashMap::new();
1125 for (i, term) in left_atom.terms.iter().enumerate() {
1126 if let Some(name) = term.variable_name() {
1127 var_to_col.entry(name.to_string()).or_insert(i);
1128 }
1129 }
1130 for (i, term) in right_atom.terms.iter().enumerate() {
1131 if let Some(name) = term.variable_name() {
1132 var_to_col.entry(name.to_string()).or_insert(left_arity + i);
1133 }
1134 }
1135
1136 let mut head_projection: Vec<usize> = Vec::new();
1137 for term in &rule.head.terms {
1138 if let Some(name) = term.variable_name() {
1139 let col = var_to_col.get(name).ok_or_else(|| {
1140 XlogError::Compilation(format!(
1141 "Learnable rule head variable '{}' not found in body atoms \
1142 ({}, {}). All head variables must appear in the body.",
1143 name, left_atom.predicate, right_atom.predicate,
1144 ))
1145 })?;
1146 head_projection.push(*col);
1147 } else {
1148 return Err(XlogError::Compilation(format!(
1149 "Learnable rule head must contain only variables, \
1150 found constant {:?} in head of '{}'",
1151 term, head_rel_name,
1152 )));
1153 }
1154 }
1155
1156 if !self.schemas.contains_key(&head_rel_name) {
1159 let columns: Vec<(String, ScalarType)> = head_projection
1160 .iter()
1161 .enumerate()
1162 .map(|(i, &col)| {
1163 let ty = if col < left_arity {
1165 self.schemas
1166 .get(&left_atom.predicate)
1167 .and_then(|s| s.column_type(col))
1168 .unwrap_or(ScalarType::U32)
1169 } else {
1170 self.schemas
1171 .get(&right_atom.predicate)
1172 .and_then(|s| s.column_type(col - left_arity))
1173 .unwrap_or(ScalarType::U32)
1174 };
1175 (format!("c{}", i), ty)
1176 })
1177 .collect();
1178 self.schemas
1179 .insert(head_rel_name.clone(), Schema::new(columns));
1180 }
1181
1182 Ok(RirNode::TensorMaskedJoin {
1183 mask_name: rule.mask_name.clone(),
1184 schema_size,
1185 left_keys,
1186 right_keys,
1187 rel_index,
1188 head_rel_name,
1189 head_rel_id,
1190 max_active_rules: self.max_active_rules,
1191 head_projection,
1192 })
1193 }
1194
1195 fn extract_template_join_keys(
1198 &self,
1199 left: &BodyLiteral,
1200 right: &BodyLiteral,
1201 ) -> Result<(Vec<usize>, Vec<usize>)> {
1202 let left_atom = left
1203 .atom()
1204 .ok_or_else(|| XlogError::Compilation("Learnable body[0] is not an atom".into()))?;
1205 let right_atom = right
1206 .atom()
1207 .ok_or_else(|| XlogError::Compilation("Learnable body[1] is not an atom".into()))?;
1208
1209 let mut left_keys = Vec::new();
1210 let mut right_keys = Vec::new();
1211
1212 for (li, lt) in left_atom.terms.iter().enumerate() {
1213 if let Some(lname) = lt.variable_name() {
1214 for (ri, rt) in right_atom.terms.iter().enumerate() {
1215 if let Some(rname) = rt.variable_name() {
1216 if lname == rname {
1217 left_keys.push(li);
1218 right_keys.push(ri);
1219 }
1220 }
1221 }
1222 }
1223 }
1224
1225 Ok((left_keys, right_keys))
1226 }
1227
1228 fn lower_rule(&mut self, rule: &Rule) -> Result<RirNode> {
1230 if let Some(lit) = rule.body.iter().find_map(|lit| match lit {
1231 BodyLiteral::Epistemic(lit) => Some(lit),
1232 _ => None,
1233 }) {
1234 return Err(XlogError::UnsupportedEpistemicConstruct {
1235 construct: "RIR lowering boundary".to_string(),
1236 context: format!("{:?} {}({})", lit.op, lit.atom.predicate, lit.atom.arity()),
1237 });
1238 }
1239
1240 let (positive_atoms, negated_atoms, comparisons, is_exprs) =
1242 Self::split_body_literals(&rule.body);
1243
1244 for lit in &rule.body {
1247 match lit {
1248 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
1249 self.get_or_create_rel_id(&atom.predicate);
1250 }
1251 BodyLiteral::Epistemic(_)
1252 | BodyLiteral::Comparison(_)
1253 | BodyLiteral::IsExpr(_)
1254 | BodyLiteral::Univ(_) => {}
1255 }
1256 }
1257
1258 let (positive_root, leaf_order) = if positive_atoms.is_empty() {
1264 (RirNode::Unit, Vec::new())
1265 } else {
1266 self.plan_positive_atoms(&positive_atoms)?
1267 };
1268
1269 let mut var_env = VariableEnv::new();
1272 let mut current_col = 0;
1273 for atom in &leaf_order {
1274 let schema = self.schemas.get(&atom.predicate);
1275 for (i, term) in atom.terms.iter().enumerate() {
1276 if let Term::Variable(name) = term {
1277 if name == "_" {
1278 continue;
1279 }
1280 var_env.add_occurrence(name, atom.predicate.clone(), i, current_col + i);
1281 if !var_env.types.contains_key(name) {
1283 let typ = schema
1284 .and_then(|s| s.column_type(i))
1285 .unwrap_or(ScalarType::I64); var_env.types.insert(name.to_string(), typ);
1287 }
1288 }
1289 }
1290 current_col += atom.terms.len();
1291 }
1292 var_env.total_cols = current_col;
1293
1294 let body_node = self.lower_body_parts(
1296 positive_root,
1297 &negated_atoms,
1298 &comparisons,
1299 &is_exprs,
1300 &mut var_env,
1301 )?;
1302
1303 if rule.has_aggregation() {
1304 return self.lower_aggregate_rule(&rule.head, body_node, &var_env);
1305 }
1306
1307 let projection_exprs = self.compute_head_projection(&rule.head, &var_env)?;
1309
1310 if Self::is_identity_projection(&projection_exprs, var_env.column_count()) {
1311 Ok(body_node)
1312 } else {
1313 Ok(RirNode::Project {
1314 input: Box::new(body_node),
1315 columns: projection_exprs,
1316 })
1317 }
1318 }
1319
1320 fn split_body_literals(
1321 body: &[BodyLiteral],
1322 ) -> (Vec<&Atom>, Vec<&Atom>, Vec<&Comparison>, Vec<&IsExpr>) {
1323 let mut positive_atoms: Vec<&Atom> = Vec::new();
1324 let mut negated_atoms: Vec<&Atom> = Vec::new();
1325 let mut comparisons: Vec<&Comparison> = Vec::new();
1326 let mut is_exprs: Vec<&IsExpr> = Vec::new();
1327
1328 for lit in body {
1329 match lit {
1330 BodyLiteral::Positive(atom) => positive_atoms.push(atom),
1331 BodyLiteral::Negated(atom) => negated_atoms.push(atom),
1332 BodyLiteral::Epistemic(_) => {}
1333 BodyLiteral::Comparison(cmp) => comparisons.push(cmp),
1334 BodyLiteral::IsExpr(is_expr) => is_exprs.push(is_expr),
1335 BodyLiteral::Univ(_) => {}
1336 }
1337 }
1338
1339 (positive_atoms, negated_atoms, comparisons, is_exprs)
1340 }
1341
1342 fn atom_vars(atom: &Atom) -> std::collections::HashSet<String> {
1343 atom.terms
1344 .iter()
1345 .flat_map(|t| t.variables().into_iter())
1346 .filter(|name| *name != "_")
1347 .map(ToOwned::to_owned)
1348 .collect()
1349 }
1350
1351 fn estimate_atom_rows(&self, atom: &Atom) -> f64 {
1352 let base = self
1353 .est_cardinality
1354 .get(&atom.predicate)
1355 .copied()
1356 .unwrap_or(1000)
1357 .max(1) as f64;
1358
1359 let const_count = atom
1360 .terms
1361 .iter()
1362 .filter(|t| term_to_const_value(t).is_some())
1363 .count();
1364
1365 let selectivity = 0.1_f64.powi(const_count as i32);
1367 (base * selectivity).max(1.0)
1368 }
1369
1370 fn build_cartesian_join(
1371 &self,
1372 left: RirNode,
1373 right: RirNode,
1374 left_width: usize,
1375 right_width: usize,
1376 ) -> RirNode {
1377 let left_const_col =
1380 ProjectExpr::Computed(Expr::Const(ConstValue::U32(0)), ScalarType::U32);
1381 let right_const_col =
1382 ProjectExpr::Computed(Expr::Const(ConstValue::U32(0)), ScalarType::U32);
1383
1384 let mut left_cols: Vec<ProjectExpr> = (0..left_width).map(ProjectExpr::Column).collect();
1385 left_cols.push(left_const_col);
1386 let left_aug = RirNode::Project {
1387 input: Box::new(left),
1388 columns: left_cols,
1389 };
1390
1391 let mut right_cols: Vec<ProjectExpr> = (0..right_width).map(ProjectExpr::Column).collect();
1392 right_cols.push(right_const_col);
1393 let right_aug = RirNode::Project {
1394 input: Box::new(right),
1395 columns: right_cols,
1396 };
1397
1398 let joined = RirNode::Join {
1399 left: Box::new(left_aug),
1400 right: Box::new(right_aug),
1401 left_keys: vec![left_width],
1402 right_keys: vec![right_width],
1403 join_type: JoinType::Inner,
1404 };
1405
1406 let mut keep: Vec<ProjectExpr> = Vec::with_capacity(left_width + right_width);
1407 keep.extend((0..left_width).map(ProjectExpr::Column));
1408 let right_start = left_width + 1;
1409 keep.extend((right_start..right_start + right_width).map(ProjectExpr::Column));
1410
1411 RirNode::Project {
1412 input: Box::new(joined),
1413 columns: keep,
1414 }
1415 }
1416
1417 fn make_leaf_plan<'a>(&mut self, atom: &'a Atom, orig_idx: usize) -> Result<JoinPlan<'a>> {
1418 let rel_id = self.get_or_create_rel_id(&atom.predicate);
1419 let scan = RirNode::Scan { rel: rel_id };
1420 let node = self.apply_constant_filters(scan, atom, 0)?;
1421
1422 let mut var_pos: HashMap<String, usize> = HashMap::new();
1423 for (i, term) in atom.terms.iter().enumerate() {
1424 if let Term::Variable(name) = term {
1425 if name != "_" {
1426 var_pos.entry(name.clone()).or_insert(i);
1427 }
1428 }
1429 }
1430
1431 let est_rows = self.estimate_atom_rows(atom);
1432 Ok(JoinPlan {
1433 node,
1434 leaf_order: vec![atom],
1435 leaf_order_idx: vec![orig_idx],
1436 var_pos,
1437 width: atom.terms.len(),
1438 est_rows,
1439 total_cost: est_rows,
1440 })
1441 }
1442
1443 fn join_plans<'a>(&self, left: &JoinPlan<'a>, right: &JoinPlan<'a>) -> JoinPlan<'a> {
1444 let shared_vars: Vec<&String> = left
1445 .var_pos
1446 .keys()
1447 .filter(|v| right.var_pos.contains_key(*v))
1448 .collect();
1449
1450 let node = if shared_vars.is_empty() {
1451 self.build_cartesian_join(
1452 left.node.clone(),
1453 right.node.clone(),
1454 left.width,
1455 right.width,
1456 )
1457 } else {
1458 let mut key_pairs: Vec<(usize, usize)> = shared_vars
1459 .iter()
1460 .filter_map(|v| {
1461 Some((
1462 left.var_pos.get(*v).copied()?,
1463 right.var_pos.get(*v).copied()?,
1464 ))
1465 })
1466 .collect();
1467 key_pairs.sort_unstable();
1468
1469 let (left_keys, right_keys): (Vec<usize>, Vec<usize>) = key_pairs.into_iter().unzip();
1470
1471 RirNode::Join {
1472 left: Box::new(left.node.clone()),
1473 right: Box::new(right.node.clone()),
1474 left_keys,
1475 right_keys,
1476 join_type: JoinType::Inner,
1477 }
1478 };
1479
1480 let mut leaf_order = left.leaf_order.clone();
1481 leaf_order.extend(right.leaf_order.iter().copied());
1482
1483 let mut leaf_order_idx = left.leaf_order_idx.clone();
1484 leaf_order_idx.extend_from_slice(&right.leaf_order_idx);
1485
1486 let mut var_pos = left.var_pos.clone();
1487 for (var, pos) in &right.var_pos {
1488 var_pos.entry(var.clone()).or_insert(left.width + *pos);
1489 }
1490
1491 let shared = shared_vars.len();
1492 let mut selectivity = if shared == 0 {
1493 1.0
1494 } else {
1495 0.1_f64.powi(shared as i32)
1496 };
1497 if shared == 0 {
1498 selectivity *= 1.0e6;
1500 }
1501
1502 let output_rows = (left.est_rows * right.est_rows * selectivity).max(1.0);
1503
1504 let build_cost = right.est_rows;
1506 let probe_cost = left.est_rows * 0.5;
1507 let total_cost = left.total_cost + right.total_cost + build_cost + probe_cost + output_rows;
1508
1509 JoinPlan {
1510 node,
1511 leaf_order,
1512 leaf_order_idx,
1513 var_pos,
1514 width: left.width + right.width,
1515 est_rows: output_rows,
1516 total_cost,
1517 }
1518 }
1519
1520 fn plan_positive_atoms_bushy<'a>(
1521 &mut self,
1522 atoms: &[&'a Atom],
1523 ) -> Result<(RirNode, Vec<&'a Atom>)> {
1524 let n = atoms.len();
1525 if n == 0 {
1526 return Err(XlogError::Compilation("Empty rule body".to_string()));
1527 }
1528 if n == 1 {
1529 let plan = self.make_leaf_plan(atoms[0], 0)?;
1530 return Ok((plan.node, plan.leaf_order));
1531 }
1532
1533 let size = 1usize << n;
1534 let mut best: Vec<Option<JoinPlan<'a>>> = (0..size).map(|_| None).collect();
1535
1536 for (i, atom) in atoms.iter().enumerate() {
1537 best[1usize << i] = Some(self.make_leaf_plan(atom, i)?);
1538 }
1539
1540 fn lex_lt(a: &[usize], b: &[usize]) -> bool {
1541 for (ai, bi) in a.iter().zip(b.iter()) {
1542 if ai != bi {
1543 return ai < bi;
1544 }
1545 }
1546 a.len() < b.len()
1547 }
1548
1549 for mask in 1..size {
1550 if mask.count_ones() <= 1 {
1551 continue;
1552 }
1553
1554 let mut best_for_mask: Option<JoinPlan<'a>> = None;
1555
1556 let mut sub = (mask - 1) & mask;
1557 while sub > 0 {
1558 let a = sub;
1559 let b = mask ^ a;
1560 if b == 0 {
1561 sub = (sub - 1) & mask;
1562 continue;
1563 }
1564
1565 let (Some(plan_a), Some(plan_b)) = (&best[a], &best[b]) else {
1566 sub = (sub - 1) & mask;
1567 continue;
1568 };
1569
1570 for (left, right) in [(plan_a, plan_b), (plan_b, plan_a)] {
1572 let cand = self.join_plans(left, right);
1573 let replace = match &best_for_mask {
1574 None => true,
1575 Some(current) => {
1576 if cand.total_cost < current.total_cost {
1577 true
1578 } else if (cand.total_cost - current.total_cost).abs() < 1e-9 {
1579 lex_lt(&cand.leaf_order_idx, ¤t.leaf_order_idx)
1580 } else {
1581 false
1582 }
1583 }
1584 };
1585
1586 if replace {
1587 best_for_mask = Some(cand);
1588 }
1589 }
1590
1591 sub = (sub - 1) & mask;
1592 }
1593
1594 best[mask] = best_for_mask;
1595 }
1596
1597 let full_mask = size - 1;
1598 if let Some(plan) = best[full_mask].take() {
1599 return Ok((plan.node, plan.leaf_order));
1600 }
1601
1602 let ordered = self.order_positive_atoms_greedy(atoms);
1604 let mut dummy_env = VariableEnv::new();
1605 let node = self.build_join_tree(&ordered, &mut dummy_env)?;
1606 Ok((node, ordered))
1607 }
1608
1609 fn plan_positive_atoms<'a>(&mut self, atoms: &[&'a Atom]) -> Result<(RirNode, Vec<&'a Atom>)> {
1610 if atoms.len() <= 1 {
1611 if atoms.is_empty() {
1612 return Err(XlogError::Compilation("Empty rule body".to_string()));
1613 }
1614 let plan = self.make_leaf_plan(atoms[0], 0)?;
1615 return Ok((plan.node, plan.leaf_order));
1616 }
1617
1618 const MAX_BUSHY_DP_ATOMS: usize = 10;
1619 if atoms.len() <= MAX_BUSHY_DP_ATOMS {
1620 return self.plan_positive_atoms_bushy(atoms);
1621 }
1622
1623 self.plan_positive_atoms_bushy_greedy(atoms)
1625 }
1626
1627 fn plan_positive_atoms_bushy_greedy<'a>(
1628 &mut self,
1629 atoms: &[&'a Atom],
1630 ) -> Result<(RirNode, Vec<&'a Atom>)> {
1631 if atoms.is_empty() {
1632 return Err(XlogError::Compilation("Empty rule body".to_string()));
1633 }
1634
1635 fn lex_lt(a: &[usize], b: &[usize]) -> bool {
1636 for (ai, bi) in a.iter().zip(b.iter()) {
1637 if ai != bi {
1638 return ai < bi;
1639 }
1640 }
1641 a.len() < b.len()
1642 }
1643
1644 let mut plans: Vec<JoinPlan<'a>> = Vec::with_capacity(atoms.len());
1645 for (idx, atom) in atoms.iter().enumerate() {
1646 plans.push(self.make_leaf_plan(atom, idx)?);
1647 }
1648
1649 while plans.len() > 1 {
1650 let mut best_pair: Option<(usize, usize, JoinPlan<'a>)> = None;
1651
1652 for i in 0..plans.len() {
1653 for j in (i + 1)..plans.len() {
1654 let a = &plans[i];
1655 let b = &plans[j];
1656
1657 let cand_ab = self.join_plans(a, b);
1658 let cand_ba = self.join_plans(b, a);
1659
1660 let cand = if cand_ab.total_cost < cand_ba.total_cost
1661 || (cand_ab.total_cost - cand_ba.total_cost).abs() < 1e-9
1662 && lex_lt(&cand_ab.leaf_order_idx, &cand_ba.leaf_order_idx)
1663 {
1664 cand_ab
1665 } else {
1666 cand_ba
1667 };
1668
1669 let replace = match &best_pair {
1670 None => true,
1671 Some((_bi, _bj, best)) => {
1672 if cand.total_cost < best.total_cost {
1673 true
1674 } else if (cand.total_cost - best.total_cost).abs() < 1e-9 {
1675 lex_lt(&cand.leaf_order_idx, &best.leaf_order_idx)
1676 } else {
1677 false
1678 }
1679 }
1680 };
1681
1682 if replace {
1683 best_pair = Some((i, j, cand));
1684 }
1685 }
1686 }
1687
1688 let Some((i, j, joined)) = best_pair else {
1689 break;
1690 };
1691
1692 let (a, b) = if i < j { (i, j) } else { (j, i) };
1694 plans.remove(b);
1695 plans.remove(a);
1696 plans.push(joined);
1697 }
1698
1699 let plan = plans
1700 .pop()
1701 .ok_or_else(|| XlogError::Compilation("Join planning failed".to_string()))?;
1702 Ok((plan.node, plan.leaf_order))
1703 }
1704
1705 fn order_positive_atoms_greedy<'a>(&self, atoms: &[&'a Atom]) -> Vec<&'a Atom> {
1706 let mut remaining: Vec<(usize, &Atom)> = atoms.iter().copied().enumerate().collect();
1707 let mut ordered: Vec<&Atom> = Vec::with_capacity(atoms.len());
1708 let mut bound_vars: HashSet<String> = HashSet::new();
1709
1710 while !remaining.is_empty() {
1711 let pick_idx = if ordered.is_empty() {
1712 remaining
1713 .iter()
1714 .enumerate()
1715 .min_by(|(_, a), (_, b)| {
1716 let (ai, aa) = **a;
1717 let (bi, bb) = **b;
1718 self.estimate_atom_rows(aa)
1719 .partial_cmp(&self.estimate_atom_rows(bb))
1720 .unwrap_or(std::cmp::Ordering::Equal)
1721 .then(ai.cmp(&bi))
1722 })
1723 .map(|(idx, _)| idx)
1724 .unwrap()
1725 } else {
1726 remaining
1727 .iter()
1728 .enumerate()
1729 .min_by(|(_, a), (_, b)| {
1730 let (ai, aa) = **a;
1731 let (bi, bb) = **b;
1732
1733 let a_vars = Self::atom_vars(aa);
1734 let b_vars = Self::atom_vars(bb);
1735
1736 let a_shared = a_vars.intersection(&bound_vars).count();
1737 let b_shared = b_vars.intersection(&bound_vars).count();
1738
1739 let a_score = if a_shared == 0 {
1740 self.estimate_atom_rows(aa) * 1.0e12
1741 } else {
1742 self.estimate_atom_rows(aa) / a_shared as f64
1743 };
1744 let b_score = if b_shared == 0 {
1745 self.estimate_atom_rows(bb) * 1.0e12
1746 } else {
1747 self.estimate_atom_rows(bb) / b_shared as f64
1748 };
1749
1750 a_score
1751 .partial_cmp(&b_score)
1752 .unwrap_or(std::cmp::Ordering::Equal)
1753 .then(ai.cmp(&bi))
1754 })
1755 .map(|(idx, _)| idx)
1756 .unwrap()
1757 };
1758
1759 let (_orig_idx, atom) = remaining.remove(pick_idx);
1760 ordered.push(atom);
1761 bound_vars.extend(Self::atom_vars(atom));
1762 }
1763
1764 ordered
1765 }
1766
1767 fn lower_body_parts(
1768 &mut self,
1769 positive_root: RirNode,
1770 negated_atoms: &[&Atom],
1771 comparisons: &[&Comparison],
1772 is_exprs: &[&IsExpr],
1773 var_env: &mut VariableEnv,
1774 ) -> Result<RirNode> {
1775 let mut result = positive_root;
1776
1777 for cmp in comparisons {
1779 result = self.apply_comparison(result, cmp, var_env)?;
1780 }
1781
1782 for is_expr in is_exprs {
1784 result = self.lower_is_expr(is_expr, result, var_env)?;
1785 }
1786
1787 for neg_atom in negated_atoms {
1789 result = self.apply_negation(result, neg_atom, var_env)?;
1790 }
1791
1792 Ok(result)
1793 }
1794
1795 fn build_join_tree(&mut self, atoms: &[&Atom], var_env: &mut VariableEnv) -> Result<RirNode> {
1797 if atoms.is_empty() {
1798 return Err(XlogError::Compilation("Empty rule body".to_string()));
1799 }
1800
1801 let first_atom = atoms[0];
1803 let rel_id = self.get_or_create_rel_id(&first_atom.predicate);
1804 let mut result = RirNode::Scan { rel: rel_id };
1805 let mut result_vars = self.collect_atom_vars(first_atom);
1806 let mut result_width = first_atom.terms.len();
1807
1808 result = self.apply_constant_filters(result, first_atom, 0)?;
1810
1811 for atom in atoms.iter().skip(1) {
1813 let right_rel_id = self.get_or_create_rel_id(&atom.predicate);
1814 let right_scan = RirNode::Scan { rel: right_rel_id };
1815
1816 let right_filtered = self.apply_constant_filters(right_scan, atom, 0)?;
1818
1819 let (left_keys, right_keys) = self.compute_join_keys(&result_vars, atom, result_width);
1821
1822 if left_keys.is_empty() {
1823 result = RirNode::Join {
1825 left: Box::new(result),
1826 right: Box::new(right_filtered),
1827 left_keys: vec![],
1828 right_keys: vec![],
1829 join_type: JoinType::Inner,
1830 };
1831 } else {
1832 result = RirNode::Join {
1833 left: Box::new(result),
1834 right: Box::new(right_filtered),
1835 left_keys,
1836 right_keys,
1837 join_type: JoinType::Inner,
1838 };
1839 }
1840
1841 for (i, term) in atom.terms.iter().enumerate() {
1843 if let Term::Variable(name) = term {
1844 result_vars.push((name.clone(), result_width + i));
1845 }
1846 }
1847 result_width += atom.terms.len();
1848 }
1849
1850 var_env.total_cols = result_width;
1852
1853 Ok(result)
1854 }
1855
1856 fn collect_atom_vars(&self, atom: &Atom) -> Vec<(String, usize)> {
1858 atom.terms
1859 .iter()
1860 .enumerate()
1861 .filter_map(|(i, term)| {
1862 if let Term::Variable(name) = term {
1863 Some((name.clone(), i))
1864 } else {
1865 None
1866 }
1867 })
1868 .collect()
1869 }
1870
1871 fn compute_join_keys(
1873 &self,
1874 left_vars: &[(String, usize)],
1875 right_atom: &Atom,
1876 _left_width: usize,
1877 ) -> (Vec<usize>, Vec<usize>) {
1878 let mut left_keys = Vec::new();
1879 let mut right_keys = Vec::new();
1880
1881 for (right_idx, term) in right_atom.terms.iter().enumerate() {
1882 if let Term::Variable(name) = term {
1883 for (left_name, left_idx) in left_vars {
1885 if left_name == name {
1886 left_keys.push(*left_idx);
1887 right_keys.push(right_idx);
1888 break; }
1890 }
1891 }
1892 }
1893
1894 (left_keys, right_keys)
1895 }
1896
1897 fn apply_constant_filters(
1899 &self,
1900 input: RirNode,
1901 atom: &Atom,
1902 _base_col: usize,
1903 ) -> Result<RirNode> {
1904 let mut filters = Vec::new();
1905 let mut first_var_col: HashMap<&str, usize> = HashMap::new();
1906 let schema = self.schemas.get(&atom.predicate).ok_or_else(|| {
1907 XlogError::Compilation(format!("Missing schema for predicate {}", atom.predicate))
1908 })?;
1909
1910 for (i, term) in atom.terms.iter().enumerate() {
1911 if let Term::Variable(name) = term {
1912 if name != "_" {
1913 if let Some(&first) = first_var_col.get(name.as_str()) {
1914 filters.push(Expr::Compare {
1915 left: Box::new(Expr::Column(first)),
1916 op: CompareOp::Eq,
1917 right: Box::new(Expr::Column(i)),
1918 });
1919 } else {
1920 first_var_col.insert(name.as_str(), i);
1921 }
1922 }
1923 }
1924
1925 let col_type = schema.column_type(i).ok_or_else(|| {
1926 XlogError::Compilation(format!(
1927 "Missing column type for {} column {}",
1928 atom.predicate, i
1929 ))
1930 })?;
1931 if let Some(const_val) = term_to_typed_const_value(term, col_type)? {
1932 filters.push(Expr::Compare {
1933 left: Box::new(Expr::Column(i)),
1934 op: CompareOp::Eq,
1935 right: Box::new(Expr::Const(const_val)),
1936 });
1937 }
1938 }
1939
1940 if filters.is_empty() {
1941 Ok(input)
1942 } else {
1943 let predicate = if filters.len() == 1 {
1944 filters.pop().unwrap()
1945 } else {
1946 Expr::And(filters)
1947 };
1948
1949 Ok(RirNode::Filter {
1950 input: Box::new(input),
1951 predicate,
1952 })
1953 }
1954 }
1955
1956 fn apply_comparison(
1958 &self,
1959 input: RirNode,
1960 cmp: &Comparison,
1961 var_env: &VariableEnv,
1962 ) -> Result<RirNode> {
1963 let (left_expr, right_expr) = match (&cmp.left, &cmp.right) {
1964 (Term::Variable(name), term) => {
1965 let col = var_env.get_column(name).ok_or_else(|| {
1966 XlogError::Compilation(format!("Variable {} not found in environment", name))
1967 })?;
1968 let typ = var_env.get_type(name).ok_or_else(|| {
1969 XlogError::Compilation(format!("Missing type for variable {}", name))
1970 })?;
1971 if let Some(const_val) = term_to_typed_const_value(term, typ)? {
1972 (Expr::Column(col), Expr::Const(const_val))
1973 } else {
1974 (
1975 self.term_to_expr(&cmp.left, var_env)?,
1976 self.term_to_expr(&cmp.right, var_env)?,
1977 )
1978 }
1979 }
1980 (term, Term::Variable(name)) => {
1981 let col = var_env.get_column(name).ok_or_else(|| {
1982 XlogError::Compilation(format!("Variable {} not found in environment", name))
1983 })?;
1984 let typ = var_env.get_type(name).ok_or_else(|| {
1985 XlogError::Compilation(format!("Missing type for variable {}", name))
1986 })?;
1987 if let Some(const_val) = term_to_typed_const_value(term, typ)? {
1988 (Expr::Const(const_val), Expr::Column(col))
1989 } else {
1990 (
1991 self.term_to_expr(&cmp.left, var_env)?,
1992 self.term_to_expr(&cmp.right, var_env)?,
1993 )
1994 }
1995 }
1996 _ => (
1997 self.term_to_expr(&cmp.left, var_env)?,
1998 self.term_to_expr(&cmp.right, var_env)?,
1999 ),
2000 };
2001
2002 let op = match cmp.op {
2003 CompOp::Eq => CompareOp::Eq,
2004 CompOp::Ne => CompareOp::Ne,
2005 CompOp::Lt => CompareOp::Lt,
2006 CompOp::Le => CompareOp::Le,
2007 CompOp::Gt => CompareOp::Gt,
2008 CompOp::Ge => CompareOp::Ge,
2009 };
2010
2011 Ok(RirNode::Filter {
2012 input: Box::new(input),
2013 predicate: Expr::Compare {
2014 left: Box::new(left_expr),
2015 op,
2016 right: Box::new(right_expr),
2017 },
2018 })
2019 }
2020
2021 fn term_to_expr(&self, term: &Term, var_env: &VariableEnv) -> Result<Expr> {
2023 match term {
2024 Term::Variable(name) => {
2025 if let Some(col) = var_env.get_column(name) {
2026 Ok(Expr::Column(col))
2027 } else {
2028 Err(XlogError::Compilation(format!(
2029 "Variable {} not found in environment",
2030 name
2031 )))
2032 }
2033 }
2034 Term::Anonymous => Err(XlogError::Compilation(
2035 "Anonymous wildcard '_' not allowed in comparisons".to_string(),
2036 )),
2037 Term::Integer(i) => Ok(Expr::Const(ConstValue::I64(*i))),
2038 Term::Float(f) => Ok(Expr::Const(ConstValue::F64(*f))),
2039 Term::String(s) => Ok(Expr::Const(ConstValue::Symbol(s.clone()))),
2040 Term::Symbol(id) => Ok(Expr::Const(ConstValue::Symbol(symbol::resolve(*id)))),
2041 Term::Aggregate(_) => Err(XlogError::Compilation(
2042 "Aggregates not allowed in comparisons".to_string(),
2043 )),
2044 Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => Err(
2045 term_not_lowerable_error("comparison", term_kind_for_lowering_error(term)),
2046 ),
2047 }
2048 }
2049
2050 fn apply_negation(
2052 &mut self,
2053 input: RirNode,
2054 neg_atom: &Atom,
2055 var_env: &VariableEnv,
2056 ) -> Result<RirNode> {
2057 let rel_id = self.get_or_create_rel_id(&neg_atom.predicate);
2058 let neg_scan = RirNode::Scan { rel: rel_id };
2059
2060 let neg_filtered = self.apply_constant_filters(neg_scan, neg_atom, 0)?;
2062
2063 let mut input_cols = Vec::new();
2065 let mut neg_cols = Vec::new();
2066
2067 for (neg_idx, term) in neg_atom.terms.iter().enumerate() {
2068 if let Term::Variable(name) = term {
2069 if let Some(col) = var_env.get_column(name) {
2070 input_cols.push(col);
2071 neg_cols.push(neg_idx);
2072 }
2073 }
2074 }
2075
2076 if input_cols.is_empty() {
2077 let input_width = var_env.column_count();
2085 let join_key =
2086 || ProjectExpr::Computed(Expr::Const(ConstValue::U32(0)), ScalarType::U32);
2087
2088 let mut keyed_input_columns: Vec<ProjectExpr> =
2089 (0..input_width).map(ProjectExpr::Column).collect();
2090 keyed_input_columns.push(join_key());
2091 let keyed_input = RirNode::Project {
2092 input: Box::new(input),
2093 columns: keyed_input_columns,
2094 };
2095 let keyed_negation = RirNode::Project {
2096 input: Box::new(neg_filtered),
2097 columns: vec![join_key()],
2098 };
2099 let gated_input = RirNode::Join {
2100 left: Box::new(keyed_input),
2101 right: Box::new(keyed_negation),
2102 left_keys: vec![input_width],
2103 right_keys: vec![0],
2104 join_type: JoinType::Anti,
2105 };
2106
2107 Ok(RirNode::Project {
2108 input: Box::new(gated_input),
2109 columns: (0..input_width).map(ProjectExpr::Column).collect(),
2110 })
2111 } else {
2112 let neg_projected = if neg_cols.len() < neg_atom.terms.len() {
2114 let neg_proj_exprs: Vec<ProjectExpr> =
2115 neg_cols.iter().map(|&c| ProjectExpr::Column(c)).collect();
2116 RirNode::Project {
2117 input: Box::new(neg_filtered),
2118 columns: neg_proj_exprs,
2119 }
2120 } else {
2121 neg_filtered
2122 };
2123
2124 let input_proj_exprs: Vec<ProjectExpr> =
2132 input_cols.iter().map(|&c| ProjectExpr::Column(c)).collect();
2133 let input_projected = RirNode::Project {
2134 input: Box::new(input.clone()),
2135 columns: input_proj_exprs,
2136 };
2137
2138 let kept_keys = RirNode::Diff {
2140 left: Box::new(input_projected),
2141 right: Box::new(neg_projected),
2142 };
2143
2144 Ok(RirNode::Join {
2148 left: Box::new(input),
2149 right: Box::new(kept_keys),
2150 left_keys: input_cols.clone(),
2151 right_keys: (0..input_cols.len()).collect(),
2152 join_type: JoinType::Semi,
2153 })
2154 }
2155 }
2156
2157 fn is_identity_projection(proj: &[ProjectExpr], input_cols: usize) -> bool {
2158 if proj.len() != input_cols {
2159 return false;
2160 }
2161 proj.iter()
2162 .enumerate()
2163 .all(|(i, e)| matches!(e, ProjectExpr::Column(c) if *c == i))
2164 }
2165
2166 fn compute_head_projection(
2172 &self,
2173 head: &Atom,
2174 var_env: &VariableEnv,
2175 ) -> Result<Vec<ProjectExpr>> {
2176 let mut cols = Vec::with_capacity(head.terms.len());
2177
2178 for (index, term) in head.terms.iter().enumerate() {
2179 match term {
2180 Term::Variable(name) => {
2181 let col = var_env
2182 .get_column(name)
2183 .ok_or_else(|| XlogError::UnsafeVariable(name.clone()))?;
2184 cols.push(ProjectExpr::Column(col));
2185 }
2186 Term::Anonymous => {
2187 return Err(XlogError::Compilation(
2188 "Anonymous wildcard '_' not allowed in rule head".to_string(),
2189 ));
2190 }
2191 Term::Aggregate(_) => {
2192 return Err(XlogError::Compilation(
2193 "Aggregate term in non-aggregate rule head".to_string(),
2194 ));
2195 }
2196 Term::Integer(_) | Term::Float(_) | Term::String(_) | Term::Symbol(_) => {
2197 let typ = self
2198 .schemas
2199 .get(&head.predicate)
2200 .and_then(|schema| schema.column_type(index))
2201 .ok_or_else(|| {
2202 XlogError::Compilation(format!(
2203 "Missing schema type for '{}' column {}",
2204 head.predicate, index
2205 ))
2206 })?;
2207 let value = term_to_typed_const_value(term, typ)?.ok_or_else(|| {
2208 XlogError::Compilation("Expected constant term".to_string())
2209 })?;
2210 cols.push(ProjectExpr::Computed(Expr::Const(value), typ));
2211 }
2212 Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => {
2213 return Err(term_not_lowerable_error(
2214 "rule head projection",
2215 term_kind_for_lowering_error(term),
2216 ));
2217 }
2218 }
2219 }
2220
2221 Ok(cols)
2222 }
2223
2224 fn lower_aggregate_rule(
2226 &mut self,
2227 head: &Atom,
2228 body: RirNode,
2229 var_env: &VariableEnv,
2230 ) -> Result<RirNode> {
2231 let mut key_vars: Vec<String> = Vec::new();
2233 let mut key_var_to_pos: HashMap<String, usize> = HashMap::new();
2234 let mut key_src_cols: Vec<usize> = Vec::new();
2235
2236 let mut agg_specs: Vec<(AggOp, String)> = Vec::new();
2238 let mut agg_to_pos: HashMap<(AggOp, String), usize> = HashMap::new();
2239 let mut value_vars: Vec<String> = Vec::new();
2240 let mut value_var_to_pos: HashMap<String, usize> = HashMap::new();
2241 let mut value_src_cols: Vec<usize> = Vec::new();
2242
2243 for term in &head.terms {
2244 match term {
2245 Term::Variable(name) => {
2246 if !key_var_to_pos.contains_key(name) {
2247 let col = var_env
2248 .get_column(name)
2249 .ok_or_else(|| XlogError::UnsafeVariable(name.clone()))?;
2250 let pos = key_vars.len();
2251 key_vars.push(name.clone());
2252 key_var_to_pos.insert(name.clone(), pos);
2253 key_src_cols.push(col);
2254 }
2255 }
2256 Term::Aggregate(agg) => {
2257 let key = (agg.op, agg.variable.clone());
2258 if let std::collections::hash_map::Entry::Vacant(entry) = agg_to_pos.entry(key)
2259 {
2260 let col = var_env
2262 .get_column(&agg.variable)
2263 .ok_or_else(|| XlogError::UnsafeVariable(agg.variable.clone()))?;
2264
2265 let value_pos = *value_var_to_pos
2267 .entry(agg.variable.clone())
2268 .or_insert_with(|| {
2269 let p = value_vars.len();
2270 value_vars.push(agg.variable.clone());
2271 value_src_cols.push(col);
2272 p
2273 });
2274
2275 let agg_pos = agg_specs.len();
2276 agg_specs.push((agg.op, agg.variable.clone()));
2277 entry.insert(agg_pos);
2278
2279 let _ = value_pos;
2281 }
2282 }
2283 Term::Anonymous => {
2284 return Err(XlogError::Compilation(
2285 "Anonymous wildcard '_' not allowed in rule head".to_string(),
2286 ));
2287 }
2288 Term::Integer(_) | Term::Float(_) | Term::String(_) | Term::Symbol(_) => {
2289 }
2291 Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => {
2292 return Err(term_not_lowerable_error(
2293 "aggregate rule head",
2294 term_kind_for_lowering_error(term),
2295 ));
2296 }
2297 }
2298 }
2299
2300 if agg_specs.is_empty() {
2301 return Err(XlogError::Compilation(
2302 "Rule marked as aggregate but no aggregate terms found".to_string(),
2303 ));
2304 }
2305
2306 let mut group_input_cols: Vec<ProjectExpr> = Vec::new();
2309 let mut key_cols: Vec<usize> = Vec::new();
2310
2311 if key_src_cols.is_empty() {
2312 group_input_cols.push(ProjectExpr::Computed(
2313 Expr::Const(ConstValue::U32(0)),
2314 ScalarType::U32,
2315 ));
2316 key_cols.push(0);
2317 } else {
2318 for (i, &col) in key_src_cols.iter().enumerate() {
2319 group_input_cols.push(ProjectExpr::Column(col));
2320 key_cols.push(i);
2321 }
2322 }
2323
2324 let value_offset = group_input_cols.len();
2325 for &col in &value_src_cols {
2326 group_input_cols.push(ProjectExpr::Column(col));
2327 }
2328
2329 let group_input = RirNode::Project {
2330 input: Box::new(body),
2331 columns: group_input_cols,
2332 };
2333
2334 let mut aggs: Vec<(usize, CoreAggOp)> = Vec::with_capacity(agg_specs.len());
2336 for (op, var) in &agg_specs {
2337 let value_pos = *value_var_to_pos
2338 .get(var)
2339 .ok_or_else(|| XlogError::UnsafeVariable(var.clone()))?;
2340 let value_col = value_offset + value_pos;
2341 aggs.push((value_col, convert_agg_op(op)));
2342 }
2343
2344 let groupby = RirNode::GroupBy {
2345 input: Box::new(group_input),
2346 key_cols,
2347 aggs,
2348 };
2349
2350 let key_count = if key_src_cols.is_empty() {
2355 1
2356 } else {
2357 key_vars.len()
2358 };
2359
2360 let mut final_proj: Vec<ProjectExpr> = Vec::with_capacity(head.terms.len());
2361 for (index, term) in head.terms.iter().enumerate() {
2362 match term {
2363 Term::Variable(name) => {
2364 let idx = if key_src_cols.is_empty() {
2365 return Err(XlogError::UnsafeVariable(name.clone()));
2368 } else {
2369 *key_var_to_pos
2370 .get(name)
2371 .ok_or_else(|| XlogError::UnsafeVariable(name.clone()))?
2372 };
2373 final_proj.push(ProjectExpr::Column(idx));
2374 }
2375 Term::Aggregate(agg) => {
2376 let pos = *agg_to_pos
2377 .get(&(agg.op, agg.variable.clone()))
2378 .ok_or_else(|| XlogError::UnsafeVariable(agg.variable.clone()))?;
2379 final_proj.push(ProjectExpr::Column(key_count + pos));
2380 }
2381 Term::Anonymous => {
2382 return Err(XlogError::Compilation(
2383 "Anonymous wildcard '_' not allowed in rule head".to_string(),
2384 ));
2385 }
2386 Term::Integer(_) | Term::Float(_) | Term::String(_) | Term::Symbol(_) => {
2387 let typ = self
2388 .schemas
2389 .get(&head.predicate)
2390 .and_then(|schema| schema.column_type(index))
2391 .ok_or_else(|| {
2392 XlogError::Compilation(format!(
2393 "Missing schema type for '{}' column {}",
2394 head.predicate, index
2395 ))
2396 })?;
2397 let value = term_to_typed_const_value(term, typ)?.ok_or_else(|| {
2398 XlogError::Compilation("Expected constant term".to_string())
2399 })?;
2400 final_proj.push(ProjectExpr::Computed(Expr::Const(value), typ));
2401 }
2402 Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => {
2403 return Err(term_not_lowerable_error(
2404 "aggregate rule projection",
2405 term_kind_for_lowering_error(term),
2406 ));
2407 }
2408 }
2409 }
2410
2411 if final_proj.is_empty() {
2412 return Err(XlogError::Compilation(
2413 "Aggregate rule produced empty head projection".to_string(),
2414 ));
2415 }
2416
2417 Ok(RirNode::Project {
2418 input: Box::new(groupby),
2419 columns: final_proj,
2420 })
2421 }
2422
2423 fn infer_arith_type_from_known_variables<F>(
2428 expr: &ArithExpr,
2429 variable_type: &F,
2430 user_functions: UserFunctionTypeEvidence,
2431 ) -> Result<Option<ScalarType>>
2432 where
2433 F: Fn(&str) -> Option<ScalarType>,
2434 {
2435 let mut tasks = vec![ArithmeticTypeTask::Visit(expr)];
2436 let mut values: Vec<Option<ScalarType>> = Vec::new();
2437 while let Some(task) = tasks.pop() {
2438 match task {
2439 ArithmeticTypeTask::Visit(expression) => match expression {
2440 ArithExpr::Variable(name) => values.push(variable_type(name)),
2441 ArithExpr::Integer(_) => values.push(Some(ScalarType::I64)),
2442 ArithExpr::Float(_) => values.push(Some(ScalarType::F64)),
2443 ArithExpr::Add(left, right)
2444 | ArithExpr::Sub(left, right)
2445 | ArithExpr::Mul(left, right)
2446 | ArithExpr::Div(left, right) => {
2447 tasks.push(ArithmeticTypeTask::FinishBinary(
2448 ArithmeticTypeOperation::Standard,
2449 ));
2450 tasks.push(ArithmeticTypeTask::Visit(right));
2451 tasks.push(ArithmeticTypeTask::Visit(left));
2452 }
2453 ArithExpr::Mod(left, right) => {
2454 tasks.push(ArithmeticTypeTask::FinishBinary(
2455 ArithmeticTypeOperation::Modulo,
2456 ));
2457 tasks.push(ArithmeticTypeTask::Visit(right));
2458 tasks.push(ArithmeticTypeTask::Visit(left));
2459 }
2460 ArithExpr::Min(left, right) | ArithExpr::Max(left, right) => {
2461 tasks.push(ArithmeticTypeTask::FinishBinary(
2462 ArithmeticTypeOperation::MinMax,
2463 ));
2464 tasks.push(ArithmeticTypeTask::Visit(right));
2465 tasks.push(ArithmeticTypeTask::Visit(left));
2466 }
2467 ArithExpr::Pow(left, right) => {
2468 tasks.push(ArithmeticTypeTask::FinishBinary(
2469 ArithmeticTypeOperation::Power,
2470 ));
2471 tasks.push(ArithmeticTypeTask::Visit(right));
2472 tasks.push(ArithmeticTypeTask::Visit(left));
2473 }
2474 ArithExpr::Abs(inner) => {
2475 tasks.push(ArithmeticTypeTask::FinishAbs);
2476 tasks.push(ArithmeticTypeTask::Visit(inner));
2477 }
2478 ArithExpr::Cast(inner, target) => {
2479 tasks.push(ArithmeticTypeTask::FinishCast(*target));
2480 tasks.push(ArithmeticTypeTask::Visit(inner));
2481 }
2482 ArithExpr::FuncCall { name, args } => match user_functions {
2483 UserFunctionTypeEvidence::RequireExpansion => {
2484 return Err(XlogError::Compilation(format!(
2485 "User-defined function '{}' must be inlined before lowering",
2486 name
2487 )));
2488 }
2489 UserFunctionTypeEvidence::Defer => {
2490 tasks.push(ArithmeticTypeTask::FinishFunctionArguments(args.len()));
2491 tasks.extend(args.iter().rev().map(ArithmeticTypeTask::Visit));
2492 }
2493 },
2494 ArithExpr::Conditional {
2495 then_expr,
2496 else_expr,
2497 ..
2498 } => {
2499 tasks.push(ArithmeticTypeTask::FinishConditional);
2500 tasks.push(ArithmeticTypeTask::Visit(else_expr));
2501 tasks.push(ArithmeticTypeTask::Visit(then_expr));
2502 }
2503 },
2504 ArithmeticTypeTask::FinishBinary(operation) => {
2505 let right = values.pop().expect("right arithmetic type is inferred");
2506 let left = values.pop().expect("left arithmetic type is inferred");
2507 values.push(Self::finish_binary_arithmetic_type(operation, left, right)?);
2508 }
2509 ArithmeticTypeTask::FinishAbs => {
2510 let inferred = values.pop().expect("absolute-value type is inferred");
2511 if let Some(typ) = inferred {
2512 if !Self::is_numeric_type(&typ) {
2513 return Err(XlogError::Compilation(format!(
2514 "abs requires numeric type, got {:?}",
2515 typ
2516 )));
2517 }
2518 }
2519 values.push(inferred);
2520 }
2521 ArithmeticTypeTask::FinishCast(target) => {
2522 values.pop().expect("cast operand type is inferred");
2523 values.push(Some(target));
2524 }
2525 ArithmeticTypeTask::FinishFunctionArguments(argument_count) => {
2526 let start = values
2527 .len()
2528 .checked_sub(argument_count)
2529 .expect("function argument types are inferred");
2530 values.truncate(start);
2531 values.push(None);
2532 }
2533 ArithmeticTypeTask::FinishConditional => {
2534 let else_type = values.pop().expect("else branch type is inferred");
2535 let then_type = values.pop().expect("then branch type is inferred");
2536 let inferred = match (then_type, else_type) {
2537 (Some(then_type), Some(else_type)) => {
2538 if then_type != else_type {
2539 return Err(XlogError::Compilation(format!(
2540 "Conditional branches have different types: {:?} vs {:?}",
2541 then_type, else_type
2542 )));
2543 }
2544 Some(then_type)
2545 }
2546 _ => None,
2547 };
2548 values.push(inferred);
2549 }
2550 }
2551 }
2552 let inferred = values
2553 .pop()
2554 .expect("arithmetic expression produces one inferred type");
2555 debug_assert!(values.is_empty());
2556 Ok(inferred)
2557 }
2558
2559 fn finish_binary_arithmetic_type(
2560 operation: ArithmeticTypeOperation,
2561 left: Option<ScalarType>,
2562 right: Option<ScalarType>,
2563 ) -> Result<Option<ScalarType>> {
2564 let matching_numeric_type = |context: &str| -> Result<Option<ScalarType>> {
2565 if let (Some(left), Some(right)) = (left, right) {
2566 if left != right {
2567 return Err(XlogError::Compilation(format!(
2568 "Type mismatch in {context}: {:?} vs {:?}",
2569 left, right
2570 )));
2571 }
2572 }
2573 for typ in [left, right].into_iter().flatten() {
2574 if !Self::is_numeric_type(&typ) {
2575 return Err(XlogError::Compilation(format!(
2576 "{context} requires numeric type, got {:?}",
2577 typ
2578 )));
2579 }
2580 }
2581 Ok(match (left, right) {
2582 (Some(left), Some(_)) => Some(left),
2583 _ => None,
2584 })
2585 };
2586
2587 match operation {
2588 ArithmeticTypeOperation::Standard => {
2589 if let (Some(left), Some(right)) = (left, right) {
2590 if left != right {
2591 return Err(XlogError::Compilation(format!(
2592 "Type mismatch in arithmetic: {:?} vs {:?}. Use cast() for conversion.",
2593 left, right
2594 )));
2595 }
2596 }
2597 for typ in [left, right].into_iter().flatten() {
2598 if !Self::is_numeric_type(&typ) {
2599 return Err(XlogError::Compilation(format!(
2600 "Arithmetic requires numeric type, got {:?}",
2601 typ
2602 )));
2603 }
2604 }
2605 Ok(match (left, right) {
2606 (Some(left), Some(_)) => Some(left),
2607 _ => None,
2608 })
2609 }
2610 ArithmeticTypeOperation::Modulo => {
2611 if let (Some(left), Some(right)) = (left, right) {
2612 if left != right {
2613 return Err(XlogError::Compilation(format!(
2614 "Type mismatch in mod: {:?} vs {:?}",
2615 left, right
2616 )));
2617 }
2618 }
2619 for typ in [left, right].into_iter().flatten() {
2620 if matches!(typ, ScalarType::F32 | ScalarType::F64) {
2621 return Err(XlogError::Compilation(
2622 "Modulo (%) not supported for floating point".into(),
2623 ));
2624 }
2625 if !Self::is_numeric_type(&typ) {
2626 return Err(XlogError::Compilation(format!(
2627 "Modulo (%) requires integer operands, got {:?}",
2628 typ
2629 )));
2630 }
2631 }
2632 Ok(match (left, right) {
2633 (Some(left), Some(_)) => Some(left),
2634 _ => None,
2635 })
2636 }
2637 ArithmeticTypeOperation::MinMax => matching_numeric_type("min/max"),
2638 ArithmeticTypeOperation::Power => {
2639 if let Some(invalid) = [left, right]
2640 .into_iter()
2641 .flatten()
2642 .find(|typ| !Self::is_numeric_type(typ))
2643 {
2644 let detail = match (left, right) {
2645 (Some(left), Some(right)) => format!("{:?} and {:?}", left, right),
2646 _ => format!("{:?}", invalid),
2647 };
2648 return Err(XlogError::Compilation(format!(
2649 "pow requires numeric operands, got {detail}"
2650 )));
2651 }
2652 Ok((left.is_some() && right.is_some()).then_some(ScalarType::F64))
2653 }
2654 }
2655 }
2656 pub(crate) fn infer_arith_type(
2658 &self,
2659 expr: &ArithExpr,
2660 var_env: &VariableEnv,
2661 ) -> Result<ScalarType> {
2662 if let Some(typ) = Self::infer_arith_type_from_known_variables(
2663 expr,
2664 &|variable| var_env.get_type(variable),
2665 UserFunctionTypeEvidence::RequireExpansion,
2666 )? {
2667 return Ok(typ);
2668 }
2669
2670 let variable = expr
2671 .variables()
2672 .into_iter()
2673 .find(|variable| var_env.get_type(variable).is_none())
2674 .unwrap_or("<unknown>");
2675 Err(XlogError::Compilation(format!(
2676 "Unknown variable {} in arithmetic",
2677 variable
2678 )))
2679 }
2680
2681 fn is_numeric_type(t: &ScalarType) -> bool {
2682 matches!(
2683 t,
2684 ScalarType::I32
2685 | ScalarType::I64
2686 | ScalarType::U32
2687 | ScalarType::U64
2688 | ScalarType::F32
2689 | ScalarType::F64
2690 )
2691 }
2692
2693 fn arith_to_expr(&self, arith: &ArithExpr, var_env: &VariableEnv) -> Result<Expr> {
2695 let mut tasks = vec![ArithmeticExpressionTask::Visit(arith)];
2696 let mut values = Vec::new();
2697 while let Some(task) = tasks.pop() {
2698 match task {
2699 ArithmeticExpressionTask::Visit(expression) => match expression {
2700 ArithExpr::Variable(name) => {
2701 let column = var_env.get_column(name).ok_or_else(|| {
2702 XlogError::Compilation(format!(
2703 "Variable {} not bound before use in arithmetic",
2704 name
2705 ))
2706 })?;
2707 values.push(Expr::Column(column));
2708 }
2709 ArithExpr::Integer(value) => {
2710 values.push(Expr::Const(ConstValue::I64(*value)));
2711 }
2712 ArithExpr::Float(value) => {
2713 values.push(Expr::Const(ConstValue::F64(*value)));
2714 }
2715 ArithExpr::Add(left, right) => Self::schedule_arithmetic_expression(
2716 &mut tasks,
2717 left,
2718 right,
2719 ArithmeticExpressionOperation::Add,
2720 ),
2721 ArithExpr::Sub(left, right) => Self::schedule_arithmetic_expression(
2722 &mut tasks,
2723 left,
2724 right,
2725 ArithmeticExpressionOperation::Sub,
2726 ),
2727 ArithExpr::Mul(left, right) => Self::schedule_arithmetic_expression(
2728 &mut tasks,
2729 left,
2730 right,
2731 ArithmeticExpressionOperation::Mul,
2732 ),
2733 ArithExpr::Div(left, right) => Self::schedule_arithmetic_expression(
2734 &mut tasks,
2735 left,
2736 right,
2737 ArithmeticExpressionOperation::Div,
2738 ),
2739 ArithExpr::Mod(left, right) => Self::schedule_arithmetic_expression(
2740 &mut tasks,
2741 left,
2742 right,
2743 ArithmeticExpressionOperation::Mod,
2744 ),
2745 ArithExpr::Min(left, right) => Self::schedule_arithmetic_expression(
2746 &mut tasks,
2747 left,
2748 right,
2749 ArithmeticExpressionOperation::Min,
2750 ),
2751 ArithExpr::Max(left, right) => Self::schedule_arithmetic_expression(
2752 &mut tasks,
2753 left,
2754 right,
2755 ArithmeticExpressionOperation::Max,
2756 ),
2757 ArithExpr::Pow(left, right) => Self::schedule_arithmetic_expression(
2758 &mut tasks,
2759 left,
2760 right,
2761 ArithmeticExpressionOperation::Pow,
2762 ),
2763 ArithExpr::Abs(inner) => {
2764 tasks.push(ArithmeticExpressionTask::FinishAbs);
2765 tasks.push(ArithmeticExpressionTask::Visit(inner));
2766 }
2767 ArithExpr::Cast(inner, target) => {
2768 tasks.push(ArithmeticExpressionTask::FinishCast(*target));
2769 tasks.push(ArithmeticExpressionTask::Visit(inner));
2770 }
2771 ArithExpr::FuncCall { name, .. } => {
2772 return Err(XlogError::Compilation(format!(
2773 "User-defined function '{}' must be inlined before lowering",
2774 name
2775 )));
2776 }
2777 ArithExpr::Conditional {
2778 cond_left,
2779 cond_op,
2780 cond_right,
2781 then_expr,
2782 else_expr,
2783 } => {
2784 tasks.push(ArithmeticExpressionTask::FinishConditional(*cond_op));
2785 tasks.push(ArithmeticExpressionTask::Visit(else_expr));
2786 tasks.push(ArithmeticExpressionTask::Visit(then_expr));
2787 tasks.push(ArithmeticExpressionTask::Visit(cond_right));
2788 tasks.push(ArithmeticExpressionTask::Visit(cond_left));
2789 }
2790 },
2791 ArithmeticExpressionTask::FinishBinary(operation) => {
2792 let right = values
2793 .pop()
2794 .expect("right arithmetic expression is lowered");
2795 let left = values.pop().expect("left arithmetic expression is lowered");
2796 values.push(match operation {
2797 ArithmeticExpressionOperation::Add => {
2798 Expr::Add(Box::new(left), Box::new(right))
2799 }
2800 ArithmeticExpressionOperation::Sub => {
2801 Expr::Sub(Box::new(left), Box::new(right))
2802 }
2803 ArithmeticExpressionOperation::Mul => {
2804 Expr::Mul(Box::new(left), Box::new(right))
2805 }
2806 ArithmeticExpressionOperation::Div => {
2807 Expr::Div(Box::new(left), Box::new(right))
2808 }
2809 ArithmeticExpressionOperation::Mod => {
2810 Expr::Mod(Box::new(left), Box::new(right))
2811 }
2812 ArithmeticExpressionOperation::Min => {
2813 Expr::Min(Box::new(left), Box::new(right))
2814 }
2815 ArithmeticExpressionOperation::Max => {
2816 Expr::Max(Box::new(left), Box::new(right))
2817 }
2818 ArithmeticExpressionOperation::Pow => {
2819 Expr::Pow(Box::new(left), Box::new(right))
2820 }
2821 });
2822 }
2823 ArithmeticExpressionTask::FinishAbs => {
2824 let inner = values.pop().expect("absolute-value expression is lowered");
2825 values.push(Expr::Abs(Box::new(inner)));
2826 }
2827 ArithmeticExpressionTask::FinishCast(target) => {
2828 let inner = values.pop().expect("cast expression is lowered");
2829 values.push(Expr::Cast(Box::new(inner), target));
2830 }
2831 ArithmeticExpressionTask::FinishConditional(op) => {
2832 let else_expr = values.pop().expect("else expression is lowered");
2833 let then_expr = values.pop().expect("then expression is lowered");
2834 let right = values.pop().expect("condition right side is lowered");
2835 let left = values.pop().expect("condition left side is lowered");
2836 let op = match op {
2837 CompOp::Eq => CompareOp::Eq,
2838 CompOp::Ne => CompareOp::Ne,
2839 CompOp::Lt => CompareOp::Lt,
2840 CompOp::Le => CompareOp::Le,
2841 CompOp::Gt => CompareOp::Gt,
2842 CompOp::Ge => CompareOp::Ge,
2843 };
2844 values.push(Expr::Conditional {
2845 condition: Box::new(Expr::Compare {
2846 left: Box::new(left),
2847 op,
2848 right: Box::new(right),
2849 }),
2850 then_expr: Box::new(then_expr),
2851 else_expr: Box::new(else_expr),
2852 });
2853 }
2854 }
2855 }
2856 let expression = values
2857 .pop()
2858 .expect("arithmetic expression produces one lowered value");
2859 debug_assert!(values.is_empty());
2860 Ok(expression)
2861 }
2862
2863 fn schedule_arithmetic_expression<'a>(
2864 tasks: &mut Vec<ArithmeticExpressionTask<'a>>,
2865 left: &'a ArithExpr,
2866 right: &'a ArithExpr,
2867 operation: ArithmeticExpressionOperation,
2868 ) {
2869 tasks.push(ArithmeticExpressionTask::FinishBinary(operation));
2870 tasks.push(ArithmeticExpressionTask::Visit(right));
2871 tasks.push(ArithmeticExpressionTask::Visit(left));
2872 }
2873 fn lower_is_expr(
2875 &mut self,
2876 is_expr: &IsExpr,
2877 input: RirNode,
2878 var_env: &mut VariableEnv,
2879 ) -> Result<RirNode> {
2880 if var_env.contains(&is_expr.target) {
2882 return Err(XlogError::Compilation(format!(
2883 "Variable {} already bound; 'is' requires fresh variable",
2884 is_expr.target
2885 )));
2886 }
2887
2888 for var in is_expr.expr.variables() {
2890 if !var_env.contains(var) {
2891 return Err(XlogError::Compilation(format!(
2892 "Variable {} used in arithmetic but not bound",
2893 var
2894 )));
2895 }
2896 }
2897
2898 let result_type = self.infer_arith_type(&is_expr.expr, var_env)?;
2900
2901 let ir_expr = self.arith_to_expr(&is_expr.expr, var_env)?;
2903
2904 let num_cols = var_env.column_count();
2906 let mut proj_exprs: Vec<ProjectExpr> = (0..num_cols).map(ProjectExpr::Column).collect();
2907 proj_exprs.push(ProjectExpr::Computed(ir_expr, result_type));
2908
2909 var_env.bind(&is_expr.target, num_cols, result_type);
2911
2912 Ok(RirNode::Project {
2913 input: Box::new(input),
2914 columns: proj_exprs,
2915 })
2916 }
2917}
2918
2919pub(crate) struct VariableEnv {
2921 occurrences: HashMap<String, Vec<(String, usize, usize)>>,
2923 total_cols: usize,
2925 types: HashMap<String, ScalarType>,
2927}
2928
2929impl VariableEnv {
2930 fn new() -> Self {
2931 Self {
2932 occurrences: HashMap::new(),
2933 total_cols: 0,
2934 types: HashMap::new(),
2935 }
2936 }
2937
2938 fn add_occurrence(&mut self, var: &str, pred: String, atom_pos: usize, global_col: usize) {
2939 self.occurrences
2940 .entry(var.to_string())
2941 .or_default()
2942 .push((pred, atom_pos, global_col));
2943 }
2944
2945 fn get_column(&self, var: &str) -> Option<usize> {
2946 self.occurrences
2947 .get(var)
2948 .and_then(|occs| occs.first())
2949 .map(|(_, _, col)| *col)
2950 }
2951
2952 fn bind(&mut self, name: &str, column: usize, typ: ScalarType) {
2954 self.types.insert(name.to_string(), typ);
2955 self.occurrences
2957 .entry(name.to_string())
2958 .or_default()
2959 .push(("".to_string(), 0, column));
2960 if column >= self.total_cols {
2963 self.total_cols = column + 1;
2964 }
2965 }
2966
2967 fn get_type(&self, name: &str) -> Option<ScalarType> {
2969 self.types.get(name).copied()
2970 }
2971
2972 fn contains(&self, name: &str) -> bool {
2974 self.occurrences.contains_key(name)
2975 }
2976
2977 fn column_count(&self) -> usize {
2979 self.total_cols
2980 }
2981}
2982
2983fn sort_labels_from_terms(terms: &[Term]) -> Vec<String> {
2984 terms
2985 .iter()
2986 .enumerate()
2987 .map(|(idx, term)| match term {
2988 Term::Variable(name) if !name.trim().is_empty() => name.clone(),
2989 Term::Aggregate(agg) => format!("{:?}_{}", agg.op, agg.variable),
2990 Term::List(_) => format!("list{}", idx),
2991 Term::Cons { .. } => format!("cons{}", idx),
2992 Term::Compound { functor, .. } => functor.clone(),
2993 Term::PredRef(name) => name.clone(),
2994 _ => format!("c{}", idx),
2995 })
2996 .collect()
2997}
2998
2999fn term_to_const_value(term: &Term) -> Option<ConstValue> {
3001 match term {
3002 Term::Integer(i) => Some(ConstValue::I64(*i)),
3003 Term::Float(f) => Some(ConstValue::F64(*f)),
3004 Term::String(s) => Some(ConstValue::Symbol(s.clone())),
3005 Term::Symbol(id) => Some(ConstValue::Symbol(symbol::resolve(*id))),
3006 Term::Variable(_)
3007 | Term::Anonymous
3008 | Term::Aggregate(_)
3009 | Term::List(_)
3010 | Term::Cons { .. }
3011 | Term::Compound { .. }
3012 | Term::PredRef(_) => None,
3013 }
3014}
3015
3016pub(crate) fn term_to_typed_const_value(
3017 term: &Term,
3018 expected: ScalarType,
3019) -> Result<Option<ConstValue>> {
3020 let const_val = match term {
3021 Term::Integer(i) => match expected {
3022 ScalarType::U32 => {
3023 if *i >= 0 && *i <= u32::MAX as i64 {
3024 ConstValue::U32(*i as u32)
3025 } else {
3026 return Err(XlogError::Compilation(format!(
3027 "Integer literal {} out of range for {:?}",
3028 i, expected
3029 )));
3030 }
3031 }
3032 ScalarType::U64 => {
3033 if *i >= 0 {
3034 ConstValue::U64(*i as u64)
3035 } else {
3036 return Err(XlogError::Compilation(format!(
3037 "Integer literal {} out of range for {:?}",
3038 i, expected
3039 )));
3040 }
3041 }
3042 ScalarType::I32 => {
3043 if *i >= i32::MIN as i64 && *i <= i32::MAX as i64 {
3044 ConstValue::I32(*i as i32)
3045 } else {
3046 return Err(XlogError::Compilation(format!(
3047 "Integer literal {} out of range for {:?}",
3048 i, expected
3049 )));
3050 }
3051 }
3052 ScalarType::I64 => ConstValue::I64(*i),
3053 ScalarType::F32 => {
3054 let value = *i as f64;
3055 if value < f32::MIN as f64 || value > f32::MAX as f64 {
3056 return Err(XlogError::Compilation(format!(
3057 "Integer literal {} out of range for {:?}",
3058 i, expected
3059 )));
3060 }
3061 ConstValue::F32(value as f32)
3062 }
3063 ScalarType::F64 => ConstValue::F64(*i as f64),
3064 ScalarType::Bool => {
3065 if *i == 0 || *i == 1 {
3066 ConstValue::Bool(*i == 1)
3067 } else {
3068 return Err(XlogError::Compilation(format!(
3069 "Integer literal {} not valid for {:?}",
3070 i, expected
3071 )));
3072 }
3073 }
3074 ScalarType::Symbol => {
3075 return Err(XlogError::Compilation(format!(
3076 "Integer literal {} not valid for {:?}",
3077 i, expected
3078 )));
3079 }
3080 },
3081 Term::Float(f) => match expected {
3082 ScalarType::F32 => {
3083 if !f.is_finite() {
3084 return Err(XlogError::Compilation(format!(
3085 "Float literal {} not valid for {:?}",
3086 f, expected
3087 )));
3088 }
3089 if *f < f32::MIN as f64 || *f > f32::MAX as f64 {
3090 return Err(XlogError::Compilation(format!(
3091 "Float literal {} out of range for {:?}",
3092 f, expected
3093 )));
3094 }
3095 ConstValue::F32(*f as f32)
3096 }
3097 ScalarType::F64 => ConstValue::F64(*f),
3098 ScalarType::U32
3099 | ScalarType::U64
3100 | ScalarType::I32
3101 | ScalarType::I64
3102 | ScalarType::Bool
3103 | ScalarType::Symbol => {
3104 return Err(XlogError::Compilation(format!(
3105 "Float literal {} not valid for {:?}",
3106 f, expected
3107 )));
3108 }
3109 },
3110 Term::String(s) => {
3111 if expected == ScalarType::Symbol {
3112 ConstValue::Symbol(s.clone())
3113 } else {
3114 return Err(XlogError::Compilation(format!(
3115 "String literal {} not valid for {:?}",
3116 s, expected
3117 )));
3118 }
3119 }
3120 Term::Symbol(id) => {
3121 let value = symbol::resolve(*id);
3122 match expected {
3123 ScalarType::Symbol => ConstValue::Symbol(value),
3124 ScalarType::Bool if matches!(value.as_str(), "true" | "false") => {
3125 ConstValue::Bool(value == "true")
3126 }
3127 _ => {
3128 return Err(XlogError::Compilation(format!(
3129 "Symbol literal {} not valid for {:?}",
3130 value, expected
3131 )));
3132 }
3133 }
3134 }
3135 Term::Variable(_)
3136 | Term::Anonymous
3137 | Term::Aggregate(_)
3138 | Term::List(_)
3139 | Term::Cons { .. }
3140 | Term::Compound { .. }
3141 | Term::PredRef(_) => return Ok(None),
3142 };
3143
3144 Ok(Some(const_val))
3145}
3146
3147fn convert_agg_op(op: &AggOp) -> CoreAggOp {
3149 match op {
3150 AggOp::Count => CoreAggOp::Count,
3151 AggOp::Sum => CoreAggOp::Sum,
3152 AggOp::Min => CoreAggOp::Min,
3153 AggOp::Max => CoreAggOp::Max,
3154 AggOp::LogSumExp => CoreAggOp::LogSumExp,
3155 }
3156}
3157
3158#[cfg(test)]
3162mod arith_type_tests {
3163 use super::*;
3164 use crate::ast::ArithExpr;
3165
3166 #[test]
3167 fn test_arith_type_inference_same_type() {
3168 let lowerer = Lowerer::new();
3170 let mut var_env = VariableEnv::new();
3171 var_env.bind("X", 0, ScalarType::I64);
3172 var_env.bind("Y", 1, ScalarType::I64);
3173
3174 let expr = ArithExpr::Add(
3175 Box::new(ArithExpr::Variable("X".to_string())),
3176 Box::new(ArithExpr::Variable("Y".to_string())),
3177 );
3178 let result = lowerer.infer_arith_type(&expr, &var_env);
3179 assert!(result.is_ok());
3180 assert_eq!(result.unwrap(), ScalarType::I64);
3181 }
3182
3183 #[test]
3184 fn test_arith_type_inference_mismatch() {
3185 let lowerer = Lowerer::new();
3187 let mut var_env = VariableEnv::new();
3188 var_env.bind("X", 0, ScalarType::I64);
3189 var_env.bind("Y", 1, ScalarType::F64);
3190
3191 let expr = ArithExpr::Add(
3192 Box::new(ArithExpr::Variable("X".to_string())),
3193 Box::new(ArithExpr::Variable("Y".to_string())),
3194 );
3195 let result = lowerer.infer_arith_type(&expr, &var_env);
3196 assert!(result.is_err());
3197 }
3198}
3199
3200#[cfg(test)]
3201mod tests {
3202 use super::*;
3203 use crate::ast::*;
3204
3205 fn pred_decl(name: &str, types: Vec<ScalarType>) -> PredDecl {
3206 let type_refs: Vec<TypeRef> = types.into_iter().map(TypeRef::Scalar).collect();
3207 let columns = type_refs
3208 .iter()
3209 .cloned()
3210 .map(|typ| PredColumn { name: None, typ })
3211 .collect();
3212 PredDecl {
3213 name: name.to_string(),
3214 types: type_refs,
3215 columns,
3216 is_private: false,
3217 }
3218 }
3219
3220 fn edge_atom(x: &str, y: &str) -> Atom {
3222 Atom {
3223 predicate: "edge".to_string(),
3224 terms: vec![Term::Variable(x.to_string()), Term::Variable(y.to_string())],
3225 }
3226 }
3227
3228 fn reach_atom(x: &str, y: &str) -> Atom {
3230 Atom {
3231 predicate: "reach".to_string(),
3232 terms: vec![Term::Variable(x.to_string()), Term::Variable(y.to_string())],
3233 }
3234 }
3235
3236 fn node_atom(x: &str) -> Atom {
3238 Atom {
3239 predicate: "node".to_string(),
3240 terms: vec![Term::Variable(x.to_string())],
3241 }
3242 }
3243
3244 #[test]
3245 fn test_lowerer_new() {
3246 let lowerer = Lowerer::new();
3247 assert!(lowerer.schemas.is_empty());
3248 assert!(lowerer.strata.is_empty());
3249 assert_eq!(lowerer.next_rel_id, 0);
3250 }
3251
3252 #[test]
3253 fn test_get_or_create_rel_id() {
3254 let mut lowerer = Lowerer::new();
3255 let id1 = lowerer.get_or_create_rel_id("edge");
3256 let id2 = lowerer.get_or_create_rel_id("reach");
3257 let id3 = lowerer.get_or_create_rel_id("edge");
3258
3259 assert_eq!(id1, RelId(0));
3260 assert_eq!(id2, RelId(1));
3261 assert_eq!(id3, RelId(0)); }
3263
3264 #[test]
3265 fn test_infer_schemas_from_facts() {
3266 let mut program = Program::new();
3267 program.rules.push(Rule {
3268 head: Atom {
3269 predicate: "edge".to_string(),
3270 terms: vec![Term::Integer(1), Term::Integer(2)],
3271 },
3272 body: vec![],
3273 });
3274
3275 let mut lowerer = Lowerer::new();
3276 lowerer.infer_schemas(&program).unwrap();
3277
3278 assert!(lowerer.schemas.contains_key("edge"));
3279 let schema = lowerer.schemas.get("edge").unwrap();
3280 assert_eq!(schema.arity(), 2);
3281 }
3282
3283 #[test]
3284 fn test_infer_schemas_propagates_through_reversed_rule_chains() {
3285 let mut program = Program::new();
3286 program.rules.push(Rule {
3287 head: Atom {
3288 predicate: "shared".to_string(),
3289 terms: vec![Term::Variable("X".to_string())],
3290 },
3291 body: vec![BodyLiteral::Positive(Atom {
3292 predicate: "intermediate".to_string(),
3293 terms: vec![Term::Variable("X".to_string())],
3294 })],
3295 });
3296 program.rules.push(Rule {
3297 head: Atom {
3298 predicate: "intermediate".to_string(),
3299 terms: vec![Term::Variable("X".to_string())],
3300 },
3301 body: vec![BodyLiteral::Positive(Atom {
3302 predicate: "source".to_string(),
3303 terms: vec![Term::Variable("X".to_string())],
3304 })],
3305 });
3306 program.rules.push(Rule {
3307 head: Atom {
3308 predicate: "source".to_string(),
3309 terms: vec![Term::Symbol(symbol::intern("value"))],
3310 },
3311 body: vec![],
3312 });
3313
3314 let mut lowerer = Lowerer::new();
3315 lowerer.infer_schemas(&program).unwrap();
3316
3317 assert_eq!(
3318 lowerer
3319 .schemas
3320 .get("shared")
3321 .and_then(|schema| schema.column_type(0)),
3322 Some(ScalarType::Symbol)
3323 );
3324 }
3325
3326 #[test]
3327 fn test_lower_simple_rule() {
3328 let rule = Rule {
3330 head: reach_atom("X", "Y"),
3331 body: vec![BodyLiteral::Positive(edge_atom("X", "Y"))],
3332 };
3333
3334 let mut lowerer = Lowerer::new();
3335 lowerer.schemas.insert(
3336 "edge".to_string(),
3337 Schema::new(vec![
3338 ("c0".to_string(), ScalarType::U32),
3339 ("c1".to_string(), ScalarType::U32),
3340 ]),
3341 );
3342
3343 let result = lowerer.lower_rule(&rule);
3344 assert!(result.is_ok());
3345
3346 let node = result.unwrap();
3347 assert!(matches!(node, RirNode::Scan { .. }));
3349 }
3350
3351 #[test]
3352 fn test_lower_join_rule() {
3353 let rule = Rule {
3355 head: Atom {
3356 predicate: "reach".to_string(),
3357 terms: vec![
3358 Term::Variable("X".to_string()),
3359 Term::Variable("Z".to_string()),
3360 ],
3361 },
3362 body: vec![
3363 BodyLiteral::Positive(reach_atom("X", "Y")),
3364 BodyLiteral::Positive(edge_atom("Y", "Z")),
3365 ],
3366 };
3367
3368 let mut lowerer = Lowerer::new();
3369 lowerer.schemas.insert(
3370 "reach".to_string(),
3371 Schema::new(vec![
3372 ("c0".to_string(), ScalarType::U32),
3373 ("c1".to_string(), ScalarType::U32),
3374 ]),
3375 );
3376 lowerer.schemas.insert(
3377 "edge".to_string(),
3378 Schema::new(vec![
3379 ("c0".to_string(), ScalarType::U32),
3380 ("c1".to_string(), ScalarType::U32),
3381 ]),
3382 );
3383
3384 let result = lowerer.lower_rule(&rule);
3385 assert!(result.is_ok());
3386
3387 let node = result.unwrap();
3388 if let RirNode::Project { input, columns } = node {
3390 assert_eq!(
3392 columns,
3393 vec![ProjectExpr::Column(0), ProjectExpr::Column(3)]
3394 );
3395 assert!(matches!(*input, RirNode::Join { .. }));
3396 if let RirNode::Join {
3397 left_keys,
3398 right_keys,
3399 ..
3400 } = *input
3401 {
3402 assert_eq!(left_keys, vec![1]); assert_eq!(right_keys, vec![0]); }
3405 } else {
3406 panic!("Expected Project node");
3407 }
3408 }
3409
3410 #[test]
3411 fn test_join_order_prefers_smaller_relation() {
3412 let rule = Rule {
3414 head: Atom {
3415 predicate: "out".to_string(),
3416 terms: vec![Term::Variable("X".to_string())],
3417 },
3418 body: vec![
3419 BodyLiteral::Positive(Atom {
3420 predicate: "big".to_string(),
3421 terms: vec![Term::Variable("X".to_string())],
3422 }),
3423 BodyLiteral::Positive(Atom {
3424 predicate: "small".to_string(),
3425 terms: vec![Term::Variable("X".to_string())],
3426 }),
3427 ],
3428 };
3429
3430 let mut lowerer = Lowerer::new();
3431 lowerer.schemas.insert(
3432 "big".to_string(),
3433 Schema::new(vec![("c0".to_string(), ScalarType::U32)]),
3434 );
3435 lowerer.schemas.insert(
3436 "small".to_string(),
3437 Schema::new(vec![("c0".to_string(), ScalarType::U32)]),
3438 );
3439
3440 let big_id = lowerer.get_or_create_rel_id("big");
3442 let small_id = lowerer.get_or_create_rel_id("small");
3443 assert_eq!(big_id, RelId(0));
3444 assert_eq!(small_id, RelId(1));
3445
3446 lowerer.est_cardinality.insert("big".to_string(), 10_000);
3448 lowerer.est_cardinality.insert("small".to_string(), 10);
3449
3450 let node = lowerer.lower_rule(&rule).unwrap();
3451 let join = match node {
3452 RirNode::Project { input, .. } => *input,
3453 other => other,
3454 };
3455
3456 match join {
3457 RirNode::Join { left, right, .. } => {
3458 assert!(matches!(*left, RirNode::Scan { rel } if rel == big_id));
3460 assert!(matches!(*right, RirNode::Scan { rel } if rel == small_id));
3461 }
3462 other => panic!("Expected Join node, got {:?}", other),
3463 }
3464 }
3465
3466 #[test]
3467 fn test_lower_negation() {
3468 let rule = Rule {
3470 head: Atom {
3471 predicate: "isolated".to_string(),
3472 terms: vec![Term::Variable("X".to_string())],
3473 },
3474 body: vec![
3475 BodyLiteral::Positive(node_atom("X")),
3476 BodyLiteral::Negated(Atom {
3477 predicate: "edge".to_string(),
3478 terms: vec![
3479 Term::Variable("X".to_string()),
3480 Term::Variable("_".to_string()),
3481 ],
3482 }),
3483 ],
3484 };
3485
3486 let mut lowerer = Lowerer::new();
3487 lowerer.schemas.insert(
3488 "node".to_string(),
3489 Schema::new(vec![("c0".to_string(), ScalarType::U32)]),
3490 );
3491 lowerer.schemas.insert(
3492 "edge".to_string(),
3493 Schema::new(vec![
3494 ("c0".to_string(), ScalarType::U32),
3495 ("c1".to_string(), ScalarType::U32),
3496 ]),
3497 );
3498
3499 let result = lowerer.lower_rule(&rule);
3500 assert!(result.is_ok());
3501
3502 let node = result.unwrap();
3504 fn contains_diff_or_semi(node: &RirNode) -> bool {
3506 match node {
3507 RirNode::Diff { .. } => true,
3508 RirNode::Join {
3509 join_type: JoinType::Semi,
3510 ..
3511 } => true,
3512 RirNode::Join { left, right, .. } => {
3513 contains_diff_or_semi(left) || contains_diff_or_semi(right)
3514 }
3515 RirNode::Project { input, .. } => contains_diff_or_semi(input),
3516 RirNode::Filter { input, .. } => contains_diff_or_semi(input),
3517 _ => false,
3518 }
3519 }
3520 assert!(contains_diff_or_semi(&node));
3521 }
3522
3523 #[test]
3524 fn test_lower_ground_negation_preserves_the_positive_input_schema() {
3525 let rule = Rule {
3527 head: Atom {
3528 predicate: "ok".to_string(),
3529 terms: vec![Term::Variable("X".to_string())],
3530 },
3531 body: vec![
3532 BodyLiteral::Positive(Atom {
3533 predicate: "x".to_string(),
3534 terms: vec![Term::Variable("X".to_string())],
3535 }),
3536 BodyLiteral::Negated(Atom {
3537 predicate: "p".to_string(),
3538 terms: vec![Term::Integer(3)],
3539 }),
3540 ],
3541 };
3542
3543 let mut lowerer = Lowerer::new();
3544 for predicate in ["ok", "x", "p"] {
3545 lowerer.schemas.insert(
3546 predicate.to_string(),
3547 Schema::new(vec![("c0".to_string(), ScalarType::U32)]),
3548 );
3549 }
3550
3551 let node = lowerer.lower_rule(&rule).expect("lower ground negation");
3552
3553 let RirNode::Project { input, columns } = node else {
3554 panic!("ground negation must project away its internal existence key");
3555 };
3556 assert_eq!(columns, vec![ProjectExpr::Column(0)]);
3557 assert!(matches!(
3558 *input,
3559 RirNode::Join {
3560 left_keys,
3561 right_keys,
3562 join_type: JoinType::Anti,
3563 ..
3564 } if left_keys == vec![1] && right_keys == vec![0]
3565 ));
3566 }
3567
3568 #[test]
3569 fn test_lower_comparison() {
3570 let rule = Rule {
3572 head: Atom {
3573 predicate: "greater".to_string(),
3574 terms: vec![
3575 Term::Variable("X".to_string()),
3576 Term::Variable("Y".to_string()),
3577 ],
3578 },
3579 body: vec![
3580 BodyLiteral::Positive(Atom {
3581 predicate: "pair".to_string(),
3582 terms: vec![
3583 Term::Variable("X".to_string()),
3584 Term::Variable("Y".to_string()),
3585 ],
3586 }),
3587 BodyLiteral::Comparison(Comparison {
3588 left: Term::Variable("X".to_string()),
3589 op: CompOp::Gt,
3590 right: Term::Variable("Y".to_string()),
3591 }),
3592 ],
3593 };
3594
3595 let mut lowerer = Lowerer::new();
3596 lowerer.schemas.insert(
3597 "pair".to_string(),
3598 Schema::new(vec![
3599 ("c0".to_string(), ScalarType::U32),
3600 ("c1".to_string(), ScalarType::U32),
3601 ]),
3602 );
3603
3604 let result = lowerer.lower_rule(&rule);
3605 assert!(result.is_ok());
3606
3607 let node = result.unwrap();
3608 fn contains_filter(node: &RirNode) -> bool {
3610 match node {
3611 RirNode::Filter { .. } => true,
3612 RirNode::Project { input, .. } => contains_filter(input),
3613 RirNode::Join { left, right, .. } => {
3614 contains_filter(left) || contains_filter(right)
3615 }
3616 _ => false,
3617 }
3618 }
3619 assert!(contains_filter(&node));
3620 }
3621
3622 #[test]
3623 fn test_lower_constant_filter() {
3624 let rule = Rule {
3626 head: Atom {
3627 predicate: "specific_edge".to_string(),
3628 terms: vec![Term::Variable("Y".to_string())],
3629 },
3630 body: vec![BodyLiteral::Positive(Atom {
3631 predicate: "edge".to_string(),
3632 terms: vec![Term::Integer(1), Term::Variable("Y".to_string())],
3633 })],
3634 };
3635
3636 let mut lowerer = Lowerer::new();
3637 lowerer.schemas.insert(
3638 "edge".to_string(),
3639 Schema::new(vec![
3640 ("c0".to_string(), ScalarType::U32),
3641 ("c1".to_string(), ScalarType::U32),
3642 ]),
3643 );
3644
3645 let result = lowerer.lower_rule(&rule);
3646 assert!(result.is_ok());
3647
3648 let node = result.unwrap();
3649 fn has_const_filter(node: &RirNode) -> bool {
3651 match node {
3652 RirNode::Filter {
3653 predicate: Expr::Compare { right, .. },
3654 ..
3655 } => matches!(**right, Expr::Const(_)),
3656 RirNode::Project { input, .. } => has_const_filter(input),
3657 _ => false,
3658 }
3659 }
3660 assert!(has_const_filter(&node));
3661 }
3662
3663 #[test]
3664 fn test_lower_repeated_variable_filter() {
3665 let rule = Rule {
3667 head: Atom {
3668 predicate: "self_loop".to_string(),
3669 terms: vec![Term::Variable("X".to_string())],
3670 },
3671 body: vec![BodyLiteral::Positive(Atom {
3672 predicate: "edge".to_string(),
3673 terms: vec![
3674 Term::Variable("X".to_string()),
3675 Term::Variable("X".to_string()),
3676 ],
3677 })],
3678 };
3679
3680 let mut lowerer = Lowerer::new();
3681 lowerer.schemas.insert(
3682 "edge".to_string(),
3683 Schema::new(vec![
3684 ("c0".to_string(), ScalarType::U32),
3685 ("c1".to_string(), ScalarType::U32),
3686 ]),
3687 );
3688
3689 let node = lowerer.lower_rule(&rule).expect("lower_rule failed");
3690
3691 fn has_col_eq_filter(node: &RirNode) -> bool {
3692 match node {
3693 RirNode::Filter { predicate, .. } => match predicate {
3694 Expr::Compare {
3695 left,
3696 op: CompareOp::Eq,
3697 right,
3698 } => {
3699 matches!((&**left, &**right), (Expr::Column(0), Expr::Column(1)))
3700 || matches!((&**left, &**right), (Expr::Column(1), Expr::Column(0)))
3701 }
3702 Expr::And(exprs) => exprs.iter().any(|e| match e {
3703 Expr::Compare {
3704 left,
3705 op: CompareOp::Eq,
3706 right,
3707 } => {
3708 matches!((&**left, &**right), (Expr::Column(0), Expr::Column(1)))
3709 || matches!((&**left, &**right), (Expr::Column(1), Expr::Column(0)))
3710 }
3711 _ => false,
3712 }),
3713 _ => false,
3714 },
3715 RirNode::Project { input, .. } => has_col_eq_filter(input),
3716 _ => false,
3717 }
3718 }
3719
3720 assert!(has_col_eq_filter(&node));
3721 }
3722
3723 #[test]
3724 fn test_lower_program_simple() {
3725 let mut program = Program::new();
3726
3727 program.rules.push(Rule {
3729 head: Atom {
3730 predicate: "edge".to_string(),
3731 terms: vec![Term::Integer(1), Term::Integer(2)],
3732 },
3733 body: vec![],
3734 });
3735
3736 program.rules.push(Rule {
3738 head: reach_atom("X", "Y"),
3739 body: vec![BodyLiteral::Positive(edge_atom("X", "Y"))],
3740 });
3741
3742 let mut lowerer = Lowerer::new();
3743 lowerer.set_strata(vec![vec!["edge".to_string()], vec!["reach".to_string()]]);
3744
3745 let result = lowerer.lower_program(&program);
3746 assert!(result.is_ok());
3747
3748 let plan = result.unwrap();
3749 assert!(!plan.sccs.is_empty());
3750 }
3751
3752 #[test]
3753 fn facts_are_metadata_and_not_executable_rules() {
3754 let mut program = Program::new();
3755 for value in [1, 2, 2] {
3756 program.rules.push(Rule {
3757 head: Atom {
3758 predicate: "base".to_string(),
3759 terms: vec![Term::Integer(value)],
3760 },
3761 body: vec![],
3762 });
3763 }
3764
3765 let mut lowerer = Lowerer::new();
3766 lowerer.set_strata(vec![vec!["base".to_string()]]);
3767
3768 let plan = lowerer.lower_program(&program).unwrap();
3769
3770 assert_eq!(
3771 plan.rules_by_scc.iter().map(Vec::len).sum::<usize>(),
3772 0,
3773 "facts are materialized by the relation loader, not executed as rules"
3774 );
3775 assert_eq!(lowerer.schemas.get("base").unwrap().arity(), 1);
3776 assert_eq!(lowerer.est_cardinality.get("base"), Some(&3));
3777 assert!(lowerer
3778 .sccs
3779 .iter()
3780 .any(|scc| scc.predicates.iter().any(|predicate| predicate == "base")));
3781
3782 let base_id = lowerer.rel_ids().get("base").copied().unwrap();
3783 assert_eq!(plan.rel_arities.get(&base_id), Some(&1));
3784 }
3785
3786 #[test]
3787 fn test_variable_env() {
3788 let mut env = VariableEnv::new();
3789 env.add_occurrence("X", "edge".to_string(), 0, 0);
3790 env.add_occurrence("Y", "edge".to_string(), 1, 1);
3791 env.add_occurrence("Y", "node".to_string(), 0, 2);
3792
3793 assert_eq!(env.get_column("X"), Some(0));
3794 assert_eq!(env.get_column("Y"), Some(1)); assert_eq!(env.get_column("Z"), None);
3796 }
3797
3798 #[test]
3799 fn test_inferred_scalar_type() {
3800 assert_eq!(
3801 Term::Variable("X".to_string()).inferred_scalar_type(),
3802 ScalarType::U64
3803 );
3804 assert_eq!(Term::Integer(42).inferred_scalar_type(), ScalarType::U32);
3805 assert_eq!(
3806 Term::Integer(i64::MAX).inferred_scalar_type(),
3807 ScalarType::I64
3808 );
3809 assert_eq!(Term::Float(3.25).inferred_scalar_type(), ScalarType::F64);
3810 assert_eq!(
3811 Term::Symbol(symbol::intern("foo")).inferred_scalar_type(),
3812 ScalarType::Symbol
3813 );
3814 }
3815
3816 #[test]
3817 fn test_convert_agg_op() {
3818 assert_eq!(convert_agg_op(&AggOp::Count), CoreAggOp::Count);
3819 assert_eq!(convert_agg_op(&AggOp::Sum), CoreAggOp::Sum);
3820 assert_eq!(convert_agg_op(&AggOp::Min), CoreAggOp::Min);
3821 assert_eq!(convert_agg_op(&AggOp::Max), CoreAggOp::Max);
3822 assert_eq!(convert_agg_op(&AggOp::LogSumExp), CoreAggOp::LogSumExp);
3823 }
3824
3825 #[test]
3826 fn test_variable_env_bind_updates_total_cols() {
3827 let mut env = VariableEnv::new();
3829 env.total_cols = 2; env.bind("A", 2, ScalarType::I64);
3833 assert_eq!(
3834 env.column_count(),
3835 3,
3836 "total_cols should be 3 after first bind"
3837 );
3838 assert_eq!(env.get_column("A"), Some(2));
3839
3840 env.bind("B", 3, ScalarType::I64);
3842 assert_eq!(
3843 env.column_count(),
3844 4,
3845 "total_cols should be 4 after second bind"
3846 );
3847 assert_eq!(env.get_column("B"), Some(3));
3848 }
3849
3850 #[test]
3851 fn test_lower_chained_is_expressions() {
3852 let rule = Rule {
3855 head: Atom {
3856 predicate: "result".to_string(),
3857 terms: vec![
3858 Term::Variable("A".to_string()),
3859 Term::Variable("B".to_string()),
3860 ],
3861 },
3862 body: vec![
3863 BodyLiteral::Positive(Atom {
3864 predicate: "input".to_string(),
3865 terms: vec![
3866 Term::Variable("X".to_string()),
3867 Term::Variable("Y".to_string()),
3868 ],
3869 }),
3870 BodyLiteral::IsExpr(IsExpr {
3871 target: "A".to_string(),
3872 expr: ArithExpr::Add(
3873 Box::new(ArithExpr::Variable("X".to_string())),
3874 Box::new(ArithExpr::Variable("Y".to_string())),
3875 ),
3876 }),
3877 BodyLiteral::IsExpr(IsExpr {
3878 target: "B".to_string(),
3879 expr: ArithExpr::Mul(
3880 Box::new(ArithExpr::Variable("A".to_string())),
3881 Box::new(ArithExpr::Integer(2)),
3882 ),
3883 }),
3884 ],
3885 };
3886
3887 let mut lowerer = Lowerer::new();
3888 lowerer.schemas.insert(
3889 "input".to_string(),
3890 Schema::new(vec![
3891 ("c0".to_string(), ScalarType::I64),
3892 ("c1".to_string(), ScalarType::I64),
3893 ]),
3894 );
3895
3896 let result = lowerer.lower_rule(&rule);
3897 assert!(
3898 result.is_ok(),
3899 "Lowering chained is-expressions should succeed: {:?}",
3900 result.err()
3901 );
3902
3903 let node = result.unwrap();
3904
3905 fn count_projects(node: &RirNode) -> usize {
3913 match node {
3914 RirNode::Project { input, .. } => 1 + count_projects(input),
3915 _ => 0,
3916 }
3917 }
3918
3919 let project_count = count_projects(&node);
3921 assert!(
3922 project_count >= 2,
3923 "Expected at least 2 Project nodes for chained is-exprs, got {}",
3924 project_count
3925 );
3926
3927 if let RirNode::Project { columns, .. } = &node {
3929 assert_eq!(columns.len(), 2, "Head has 2 variables");
3930 assert_eq!(columns[0], ProjectExpr::Column(2), "A should be column 2");
3932 assert_eq!(columns[1], ProjectExpr::Column(3), "B should be column 3");
3933 } else {
3934 panic!("Expected top-level Project node");
3935 }
3936 }
3937
3938 #[test]
3939 fn test_u64_comparison_type_from_pred_decl() {
3940 let mut program = Program::new();
3942
3943 program.predicates.push(pred_decl(
3945 "count_data",
3946 vec![ScalarType::Symbol, ScalarType::U64],
3947 ));
3948
3949 program.rules.push(Rule {
3951 head: Atom {
3952 predicate: "count_data".to_string(),
3953 terms: vec![
3954 Term::Symbol(xlog_core::symbol::intern("alice")),
3955 Term::Integer(5),
3956 ],
3957 },
3958 body: vec![],
3959 });
3960
3961 program.predicates.push(pred_decl(
3963 "big_count",
3964 vec![ScalarType::Symbol, ScalarType::U64],
3965 ));
3966
3967 program.rules.push(Rule {
3969 head: Atom {
3970 predicate: "big_count".to_string(),
3971 terms: vec![
3972 Term::Variable("Name".to_string()),
3973 Term::Variable("Count".to_string()),
3974 ],
3975 },
3976 body: vec![
3977 BodyLiteral::Positive(Atom {
3978 predicate: "count_data".to_string(),
3979 terms: vec![
3980 Term::Variable("Name".to_string()),
3981 Term::Variable("Count".to_string()),
3982 ],
3983 }),
3984 BodyLiteral::Comparison(Comparison {
3985 left: Term::Variable("Count".to_string()),
3986 op: CompOp::Ge,
3987 right: Term::Integer(3),
3988 }),
3989 ],
3990 });
3991
3992 let mut lowerer = Lowerer::new();
3993 lowerer.infer_schemas(&program).unwrap();
3994
3995 let schema = lowerer
3997 .schemas
3998 .get("count_data")
3999 .expect("schema for count_data");
4000 assert_eq!(
4001 schema.column_type(0),
4002 Some(ScalarType::Symbol),
4003 "First column should be Symbol"
4004 );
4005 assert_eq!(
4006 schema.column_type(1),
4007 Some(ScalarType::U64),
4008 "Second column should be U64"
4009 );
4010
4011 lowerer.set_strata(vec![
4013 vec!["count_data".to_string()],
4014 vec!["big_count".to_string()],
4015 ]);
4016 lowerer.build_sccs(&program);
4017
4018 let rule = &program.rules[1]; let result = lowerer.lower_rule(rule);
4020 assert!(
4021 result.is_ok(),
4022 "Lowering should succeed: {:?}",
4023 result.err()
4024 );
4025
4026 fn find_compare_const(node: &RirNode) -> Option<&ConstValue> {
4028 match node {
4029 RirNode::Filter { predicate, input } => {
4030 if let Expr::Compare { right, .. } = predicate {
4031 if let Expr::Const(val) = right.as_ref() {
4032 return Some(val);
4033 }
4034 }
4035 find_compare_const(input)
4036 }
4037 RirNode::Project { input, .. } => find_compare_const(input),
4038 RirNode::Join { left, right, .. } => {
4039 find_compare_const(left).or_else(|| find_compare_const(right))
4040 }
4041 _ => None,
4042 }
4043 }
4044
4045 let node = result.unwrap();
4046 let const_val = find_compare_const(&node);
4047 assert!(const_val.is_some(), "Should find a constant in comparison");
4048
4049 match const_val.unwrap() {
4051 ConstValue::U64(v) => assert_eq!(*v, 3, "Value should be 3"),
4052 other => panic!("Expected U64(3), got {:?}", other),
4053 }
4054 }
4055
4056 #[test]
4057 fn test_u64_comparison_with_aggregation() {
4058 use crate::ast::AggExpr;
4059
4060 let mut program = Program::new();
4062
4063 program.predicates.push(pred_decl(
4065 "reports_to",
4066 vec![ScalarType::Symbol, ScalarType::Symbol],
4067 ));
4068
4069 program.rules.push(Rule {
4071 head: Atom {
4072 predicate: "reports_to".to_string(),
4073 terms: vec![
4074 Term::Symbol(xlog_core::symbol::intern("alice")),
4075 Term::Symbol(xlog_core::symbol::intern("bob")),
4076 ],
4077 },
4078 body: vec![],
4079 });
4080 program.rules.push(Rule {
4081 head: Atom {
4082 predicate: "reports_to".to_string(),
4083 terms: vec![
4084 Term::Symbol(xlog_core::symbol::intern("carol")),
4085 Term::Symbol(xlog_core::symbol::intern("bob")),
4086 ],
4087 },
4088 body: vec![],
4089 });
4090
4091 program.predicates.push(pred_decl(
4093 "direct_count",
4094 vec![ScalarType::Symbol, ScalarType::U64],
4095 ));
4096
4097 program.rules.push(Rule {
4099 head: Atom {
4100 predicate: "direct_count".to_string(),
4101 terms: vec![
4102 Term::Variable("Mgr".to_string()),
4103 Term::Aggregate(AggExpr {
4104 op: AggOp::Count,
4105 variable: "Emp".to_string(),
4106 }),
4107 ],
4108 },
4109 body: vec![BodyLiteral::Positive(Atom {
4110 predicate: "reports_to".to_string(),
4111 terms: vec![
4112 Term::Variable("Emp".to_string()),
4113 Term::Variable("Mgr".to_string()),
4114 ],
4115 })],
4116 });
4117
4118 program.predicates.push(pred_decl(
4120 "big_manager",
4121 vec![ScalarType::Symbol, ScalarType::U64],
4122 ));
4123
4124 program.rules.push(Rule {
4126 head: Atom {
4127 predicate: "big_manager".to_string(),
4128 terms: vec![
4129 Term::Variable("Mgr".to_string()),
4130 Term::Variable("Count".to_string()),
4131 ],
4132 },
4133 body: vec![
4134 BodyLiteral::Positive(Atom {
4135 predicate: "direct_count".to_string(),
4136 terms: vec![
4137 Term::Variable("Mgr".to_string()),
4138 Term::Variable("Count".to_string()),
4139 ],
4140 }),
4141 BodyLiteral::Comparison(Comparison {
4142 left: Term::Variable("Count".to_string()),
4143 op: CompOp::Ge,
4144 right: Term::Integer(2),
4145 }),
4146 ],
4147 });
4148
4149 let mut lowerer = Lowerer::new();
4150 lowerer.infer_schemas(&program).unwrap();
4151
4152 let schema = lowerer
4154 .schemas
4155 .get("direct_count")
4156 .expect("schema for direct_count");
4157 assert_eq!(
4158 schema.column_type(0),
4159 Some(ScalarType::Symbol),
4160 "First column should be Symbol"
4161 );
4162 assert_eq!(
4163 schema.column_type(1),
4164 Some(ScalarType::U64),
4165 "Second column should be U64"
4166 );
4167
4168 lowerer.set_strata(vec![
4169 vec!["reports_to".to_string()],
4170 vec!["direct_count".to_string()],
4171 vec!["big_manager".to_string()],
4172 ]);
4173 lowerer.build_sccs(&program);
4174
4175 let big_manager_rule = &program.rules[3];
4177 let result = lowerer.lower_rule(big_manager_rule);
4178 assert!(
4179 result.is_ok(),
4180 "Lowering should succeed: {:?}",
4181 result.err()
4182 );
4183
4184 fn find_compare_const(node: &RirNode) -> Option<&ConstValue> {
4186 match node {
4187 RirNode::Filter { predicate, input } => {
4188 if let Expr::Compare { right, .. } = predicate {
4189 if let Expr::Const(val) = right.as_ref() {
4190 return Some(val);
4191 }
4192 }
4193 find_compare_const(input)
4194 }
4195 RirNode::Project { input, .. } => find_compare_const(input),
4196 RirNode::Join { left, right, .. } => {
4197 find_compare_const(left).or_else(|| find_compare_const(right))
4198 }
4199 _ => None,
4200 }
4201 }
4202
4203 let node = result.unwrap();
4204 let const_val = find_compare_const(&node);
4205 assert!(const_val.is_some(), "Should find a constant in comparison");
4206
4207 match const_val.unwrap() {
4209 ConstValue::U64(v) => assert_eq!(*v, 2, "Value should be 2"),
4210 other => panic!("Expected U64(2), got {:?}", other),
4211 }
4212 }
4213
4214 #[test]
4215 fn declared_head_constant_is_projected_with_the_schema_type() {
4216 let program = crate::parse_program(
4217 r#"
4218 pred real(f64).
4219 seed().
4220 real(1) :- seed().
4221 "#,
4222 )
4223 .expect("parse typed head-constant fixture");
4224 let mut lowerer = Lowerer::new();
4225 lowerer
4226 .infer_schemas(&program)
4227 .expect("infer declared schemas");
4228 lowerer
4229 .validate_rule_types(&program)
4230 .expect("validate supported literal conversion");
4231
4232 let node = lowerer
4233 .lower_rule(&program.rules[1])
4234 .expect("lower typed head constant");
4235 let RirNode::Project { columns, .. } = node else {
4236 panic!("constant rule head should lower to a projection");
4237 };
4238 assert_eq!(
4239 columns,
4240 vec![ProjectExpr::Computed(
4241 Expr::Const(ConstValue::F64(1.0)),
4242 ScalarType::F64,
4243 )]
4244 );
4245 }
4246}
4247
4248#[cfg(test)]
4249mod cross_predicate_type_tests {
4250 use super::*;
4251
4252 const ILL_TYPED_BRIDGE: &str = r#"
4258pred node(u32, symbol).
4259pred connected(u32, u32).
4260pred bridge(symbol, u32).
4261
4262node(1, "alice").
4263connected(1, 2).
4264
4265bridge(A, B) :- connected(A, B), node(A, _).
4266
4267?- bridge(W, X).
4268"#;
4269
4270 #[test]
4271 fn cross_predicate_schema_mismatch_is_a_compilation_error() {
4272 let program = crate::parse_program(ILL_TYPED_BRIDGE).expect("example parses");
4273 let mut lowerer = Lowerer::new();
4274 let err = lowerer
4275 .lower_program(&program)
4276 .expect_err("ill-typed program must not lower");
4277 let msg = err.to_string();
4278 assert!(
4279 matches!(err, xlog_core::XlogError::Compilation(_)),
4280 "expected a compilation error, got: {msg}"
4281 );
4282 assert!(
4283 msg.contains("bridge"),
4284 "must name the head predicate: {msg}"
4285 );
4286 assert!(
4287 msg.contains("connected") || msg.contains("node"),
4288 "must name the conflicting body predicate: {msg}"
4289 );
4290 assert!(msg.contains('A'), "must name the variable: {msg}");
4291 assert!(msg.contains("position 0"), "must name the position: {msg}");
4292 }
4293
4294 #[test]
4295 fn well_typed_cross_predicate_rule_still_lowers() {
4296 let source = r#"
4297pred node(u32, symbol).
4298pred connected(u32, u32).
4299pred bridge(u32, u32).
4300
4301node(1, "alice").
4302connected(1, 2).
4303
4304bridge(A, B) :- connected(A, B), node(A, _).
4305
4306?- bridge(W, X).
4307"#;
4308 let program = crate::parse_program(source).expect("example parses");
4309 let mut lowerer = Lowerer::new();
4310 lowerer
4311 .lower_program(&program)
4312 .expect("well-typed program lowers");
4313 }
4314
4315 #[test]
4316 fn probabilistic_fact_schema_precedes_body_only_variable_defaults() {
4317 let source = r#"
43180.5::possible(0).
4319observed(0).
4320accepted(X) :- possible(X), observed(X).
4321?- accepted(X).
4322"#;
4323 let program = crate::parse_program(source).expect("probabilistic join parses");
4324 let mut lowerer = Lowerer::new();
4325 lowerer
4326 .lower_program(&program)
4327 .expect("probabilistic facts provide body predicate schemas");
4328 assert_eq!(
4329 lowerer
4330 .schemas()
4331 .get("possible")
4332 .expect("possible schema")
4333 .column_type(0),
4334 Some(ScalarType::U32)
4335 );
4336 }
4337
4338 #[test]
4339 fn annotated_disjunction_schema_precedes_body_only_variable_defaults() {
4340 let source = r#"
43410.5::possible(0); 0.5::possible(1).
4342observed(0).
4343accepted(X) :- possible(X), observed(X).
4344?- accepted(X).
4345"#;
4346 let program = crate::parse_program(source).expect("annotated-disjunction join parses");
4347 let mut lowerer = Lowerer::new();
4348 lowerer
4349 .lower_program(&program)
4350 .expect("annotated-disjunction choices provide body predicate schemas");
4351 assert_eq!(
4352 lowerer
4353 .schemas()
4354 .get("possible")
4355 .expect("possible schema")
4356 .column_type(0),
4357 Some(ScalarType::U32)
4358 );
4359 }
4360
4361 #[test]
4362 fn body_body_schema_conflict_is_a_compilation_error() {
4363 let source = r#"
4366pred label(symbol).
4367pred count(u32).
4368pred out(u32).
4369
4370label("x").
4371count(1).
4372
4373out(A) :- count(A), label(A).
4374
4375?- out(W).
4376"#;
4377 let program = crate::parse_program(source).expect("example parses");
4378 let mut lowerer = Lowerer::new();
4379 let err = lowerer
4380 .lower_program(&program)
4381 .expect_err("body-body conflict must not lower");
4382 assert!(
4383 matches!(err, xlog_core::XlogError::Compilation(_)),
4384 "expected a compilation error, got: {err}"
4385 );
4386 }
4387}