1use anyhow::{Result, anyhow};
27use rustc_hash::{FxHashMap, FxHashSet};
28use mangle_ir::{Inst, InstId, Ir, NameId};
29
30use crate::name_trie::NameTrie;
31use crate::type_expr::{self, TypeContext};
32
33pub struct BoundsChecker<'a> {
35 ir: &'a mut Ir,
36 name_trie: NameTrie,
37 rel_type_map: FxHashMap<NameId, Vec<Vec<InstId>>>,
40 rules_map: FxHashMap<NameId, Vec<(InstId, Vec<InstId>, Vec<InstId>)>>,
42 inferred: FxHashMap<NameId, Vec<Vec<InstId>>>,
44 visiting: FxHashSet<NameId>,
46 fresh_var_counter: usize,
48}
49
50impl<'a> BoundsChecker<'a> {
51 pub fn new(ir: &'a mut Ir) -> Self {
52 Self {
53 ir,
54 name_trie: NameTrie::new(),
55 rel_type_map: FxHashMap::default(),
56 rules_map: FxHashMap::default(),
57 inferred: FxHashMap::default(),
58 visiting: FxHashSet::default(),
59 fresh_var_counter: 0,
60 }
61 }
62
63 pub fn check(&mut self) -> Result<()> {
65 self.collect_declarations()?;
66 self.build_rules_map();
67 self.check_arity_consistency()?;
68 self.check_all_clauses()
69 }
70
71 fn fresh_var(&mut self) -> NameId {
73 let name = format!("?X{}", self.fresh_var_counter);
74 self.fresh_var_counter += 1;
75 self.ir.intern_name(&name)
76 }
77
78 fn collect_declarations(&mut self) -> Result<()> {
80 let insts: Vec<Inst> = self.ir.insts.clone();
81 for inst in &insts {
82 if let Inst::Decl { atom, bounds, .. } = inst {
83 let pred_name = self.atom_predicate(*atom);
84 if let Some(pred) = pred_name {
85 let mut alternatives = Vec::new();
86 for bound_id in bounds {
87 if let Inst::BoundDecl { base_terms } = self.ir.get(*bound_id) {
88 let base_terms = base_terms.clone();
89 for term in &base_terms {
91 self.name_trie.collect(self.ir, *term);
92 }
93 let any = type_expr::find_or_create_name(self.ir, "/any");
95 let mut ctx = TypeContext::default();
96 for term in &base_terms {
97 let mut vars = FxHashSet::default();
98 type_expr::collect_vars(self.ir, *term, &mut vars);
99 for v in vars {
100 ctx.entry(v).or_insert(any);
101 }
102 }
103 for term in &base_terms {
105 type_expr::wellformed_type(self.ir, &ctx, *term)?;
106 }
107 alternatives.push(base_terms);
108 }
109 }
110 if !alternatives.is_empty() {
111 self.rel_type_map.insert(pred, alternatives);
112 }
113 }
114 }
115 }
116 Ok(())
117 }
118
119 fn build_rules_map(&mut self) {
121 let insts: Vec<Inst> = self.ir.insts.clone();
122 for inst in &insts {
123 if let Inst::Rule {
124 head,
125 premises,
126 transform,
127 } = inst
128 {
129 if !premises.is_empty() || !transform.is_empty() {
131 if let Some(pred) = self.atom_predicate(*head) {
132 self.rules_map
133 .entry(pred)
134 .or_default()
135 .push((*head, premises.clone(), transform.clone()));
136 }
137 }
138 }
139 }
140 }
141
142 fn check_arity_consistency(&self) -> Result<()> {
147 let mut arity_map: FxHashMap<NameId, (usize, String)> = FxHashMap::default();
149 let mut errors: Vec<String> = Vec::new();
150
151 let insts: Vec<Inst> = self.ir.insts.clone();
152 for inst in &insts {
153 if let Inst::Rule { head, premises, transform } = inst {
154 let is_fact = premises.is_empty() && transform.is_empty();
155 if let Some(pred) = self.atom_predicate(*head) {
156 let expected_args = if self.ir.temporal_predicates.contains(&pred) {
157 let args = self.atom_args(*head);
158 if args.len() >= 2 {
159 args.len() - 2
160 } else {
161 args.len()
162 }
163 } else {
164 self.atom_args(*head).len()
165 };
166
167 let kind = if is_fact { "fact" } else { "rule" };
168 let pred_name = self.ir.resolve_name(pred).to_string();
169 let location = format!("{} {}({} arg(s))", kind, pred_name, expected_args);
170
171 if let Some(&(first_arity, ref first_location)) = arity_map.get(&pred) {
172 if first_arity != expected_args {
173 errors.push(format!(
174 "predicate '{}' used with inconsistent arity: {} vs {}",
175 pred_name, first_location, location
176 ));
177 }
178 } else {
179 arity_map.insert(pred, (expected_args, location));
180 }
181 }
182 }
183 }
184
185 if errors.is_empty() {
186 Ok(())
187 } else {
188 Err(anyhow!("arity error: {}", errors.join("; ")))
189 }
190 }
191
192 fn check_all_clauses(&mut self) -> Result<()> {
194 let insts: Vec<Inst> = self.ir.insts.clone();
195 for inst in &insts {
196 match inst {
197 Inst::Rule {
198 head,
199 premises,
200 transform,
201 } => {
202 let head = *head;
203 let premises = premises.clone();
204 let transform = transform.clone();
205 if let Some(pred) = self.atom_predicate(head) {
206 if let Some(alternatives) = self.rel_type_map.get(&pred).cloned() {
207 if premises.is_empty() && transform.is_empty() {
208 self.check_fact(head, &alternatives)?;
209 } else {
210 self.check_rule(head, &premises, &transform, &alternatives)?;
211 }
212 }
213 }
214 }
215 _ => {}
216 }
217 }
218 Ok(())
219 }
220
221 fn check_fact(&self, head: InstId, alternatives: &[Vec<InstId>]) -> Result<()> {
223 let args = self.atom_args(head);
224 let pred = self.atom_predicate(head).unwrap();
225 if args.is_empty() && alternatives.is_empty() {
226 return Ok(());
227 }
228
229 let mut errors = Vec::new();
230 for alt in alternatives {
231 match self.check_fact_against_bound(pred, &args, alt) {
232 Ok(()) => return Ok(()),
233 Err(e) => errors.push(e.to_string()),
234 }
235 }
236
237 if errors.is_empty() {
238 return Ok(());
239 }
240
241 let pred_name = self
242 .atom_predicate(head)
243 .map(|p| self.ir.resolve_name(p).to_string())
244 .unwrap_or_else(|| "?".to_string());
245 Err(anyhow!(
246 "fact {}(...) matches none of the bound decls: {}",
247 pred_name,
248 errors.join("; ")
249 ))
250 }
251
252 fn check_fact_against_bound(
254 &self,
255 pred: NameId,
256 args: &[InstId],
257 bound: &[InstId],
258 ) -> Result<()> {
259 let is_temporal = self.ir.temporal_predicates.contains(&pred);
260 let expected_args = if is_temporal {
261 bound.len() + 2
262 } else {
263 bound.len()
264 };
265 if args.len() != expected_args {
266 return Err(anyhow!(
267 "arity mismatch: fact has {} args, bound has {}{}",
268 args.len(),
269 bound.len(),
270 if is_temporal { " (+2 temporal)" } else { "" }
271 ));
272 }
273 for (i, (arg, type_expr)) in args.iter().zip(bound.iter()).enumerate() {
274 if !type_expr::has_type(self.ir, *type_expr, *arg) {
275 let arg_desc = self.describe_inst(*arg);
276 let type_desc = self.describe_inst(*type_expr);
277 return Err(anyhow!(
278 "argument {} ({}) does not have type {}",
279 i,
280 arg_desc,
281 type_desc
282 ));
283 }
284 }
285 Ok(())
286 }
287
288 fn check_rule(
293 &mut self,
294 head: InstId,
295 premises: &[InstId],
296 transforms: &[InstId],
297 alternatives: &[Vec<InstId>],
298 ) -> Result<()> {
299 let head_args = self.atom_args(head);
300 let pred = self.atom_predicate(head).unwrap();
301 let is_temporal = self.ir.temporal_predicates.contains(&pred);
302
303 let mut state = InferState::new();
305 for premise_id in premises {
306 state = self.infer_from_premise(*premise_id, state)?;
307 }
308
309 for transform_id in transforms {
311 if let Inst::Transform { var, app } = self.ir.get(*transform_id) {
312 let var = *var;
313 let app = *app;
314 if let Some(v) = var {
315 let tpe = self.bound_of_arg(app, &state.as_map());
316 state.add_or_refine_with_ir(self.ir, v, tpe);
317 }
318 }
319 }
320
321 let var_ranges = state.as_map();
323 let inferred: Vec<InstId> = head_args
324 .iter()
325 .map(|arg| self.bound_of_arg(*arg, &var_ranges))
326 .collect();
327
328 let check_len = if is_temporal && inferred.len() >= 2 {
330 inferred.len() - 2
331 } else {
332 inferred.len()
333 };
334 let inferred_trimmed = &inferred[..check_len];
335
336 let mut errors = Vec::new();
338 for alt in alternatives {
339 if alt.len() != inferred_trimmed.len() {
340 errors.push(format!(
341 "arity mismatch: head has {} args, bound has {}",
342 inferred_trimmed.len(),
343 alt.len()
344 ));
345 continue;
346 }
347 let any = type_expr::find_or_create_name(self.ir, "/any");
349 let mut ctx = TypeContext::default();
350 for t in alt.iter() {
351 let mut vars = FxHashSet::default();
352 type_expr::collect_vars(self.ir, *t, &mut vars);
353 for v in vars {
354 ctx.entry(v).or_insert(any);
355 }
356 }
357 let all_conform = inferred_trimmed
358 .iter()
359 .zip(alt.iter())
360 .all(|(inf, decl)| type_expr::set_conforms(self.ir, &ctx, *inf, *decl));
361 if all_conform {
362 return Ok(());
363 }
364 errors.push(format!(
365 "inferred [{}] does not conform to declared [{}]",
366 inferred_trimmed
367 .iter()
368 .map(|i| self.describe_inst(*i))
369 .collect::<Vec<_>>()
370 .join(", "),
371 alt.iter()
372 .map(|i| self.describe_inst(*i))
373 .collect::<Vec<_>>()
374 .join(", "),
375 ));
376 }
377
378 if errors.is_empty() {
379 return Ok(());
380 }
381
382 let pred_name = self
383 .atom_predicate(head)
384 .map(|p| self.ir.resolve_name(p).to_string())
385 .unwrap_or_else(|| "?".to_string());
386 Err(anyhow!(
387 "rule for {}(...) does not conform to declared bounds: {}",
388 pred_name,
389 errors.join("; ")
390 ))
391 }
392
393 fn infer_from_premise(
395 &mut self,
396 premise_id: InstId,
397 mut state: InferState,
398 ) -> Result<InferState> {
399 match self.ir.get(premise_id) {
400 Inst::Atom { predicate, args } => {
401 let pred = *predicate;
402 let args = args.clone();
403
404 let pred_name = self.ir.resolve_name(pred).to_string();
406 if pred_name == ":match_prefix" {
407 return self.infer_match_prefix(&args, state);
408 }
409 if pred_name == ":match_field" {
410 return self.infer_match_field(&args, state);
411 }
412 if pred_name == ":match_entry" {
413 return self.infer_match_entry(&args, state);
414 }
415 if pred_name == ":list:member" {
416 return self.infer_list_member(&args, state);
417 }
418
419 let var_ranges = state.as_map();
421 let feasible =
422 self.get_or_infer_alternatives(pred, &args, &var_ranges);
423
424 if !feasible.is_empty() {
425 let first = &feasible[0].clone();
427 for (arg, type_id) in args.iter().zip(first.iter()) {
428 if let Inst::Var(v) = self.ir.get(*arg) {
429 let v = *v;
430 state.add_or_refine_with_ir(self.ir, v, *type_id);
431 }
432 }
433 } else if let Some(alternatives) = self.rel_type_map.get(&pred).cloned() {
434 if let Some(first_alt) = alternatives.first() {
436 for (arg, type_id) in args.iter().zip(first_alt.iter()) {
437 if let Inst::Var(v) = self.ir.get(*arg) {
438 let v = *v;
439 state.add_or_refine_with_ir(self.ir, v, *type_id);
440 }
441 }
442 }
443 }
444 Ok(state)
445 }
446 Inst::NegAtom(inner) => {
447 let inner = *inner;
448 if let Inst::Atom { predicate, args } = self.ir.get(inner) {
451 let pred = *predicate;
452 let args = args.clone();
453 let pred_name = self.ir.resolve_name(pred).to_string();
454
455 if pred_name == ":match_prefix" && args.len() >= 2 {
456 if let Inst::Var(v) = self.ir.get(args[0]) {
458 let v = *v;
459 let bound = self.bound_of_arg(args[1], &state.as_map());
460 if let Some(existing) = state.as_map().get(&v).copied() {
461 if type_expr::is_union_type(self.ir, existing) {
462 let refined =
463 type_expr::remove_from_union_type(self.ir, bound, existing);
464 if !type_expr::is_empty_type(self.ir, refined) {
465 state.set_var(v, refined);
466 }
467 }
468 }
469 }
470 }
471 }
473 Ok(state)
474 }
475 Inst::Eq(left, right) => {
476 let left = *left;
477 let right = *right;
478 let var_ranges = state.as_map();
479
480 if let Inst::Var(lv) = self.ir.get(left) {
481 let lv = *lv;
482 let tpe = self.bound_of_arg(right, &var_ranges);
483 state.add_or_refine_with_ir(self.ir, lv, tpe);
484 }
485 if let Inst::Var(rv) = self.ir.get(right) {
486 let rv = *rv;
487 let tpe = self.bound_of_arg(left, &state.as_map());
488 state.add_or_refine_with_ir(self.ir, rv, tpe);
489 }
490 Ok(state)
491 }
492 Inst::Ineq(left, right) => {
493 let left = *left;
494 let right = *right;
495 let var_ranges = state.as_map();
496
497 let left_tpe = self.bound_of_arg(left, &var_ranges);
499 let right_tpe = self.bound_of_arg(right, &var_ranges);
500 let ctx = TypeContext::default();
501 let meet = type_expr::lower_bound(self.ir, &ctx, &[left_tpe, right_tpe]);
502 if !type_expr::is_empty_type(self.ir, meet) {
503 if let Inst::Var(lv) = self.ir.get(left) {
504 let lv = *lv;
505 state.add_or_refine_with_ir(self.ir, lv, meet);
506 }
507 if let Inst::Var(rv) = self.ir.get(right) {
508 let rv = *rv;
509 state.add_or_refine_with_ir(self.ir, rv, meet);
510 }
511 }
512 Ok(state)
513 }
514 _ => Ok(state),
515 }
516 }
517
518 fn feasible_alternatives(
526 &mut self,
527 alternatives: &[Vec<InstId>],
528 args: &[InstId],
529 var_ranges: &FxHashMap<NameId, InstId>,
530 ) -> Vec<Vec<InstId>> {
531 let mut feasible = Vec::new();
532
533 for alt in alternatives {
534 if alt.len() != args.len() {
535 continue;
536 }
537
538 let mut arg_bound = Vec::new();
542 for (i, arg) in args.iter().enumerate() {
543 if let Inst::Var(v) = self.ir.get(*arg) {
544 let v = *v;
545 if let Some(&range) = var_ranges.get(&v) {
546 arg_bound.push(range);
547 } else {
548 arg_bound.push(alt[i]);
550 }
551 } else {
552 arg_bound.push(self.bound_of_arg(*arg, var_ranges));
553 }
554 }
555
556 let mut type_vars = FxHashSet::default();
558 for t in alt {
559 type_expr::collect_vars(self.ir, *t, &mut type_vars);
560 }
561
562 let mut subst: FxHashMap<NameId, InstId> = FxHashMap::default();
564 if !type_vars.is_empty() {
565 for v in &type_vars {
566 let fresh = self.fresh_var();
567 let fresh_id = self.ir.add_inst(Inst::Var(fresh));
568 subst.insert(*v, fresh_id);
569 }
570 }
571
572 let arg_bound_subst: Vec<InstId> = arg_bound
574 .iter()
575 .map(|t| type_expr::apply_subst(self.ir, *t, &subst))
576 .collect();
577 let alt_subst: Vec<InstId> = alt
578 .iter()
579 .map(|t| type_expr::apply_subst(self.ir, *t, &subst))
580 .collect();
581
582 let any = type_expr::find_or_create_name(self.ir, "/any");
584 let mut ctx = TypeContext::default();
585 for fresh_id in subst.values() {
586 if let Inst::Var(v) = self.ir.get(*fresh_id) {
587 ctx.insert(*v, any);
588 }
589 }
590
591 let mut is_feasible = true;
593 let mut result_types = Vec::new();
594 for (ab, at) in arg_bound_subst.iter().zip(alt_subst.iter()) {
595 let meet = type_expr::lower_bound(self.ir, &ctx, &[*ab, *at]);
596 if type_expr::is_empty_type(self.ir, meet) {
597 is_feasible = false;
598 break;
599 }
600 result_types.push(meet);
601 }
602
603 if is_feasible {
604 feasible.push(result_types);
605 }
606 }
607 feasible
608 }
609
610 fn get_or_infer_alternatives(
615 &mut self,
616 pred: NameId,
617 args: &[InstId],
618 var_ranges: &FxHashMap<NameId, InstId>,
619 ) -> Vec<Vec<InstId>> {
620 if let Some(alts) = self.rel_type_map.get(&pred).cloned() {
622 return self.feasible_alternatives(&alts, args, var_ranges);
623 }
624
625 if let Some(alts) = self.inferred.get(&pred).cloned() {
627 return self.feasible_alternatives(&alts, args, var_ranges);
628 }
629
630 if self.visiting.contains(&pred) {
633 let any = type_expr::find_or_create_name(self.ir, "/any");
634 return vec![vec![any; args.len()]];
635 }
636
637 self.visiting.insert(pred);
639 let inferred = self.infer_rel_types(pred);
640 self.visiting.remove(&pred);
641
642 if !inferred.is_empty() {
643 self.inferred.insert(pred, inferred.clone());
644 return self.feasible_alternatives(&inferred, args, var_ranges);
645 }
646
647 Vec::new()
648 }
649
650 fn infer_rel_types(&mut self, pred: NameId) -> Vec<Vec<InstId>> {
655 let rules = match self.rules_map.get(&pred) {
656 Some(r) => r.clone(),
657 None => return Vec::new(),
658 };
659
660 let mut alternatives: Vec<Vec<InstId>> = Vec::new();
661
662 for (head, premises, transforms) in &rules {
663 if let Some(inferred) = self.infer_clause(*head, premises, transforms) {
665 alternatives.push(inferred);
666 }
667 }
668
669 alternatives
670 }
671
672 fn infer_clause(
674 &mut self,
675 head: InstId,
676 premises: &[InstId],
677 transforms: &[InstId],
678 ) -> Option<Vec<InstId>> {
679 let head_args = self.atom_args(head);
680 let mut state = InferState::new();
681
682 for premise_id in premises {
683 match self.infer_from_premise(*premise_id, state) {
684 Ok(new_state) => state = new_state,
685 Err(_) => return None,
686 }
687 }
688
689 for transform_id in transforms {
691 if let Inst::Transform { var, app } = self.ir.get(*transform_id) {
692 let var = *var;
693 let app = *app;
694 if let Some(v) = var {
695 let tpe = self.bound_of_arg(app, &state.as_map());
696 state.add_or_refine_with_ir(self.ir, v, tpe);
697 }
698 }
699 }
700
701 let var_ranges = state.as_map();
703 let inferred: Vec<InstId> = head_args
704 .iter()
705 .map(|arg| self.bound_of_arg(*arg, &var_ranges))
706 .collect();
707
708 Some(inferred)
709 }
710
711 fn infer_match_prefix(
713 &mut self,
714 args: &[InstId],
715 mut state: InferState,
716 ) -> Result<InferState> {
717 if args.len() != 2 {
718 return Ok(state);
719 }
720 let var_ranges = state.as_map();
721 let tpe = self.bound_of_arg(args[0], &var_ranges);
722 let prefix = self.bound_of_arg(args[1], &var_ranges);
723
724 let ctx = TypeContext::default();
725 let meet = type_expr::lower_bound(self.ir, &ctx, &[tpe, prefix]);
726 if !type_expr::is_empty_type(self.ir, meet) {
727 if let Inst::Var(v) = self.ir.get(args[0]) {
728 let v = *v;
729 state.add_or_refine_with_ir(self.ir, v, meet);
730 }
731 let name_type = type_expr::find_or_create_name(self.ir, "/name");
733 if let Inst::Var(v) = self.ir.get(args[1]) {
734 let v = *v;
735 state.add_or_refine_with_ir(self.ir, v, name_type);
736 }
737 }
738 Ok(state)
739 }
740
741 fn infer_match_field(
743 &mut self,
744 args: &[InstId],
745 mut state: InferState,
746 ) -> Result<InferState> {
747 if args.len() != 3 {
748 return Ok(state);
749 }
750 let var_ranges = state.as_map();
751 let scrutinee_type = self.bound_of_arg(args[0], &var_ranges);
752
753 let field_name_id = match self.ir.get(args[1]) {
755 Inst::Name(n) => Some(*n),
756 _ => None,
757 };
758
759 if let Some(field) = field_name_id {
760 if type_expr::is_struct_type(self.ir, scrutinee_type)
761 || type_expr::is_tagged_union_type(self.ir, scrutinee_type)
762 || type_expr::is_union_type(self.ir, scrutinee_type)
763 {
764 if let Some(field_type) =
765 type_expr::struct_type_field_deep(self.ir, scrutinee_type, field)
766 {
767 let ctx = TypeContext::default();
769 let value_bound = self.bound_of_arg(args[2], &state.as_map());
770 let meet =
771 type_expr::lower_bound(self.ir, &ctx, &[value_bound, field_type]);
772 if !type_expr::is_empty_type(self.ir, meet) {
773 if let Inst::Var(v) = self.ir.get(args[2]) {
774 let v = *v;
775 state.add_or_refine_with_ir(self.ir, v, meet);
776 }
777 }
778 }
779 }
780 }
781 let any = type_expr::find_or_create_name(self.ir, "/any");
783 if let Inst::Var(v) = self.ir.get(args[0]) {
784 let v = *v;
785 state.add_or_refine_with_ir(self.ir, v, any);
786 }
787 let name_type = type_expr::find_or_create_name(self.ir, "/name");
789 if let Inst::Var(v) = self.ir.get(args[1]) {
790 let v = *v;
791 state.add_or_refine_with_ir(self.ir, v, name_type);
792 }
793 Ok(state)
794 }
795
796 fn infer_match_entry(
798 &mut self,
799 args: &[InstId],
800 mut state: InferState,
801 ) -> Result<InferState> {
802 if args.len() != 3 {
803 return Ok(state);
804 }
805 let var_ranges = state.as_map();
806 let map_type = self.bound_of_arg(args[0], &var_ranges);
807
808 if type_expr::is_map_type(self.ir, map_type) {
809 if let Some((key_type, val_type)) = type_expr::map_type_args(self.ir, map_type) {
810 let ctx = TypeContext::default();
811
812 let key_bound = self.bound_of_arg(args[1], &state.as_map());
814 let key_meet =
815 type_expr::lower_bound(self.ir, &ctx, &[key_bound, key_type]);
816 if !type_expr::is_empty_type(self.ir, key_meet) {
817 if let Inst::Var(v) = self.ir.get(args[1]) {
818 let v = *v;
819 state.add_or_refine_with_ir(self.ir, v, key_meet);
820 }
821 }
822
823 let val_bound = self.bound_of_arg(args[2], &state.as_map());
825 let val_meet =
826 type_expr::lower_bound(self.ir, &ctx, &[val_bound, val_type]);
827 if !type_expr::is_empty_type(self.ir, val_meet) {
828 if let Inst::Var(v) = self.ir.get(args[2]) {
829 let v = *v;
830 state.add_or_refine_with_ir(self.ir, v, val_meet);
831 }
832 }
833 }
834 }
835 Ok(state)
836 }
837
838 fn infer_list_member(
840 &mut self,
841 args: &[InstId],
842 mut state: InferState,
843 ) -> Result<InferState> {
844 if args.len() != 2 {
845 return Ok(state);
846 }
847 let var_ranges = state.as_map();
848 let list_type = self.bound_of_arg(args[1], &var_ranges);
849
850 if type_expr::is_list_type(self.ir, list_type) {
851 if let Some(elem_type) = type_expr::list_type_arg(self.ir, list_type) {
852 let ctx = TypeContext::default();
853 let elem_bound = self.bound_of_arg(args[0], &state.as_map());
854 let meet =
855 type_expr::lower_bound(self.ir, &ctx, &[elem_bound, elem_type]);
856 if !type_expr::is_empty_type(self.ir, meet) {
857 if let Inst::Var(v) = self.ir.get(args[0]) {
858 let v = *v;
859 state.add_or_refine_with_ir(self.ir, v, meet);
860 }
861 }
862 }
863 }
864 Ok(state)
865 }
866
867 fn bound_of_arg(
869 &mut self,
870 arg: InstId,
871 var_ranges: &FxHashMap<NameId, InstId>,
872 ) -> InstId {
873 match self.ir.get(arg) {
874 Inst::Var(v) => {
875 let v = *v;
876 if let Some(&range) = var_ranges.get(&v) {
877 range
878 } else {
879 type_expr::find_or_create_name(self.ir, "/any")
880 }
881 }
882 Inst::Number(_) => type_expr::find_or_create_name(self.ir, "/number"),
883 Inst::Float(_) => type_expr::find_or_create_name(self.ir, "/float64"),
884 Inst::String(_) => type_expr::find_or_create_name(self.ir, "/string"),
885 Inst::Bool(_) => type_expr::find_or_create_name(self.ir, "/bool"),
886 Inst::Time(_) => type_expr::find_or_create_name(self.ir, "/time"),
887 Inst::Duration(_) => type_expr::find_or_create_name(self.ir, "/duration"),
888 Inst::Bytes(_) => type_expr::find_or_create_name(self.ir, "/bytes"),
889 Inst::Name(n) => {
890 let name = self.ir.resolve_name(*n).to_string();
891 let prefix = self.name_trie.prefix_name(&name);
892 type_expr::find_or_create_name(self.ir, &prefix)
893 }
894 Inst::List(elems) => {
895 let elems = elems.clone();
896 if elems.is_empty() {
897 let bot = type_expr::find_or_create_name(self.ir, "/bot");
898 return type_expr::new_list_type(self.ir, bot);
899 }
900 let ctx = TypeContext::default();
901 let elem_types: Vec<InstId> = elems
902 .iter()
903 .map(|e| self.bound_of_arg(*e, var_ranges))
904 .collect();
905 let elem_type = type_expr::upper_bound(self.ir, &ctx, &elem_types);
906 type_expr::new_list_type(self.ir, elem_type)
907 }
908 Inst::Map { keys, values } => {
909 let keys = keys.clone();
910 let values = values.clone();
911 let ctx = TypeContext::default();
912 let key_types: Vec<InstId> = keys
913 .iter()
914 .map(|k| self.bound_of_arg(*k, var_ranges))
915 .collect();
916 let val_types: Vec<InstId> = values
917 .iter()
918 .map(|v| self.bound_of_arg(*v, var_ranges))
919 .collect();
920 let kt = type_expr::upper_bound(self.ir, &ctx, &key_types);
921 let vt = type_expr::upper_bound(self.ir, &ctx, &val_types);
922 type_expr::new_map_type(self.ir, kt, vt)
923 }
924 Inst::Struct { fields, values } => {
925 let fields = fields.clone();
926 let values = values.clone();
927 let mut args = Vec::new();
928 for (f, v) in fields.iter().zip(values.iter()) {
929 let fname = self.ir.resolve_name(*f).to_string();
930 let fname_id = type_expr::find_or_create_name(self.ir, &fname);
931 let vtype = self.bound_of_arg(*v, var_ranges);
932 args.push(fname_id);
933 args.push(vtype);
934 }
935 type_expr::new_struct_type(self.ir, args)
936 }
937 Inst::ApplyFn { function, args } => {
938 let fname = self.ir.resolve_name(*function).to_string();
939 let args = args.clone();
940 self.bound_of_apply_fn(&fname, &args, var_ranges)
941 }
942 _ => type_expr::find_or_create_name(self.ir, "/any"),
943 }
944 }
945
946 fn bound_of_apply_fn(
948 &mut self,
949 fname: &str,
950 args: &[InstId],
951 var_ranges: &FxHashMap<NameId, InstId>,
952 ) -> InstId {
953 match fname {
954 "fn:list" => {
955 if args.is_empty() {
956 let bot = type_expr::find_or_create_name(self.ir, "/bot");
957 return type_expr::new_list_type(self.ir, bot);
958 }
959 let ctx = TypeContext::default();
960 let arg_types: Vec<InstId> = args
961 .iter()
962 .map(|a| self.bound_of_arg(*a, var_ranges))
963 .collect();
964 let elem = type_expr::upper_bound(self.ir, &ctx, &arg_types);
965 type_expr::new_list_type(self.ir, elem)
966 }
967 "fn:map" => {
968 let ctx = TypeContext::default();
969 let mut key_types = Vec::new();
970 let mut val_types = Vec::new();
971 let mut i = 0;
972 while i + 1 < args.len() {
973 key_types.push(self.bound_of_arg(args[i], var_ranges));
974 val_types.push(self.bound_of_arg(args[i + 1], var_ranges));
975 i += 2;
976 }
977 let kt = type_expr::upper_bound(self.ir, &ctx, &key_types);
978 let vt = type_expr::upper_bound(self.ir, &ctx, &val_types);
979 type_expr::new_map_type(self.ir, kt, vt)
980 }
981 "fn:struct" => {
982 let mut struct_args = Vec::new();
983 let mut i = 0;
984 while i + 1 < args.len() {
985 struct_args.push(args[i]); struct_args.push(self.bound_of_arg(args[i + 1], var_ranges));
987 i += 2;
988 }
989 type_expr::new_struct_type(self.ir, struct_args)
990 }
991 "fn:tuple" => {
992 let arg_types: Vec<InstId> = args
993 .iter()
994 .map(|a| self.bound_of_arg(*a, var_ranges))
995 .collect();
996 type_expr::new_tuple_type(self.ir, arg_types)
997 }
998 "fn:struct_get" if args.len() == 2 => {
999 let struct_type = self.bound_of_arg(args[0], var_ranges);
1000 if let Inst::Name(n) = self.ir.get(args[1]) {
1001 let field = *n;
1002 if let Some(ft) =
1003 type_expr::struct_type_field_deep(self.ir, struct_type, field)
1004 {
1005 return ft;
1006 }
1007 }
1008 type_expr::find_or_create_name(self.ir, "/any")
1009 }
1010 "fn:plus" | "fn:minus" | "fn:mult" | "fn:div" => {
1011 type_expr::find_or_create_name(self.ir, "/number")
1012 }
1013 "fn:float_plus" | "fn:float_mult" | "fn:float_div" => {
1014 type_expr::find_or_create_name(self.ir, "/float64")
1015 }
1016 "fn:string:concat" | "fn:string:replace" => {
1017 type_expr::find_or_create_name(self.ir, "/string")
1018 }
1019 "fn:count" | "fn:sum" | "fn:max" | "fn:min" => {
1020 type_expr::find_or_create_name(self.ir, "/number")
1021 }
1022 "fn:list:len" | "fn:len" | "fn:map:len" | "fn:struct:len" => {
1023 type_expr::find_or_create_name(self.ir, "/number")
1024 }
1025 "fn:sqrt" => type_expr::find_or_create_name(self.ir, "/float64"),
1026 "fn:list:get" if args.len() == 2 => {
1027 let list_type = self.bound_of_arg(args[0], var_ranges);
1029 match type_expr::apply_fn_args(self.ir, list_type) {
1030 Some(inner) if type_expr::apply_fn_name(self.ir, list_type)
1031 == Some(type_expr::FN_LIST)
1032 && inner.len() == 1 =>
1033 {
1034 inner[0]
1035 }
1036 _ => type_expr::find_or_create_name(self.ir, "/any"),
1037 }
1038 }
1039 "fn:list:append" if args.len() == 2 => {
1040 let list_type = self.bound_of_arg(args[0], var_ranges);
1042 let new_elem = self.bound_of_arg(args[1], var_ranges);
1043 let old_elem = match type_expr::apply_fn_args(self.ir, list_type) {
1044 Some(inner) if type_expr::apply_fn_name(self.ir, list_type)
1045 == Some(type_expr::FN_LIST)
1046 && inner.len() == 1 =>
1047 {
1048 inner[0]
1049 }
1050 _ => type_expr::find_or_create_name(self.ir, "/any"),
1051 };
1052 let ctx = TypeContext::default();
1053 let elem = type_expr::upper_bound(self.ir, &ctx, &[old_elem, new_elem]);
1054 type_expr::new_list_type(self.ir, elem)
1055 }
1056 "fn:collect" | "fn:collect_distinct" => {
1057 if args.len() == 1 {
1058 let elem_type = self.bound_of_arg(args[0], var_ranges);
1059 type_expr::new_list_type(self.ir, elem_type)
1060 } else {
1061 let any = type_expr::find_or_create_name(self.ir, "/any");
1062 type_expr::new_list_type(self.ir, any)
1063 }
1064 }
1065 _ => type_expr::find_or_create_name(self.ir, "/any"),
1066 }
1067 }
1068
1069 fn atom_predicate(&self, atom_id: InstId) -> Option<NameId> {
1072 if let Inst::Atom { predicate, .. } = self.ir.get(atom_id) {
1073 Some(*predicate)
1074 } else {
1075 None
1076 }
1077 }
1078
1079 fn atom_args(&self, atom_id: InstId) -> Vec<InstId> {
1080 if let Inst::Atom { args, .. } = self.ir.get(atom_id) {
1081 args.clone()
1082 } else {
1083 Vec::new()
1084 }
1085 }
1086
1087 fn describe_inst(&self, id: InstId) -> String {
1089 match self.ir.get(id) {
1090 Inst::Name(n) => self.ir.resolve_name(*n).to_string(),
1091 Inst::Number(n) => n.to_string(),
1092 Inst::Float(f) => f.to_string(),
1093 Inst::String(s) => format!("{:?}", self.ir.resolve_string(*s)),
1094 Inst::Bool(b) => b.to_string(),
1095 Inst::Var(v) => self.ir.resolve_name(*v).to_string(),
1096 Inst::ApplyFn { function, args } => {
1097 let fname = self.ir.resolve_name(*function);
1098 let arg_strs: Vec<String> =
1099 args.iter().map(|a| self.describe_inst(*a)).collect();
1100 format!("{}({})", fname, arg_strs.join(", "))
1101 }
1102 _ => format!("inst#{}", id.index()),
1103 }
1104 }
1105}
1106
1107struct InferState {
1115 used_vars: Vec<NameId>,
1117 var_types: Vec<InstId>,
1119}
1120
1121impl InferState {
1122 fn new() -> Self {
1123 Self {
1124 used_vars: Vec::new(),
1125 var_types: Vec::new(),
1126 }
1127 }
1128
1129 fn add_or_refine_with_ir(&mut self, ir: &mut Ir, var: NameId, tpe: InstId) {
1131 if let Some(idx) = self.used_vars.iter().position(|v| *v == var) {
1132 let existing = self.var_types[idx];
1134 let ctx = TypeContext::default();
1135 let meet = type_expr::lower_bound(ir, &ctx, &[existing, tpe]);
1136 if !type_expr::is_empty_type(ir, meet) {
1137 self.var_types[idx] = meet;
1138 }
1139 } else {
1141 self.used_vars.push(var);
1142 self.var_types.push(tpe);
1143 }
1144 }
1145
1146 fn set_var(&mut self, var: NameId, tpe: InstId) {
1148 if let Some(idx) = self.used_vars.iter().position(|v| *v == var) {
1149 self.var_types[idx] = tpe;
1150 }
1151 }
1152
1153 fn as_map(&self) -> FxHashMap<NameId, InstId> {
1155 self.used_vars
1156 .iter()
1157 .zip(self.var_types.iter())
1158 .map(|(v, t)| (*v, *t))
1159 .collect()
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crate::LoweringContext;
1167 use mangle_ast as ast;
1168 use mangle_parse::Parser;
1169
1170 fn check(source: &str) -> Result<()> {
1172 let arena = ast::Arena::new_with_global_interner();
1173 let mut parser = Parser::new(&arena, source.as_bytes(), "test");
1174 parser.next_token().unwrap();
1175 let unit = parser.parse_unit().unwrap();
1176 let ctx = LoweringContext::new(&arena);
1177 let mut ir = ctx.lower_unit(&unit);
1178 let mut checker = BoundsChecker::new(&mut ir);
1179 checker.check()
1180 }
1181
1182 #[test]
1187 fn check_valid_fact() {
1188 let arena = ast::Arena::new_with_global_interner();
1189
1190 let foo_sym = arena.predicate_sym("foo", Some(1));
1192 let var_x = arena.variable("X");
1193 let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1194 let num_type = arena.const_(arena.name("/number"));
1195 let bound_decl = ast::BoundDecl {
1196 base_terms: arena.alloc_slice_copy(&[num_type]),
1197 };
1198 let decl = ast::Decl {
1199 atom: atom_foo_x,
1200 descr: &[],
1201 bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1202 constraints: None,
1203 is_temporal: false,
1204 };
1205
1206 let const_42 = arena.const_(ast::Const::Number(42));
1208 let atom_foo_42 = arena.atom(foo_sym, &[const_42]);
1209 let clause = ast::Clause {
1210 head: atom_foo_42,
1211 head_time: None,
1212 premises: &[],
1213 transform: &[],
1214 };
1215
1216 let unit = ast::Unit {
1217 decls: arena.alloc_slice_copy(&[&decl]),
1218 clauses: arena.alloc_slice_copy(&[&clause]),
1219 };
1220
1221 let ctx = LoweringContext::new(&arena);
1222 let mut ir = ctx.lower_unit(&unit);
1223 let mut checker = BoundsChecker::new(&mut ir);
1224 assert!(checker.check().is_ok());
1225 }
1226
1227 #[test]
1228 fn check_invalid_fact_type_mismatch() {
1229 let arena = ast::Arena::new_with_global_interner();
1230
1231 let foo_sym = arena.predicate_sym("foo", Some(1));
1233 let var_x = arena.variable("X");
1234 let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1235 let num_type = arena.const_(arena.name("/number"));
1236 let bound_decl = ast::BoundDecl {
1237 base_terms: arena.alloc_slice_copy(&[num_type]),
1238 };
1239 let decl = ast::Decl {
1240 atom: atom_foo_x,
1241 descr: &[],
1242 bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1243 constraints: None,
1244 is_temporal: false,
1245 };
1246
1247 let const_str = arena.const_(ast::Const::String("hello"));
1249 let atom_foo_bad = arena.atom(foo_sym, &[const_str]);
1250 let clause = ast::Clause {
1251 head: atom_foo_bad,
1252 head_time: None,
1253 premises: &[],
1254 transform: &[],
1255 };
1256
1257 let unit = ast::Unit {
1258 decls: arena.alloc_slice_copy(&[&decl]),
1259 clauses: arena.alloc_slice_copy(&[&clause]),
1260 };
1261
1262 let ctx = LoweringContext::new(&arena);
1263 let mut ir = ctx.lower_unit(&unit);
1264 let mut checker = BoundsChecker::new(&mut ir);
1265 let result = checker.check();
1266 assert!(result.is_err(), "expected type mismatch error");
1267 }
1268
1269 #[test]
1270 fn check_valid_rule() {
1271 let arena = ast::Arena::new_with_global_interner();
1272
1273 let src_sym = arena.predicate_sym("src", Some(1));
1275 let var_x = arena.variable("X");
1276 let atom_src_x = arena.atom(src_sym, &[var_x]);
1277 let num_type = arena.const_(arena.name("/number"));
1278 let bound_decl = ast::BoundDecl {
1279 base_terms: arena.alloc_slice_copy(&[num_type]),
1280 };
1281 let decl_src = ast::Decl {
1282 atom: atom_src_x,
1283 descr: &[],
1284 bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1285 constraints: None,
1286 is_temporal: false,
1287 };
1288
1289 let dst_sym = arena.predicate_sym("dst", Some(1));
1291 let var_y = arena.variable("Y");
1292 let atom_dst_y = arena.atom(dst_sym, &[var_y]);
1293 let num_type2 = arena.const_(arena.name("/number"));
1294 let bound_decl2 = ast::BoundDecl {
1295 base_terms: arena.alloc_slice_copy(&[num_type2]),
1296 };
1297 let decl_dst = ast::Decl {
1298 atom: atom_dst_y,
1299 descr: &[],
1300 bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl2)])),
1301 constraints: None,
1302 is_temporal: false,
1303 };
1304
1305 let var_x2 = arena.variable("X");
1307 let head = arena.atom(dst_sym, &[var_x2]);
1308 let var_x3 = arena.variable("X");
1309 let body = arena.atom(src_sym, &[var_x3]);
1310 let clause = ast::Clause {
1311 head,
1312 head_time: None,
1313 premises: arena.alloc_slice_copy(&[arena.alloc(ast::Term::Atom(body))]),
1314 transform: &[],
1315 };
1316
1317 let unit = ast::Unit {
1318 decls: arena.alloc_slice_copy(&[&decl_src, &decl_dst]),
1319 clauses: arena.alloc_slice_copy(&[&clause]),
1320 };
1321
1322 let ctx = LoweringContext::new(&arena);
1323 let mut ir = ctx.lower_unit(&unit);
1324 let mut checker = BoundsChecker::new(&mut ir);
1325 assert!(checker.check().is_ok());
1326 }
1327
1328 #[test]
1329 fn check_arity_mismatch() {
1330 let arena = ast::Arena::new_with_global_interner();
1331
1332 let foo_sym = arena.predicate_sym("foo", Some(1));
1334 let var_x = arena.variable("X");
1335 let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1336 let num_type = arena.const_(arena.name("/number"));
1337 let bound_decl = ast::BoundDecl {
1338 base_terms: arena.alloc_slice_copy(&[num_type]),
1339 };
1340 let decl = ast::Decl {
1341 atom: atom_foo_x,
1342 descr: &[],
1343 bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1344 constraints: None,
1345 is_temporal: false,
1346 };
1347
1348 let const_42 = arena.const_(ast::Const::Number(42));
1350 let const_43 = arena.const_(ast::Const::Number(43));
1351 let atom_foo_bad = arena.atom(foo_sym, &[const_42, const_43]);
1352 let clause = ast::Clause {
1353 head: atom_foo_bad,
1354 head_time: None,
1355 premises: &[],
1356 transform: &[],
1357 };
1358
1359 let unit = ast::Unit {
1360 decls: arena.alloc_slice_copy(&[&decl]),
1361 clauses: arena.alloc_slice_copy(&[&clause]),
1362 };
1363
1364 let ctx = LoweringContext::new(&arena);
1365 let mut ir = ctx.lower_unit(&unit);
1366 let mut checker = BoundsChecker::new(&mut ir);
1367 let result = checker.check();
1368 assert!(result.is_err());
1369 }
1370
1371 #[test]
1376 fn multiple_alternatives_first_matches() {
1377 assert!(check(r#"
1379 Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1380 pair(42, 99).
1381 "#).is_ok());
1382 }
1383
1384 #[test]
1385 fn multiple_alternatives_second_matches() {
1386 assert!(check(r#"
1388 Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1389 pair("a", "b").
1390 "#).is_ok());
1391 }
1392
1393 #[test]
1394 fn multiple_alternatives_none_matches() {
1395 assert!(check(r#"
1397 Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1398 pair(42, "b").
1399 "#).is_err());
1400 }
1401
1402 #[test]
1407 fn rule_infers_type_from_premise() {
1408 assert!(check(r#"
1410 Decl src(X) bound [/number].
1411 Decl dst(X) bound [/number].
1412 dst(X) :- src(X).
1413 "#).is_ok());
1414 }
1415
1416 #[test]
1417 fn rule_type_mismatch_from_premise() {
1418 assert!(check(r#"
1420 Decl src(X) bound [/string].
1421 Decl dst(X) bound [/number].
1422 dst(X) :- src(X).
1423 "#).is_err());
1424 }
1425
1426 #[test]
1431 fn two_premises_refine_variable() {
1432 assert!(check(r#"
1435 Decl wide(X) bound [fn:Union(/number, /string)].
1436 Decl narrow(X) bound [/number].
1437 Decl result(X) bound [/number].
1438 result(X) :- wide(X), narrow(X).
1439 "#).is_ok());
1440 }
1441
1442 #[test]
1443 fn two_premises_refine_to_incompatible() {
1444 assert!(check(r#"
1448 Decl src1(X) bound [/string].
1449 Decl src2(X) bound [/number].
1450 Decl dst(X) bound [/number].
1451 dst(X) :- src1(X), src2(X).
1452 "#).is_err());
1453 }
1454
1455 #[test]
1460 fn polymorphic_identity_number() {
1461 assert!(check(r#"
1463 Decl pair(X, Y) bound [T, T].
1464 pair(42, 99).
1465 "#).is_ok());
1466 }
1467
1468 #[test]
1469 fn polymorphic_identity_string() {
1470 assert!(check(r#"
1472 Decl pair(X, Y) bound [T, T].
1473 pair("a", "b").
1474 "#).is_ok());
1475 }
1476
1477 #[test]
1478 fn polymorphic_rule_with_inferred_type() {
1479 assert!(check(r#"
1482 Decl src(X) bound [/number].
1483 Decl dst(X) bound [T].
1484 dst(X) :- src(X).
1485 "#).is_ok());
1486 }
1487
1488 #[test]
1493 fn cross_predicate_inference_basic() {
1494 assert!(check(r#"
1497 Decl src(X) bound [/number].
1498 Decl dst(X) bound [/number].
1499 helper(X) :- src(X).
1500 dst(X) :- helper(X).
1501 "#).is_ok());
1502 }
1503
1504 #[test]
1505 fn cross_predicate_inference_type_mismatch() {
1506 assert!(check(r#"
1508 Decl src(X) bound [/string].
1509 Decl dst(X) bound [/number].
1510 helper(X) :- src(X).
1511 dst(X) :- helper(X).
1512 "#).is_err());
1513 }
1514
1515 #[test]
1516 fn cross_predicate_inference_chain() {
1517 assert!(check(r#"
1519 Decl src(X) bound [/number].
1520 Decl dst(X) bound [/number].
1521 mid(X) :- src(X).
1522 dst(X) :- mid(X).
1523 "#).is_ok());
1524 }
1525
1526 #[test]
1531 fn equality_binds_variable() {
1532 assert!(check(r#"
1534 Decl src(X) bound [/string].
1535 Decl dst(X) bound [/string].
1536 dst(X) :- src(X), X = "hello".
1537 "#).is_ok());
1538 }
1539
1540 #[test]
1541 fn inequality_refines_variable() {
1542 assert!(check(r#"
1544 Decl src(X) bound [/string].
1545 Decl dst(X) bound [/string].
1546 dst(X) :- src(X), X != "bad".
1547 "#).is_ok());
1548 }
1549
1550 #[test]
1555 fn transform_arithmetic() {
1556 assert!(check(r#"
1558 Decl src(X) bound [/number].
1559 Decl dst(X, Y) bound [/number, /number].
1560 dst(X, Y) :- src(X) |> let Y = fn:plus(X, 1).
1561 "#).is_ok());
1562 }
1563
1564 #[test]
1565 fn transform_string_concat() {
1566 assert!(check(r#"
1568 Decl src(X) bound [/string].
1569 Decl dst(X, Y) bound [/string, /string].
1570 dst(X, Y) :- src(X) |> let Y = fn:string:concat(X, "!").
1571 "#).is_ok());
1572 }
1573
1574 #[test]
1575 fn transform_type_mismatch() {
1576 assert!(check(r#"
1578 Decl src(X) bound [/number].
1579 Decl dst(X, Y) bound [/number, /string].
1580 dst(X, Y) :- src(X) |> let Y = fn:plus(X, 1).
1581 "#).is_err());
1582 }
1583
1584 #[test]
1589 fn undeclared_predicate_passes() {
1590 assert!(check(r#"
1592 foo(1).
1593 bar(X) :- foo(X).
1594 "#).is_ok());
1595 }
1596
1597 #[test]
1602 fn arity_mismatch_facts() {
1603 let result = check(r#"
1605 p(1).
1606 p(2, 3).
1607 "#);
1608 assert!(result.is_err(), "expected arity error, got: {:?}", result);
1609 let msg = result.unwrap_err().to_string();
1610 assert!(msg.contains("inconsistent arity"), "error should mention 'inconsistent arity': {}", msg);
1611 assert!(msg.contains("p"), "error should mention predicate name: {}", msg);
1612 }
1613
1614 #[test]
1615 fn arity_mismatch_fact_and_rule() {
1616 let result = check(r#"
1618 p(1).
1619 p(X, Y) :- q(X, Y).
1620 q(1, 2).
1621 "#);
1622 assert!(result.is_err(), "expected arity error, got: {:?}", result);
1623 let msg = result.unwrap_err().to_string();
1624 assert!(msg.contains("inconsistent arity"), "error should mention 'inconsistent arity': {}", msg);
1625 }
1626
1627 #[test]
1628 fn arity_mismatch_two_rules() {
1629 let result = check(r#"
1631 p(X) :- q(X).
1632 p(X, Y) :- r(X, Y).
1633 q(1).
1634 r(1, 2).
1635 "#);
1636 assert!(result.is_err(), "expected arity error, got: {:?}", result);
1637 let msg = result.unwrap_err().to_string();
1638 assert!(msg.contains("inconsistent arity"), "error should mention 'inconsistent arity': {}", msg);
1639 }
1640
1641 #[test]
1642 fn consistent_arity_passes() {
1643 assert!(check(r#"
1645 p(1).
1646 p(2).
1647 q(X) :- p(X).
1648 "#).is_ok());
1649 }
1650
1651 #[test]
1652 fn consistent_arity_rules_passes() {
1653 assert!(check(r#"
1655 p(1, 2).
1656 p(X, Y) :- q(X), r(Y).
1657 q(1).
1658 r(2).
1659 "#).is_ok());
1660 }
1661
1662 #[test]
1663 fn arity_mismatch_undeclared_predicates() {
1664 let result = check(r#"
1666 edge(1, 2).
1667 edge(3, 4, 5).
1668 "#);
1669 assert!(result.is_err(), "expected arity error, got: {:?}", result);
1670 let msg = result.unwrap_err().to_string();
1671 assert!(msg.contains("inconsistent arity"), "error should mention 'inconsistent arity': {}", msg);
1672 }
1673}