Skip to main content

mangle_analysis/
bounds_check.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Bounds checker for Mangle.
16//!
17//! Validates that facts and rule derivations conform to declared type bounds.
18//! Implements the Go-equivalent bounds analysis with:
19//!
20//! - Inference state tracking with per-variable type accumulation
21//! - Feasible alternatives analysis with special cases for built-in predicates
22//! - Skolemization of polymorphic type variables
23//! - Cross-predicate type inference
24//! - UpperBound/LowerBound for type intersection/union
25
26use 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
33/// Bounds checker state.
34pub struct BoundsChecker<'a> {
35    ir: &'a mut Ir,
36    name_trie: NameTrie,
37    /// Predicate NameId -> declared type alternatives.
38    /// Each alternative is a Vec<InstId> of argument types.
39    rel_type_map: FxHashMap<NameId, Vec<Vec<InstId>>>,
40    /// Predicate NameId -> rules defining it: (head, premises, transforms).
41    rules_map: FxHashMap<NameId, Vec<(InstId, Vec<InstId>, Vec<InstId>)>>,
42    /// Cross-predicate inference: inferred types for predicates without declarations.
43    inferred: FxHashMap<NameId, Vec<Vec<InstId>>>,
44    /// Cycle detection for cross-predicate inference.
45    visiting: FxHashSet<NameId>,
46    /// Counter for generating fresh type variable names.
47    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    /// Main entry point: collect declarations, build rules map, check all clauses.
64    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    /// Generates a fresh type variable NameId (e.g., `?X0`, `?X1`, ...).
72    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    /// Pass 1: Collect declared types from Decl instructions and build name trie.
79    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                            // Collect name constants into trie.
90                            for term in &base_terms {
91                                self.name_trie.collect(self.ir, *term);
92                            }
93                            // Build type context with any type variables in this bound.
94                            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                            // Validate wellformedness of each type expression.
104                            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    /// Build a map from predicate NameId to rules (head, premises, transforms).
120    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                // Only non-unit clauses (actual rules with premises or transforms).
130                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    /// Pass 1.5: Check that every predicate is used with a consistent arity.
143    ///
144    /// Scans all facts and rule heads to detect arity mismatches, e.g.:
145    /// `p(1). p(2, 3).` — predicate `p` used with arity 1 and 2.
146    fn check_arity_consistency(&self) -> Result<()> {
147        // Map predicate NameId -> (first seen arity, first seen location)
148        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    /// Pass 2: Check all unit clauses and rules against declared bounds.
193    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    /// Check a fact (unit clause head) against declared bound alternatives.
222    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    /// Check a single fact against one bound alternative.
253    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    /// Check a rule against declared bound alternatives.
289    ///
290    /// Uses the inference pipeline: for each premise, infer variable types
291    /// via feasible alternatives, then check that head args conform.
292    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        // Run inference pipeline.
304        let mut state = InferState::new();
305        for premise_id in premises {
306            state = self.infer_from_premise(*premise_id, state)?;
307        }
308
309        // Process transforms.
310        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        // Compute head tuple types.
322        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        // For temporal predicates, trim synthetic time columns.
329        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        // Check inferred types against each declared alternative.
337        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            // Build type context: map any type variables in the alt to /any.
348            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    /// Infer variable types from a single premise, updating the state.
394    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                // Special case: :match_prefix
405                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                // Regular atom: look up or infer alternatives.
420                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                    // Use the first feasible alternative to bind variables.
426                    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                    // Fallback: no feasible alternative, use first declared alt.
435                    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                // Negated atoms: we can refine types via negative information,
449                // but don't add new bindings.
450                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                        // Negative :match_prefix: refine away the prefix type.
457                        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                    // Other negated atoms: no type refinement.
472                }
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                // For inequality, both sides must have compatible types.
498                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    /// Finds feasible alternatives for a subgoal p(e1...eN) with skolemization.
519    ///
520    /// For each declared alternative:
521    /// 1. Builds argument bounds (uses var_ranges for bound vars, declared type for unbound)
522    /// 2. Collects type variables from the alternative, creates fresh substitution
523    /// 3. Applies substitution to both arg bounds and alternative types
524    /// 4. Checks that LowerBound (with extended type context) is non-empty per position
525    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            // Step 1: Build argument bounds.
539            // For bound vars: use var_ranges. For unbound vars: use declared type.
540            // For constants: use bound_of_arg.
541            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                        // Unbound variable: use declared type from this alternative.
549                        arg_bound.push(alt[i]);
550                    }
551                } else {
552                    arg_bound.push(self.bound_of_arg(*arg, var_ranges));
553                }
554            }
555
556            // Step 2: Collect type variables from the alternative.
557            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            // Step 3: Skolemize — create fresh variables for each type variable.
563            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            // Step 4: Apply substitution to arg bounds and alternative.
573            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            // Step 5: Build extended type context with fresh vars -> /any.
583            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            // Step 6: Per-position feasibility check.
592            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    /// Looks up or infers type alternatives for a predicate.
611    ///
612    /// Checks declared types first, then already-inferred types, then infers
613    /// from rules. Uses cycle detection to handle recursive predicates.
614    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        // 1. Check declared types.
621        if let Some(alts) = self.rel_type_map.get(&pred).cloned() {
622            return self.feasible_alternatives(&alts, args, var_ranges);
623        }
624
625        // 2. Check already-inferred types.
626        if let Some(alts) = self.inferred.get(&pred).cloned() {
627            return self.feasible_alternatives(&alts, args, var_ranges);
628        }
629
630        // 3. Cycle detection: if we're already visiting this predicate,
631        // return [/any ... /any] to break the cycle.
632        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        // 4. Infer from rules defining this predicate.
638        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    /// Infers relation type alternatives for a predicate from its defining rules.
651    ///
652    /// For each rule defining the predicate, runs inference to determine
653    /// the head tuple types, then collects all alternatives.
654    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            // Run inference pipeline on this clause.
664            if let Some(inferred) = self.infer_clause(*head, premises, transforms) {
665                alternatives.push(inferred);
666            }
667        }
668
669        alternatives
670    }
671
672    /// Runs inference on a single clause, returning inferred head tuple types.
673    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        // Process transforms.
690        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        // Compute head tuple types.
702        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    /// Special case inference for `:match_prefix(Name, Prefix)`.
712    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            // Second arg (prefix) is typically a constant.
732            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    /// Special case inference for `:match_field(Struct, FieldName, Value)`.
742    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        // Get field name from args[1] (must be a name constant).
754        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                    // Bind the value variable.
768                    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        // Bind first arg if variable.
782        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        // Bind second arg (field name) if variable.
788        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    /// Special case inference for `:match_entry(Map, Key, Value)`.
797    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                // Bind key.
813                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                // Bind value.
824                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    /// Special case inference for `:list:member(Elem, List)`.
839    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    /// Infers the type bound for a single argument.
868    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    /// Infers a type for a function application expression.
947    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]); // field name
986                    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                // Element type of the list argument, or /any if not a list.
1028                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                // Widen the list element type to include the appended value.
1041                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    // -- Helpers --
1070
1071    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    /// Simple textual description of an IR instruction for error messages.
1088    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
1107// ---------------------------------------------------------------------------
1108// InferState
1109// ---------------------------------------------------------------------------
1110
1111/// State of type inference while iterating over premises.
1112///
1113/// Tracks variable bindings with their inferred types.
1114struct InferState {
1115    /// Variable names (parallel with `var_types`).
1116    used_vars: Vec<NameId>,
1117    /// Type bounds for each variable.
1118    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    /// Adds a new variable binding or refines an existing one via LowerBound.
1130    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            // Variable already bound: intersect existing type with new type.
1133            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            // If intersection is empty, keep the existing type (conservative).
1140        } else {
1141            self.used_vars.push(var);
1142            self.var_types.push(tpe);
1143        }
1144    }
1145
1146    /// Sets a variable's type directly (for negative refinement).
1147    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    /// Converts the state to a HashMap for lookups.
1154    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    /// Helper: parse source, lower, run bounds checker.
1171    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    // -----------------------------------------------------------------------
1183    // Basic facts and rules (existing tests, now parser-based)
1184    // -----------------------------------------------------------------------
1185
1186    #[test]
1187    fn check_valid_fact() {
1188        let arena = ast::Arena::new_with_global_interner();
1189
1190        // Decl foo(X) bound [/number].
1191        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        // foo(42).
1207        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        // Decl foo(X) bound [/number].
1232        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        // foo("hello"). -> Type mismatch.
1248        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        // Decl src(X) bound [/number].
1274        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        // Decl dst(X) bound [/number].
1290        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        // dst(X) :- src(X).
1306        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        // Decl foo(X) bound [/number].
1333        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        // foo(42, 43). -> Arity mismatch.
1349        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    // -----------------------------------------------------------------------
1372    // Parser-based tests: multiple bound alternatives
1373    // -----------------------------------------------------------------------
1374
1375    #[test]
1376    fn multiple_alternatives_first_matches() {
1377        // pair(42, 99) matches first alternative [/number, /number].
1378        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        // pair("a", "b") matches second alternative [/string, /string].
1387        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        // pair(42, "b") matches neither alternative.
1396        assert!(check(r#"
1397            Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1398            pair(42, "b").
1399        "#).is_err());
1400    }
1401
1402    // -----------------------------------------------------------------------
1403    // Rule type inference: variable binding from premises
1404    // -----------------------------------------------------------------------
1405
1406    #[test]
1407    fn rule_infers_type_from_premise() {
1408        // X gets type /number from src, which conforms to dst's bound.
1409        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        // X inferred as /string from src, but dst expects /number.
1419        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    // -----------------------------------------------------------------------
1427    // Multiple body atoms refining the same variable (LowerBound)
1428    // -----------------------------------------------------------------------
1429
1430    #[test]
1431    fn two_premises_refine_variable() {
1432        // X starts as fn:Union(/number, /string) from 'wide',
1433        // then refined to /number from 'narrow'. Should conform to /number.
1434        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        // X inferred as /string from src1, then /number from src2.
1445        // Intersection is empty, so X keeps /string (conservative).
1446        // /string does not conform to /number → error.
1447        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    // -----------------------------------------------------------------------
1456    // Polymorphic type declarations (skolemization)
1457    // -----------------------------------------------------------------------
1458
1459    #[test]
1460    fn polymorphic_identity_number() {
1461        // T is a type variable. pair(42, 99) should pass: T can be /number.
1462        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        // T is a type variable. pair("a", "b") should pass: T can be /string.
1471        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        // T skolemized to fresh var. X inferred as /number from src.
1480        // /number conforms to ?X0 (mapped to /any in context) → passes.
1481        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    // -----------------------------------------------------------------------
1489    // Cross-predicate inference
1490    // -----------------------------------------------------------------------
1491
1492    #[test]
1493    fn cross_predicate_inference_basic() {
1494        // 'helper' has no declaration. Its type is inferred from its rule
1495        // (which uses 'src' with bound [/number]). Then 'dst' uses 'helper'.
1496        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        // 'helper' inferred as /string from src. dst expects /number → error.
1507        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        // Chain: src → mid → dst, only src and dst declared.
1518        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    // -----------------------------------------------------------------------
1527    // Equality and inequality premises
1528    // -----------------------------------------------------------------------
1529
1530    #[test]
1531    fn equality_binds_variable() {
1532        // X = "hello" gives X type /string.
1533        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        // X from src is /string, X != "bad" should still be /string.
1543        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    // -----------------------------------------------------------------------
1551    // Transform (let) expressions
1552    // -----------------------------------------------------------------------
1553
1554    #[test]
1555    fn transform_arithmetic() {
1556        // let Y = fn:plus(X, 1) → Y inferred as /number.
1557        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        // let Y = fn:string:concat(X, "!") → Y inferred as /string.
1567        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        // Y = fn:plus(X, 1) → /number, but dst expects /string for Y.
1577        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    // -----------------------------------------------------------------------
1585    // No-declaration predicates (no bounds checking needed)
1586    // -----------------------------------------------------------------------
1587
1588    #[test]
1589    fn undeclared_predicate_passes() {
1590        // Rules with no declarations should pass without error.
1591        assert!(check(r#"
1592            foo(1).
1593            bar(X) :- foo(X).
1594        "#).is_ok());
1595    }
1596
1597    // -----------------------------------------------------------------------
1598    // Arity consistency checking (no Decl required)
1599    // -----------------------------------------------------------------------
1600
1601    #[test]
1602    fn arity_mismatch_facts() {
1603        // Same predicate with different arity: p(1) vs p(2, 3).
1604        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        // Fact p(1) vs rule head p(X, Y) — different arity.
1617        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        // Two rules with different head arity for same predicate.
1630        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        // All uses of p have arity 1 — should pass.
1644        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        // All uses of p have arity 2 — should pass.
1654        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        // Even without any Decl, arity mismatch should be caught.
1665        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}