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 mangle_ir::{Inst, InstId, Ir, NameId};
28use rustc_hash::{FxHashMap, FxHashSet};
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                    && let Some(pred) = self.atom_predicate(*head)
132                {
133                    self.rules_map.entry(pred).or_default().push((
134                        *head,
135                        premises.clone(),
136                        transform.clone(),
137                    ));
138                }
139            }
140        }
141    }
142
143    /// Pass 1.5: Check that every predicate is used with a consistent arity.
144    ///
145    /// Scans all facts and rule heads to detect arity mismatches, e.g.:
146    /// `p(1). p(2, 3).` — predicate `p` used with arity 1 and 2.
147    fn check_arity_consistency(&self) -> Result<()> {
148        // Map predicate NameId -> (first seen arity, first seen location)
149        let mut arity_map: FxHashMap<NameId, (usize, String)> = FxHashMap::default();
150        let mut errors: Vec<String> = Vec::new();
151
152        let insts: Vec<Inst> = self.ir.insts.clone();
153        for inst in &insts {
154            if let Inst::Rule {
155                head,
156                premises,
157                transform,
158            } = inst
159            {
160                let is_fact = premises.is_empty() && transform.is_empty();
161                if let Some(pred) = self.atom_predicate(*head) {
162                    let expected_args = if self.ir.temporal_predicates.contains(&pred) {
163                        let args = self.atom_args(*head);
164                        if args.len() >= 2 {
165                            args.len() - 2
166                        } else {
167                            args.len()
168                        }
169                    } else {
170                        self.atom_args(*head).len()
171                    };
172
173                    let kind = if is_fact { "fact" } else { "rule" };
174                    let pred_name = self.ir.resolve_name(pred).to_string();
175                    let location = format!("{} {}({} arg(s))", kind, pred_name, expected_args);
176
177                    if let Some(&(first_arity, ref first_location)) = arity_map.get(&pred) {
178                        if first_arity != expected_args {
179                            errors.push(format!(
180                                "predicate '{}' used with inconsistent arity: {} vs {}",
181                                pred_name, first_location, location
182                            ));
183                        }
184                    } else {
185                        arity_map.insert(pred, (expected_args, location));
186                    }
187                }
188            }
189        }
190
191        if errors.is_empty() {
192            Ok(())
193        } else {
194            Err(anyhow!("arity error: {}", errors.join("; ")))
195        }
196    }
197
198    /// Pass 2: Check all unit clauses and rules against declared bounds.
199    fn check_all_clauses(&mut self) -> Result<()> {
200        let insts: Vec<Inst> = self.ir.insts.clone();
201        for inst in &insts {
202            if let Inst::Rule {
203                head,
204                premises,
205                transform,
206            } = inst
207            {
208                let head = *head;
209                let premises = premises.clone();
210                let transform = transform.clone();
211                if let Some(pred) = self.atom_predicate(head)
212                    && let Some(alternatives) = self.rel_type_map.get(&pred).cloned()
213                {
214                    if premises.is_empty() && transform.is_empty() {
215                        self.check_fact(head, &alternatives)?;
216                    } else {
217                        self.check_rule(head, &premises, &transform, &alternatives)?;
218                    }
219                }
220            }
221        }
222        Ok(())
223    }
224
225    /// Check a fact (unit clause head) against declared bound alternatives.
226    fn check_fact(&self, head: InstId, alternatives: &[Vec<InstId>]) -> Result<()> {
227        let args = self.atom_args(head);
228        let pred = self.atom_predicate(head).unwrap();
229        if args.is_empty() && alternatives.is_empty() {
230            return Ok(());
231        }
232
233        let mut errors = Vec::new();
234        for alt in alternatives {
235            match self.check_fact_against_bound(pred, &args, alt) {
236                Ok(()) => return Ok(()),
237                Err(e) => errors.push(e.to_string()),
238            }
239        }
240
241        if errors.is_empty() {
242            return Ok(());
243        }
244
245        let pred_name = self
246            .atom_predicate(head)
247            .map(|p| self.ir.resolve_name(p).to_string())
248            .unwrap_or_else(|| "?".to_string());
249        Err(anyhow!(
250            "fact {}(...) matches none of the bound decls: {}",
251            pred_name,
252            errors.join("; ")
253        ))
254    }
255
256    /// Check a single fact against one bound alternative.
257    fn check_fact_against_bound(
258        &self,
259        pred: NameId,
260        args: &[InstId],
261        bound: &[InstId],
262    ) -> Result<()> {
263        let is_temporal = self.ir.temporal_predicates.contains(&pred);
264        let expected_args = if is_temporal {
265            bound.len() + 2
266        } else {
267            bound.len()
268        };
269        if args.len() != expected_args {
270            return Err(anyhow!(
271                "arity mismatch: fact has {} args, bound has {}{}",
272                args.len(),
273                bound.len(),
274                if is_temporal { " (+2 temporal)" } else { "" }
275            ));
276        }
277        for (i, (arg, type_expr)) in args.iter().zip(bound.iter()).enumerate() {
278            if !type_expr::has_type(self.ir, *type_expr, *arg) {
279                let arg_desc = self.describe_inst(*arg);
280                let type_desc = self.describe_inst(*type_expr);
281                return Err(anyhow!(
282                    "argument {} ({}) does not have type {}",
283                    i,
284                    arg_desc,
285                    type_desc
286                ));
287            }
288        }
289        Ok(())
290    }
291
292    /// Check a rule against declared bound alternatives.
293    ///
294    /// Uses the inference pipeline: for each premise, infer variable types
295    /// via feasible alternatives, then check that head args conform.
296    fn check_rule(
297        &mut self,
298        head: InstId,
299        premises: &[InstId],
300        transforms: &[InstId],
301        alternatives: &[Vec<InstId>],
302    ) -> Result<()> {
303        let head_args = self.atom_args(head);
304        let pred = self.atom_predicate(head).unwrap();
305        let is_temporal = self.ir.temporal_predicates.contains(&pred);
306
307        // Run inference pipeline.
308        let mut state = InferState::new();
309        for premise_id in premises {
310            state = self.infer_from_premise(*premise_id, state)?;
311        }
312
313        // Process transforms.
314        for transform_id in transforms {
315            if let Inst::Transform { var, app } = self.ir.get(*transform_id) {
316                let var = *var;
317                let app = *app;
318                if let Some(v) = var {
319                    let tpe = self.bound_of_arg(app, &state.as_map());
320                    state.add_or_refine_with_ir(self.ir, v, tpe);
321                }
322            }
323        }
324
325        // Compute head tuple types.
326        let var_ranges = state.as_map();
327        let inferred: Vec<InstId> = head_args
328            .iter()
329            .map(|arg| self.bound_of_arg(*arg, &var_ranges))
330            .collect();
331
332        // For temporal predicates, trim synthetic time columns.
333        let check_len = if is_temporal && inferred.len() >= 2 {
334            inferred.len() - 2
335        } else {
336            inferred.len()
337        };
338        let inferred_trimmed = &inferred[..check_len];
339
340        // Check inferred types against each declared alternative.
341        let mut errors = Vec::new();
342        for alt in alternatives {
343            if alt.len() != inferred_trimmed.len() {
344                errors.push(format!(
345                    "arity mismatch: head has {} args, bound has {}",
346                    inferred_trimmed.len(),
347                    alt.len()
348                ));
349                continue;
350            }
351            // Build type context: map any type variables in the alt to /any.
352            let any = type_expr::find_or_create_name(self.ir, "/any");
353            let mut ctx = TypeContext::default();
354            for t in alt.iter() {
355                let mut vars = FxHashSet::default();
356                type_expr::collect_vars(self.ir, *t, &mut vars);
357                for v in vars {
358                    ctx.entry(v).or_insert(any);
359                }
360            }
361            let all_conform = inferred_trimmed
362                .iter()
363                .zip(alt.iter())
364                .all(|(inf, decl)| type_expr::set_conforms(self.ir, &ctx, *inf, *decl));
365            if all_conform {
366                return Ok(());
367            }
368            errors.push(format!(
369                "inferred [{}] does not conform to declared [{}]",
370                inferred_trimmed
371                    .iter()
372                    .map(|i| self.describe_inst(*i))
373                    .collect::<Vec<_>>()
374                    .join(", "),
375                alt.iter()
376                    .map(|i| self.describe_inst(*i))
377                    .collect::<Vec<_>>()
378                    .join(", "),
379            ));
380        }
381
382        if errors.is_empty() {
383            return Ok(());
384        }
385
386        let pred_name = self
387            .atom_predicate(head)
388            .map(|p| self.ir.resolve_name(p).to_string())
389            .unwrap_or_else(|| "?".to_string());
390        Err(anyhow!(
391            "rule for {}(...) does not conform to declared bounds: {}",
392            pred_name,
393            errors.join("; ")
394        ))
395    }
396
397    /// Infer variable types from a single premise, updating the state.
398    fn infer_from_premise(
399        &mut self,
400        premise_id: InstId,
401        mut state: InferState,
402    ) -> Result<InferState> {
403        match self.ir.get(premise_id) {
404            Inst::Atom { predicate, args } => {
405                let pred = *predicate;
406                let args = args.clone();
407
408                // Special case: :match_prefix
409                let pred_name = self.ir.resolve_name(pred).to_string();
410                if pred_name == ":match_prefix" {
411                    return self.infer_match_prefix(&args, state);
412                }
413                if pred_name == ":match_field" {
414                    return self.infer_match_field(&args, state);
415                }
416                if pred_name == ":match_entry" {
417                    return self.infer_match_entry(&args, state);
418                }
419                if pred_name == ":list:member" {
420                    return self.infer_list_member(&args, state);
421                }
422
423                // Regular atom: look up or infer alternatives.
424                let var_ranges = state.as_map();
425                let feasible = self.get_or_infer_alternatives(pred, &args, &var_ranges);
426
427                if !feasible.is_empty() {
428                    // Use the first feasible alternative to bind variables.
429                    let first = &feasible[0].clone();
430                    for (arg, type_id) in args.iter().zip(first.iter()) {
431                        if let Inst::Var(v) = self.ir.get(*arg) {
432                            let v = *v;
433                            state.add_or_refine_with_ir(self.ir, v, *type_id);
434                        }
435                    }
436                } else if let Some(alternatives) = self.rel_type_map.get(&pred).cloned() {
437                    // Fallback: no feasible alternative, use first declared alt.
438                    if let Some(first_alt) = alternatives.first() {
439                        for (arg, type_id) in args.iter().zip(first_alt.iter()) {
440                            if let Inst::Var(v) = self.ir.get(*arg) {
441                                let v = *v;
442                                state.add_or_refine_with_ir(self.ir, v, *type_id);
443                            }
444                        }
445                    }
446                }
447                Ok(state)
448            }
449            Inst::NegAtom(inner) => {
450                let inner = *inner;
451                // Negated atoms: we can refine types via negative information,
452                // but don't add new bindings.
453                if let Inst::Atom { predicate, args } = self.ir.get(inner) {
454                    let pred = *predicate;
455                    let args = args.clone();
456                    let pred_name = self.ir.resolve_name(pred).to_string();
457
458                    if pred_name == ":match_prefix" && args.len() >= 2 {
459                        // Negative :match_prefix: refine away the prefix type.
460                        if let Inst::Var(v) = self.ir.get(args[0]) {
461                            let v = *v;
462                            let bound = self.bound_of_arg(args[1], &state.as_map());
463                            if let Some(existing) = state.as_map().get(&v).copied()
464                                && type_expr::is_union_type(self.ir, existing)
465                            {
466                                let refined =
467                                    type_expr::remove_from_union_type(self.ir, bound, existing);
468                                if !type_expr::is_empty_type(self.ir, refined) {
469                                    state.set_var(v, refined);
470                                }
471                            }
472                        }
473                    }
474                    // Other negated atoms: no type refinement.
475                }
476                Ok(state)
477            }
478            Inst::Eq(left, right) => {
479                let left = *left;
480                let right = *right;
481                let var_ranges = state.as_map();
482
483                if let Inst::Var(lv) = self.ir.get(left) {
484                    let lv = *lv;
485                    let tpe = self.bound_of_arg(right, &var_ranges);
486                    state.add_or_refine_with_ir(self.ir, lv, tpe);
487                }
488                if let Inst::Var(rv) = self.ir.get(right) {
489                    let rv = *rv;
490                    let tpe = self.bound_of_arg(left, &state.as_map());
491                    state.add_or_refine_with_ir(self.ir, rv, tpe);
492                }
493                Ok(state)
494            }
495            Inst::Ineq(left, right) => {
496                let left = *left;
497                let right = *right;
498                let var_ranges = state.as_map();
499
500                // For inequality, both sides must have compatible types.
501                let left_tpe = self.bound_of_arg(left, &var_ranges);
502                let right_tpe = self.bound_of_arg(right, &var_ranges);
503                let ctx = TypeContext::default();
504                let meet = type_expr::lower_bound(self.ir, &ctx, &[left_tpe, right_tpe]);
505                if !type_expr::is_empty_type(self.ir, meet) {
506                    if let Inst::Var(lv) = self.ir.get(left) {
507                        let lv = *lv;
508                        state.add_or_refine_with_ir(self.ir, lv, meet);
509                    }
510                    if let Inst::Var(rv) = self.ir.get(right) {
511                        let rv = *rv;
512                        state.add_or_refine_with_ir(self.ir, rv, meet);
513                    }
514                }
515                Ok(state)
516            }
517            _ => Ok(state),
518        }
519    }
520
521    /// Finds feasible alternatives for a subgoal p(e1...eN) with skolemization.
522    ///
523    /// For each declared alternative:
524    /// 1. Builds argument bounds (uses var_ranges for bound vars, declared type for unbound)
525    /// 2. Collects type variables from the alternative, creates fresh substitution
526    /// 3. Applies substitution to both arg bounds and alternative types
527    /// 4. Checks that LowerBound (with extended type context) is non-empty per position
528    fn feasible_alternatives(
529        &mut self,
530        alternatives: &[Vec<InstId>],
531        args: &[InstId],
532        var_ranges: &FxHashMap<NameId, InstId>,
533    ) -> Vec<Vec<InstId>> {
534        let mut feasible = Vec::new();
535
536        for alt in alternatives {
537            if alt.len() != args.len() {
538                continue;
539            }
540
541            // Step 1: Build argument bounds.
542            // For bound vars: use var_ranges. For unbound vars: use declared type.
543            // For constants: use bound_of_arg.
544            let mut arg_bound = Vec::new();
545            for (i, arg) in args.iter().enumerate() {
546                if let Inst::Var(v) = self.ir.get(*arg) {
547                    let v = *v;
548                    if let Some(&range) = var_ranges.get(&v) {
549                        arg_bound.push(range);
550                    } else {
551                        // Unbound variable: use declared type from this alternative.
552                        arg_bound.push(alt[i]);
553                    }
554                } else {
555                    arg_bound.push(self.bound_of_arg(*arg, var_ranges));
556                }
557            }
558
559            // Step 2: Collect type variables from the alternative.
560            let mut type_vars = FxHashSet::default();
561            for t in alt {
562                type_expr::collect_vars(self.ir, *t, &mut type_vars);
563            }
564
565            // Step 3: Skolemize — create fresh variables for each type variable.
566            let mut subst: FxHashMap<NameId, InstId> = FxHashMap::default();
567            if !type_vars.is_empty() {
568                for v in &type_vars {
569                    let fresh = self.fresh_var();
570                    let fresh_id = self.ir.add_inst(Inst::Var(fresh));
571                    subst.insert(*v, fresh_id);
572                }
573            }
574
575            // Step 4: Apply substitution to arg bounds and alternative.
576            let arg_bound_subst: Vec<InstId> = arg_bound
577                .iter()
578                .map(|t| type_expr::apply_subst(self.ir, *t, &subst))
579                .collect();
580            let alt_subst: Vec<InstId> = alt
581                .iter()
582                .map(|t| type_expr::apply_subst(self.ir, *t, &subst))
583                .collect();
584
585            // Step 5: Build extended type context with fresh vars -> /any.
586            let any = type_expr::find_or_create_name(self.ir, "/any");
587            let mut ctx = TypeContext::default();
588            for fresh_id in subst.values() {
589                if let Inst::Var(v) = self.ir.get(*fresh_id) {
590                    ctx.insert(*v, any);
591                }
592            }
593
594            // Step 6: Per-position feasibility check.
595            let mut is_feasible = true;
596            let mut result_types = Vec::new();
597            for (ab, at) in arg_bound_subst.iter().zip(alt_subst.iter()) {
598                let meet = type_expr::lower_bound(self.ir, &ctx, &[*ab, *at]);
599                if type_expr::is_empty_type(self.ir, meet) {
600                    is_feasible = false;
601                    break;
602                }
603                result_types.push(meet);
604            }
605
606            if is_feasible {
607                feasible.push(result_types);
608            }
609        }
610        feasible
611    }
612
613    /// Looks up or infers type alternatives for a predicate.
614    ///
615    /// Checks declared types first, then already-inferred types, then infers
616    /// from rules. Uses cycle detection to handle recursive predicates.
617    fn get_or_infer_alternatives(
618        &mut self,
619        pred: NameId,
620        args: &[InstId],
621        var_ranges: &FxHashMap<NameId, InstId>,
622    ) -> Vec<Vec<InstId>> {
623        // 1. Check declared types.
624        if let Some(alts) = self.rel_type_map.get(&pred).cloned() {
625            return self.feasible_alternatives(&alts, args, var_ranges);
626        }
627
628        // 2. Check already-inferred types.
629        if let Some(alts) = self.inferred.get(&pred).cloned() {
630            return self.feasible_alternatives(&alts, args, var_ranges);
631        }
632
633        // 3. Cycle detection: if we're already visiting this predicate,
634        // return [/any ... /any] to break the cycle.
635        if self.visiting.contains(&pred) {
636            let any = type_expr::find_or_create_name(self.ir, "/any");
637            return vec![vec![any; args.len()]];
638        }
639
640        // 4. Infer from rules defining this predicate.
641        self.visiting.insert(pred);
642        let inferred = self.infer_rel_types(pred);
643        self.visiting.remove(&pred);
644
645        if !inferred.is_empty() {
646            self.inferred.insert(pred, inferred.clone());
647            return self.feasible_alternatives(&inferred, args, var_ranges);
648        }
649
650        Vec::new()
651    }
652
653    /// Infers relation type alternatives for a predicate from its defining rules.
654    ///
655    /// For each rule defining the predicate, runs inference to determine
656    /// the head tuple types, then collects all alternatives.
657    fn infer_rel_types(&mut self, pred: NameId) -> Vec<Vec<InstId>> {
658        let rules = match self.rules_map.get(&pred) {
659            Some(r) => r.clone(),
660            None => return Vec::new(),
661        };
662
663        let mut alternatives: Vec<Vec<InstId>> = Vec::new();
664
665        for (head, premises, transforms) in &rules {
666            // Run inference pipeline on this clause.
667            if let Some(inferred) = self.infer_clause(*head, premises, transforms) {
668                alternatives.push(inferred);
669            }
670        }
671
672        alternatives
673    }
674
675    /// Runs inference on a single clause, returning inferred head tuple types.
676    fn infer_clause(
677        &mut self,
678        head: InstId,
679        premises: &[InstId],
680        transforms: &[InstId],
681    ) -> Option<Vec<InstId>> {
682        let head_args = self.atom_args(head);
683        let mut state = InferState::new();
684
685        for premise_id in premises {
686            match self.infer_from_premise(*premise_id, state) {
687                Ok(new_state) => state = new_state,
688                Err(_) => return None,
689            }
690        }
691
692        // Process transforms.
693        for transform_id in transforms {
694            if let Inst::Transform { var, app } = self.ir.get(*transform_id) {
695                let var = *var;
696                let app = *app;
697                if let Some(v) = var {
698                    let tpe = self.bound_of_arg(app, &state.as_map());
699                    state.add_or_refine_with_ir(self.ir, v, tpe);
700                }
701            }
702        }
703
704        // Compute head tuple types.
705        let var_ranges = state.as_map();
706        let inferred: Vec<InstId> = head_args
707            .iter()
708            .map(|arg| self.bound_of_arg(*arg, &var_ranges))
709            .collect();
710
711        Some(inferred)
712    }
713
714    /// Special case inference for `:match_prefix(Name, Prefix)`.
715    fn infer_match_prefix(&mut self, args: &[InstId], mut state: InferState) -> Result<InferState> {
716        if args.len() != 2 {
717            return Ok(state);
718        }
719        let var_ranges = state.as_map();
720        let tpe = self.bound_of_arg(args[0], &var_ranges);
721        let prefix = self.bound_of_arg(args[1], &var_ranges);
722
723        let ctx = TypeContext::default();
724        let meet = type_expr::lower_bound(self.ir, &ctx, &[tpe, prefix]);
725        if !type_expr::is_empty_type(self.ir, meet) {
726            if let Inst::Var(v) = self.ir.get(args[0]) {
727                let v = *v;
728                state.add_or_refine_with_ir(self.ir, v, meet);
729            }
730            // Second arg (prefix) is typically a constant.
731            let name_type = type_expr::find_or_create_name(self.ir, "/name");
732            if let Inst::Var(v) = self.ir.get(args[1]) {
733                let v = *v;
734                state.add_or_refine_with_ir(self.ir, v, name_type);
735            }
736        }
737        Ok(state)
738    }
739
740    /// Special case inference for `:match_field(Struct, FieldName, Value)`.
741    fn infer_match_field(&mut self, args: &[InstId], mut state: InferState) -> Result<InferState> {
742        if args.len() != 3 {
743            return Ok(state);
744        }
745        let var_ranges = state.as_map();
746        let scrutinee_type = self.bound_of_arg(args[0], &var_ranges);
747
748        // Get field name from args[1] (must be a name constant).
749        let field_name_id = match self.ir.get(args[1]) {
750            Inst::Name(n) => Some(*n),
751            _ => None,
752        };
753
754        if let Some(field) = field_name_id
755            && (type_expr::is_struct_type(self.ir, scrutinee_type)
756                || type_expr::is_tagged_union_type(self.ir, scrutinee_type)
757                || type_expr::is_union_type(self.ir, scrutinee_type))
758            && let Some(field_type) =
759                type_expr::struct_type_field_deep(self.ir, scrutinee_type, field)
760        {
761            // Bind the value variable.
762            let ctx = TypeContext::default();
763            let value_bound = self.bound_of_arg(args[2], &state.as_map());
764            let meet = type_expr::lower_bound(self.ir, &ctx, &[value_bound, field_type]);
765            if !type_expr::is_empty_type(self.ir, meet)
766                && let Inst::Var(v) = self.ir.get(args[2])
767            {
768                let v = *v;
769                state.add_or_refine_with_ir(self.ir, v, meet);
770            }
771        }
772        // Bind first arg if variable.
773        let any = type_expr::find_or_create_name(self.ir, "/any");
774        if let Inst::Var(v) = self.ir.get(args[0]) {
775            let v = *v;
776            state.add_or_refine_with_ir(self.ir, v, any);
777        }
778        // Bind second arg (field name) if variable.
779        let name_type = type_expr::find_or_create_name(self.ir, "/name");
780        if let Inst::Var(v) = self.ir.get(args[1]) {
781            let v = *v;
782            state.add_or_refine_with_ir(self.ir, v, name_type);
783        }
784        Ok(state)
785    }
786
787    /// Special case inference for `:match_entry(Map, Key, Value)`.
788    fn infer_match_entry(&mut self, args: &[InstId], mut state: InferState) -> Result<InferState> {
789        if args.len() != 3 {
790            return Ok(state);
791        }
792        let var_ranges = state.as_map();
793        let map_type = self.bound_of_arg(args[0], &var_ranges);
794
795        if type_expr::is_map_type(self.ir, map_type)
796            && let Some((key_type, val_type)) = type_expr::map_type_args(self.ir, map_type)
797        {
798            let ctx = TypeContext::default();
799
800            // Bind key.
801            let key_bound = self.bound_of_arg(args[1], &state.as_map());
802            let key_meet = type_expr::lower_bound(self.ir, &ctx, &[key_bound, key_type]);
803            if !type_expr::is_empty_type(self.ir, key_meet)
804                && let Inst::Var(v) = self.ir.get(args[1])
805            {
806                let v = *v;
807                state.add_or_refine_with_ir(self.ir, v, key_meet);
808            }
809
810            // Bind value.
811            let val_bound = self.bound_of_arg(args[2], &state.as_map());
812            let val_meet = type_expr::lower_bound(self.ir, &ctx, &[val_bound, val_type]);
813            if !type_expr::is_empty_type(self.ir, val_meet)
814                && let Inst::Var(v) = self.ir.get(args[2])
815            {
816                let v = *v;
817                state.add_or_refine_with_ir(self.ir, v, val_meet);
818            }
819        }
820        Ok(state)
821    }
822
823    /// Special case inference for `:list:member(Elem, List)`.
824    fn infer_list_member(&mut self, args: &[InstId], mut state: InferState) -> Result<InferState> {
825        if args.len() != 2 {
826            return Ok(state);
827        }
828        let var_ranges = state.as_map();
829        let list_type = self.bound_of_arg(args[1], &var_ranges);
830
831        if type_expr::is_list_type(self.ir, list_type)
832            && let Some(elem_type) = type_expr::list_type_arg(self.ir, list_type)
833        {
834            let ctx = TypeContext::default();
835            let elem_bound = self.bound_of_arg(args[0], &state.as_map());
836            let meet = type_expr::lower_bound(self.ir, &ctx, &[elem_bound, elem_type]);
837            if !type_expr::is_empty_type(self.ir, meet)
838                && let Inst::Var(v) = self.ir.get(args[0])
839            {
840                let v = *v;
841                state.add_or_refine_with_ir(self.ir, v, meet);
842            }
843        }
844        Ok(state)
845    }
846
847    /// Infers the type bound for a single argument.
848    fn bound_of_arg(&mut self, arg: InstId, var_ranges: &FxHashMap<NameId, InstId>) -> InstId {
849        match self.ir.get(arg) {
850            Inst::Var(v) => {
851                let v = *v;
852                if let Some(&range) = var_ranges.get(&v) {
853                    range
854                } else {
855                    type_expr::find_or_create_name(self.ir, "/any")
856                }
857            }
858            Inst::Number(_) => type_expr::find_or_create_name(self.ir, "/number"),
859            Inst::Float(_) => type_expr::find_or_create_name(self.ir, "/float64"),
860            Inst::String(_) => type_expr::find_or_create_name(self.ir, "/string"),
861            Inst::Bool(_) => type_expr::find_or_create_name(self.ir, "/bool"),
862            Inst::Time(_) => type_expr::find_or_create_name(self.ir, "/time"),
863            Inst::Duration(_) => type_expr::find_or_create_name(self.ir, "/duration"),
864            Inst::Bytes(_) => type_expr::find_or_create_name(self.ir, "/bytes"),
865            Inst::Name(n) => {
866                let name = self.ir.resolve_name(*n).to_string();
867                let prefix = self.name_trie.prefix_name(&name);
868                type_expr::find_or_create_name(self.ir, &prefix)
869            }
870            Inst::List(elems) => {
871                let elems = elems.clone();
872                if elems.is_empty() {
873                    let bot = type_expr::find_or_create_name(self.ir, "/bot");
874                    return type_expr::new_list_type(self.ir, bot);
875                }
876                let ctx = TypeContext::default();
877                let elem_types: Vec<InstId> = elems
878                    .iter()
879                    .map(|e| self.bound_of_arg(*e, var_ranges))
880                    .collect();
881                let elem_type = type_expr::upper_bound(self.ir, &ctx, &elem_types);
882                type_expr::new_list_type(self.ir, elem_type)
883            }
884            Inst::Map { keys, values } => {
885                let keys = keys.clone();
886                let values = values.clone();
887                let ctx = TypeContext::default();
888                let key_types: Vec<InstId> = keys
889                    .iter()
890                    .map(|k| self.bound_of_arg(*k, var_ranges))
891                    .collect();
892                let val_types: Vec<InstId> = values
893                    .iter()
894                    .map(|v| self.bound_of_arg(*v, var_ranges))
895                    .collect();
896                let kt = type_expr::upper_bound(self.ir, &ctx, &key_types);
897                let vt = type_expr::upper_bound(self.ir, &ctx, &val_types);
898                type_expr::new_map_type(self.ir, kt, vt)
899            }
900            Inst::Struct { fields, values } => {
901                let fields = fields.clone();
902                let values = values.clone();
903                let mut args = Vec::new();
904                for (f, v) in fields.iter().zip(values.iter()) {
905                    let fname = self.ir.resolve_name(*f).to_string();
906                    let fname_id = type_expr::find_or_create_name(self.ir, &fname);
907                    let vtype = self.bound_of_arg(*v, var_ranges);
908                    args.push(fname_id);
909                    args.push(vtype);
910                }
911                type_expr::new_struct_type(self.ir, args)
912            }
913            Inst::ApplyFn { function, args } => {
914                let fname = self.ir.resolve_name(*function).to_string();
915                let args = args.clone();
916                self.bound_of_apply_fn(&fname, &args, var_ranges)
917            }
918            _ => type_expr::find_or_create_name(self.ir, "/any"),
919        }
920    }
921
922    /// Infers a type for a function application expression.
923    fn bound_of_apply_fn(
924        &mut self,
925        fname: &str,
926        args: &[InstId],
927        var_ranges: &FxHashMap<NameId, InstId>,
928    ) -> InstId {
929        match fname {
930            "fn:list" => {
931                if args.is_empty() {
932                    let bot = type_expr::find_or_create_name(self.ir, "/bot");
933                    return type_expr::new_list_type(self.ir, bot);
934                }
935                let ctx = TypeContext::default();
936                let arg_types: Vec<InstId> = args
937                    .iter()
938                    .map(|a| self.bound_of_arg(*a, var_ranges))
939                    .collect();
940                let elem = type_expr::upper_bound(self.ir, &ctx, &arg_types);
941                type_expr::new_list_type(self.ir, elem)
942            }
943            "fn:map" => {
944                let ctx = TypeContext::default();
945                let mut key_types = Vec::new();
946                let mut val_types = Vec::new();
947                let mut i = 0;
948                while i + 1 < args.len() {
949                    key_types.push(self.bound_of_arg(args[i], var_ranges));
950                    val_types.push(self.bound_of_arg(args[i + 1], var_ranges));
951                    i += 2;
952                }
953                let kt = type_expr::upper_bound(self.ir, &ctx, &key_types);
954                let vt = type_expr::upper_bound(self.ir, &ctx, &val_types);
955                type_expr::new_map_type(self.ir, kt, vt)
956            }
957            "fn:struct" => {
958                let mut struct_args = Vec::new();
959                let mut i = 0;
960                while i + 1 < args.len() {
961                    struct_args.push(args[i]); // field name
962                    struct_args.push(self.bound_of_arg(args[i + 1], var_ranges));
963                    i += 2;
964                }
965                type_expr::new_struct_type(self.ir, struct_args)
966            }
967            "fn:tuple" => {
968                let arg_types: Vec<InstId> = args
969                    .iter()
970                    .map(|a| self.bound_of_arg(*a, var_ranges))
971                    .collect();
972                type_expr::new_tuple_type(self.ir, arg_types)
973            }
974            "fn:struct_get" if args.len() == 2 => {
975                let struct_type = self.bound_of_arg(args[0], var_ranges);
976                if let Inst::Name(n) = self.ir.get(args[1]) {
977                    let field = *n;
978                    if let Some(ft) = type_expr::struct_type_field_deep(self.ir, struct_type, field)
979                    {
980                        return ft;
981                    }
982                }
983                type_expr::find_or_create_name(self.ir, "/any")
984            }
985            "fn:plus" | "fn:minus" | "fn:mult" | "fn:div" => {
986                type_expr::find_or_create_name(self.ir, "/number")
987            }
988            "fn:float_plus" | "fn:float_mult" | "fn:float_div" => {
989                type_expr::find_or_create_name(self.ir, "/float64")
990            }
991            "fn:string:concat" | "fn:string:replace" => {
992                type_expr::find_or_create_name(self.ir, "/string")
993            }
994            "fn:count" | "fn:sum" | "fn:max" | "fn:min" => {
995                type_expr::find_or_create_name(self.ir, "/number")
996            }
997            "fn:list:len" | "fn:len" | "fn:map:len" | "fn:struct:len" => {
998                type_expr::find_or_create_name(self.ir, "/number")
999            }
1000            "fn:sqrt" => type_expr::find_or_create_name(self.ir, "/float64"),
1001            "fn:list:get" if args.len() == 2 => {
1002                // Element type of the list argument, or /any if not a list.
1003                let list_type = self.bound_of_arg(args[0], var_ranges);
1004                match type_expr::apply_fn_args(self.ir, list_type) {
1005                    Some(inner)
1006                        if type_expr::apply_fn_name(self.ir, list_type)
1007                            == Some(type_expr::FN_LIST)
1008                            && inner.len() == 1 =>
1009                    {
1010                        inner[0]
1011                    }
1012                    _ => type_expr::find_or_create_name(self.ir, "/any"),
1013                }
1014            }
1015            "fn:list:append" if args.len() == 2 => {
1016                // Widen the list element type to include the appended value.
1017                let list_type = self.bound_of_arg(args[0], var_ranges);
1018                let new_elem = self.bound_of_arg(args[1], var_ranges);
1019                let old_elem = match type_expr::apply_fn_args(self.ir, list_type) {
1020                    Some(inner)
1021                        if type_expr::apply_fn_name(self.ir, list_type)
1022                            == Some(type_expr::FN_LIST)
1023                            && inner.len() == 1 =>
1024                    {
1025                        inner[0]
1026                    }
1027                    _ => type_expr::find_or_create_name(self.ir, "/any"),
1028                };
1029                let ctx = TypeContext::default();
1030                let elem = type_expr::upper_bound(self.ir, &ctx, &[old_elem, new_elem]);
1031                type_expr::new_list_type(self.ir, elem)
1032            }
1033            "fn:collect" | "fn:collect_distinct" => {
1034                if args.len() == 1 {
1035                    let elem_type = self.bound_of_arg(args[0], var_ranges);
1036                    type_expr::new_list_type(self.ir, elem_type)
1037                } else {
1038                    let any = type_expr::find_or_create_name(self.ir, "/any");
1039                    type_expr::new_list_type(self.ir, any)
1040                }
1041            }
1042            _ => type_expr::find_or_create_name(self.ir, "/any"),
1043        }
1044    }
1045
1046    // -- Helpers --
1047
1048    fn atom_predicate(&self, atom_id: InstId) -> Option<NameId> {
1049        if let Inst::Atom { predicate, .. } = self.ir.get(atom_id) {
1050            Some(*predicate)
1051        } else {
1052            None
1053        }
1054    }
1055
1056    fn atom_args(&self, atom_id: InstId) -> Vec<InstId> {
1057        if let Inst::Atom { args, .. } = self.ir.get(atom_id) {
1058            args.clone()
1059        } else {
1060            Vec::new()
1061        }
1062    }
1063
1064    /// Simple textual description of an IR instruction for error messages.
1065    fn describe_inst(&self, id: InstId) -> String {
1066        match self.ir.get(id) {
1067            Inst::Name(n) => self.ir.resolve_name(*n).to_string(),
1068            Inst::Number(n) => n.to_string(),
1069            Inst::Float(f) => f.to_string(),
1070            Inst::String(s) => format!("{:?}", self.ir.resolve_string(*s)),
1071            Inst::Bool(b) => b.to_string(),
1072            Inst::Var(v) => self.ir.resolve_name(*v).to_string(),
1073            Inst::ApplyFn { function, args } => {
1074                let fname = self.ir.resolve_name(*function);
1075                let arg_strs: Vec<String> = args.iter().map(|a| self.describe_inst(*a)).collect();
1076                format!("{}({})", fname, arg_strs.join(", "))
1077            }
1078            _ => format!("inst#{}", id.index()),
1079        }
1080    }
1081}
1082
1083// ---------------------------------------------------------------------------
1084// InferState
1085// ---------------------------------------------------------------------------
1086
1087/// State of type inference while iterating over premises.
1088///
1089/// Tracks variable bindings with their inferred types.
1090struct InferState {
1091    /// Variable names (parallel with `var_types`).
1092    used_vars: Vec<NameId>,
1093    /// Type bounds for each variable.
1094    var_types: Vec<InstId>,
1095}
1096
1097impl InferState {
1098    fn new() -> Self {
1099        Self {
1100            used_vars: Vec::new(),
1101            var_types: Vec::new(),
1102        }
1103    }
1104
1105    /// Adds a new variable binding or refines an existing one via LowerBound.
1106    fn add_or_refine_with_ir(&mut self, ir: &mut Ir, var: NameId, tpe: InstId) {
1107        if let Some(idx) = self.used_vars.iter().position(|v| *v == var) {
1108            // Variable already bound: intersect existing type with new type.
1109            let existing = self.var_types[idx];
1110            let ctx = TypeContext::default();
1111            let meet = type_expr::lower_bound(ir, &ctx, &[existing, tpe]);
1112            if !type_expr::is_empty_type(ir, meet) {
1113                self.var_types[idx] = meet;
1114            }
1115            // If intersection is empty, keep the existing type (conservative).
1116        } else {
1117            self.used_vars.push(var);
1118            self.var_types.push(tpe);
1119        }
1120    }
1121
1122    /// Sets a variable's type directly (for negative refinement).
1123    fn set_var(&mut self, var: NameId, tpe: InstId) {
1124        if let Some(idx) = self.used_vars.iter().position(|v| *v == var) {
1125            self.var_types[idx] = tpe;
1126        }
1127    }
1128
1129    /// Converts the state to a HashMap for lookups.
1130    fn as_map(&self) -> FxHashMap<NameId, InstId> {
1131        self.used_vars
1132            .iter()
1133            .zip(self.var_types.iter())
1134            .map(|(v, t)| (*v, *t))
1135            .collect()
1136    }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use super::*;
1142    use crate::LoweringContext;
1143    use mangle_ast as ast;
1144    use mangle_parse::Parser;
1145
1146    /// Helper: parse source, lower, run bounds checker.
1147    fn check(source: &str) -> Result<()> {
1148        let arena = ast::Arena::new_with_global_interner();
1149        let mut parser = Parser::new(&arena, source.as_bytes(), "test");
1150        parser.next_token().unwrap();
1151        let unit = parser.parse_unit().unwrap();
1152        let ctx = LoweringContext::new(&arena);
1153        let mut ir = ctx.lower_unit(&unit);
1154        let mut checker = BoundsChecker::new(&mut ir);
1155        checker.check()
1156    }
1157
1158    // -----------------------------------------------------------------------
1159    // Basic facts and rules (existing tests, now parser-based)
1160    // -----------------------------------------------------------------------
1161
1162    #[test]
1163    fn check_valid_fact() {
1164        let arena = ast::Arena::new_with_global_interner();
1165
1166        // Decl foo(X) bound [/number].
1167        let foo_sym = arena.predicate_sym("foo", Some(1));
1168        let var_x = arena.variable("X");
1169        let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1170        let num_type = arena.const_(arena.name("/number"));
1171        let bound_decl = ast::BoundDecl {
1172            base_terms: arena.alloc_slice_copy(&[num_type]),
1173        };
1174        let decl = ast::Decl {
1175            atom: atom_foo_x,
1176            descr: &[],
1177            bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1178            constraints: None,
1179            is_temporal: false,
1180        };
1181
1182        // foo(42).
1183        let const_42 = arena.const_(ast::Const::Number(42));
1184        let atom_foo_42 = arena.atom(foo_sym, &[const_42]);
1185        let clause = ast::Clause {
1186            head: atom_foo_42,
1187            head_time: None,
1188            premises: &[],
1189            transform: &[],
1190        };
1191
1192        let unit = ast::Unit {
1193            decls: arena.alloc_slice_copy(&[&decl]),
1194            clauses: arena.alloc_slice_copy(&[&clause]),
1195        };
1196
1197        let ctx = LoweringContext::new(&arena);
1198        let mut ir = ctx.lower_unit(&unit);
1199        let mut checker = BoundsChecker::new(&mut ir);
1200        assert!(checker.check().is_ok());
1201    }
1202
1203    #[test]
1204    fn check_invalid_fact_type_mismatch() {
1205        let arena = ast::Arena::new_with_global_interner();
1206
1207        // Decl foo(X) bound [/number].
1208        let foo_sym = arena.predicate_sym("foo", Some(1));
1209        let var_x = arena.variable("X");
1210        let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1211        let num_type = arena.const_(arena.name("/number"));
1212        let bound_decl = ast::BoundDecl {
1213            base_terms: arena.alloc_slice_copy(&[num_type]),
1214        };
1215        let decl = ast::Decl {
1216            atom: atom_foo_x,
1217            descr: &[],
1218            bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1219            constraints: None,
1220            is_temporal: false,
1221        };
1222
1223        // foo("hello"). -> Type mismatch.
1224        let const_str = arena.const_(ast::Const::String("hello"));
1225        let atom_foo_bad = arena.atom(foo_sym, &[const_str]);
1226        let clause = ast::Clause {
1227            head: atom_foo_bad,
1228            head_time: None,
1229            premises: &[],
1230            transform: &[],
1231        };
1232
1233        let unit = ast::Unit {
1234            decls: arena.alloc_slice_copy(&[&decl]),
1235            clauses: arena.alloc_slice_copy(&[&clause]),
1236        };
1237
1238        let ctx = LoweringContext::new(&arena);
1239        let mut ir = ctx.lower_unit(&unit);
1240        let mut checker = BoundsChecker::new(&mut ir);
1241        let result = checker.check();
1242        assert!(result.is_err(), "expected type mismatch error");
1243    }
1244
1245    #[test]
1246    fn check_valid_rule() {
1247        let arena = ast::Arena::new_with_global_interner();
1248
1249        // Decl src(X) bound [/number].
1250        let src_sym = arena.predicate_sym("src", Some(1));
1251        let var_x = arena.variable("X");
1252        let atom_src_x = arena.atom(src_sym, &[var_x]);
1253        let num_type = arena.const_(arena.name("/number"));
1254        let bound_decl = ast::BoundDecl {
1255            base_terms: arena.alloc_slice_copy(&[num_type]),
1256        };
1257        let decl_src = ast::Decl {
1258            atom: atom_src_x,
1259            descr: &[],
1260            bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1261            constraints: None,
1262            is_temporal: false,
1263        };
1264
1265        // Decl dst(X) bound [/number].
1266        let dst_sym = arena.predicate_sym("dst", Some(1));
1267        let var_y = arena.variable("Y");
1268        let atom_dst_y = arena.atom(dst_sym, &[var_y]);
1269        let num_type2 = arena.const_(arena.name("/number"));
1270        let bound_decl2 = ast::BoundDecl {
1271            base_terms: arena.alloc_slice_copy(&[num_type2]),
1272        };
1273        let decl_dst = ast::Decl {
1274            atom: atom_dst_y,
1275            descr: &[],
1276            bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl2)])),
1277            constraints: None,
1278            is_temporal: false,
1279        };
1280
1281        // dst(X) :- src(X).
1282        let var_x2 = arena.variable("X");
1283        let head = arena.atom(dst_sym, &[var_x2]);
1284        let var_x3 = arena.variable("X");
1285        let body = arena.atom(src_sym, &[var_x3]);
1286        let clause = ast::Clause {
1287            head,
1288            head_time: None,
1289            premises: arena.alloc_slice_copy(&[arena.alloc(ast::Term::Atom(body))]),
1290            transform: &[],
1291        };
1292
1293        let unit = ast::Unit {
1294            decls: arena.alloc_slice_copy(&[&decl_src, &decl_dst]),
1295            clauses: arena.alloc_slice_copy(&[&clause]),
1296        };
1297
1298        let ctx = LoweringContext::new(&arena);
1299        let mut ir = ctx.lower_unit(&unit);
1300        let mut checker = BoundsChecker::new(&mut ir);
1301        assert!(checker.check().is_ok());
1302    }
1303
1304    #[test]
1305    fn check_arity_mismatch() {
1306        let arena = ast::Arena::new_with_global_interner();
1307
1308        // Decl foo(X) bound [/number].
1309        let foo_sym = arena.predicate_sym("foo", Some(1));
1310        let var_x = arena.variable("X");
1311        let atom_foo_x = arena.atom(foo_sym, &[var_x]);
1312        let num_type = arena.const_(arena.name("/number"));
1313        let bound_decl = ast::BoundDecl {
1314            base_terms: arena.alloc_slice_copy(&[num_type]),
1315        };
1316        let decl = ast::Decl {
1317            atom: atom_foo_x,
1318            descr: &[],
1319            bounds: Some(arena.alloc_slice_copy(&[arena.alloc(bound_decl)])),
1320            constraints: None,
1321            is_temporal: false,
1322        };
1323
1324        // foo(42, 43). -> Arity mismatch.
1325        let const_42 = arena.const_(ast::Const::Number(42));
1326        let const_43 = arena.const_(ast::Const::Number(43));
1327        let atom_foo_bad = arena.atom(foo_sym, &[const_42, const_43]);
1328        let clause = ast::Clause {
1329            head: atom_foo_bad,
1330            head_time: None,
1331            premises: &[],
1332            transform: &[],
1333        };
1334
1335        let unit = ast::Unit {
1336            decls: arena.alloc_slice_copy(&[&decl]),
1337            clauses: arena.alloc_slice_copy(&[&clause]),
1338        };
1339
1340        let ctx = LoweringContext::new(&arena);
1341        let mut ir = ctx.lower_unit(&unit);
1342        let mut checker = BoundsChecker::new(&mut ir);
1343        let result = checker.check();
1344        assert!(result.is_err());
1345    }
1346
1347    // -----------------------------------------------------------------------
1348    // Parser-based tests: multiple bound alternatives
1349    // -----------------------------------------------------------------------
1350
1351    #[test]
1352    fn multiple_alternatives_first_matches() {
1353        // pair(42, 99) matches first alternative [/number, /number].
1354        assert!(
1355            check(
1356                r#"
1357            Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1358            pair(42, 99).
1359        "#
1360            )
1361            .is_ok()
1362        );
1363    }
1364
1365    #[test]
1366    fn multiple_alternatives_second_matches() {
1367        // pair("a", "b") matches second alternative [/string, /string].
1368        assert!(
1369            check(
1370                r#"
1371            Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1372            pair("a", "b").
1373        "#
1374            )
1375            .is_ok()
1376        );
1377    }
1378
1379    #[test]
1380    fn multiple_alternatives_none_matches() {
1381        // pair(42, "b") matches neither alternative.
1382        assert!(
1383            check(
1384                r#"
1385            Decl pair(X, Y) bound [/number, /number] bound [/string, /string].
1386            pair(42, "b").
1387        "#
1388            )
1389            .is_err()
1390        );
1391    }
1392
1393    // -----------------------------------------------------------------------
1394    // Rule type inference: variable binding from premises
1395    // -----------------------------------------------------------------------
1396
1397    #[test]
1398    fn rule_infers_type_from_premise() {
1399        // X gets type /number from src, which conforms to dst's bound.
1400        assert!(
1401            check(
1402                r#"
1403            Decl src(X) bound [/number].
1404            Decl dst(X) bound [/number].
1405            dst(X) :- src(X).
1406        "#
1407            )
1408            .is_ok()
1409        );
1410    }
1411
1412    #[test]
1413    fn rule_type_mismatch_from_premise() {
1414        // X inferred as /string from src, but dst expects /number.
1415        assert!(
1416            check(
1417                r#"
1418            Decl src(X) bound [/string].
1419            Decl dst(X) bound [/number].
1420            dst(X) :- src(X).
1421        "#
1422            )
1423            .is_err()
1424        );
1425    }
1426
1427    // -----------------------------------------------------------------------
1428    // Multiple body atoms refining the same variable (LowerBound)
1429    // -----------------------------------------------------------------------
1430
1431    #[test]
1432    fn two_premises_refine_variable() {
1433        // X starts as fn:Union(/number, /string) from 'wide',
1434        // then refined to /number from 'narrow'. Should conform to /number.
1435        assert!(
1436            check(
1437                r#"
1438            Decl wide(X) bound [fn:Union(/number, /string)].
1439            Decl narrow(X) bound [/number].
1440            Decl result(X) bound [/number].
1441            result(X) :- wide(X), narrow(X).
1442        "#
1443            )
1444            .is_ok()
1445        );
1446    }
1447
1448    #[test]
1449    fn two_premises_refine_to_incompatible() {
1450        // X inferred as /string from src1, then /number from src2.
1451        // Intersection is empty, so X keeps /string (conservative).
1452        // /string does not conform to /number → error.
1453        assert!(
1454            check(
1455                r#"
1456            Decl src1(X) bound [/string].
1457            Decl src2(X) bound [/number].
1458            Decl dst(X) bound [/number].
1459            dst(X) :- src1(X), src2(X).
1460        "#
1461            )
1462            .is_err()
1463        );
1464    }
1465
1466    // -----------------------------------------------------------------------
1467    // Polymorphic type declarations (skolemization)
1468    // -----------------------------------------------------------------------
1469
1470    #[test]
1471    fn polymorphic_identity_number() {
1472        // T is a type variable. pair(42, 99) should pass: T can be /number.
1473        assert!(
1474            check(
1475                r#"
1476            Decl pair(X, Y) bound [T, T].
1477            pair(42, 99).
1478        "#
1479            )
1480            .is_ok()
1481        );
1482    }
1483
1484    #[test]
1485    fn polymorphic_identity_string() {
1486        // T is a type variable. pair("a", "b") should pass: T can be /string.
1487        assert!(
1488            check(
1489                r#"
1490            Decl pair(X, Y) bound [T, T].
1491            pair("a", "b").
1492        "#
1493            )
1494            .is_ok()
1495        );
1496    }
1497
1498    #[test]
1499    fn polymorphic_rule_with_inferred_type() {
1500        // T skolemized to fresh var. X inferred as /number from src.
1501        // /number conforms to ?X0 (mapped to /any in context) → passes.
1502        assert!(
1503            check(
1504                r#"
1505            Decl src(X) bound [/number].
1506            Decl dst(X) bound [T].
1507            dst(X) :- src(X).
1508        "#
1509            )
1510            .is_ok()
1511        );
1512    }
1513
1514    // -----------------------------------------------------------------------
1515    // Cross-predicate inference
1516    // -----------------------------------------------------------------------
1517
1518    #[test]
1519    fn cross_predicate_inference_basic() {
1520        // 'helper' has no declaration. Its type is inferred from its rule
1521        // (which uses 'src' with bound [/number]). Then 'dst' uses 'helper'.
1522        assert!(
1523            check(
1524                r#"
1525            Decl src(X) bound [/number].
1526            Decl dst(X) bound [/number].
1527            helper(X) :- src(X).
1528            dst(X) :- helper(X).
1529        "#
1530            )
1531            .is_ok()
1532        );
1533    }
1534
1535    #[test]
1536    fn cross_predicate_inference_type_mismatch() {
1537        // 'helper' inferred as /string from src. dst expects /number → error.
1538        assert!(
1539            check(
1540                r#"
1541            Decl src(X) bound [/string].
1542            Decl dst(X) bound [/number].
1543            helper(X) :- src(X).
1544            dst(X) :- helper(X).
1545        "#
1546            )
1547            .is_err()
1548        );
1549    }
1550
1551    #[test]
1552    fn cross_predicate_inference_chain() {
1553        // Chain: src → mid → dst, only src and dst declared.
1554        assert!(
1555            check(
1556                r#"
1557            Decl src(X) bound [/number].
1558            Decl dst(X) bound [/number].
1559            mid(X) :- src(X).
1560            dst(X) :- mid(X).
1561        "#
1562            )
1563            .is_ok()
1564        );
1565    }
1566
1567    // -----------------------------------------------------------------------
1568    // Equality and inequality premises
1569    // -----------------------------------------------------------------------
1570
1571    #[test]
1572    fn equality_binds_variable() {
1573        // X = "hello" gives X type /string.
1574        assert!(
1575            check(
1576                r#"
1577            Decl src(X) bound [/string].
1578            Decl dst(X) bound [/string].
1579            dst(X) :- src(X), X = "hello".
1580        "#
1581            )
1582            .is_ok()
1583        );
1584    }
1585
1586    #[test]
1587    fn inequality_refines_variable() {
1588        // X from src is /string, X != "bad" should still be /string.
1589        assert!(
1590            check(
1591                r#"
1592            Decl src(X) bound [/string].
1593            Decl dst(X) bound [/string].
1594            dst(X) :- src(X), X != "bad".
1595        "#
1596            )
1597            .is_ok()
1598        );
1599    }
1600
1601    // -----------------------------------------------------------------------
1602    // Transform (let) expressions
1603    // -----------------------------------------------------------------------
1604
1605    #[test]
1606    fn transform_arithmetic() {
1607        // let Y = fn:plus(X, 1) → Y inferred as /number.
1608        assert!(
1609            check(
1610                r#"
1611            Decl src(X) bound [/number].
1612            Decl dst(X, Y) bound [/number, /number].
1613            dst(X, Y) :- src(X) |> let Y = fn:plus(X, 1).
1614        "#
1615            )
1616            .is_ok()
1617        );
1618    }
1619
1620    #[test]
1621    fn transform_string_concat() {
1622        // let Y = fn:string:concat(X, "!") → Y inferred as /string.
1623        assert!(
1624            check(
1625                r#"
1626            Decl src(X) bound [/string].
1627            Decl dst(X, Y) bound [/string, /string].
1628            dst(X, Y) :- src(X) |> let Y = fn:string:concat(X, "!").
1629        "#
1630            )
1631            .is_ok()
1632        );
1633    }
1634
1635    #[test]
1636    fn transform_type_mismatch() {
1637        // Y = fn:plus(X, 1) → /number, but dst expects /string for Y.
1638        assert!(
1639            check(
1640                r#"
1641            Decl src(X) bound [/number].
1642            Decl dst(X, Y) bound [/number, /string].
1643            dst(X, Y) :- src(X) |> let Y = fn:plus(X, 1).
1644        "#
1645            )
1646            .is_err()
1647        );
1648    }
1649
1650    // -----------------------------------------------------------------------
1651    // No-declaration predicates (no bounds checking needed)
1652    // -----------------------------------------------------------------------
1653
1654    #[test]
1655    fn undeclared_predicate_passes() {
1656        // Rules with no declarations should pass without error.
1657        assert!(
1658            check(
1659                r#"
1660            foo(1).
1661            bar(X) :- foo(X).
1662        "#
1663            )
1664            .is_ok()
1665        );
1666    }
1667
1668    // -----------------------------------------------------------------------
1669    // Arity consistency checking (no Decl required)
1670    // -----------------------------------------------------------------------
1671
1672    #[test]
1673    fn arity_mismatch_facts() {
1674        // Same predicate with different arity: p(1) vs p(2, 3).
1675        let result = check(
1676            r#"
1677            p(1).
1678            p(2, 3).
1679        "#,
1680        );
1681        assert!(result.is_err(), "expected arity error, got: {:?}", result);
1682        let msg = result.unwrap_err().to_string();
1683        assert!(
1684            msg.contains("inconsistent arity"),
1685            "error should mention 'inconsistent arity': {}",
1686            msg
1687        );
1688        assert!(
1689            msg.contains("p"),
1690            "error should mention predicate name: {}",
1691            msg
1692        );
1693    }
1694
1695    #[test]
1696    fn arity_mismatch_fact_and_rule() {
1697        // Fact p(1) vs rule head p(X, Y) — different arity.
1698        let result = check(
1699            r#"
1700            p(1).
1701            p(X, Y) :- q(X, Y).
1702            q(1, 2).
1703        "#,
1704        );
1705        assert!(result.is_err(), "expected arity error, got: {:?}", result);
1706        let msg = result.unwrap_err().to_string();
1707        assert!(
1708            msg.contains("inconsistent arity"),
1709            "error should mention 'inconsistent arity': {}",
1710            msg
1711        );
1712    }
1713
1714    #[test]
1715    fn arity_mismatch_two_rules() {
1716        // Two rules with different head arity for same predicate.
1717        let result = check(
1718            r#"
1719            p(X) :- q(X).
1720            p(X, Y) :- r(X, Y).
1721            q(1).
1722            r(1, 2).
1723        "#,
1724        );
1725        assert!(result.is_err(), "expected arity error, got: {:?}", result);
1726        let msg = result.unwrap_err().to_string();
1727        assert!(
1728            msg.contains("inconsistent arity"),
1729            "error should mention 'inconsistent arity': {}",
1730            msg
1731        );
1732    }
1733
1734    #[test]
1735    fn consistent_arity_passes() {
1736        // All uses of p have arity 1 — should pass.
1737        assert!(
1738            check(
1739                r#"
1740            p(1).
1741            p(2).
1742            q(X) :- p(X).
1743        "#
1744            )
1745            .is_ok()
1746        );
1747    }
1748
1749    #[test]
1750    fn consistent_arity_rules_passes() {
1751        // All uses of p have arity 2 — should pass.
1752        assert!(
1753            check(
1754                r#"
1755            p(1, 2).
1756            p(X, Y) :- q(X), r(Y).
1757            q(1).
1758            r(2).
1759        "#
1760            )
1761            .is_ok()
1762        );
1763    }
1764
1765    #[test]
1766    fn arity_mismatch_undeclared_predicates() {
1767        // Even without any Decl, arity mismatch should be caught.
1768        let result = check(
1769            r#"
1770            edge(1, 2).
1771            edge(3, 4, 5).
1772        "#,
1773        );
1774        assert!(result.is_err(), "expected arity error, got: {:?}", result);
1775        let msg = result.unwrap_err().to_string();
1776        assert!(
1777            msg.contains("inconsistent arity"),
1778            "error should mention 'inconsistent arity': {}",
1779            msg
1780        );
1781    }
1782}