Skip to main content

assura_types/
types.rs

1//! Core type definitions for the Assura type checker.
2//!
3//! Contains the Type enum, TypeEnv, TypeError, TypedFile,
4//! and the Display implementation for Type.
5
6use std::collections::HashMap;
7use std::ops::Range;
8use std::sync::Arc;
9
10use assura_parser::ast::{Expr, SpExpr, Spanned, expr_to_string};
11use assura_resolve::ResolvedFile;
12
13use crate::checkers::PendingDecreaseCheck;
14
15// ---- Domain-checker default constants ----
16// Typed as `i64` to match `extract_int_literal` return type.
17
18/// Default circular buffer / allocator capacity (bytes).
19pub(crate) const DEFAULT_BUFFER_CAPACITY: i64 = 256;
20/// Default temporal deadline (milliseconds).
21pub(crate) const DEFAULT_DEADLINE_MS: i64 = 1000;
22/// Default bit-level container width (bits).
23pub(crate) const DEFAULT_BIT_CONTAINER_BITS: i64 = 64;
24/// Default checksum / region size (bytes).
25pub(crate) const DEFAULT_REGION_SIZE: i64 = 1024;
26/// Default page-cache page size (bytes).
27pub(crate) const DEFAULT_PAGE_SIZE: i64 = 1024;
28/// Default feature-flag maximum count.
29pub(crate) const DEFAULT_FEATURE_MAX: i64 = 256;
30/// Default hash output length (bytes).
31pub(crate) const DEFAULT_HASH_BITS: i64 = 32;
32
33// ---- Numeric precision defaults ----
34
35/// Default ULP (Unit in the Last Place) tolerance for numerical precision checks.
36pub(crate) const DEFAULT_ULP_TOLERANCE: f64 = 1.0;
37
38// ---- Parameter extraction defaults ----
39// These represent "if the user didn't specify, use zero/one as the identity."
40
41/// Default integer for absent clause arguments (zero value).
42pub(crate) const DEFAULT_PARAM_ZERO: i64 = 0;
43/// Default integer for absent clause arguments (unit value).
44pub(crate) const DEFAULT_PARAM_ONE: i64 = 1;
45
46// ---------------------------------------------------------------------------
47// Type representation
48// ---------------------------------------------------------------------------
49
50/// Represents all Assura types in the type checker.
51///
52/// # Indeterminate types
53///
54/// Two variants represent "we don't have a concrete type":
55/// - [`Unknown`](Type::Unknown): genuinely unknown (unresolved reference, missing type args)
56/// - [`Error`](Type::Error): error already reported upstream; suppresses cascading diagnostics
57///
58/// Always use [`is_indeterminate()`](Type::is_indeterminate) instead of matching
59/// `Type::Unknown` directly, to avoid missing `Error` and producing cascade false positives.
60///
61/// # Numeric types
62///
63/// `Int`, `Nat`, `Float`, and fixed-width variants (`U8`..`I64`, `F32`, `F64`) are all
64/// considered numeric. Use `is_numeric()` to test.
65#[derive(Debug, Clone, PartialEq)]
66pub enum Type {
67    // --- Base types ---
68    Int,
69    Nat,
70    Float,
71    Bool,
72    String,
73    Bytes,
74    Unit,
75    Never,
76
77    // --- Fixed-width integers ---
78    U8,
79    U16,
80    U32,
81    U64,
82    I8,
83    I16,
84    I32,
85    I64,
86    F32,
87    F64,
88
89    // --- Generic container types ---
90    List(Box<Type>),
91    Map(Box<Type>, Box<Type>),
92    Set(Box<Type>),
93    Option(Box<Type>),
94    Result(Box<Type>, Box<Type>),
95
96    // --- Sequence (used in demos) ---
97    Sequence(Box<Type>),
98
99    // --- User-defined named type ---
100    Named(String),
101
102    // --- Generic type parameter ---
103    TypeParam(String),
104
105    // --- Function type ---
106    Fn {
107        params: Vec<Type>,
108        ret: Box<Type>,
109    },
110
111    // --- Tuple type ---
112    Tuple(Vec<Type>),
113
114    // --- Refined type: base type with predicate ---
115    Refined {
116        base: Box<Type>,
117        /// Parsed predicate expression (structural AST node).
118        predicate: Box<SpExpr>,
119        /// The variable bound by the refinement (e.g., "x" in `{x: Int | x > 0}`).
120        bound_var: String,
121    },
122
123    // --- Genuinely unknown type (unresolved reference, unparsed tokens) ---
124    Unknown,
125
126    // --- Error recovery: a type error was already reported upstream ---
127    /// Distinct from `Unknown`: `Error` suppresses cascading errors,
128    /// while `Unknown` means "we genuinely don't know yet".
129    Error,
130}
131
132impl Type {
133    /// Returns `true` if this type is indeterminate (either genuinely
134    /// unknown or an error-recovery placeholder). Use this instead of
135    /// matching `Type::Unknown` directly when deciding whether to
136    /// suppress further diagnostics.
137    pub(crate) fn is_indeterminate(&self) -> bool {
138        matches!(self, Type::Unknown | Type::Error)
139    }
140
141    /// Construct a refined type from string-form predicate (convenience).
142    ///
143    /// The predicate text is stored as `Expr::Raw` tokens for backward
144    /// compatibility. Use the constructor directly with a parsed `Expr`
145    /// for structural analysis.
146    pub fn refined_from_str(base: Type, bound_var: &str, predicate_text: &str) -> Self {
147        let tokens: Vec<String> = if predicate_text.is_empty() {
148            vec![]
149        } else {
150            predicate_text
151                .split_whitespace()
152                .map(String::from)
153                .collect()
154        };
155        Type::Refined {
156            base: Box::new(base),
157            predicate: Box::new(Spanned::no_span(Expr::Raw(tokens))),
158            bound_var: bound_var.to_string(),
159        }
160    }
161
162    /// Get the predicate as a display string.
163    pub fn predicate_str(&self) -> Option<String> {
164        if let Type::Refined { predicate, .. } = self {
165            let s = expr_to_string(predicate);
166            if s.is_empty() { None } else { Some(s) }
167        } else {
168            None
169        }
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Type environment
175// ---------------------------------------------------------------------------
176
177/// Maps names to their types. This is the typing context built during
178/// type checking.
179#[derive(Debug, Clone, Default)]
180pub struct TypeEnv {
181    /// Scope stack: the last element is the innermost (current) scope.
182    /// There is always at least one scope (the global scope).
183    scopes: Vec<Scope>,
184    /// Maps struct type name -> { field_name -> field_type }.
185    /// Struct field definitions are always global (not scope-dependent).
186    pub struct_fields: HashMap<String, Vec<(String, Type)>>,
187}
188
189/// A single scope level in the type environment.
190#[derive(Debug, Clone, Default)]
191struct Scope {
192    bindings: HashMap<String, Type>,
193}
194
195impl TypeEnv {
196    /// Create an empty type environment with one global scope.
197    pub fn new() -> Self {
198        Self {
199            scopes: vec![Scope::default()],
200            struct_fields: HashMap::new(),
201        }
202    }
203
204    /// Push a new (empty) scope. Bindings inserted after this call
205    /// shadow outer names and are removed when `pop_scope` is called.
206    pub fn push_scope(&mut self) {
207        self.scopes.push(Scope::default());
208    }
209
210    /// Pop the innermost scope, removing all bindings introduced in it.
211    ///
212    /// # Panics
213    /// Panics if only the global scope remains (you cannot pop the root).
214    pub fn pop_scope(&mut self) {
215        assert!(
216            self.scopes.len() > 1,
217            "cannot pop the global scope from TypeEnv"
218        );
219        self.scopes.pop();
220    }
221
222    /// Current nesting depth (0 = global scope only).
223    pub fn depth(&self) -> usize {
224        self.scopes.len() - 1
225    }
226
227    /// Insert a binding into the *current* (innermost) scope.
228    /// Returns the previous type if the name was already bound
229    /// in this same scope.
230    pub fn insert(&mut self, name: String, ty: Type) -> Option<Type> {
231        self.scopes
232            .last_mut()
233            .expect("TypeEnv must have at least one scope")
234            .bindings
235            .insert(name, ty)
236    }
237
238    /// Look up a name, searching from the innermost scope outward.
239    pub fn lookup(&self, name: &str) -> Option<&Type> {
240        for scope in self.scopes.iter().rev() {
241            if let Some(ty) = scope.bindings.get(name) {
242                return Some(ty);
243            }
244        }
245        None
246    }
247
248    /// Look up a field type on a struct type.
249    pub(crate) fn lookup_field(&self, struct_name: &str, field_name: &str) -> Option<&Type> {
250        self.struct_fields
251            .get(struct_name)
252            .and_then(|fields| fields.iter().find(|(n, _)| n == field_name).map(|(_, t)| t))
253    }
254
255    /// Total number of bindings across all scopes.
256    pub fn len(&self) -> usize {
257        self.scopes.iter().map(|s| s.bindings.len()).sum()
258    }
259
260    /// Returns true if no bindings exist in any scope.
261    pub fn is_empty(&self) -> bool {
262        self.scopes.iter().all(|s| s.bindings.is_empty())
263    }
264
265    /// Iterate over all bindings from outermost to innermost scope.
266    /// If a name appears in multiple scopes, only the innermost (shadowing)
267    /// binding is yielded.
268    pub fn iter(&self) -> impl Iterator<Item = (&str, &Type)> {
269        let mut seen = HashMap::new();
270        for scope in &self.scopes {
271            for (name, ty) in &scope.bindings {
272                seen.insert(name.as_str(), ty);
273            }
274        }
275        seen.into_iter()
276    }
277}
278
279// ---------------------------------------------------------------------------
280// Type errors
281// ---------------------------------------------------------------------------
282
283/// A structured type error with error code, span, and message.
284#[derive(Debug, Clone)]
285pub struct TypeError {
286    /// Error code from the spec (A03xxx series).
287    pub code: assura_diagnostics::ErrorCode,
288    /// Human-readable error message.
289    pub message: String,
290    /// Source location where the error was detected.
291    pub span: Range<usize>,
292    /// Optional secondary span with label (e.g., "expected type declared here").
293    pub secondary: Option<(Range<usize>, String)>,
294    /// Optional fix suggestion (e.g., "add an explicit type annotation").
295    /// When `None`, the `From<TypeError> for Diagnostic` impl falls back to
296    /// the error catalog's `fix` text for this error code (if any).
297    pub suggestion: Option<String>,
298}
299
300impl TypeError {
301    /// Enrich the error message with additional context while preserving all other fields.
302    pub fn with_context(self, context: &str) -> Self {
303        Self {
304            message: format!("{} ({context})", self.message),
305            ..self
306        }
307    }
308}
309
310impl From<TypeError> for assura_diagnostics::Diagnostic {
311    fn from(e: TypeError) -> Self {
312        let mut d = assura_diagnostics::Diagnostic::error(e.code.clone(), e.message, e.span);
313        if let Some((span, label)) = e.secondary {
314            d.secondary.push(assura_diagnostics::SecondaryLabel {
315                span,
316                message: label,
317            });
318        }
319        // Use the explicit suggestion if provided; otherwise fall back to
320        // the error catalog's `fix` text for this error code.
321        let suggestion_text = e.suggestion.or_else(|| {
322            assura_diagnostics::explain(e.code.as_str()).map(|info| info.fix.to_string())
323        });
324        if let Some(text) = suggestion_text {
325            let span = d.primary.clone();
326            d = d.with_suggestion(text, span, "");
327        }
328        d
329    }
330}
331
332// ---------------------------------------------------------------------------
333// Typed file
334// ---------------------------------------------------------------------------
335
336/// The result of successful type checking: the resolved file plus the
337/// type environment constructed from its symbols.
338#[derive(Debug, Clone)]
339pub struct TypedFile {
340    pub resolved: Arc<ResolvedFile>,
341    pub type_env: TypeEnv,
342    /// Pending decrease checks that need SMT verification.
343    /// The CLI pipeline dispatches these to assura-smt::verify_decrease().
344    pub pending_decrease_checks: Vec<PendingDecreaseCheck>,
345    /// Generated tests from contracts (TEST.1). Populated by the type
346    /// checking pipeline when contracts have testable constraints.
347    pub generated_tests: Vec<crate::GeneratedTest>,
348    /// Non-fatal warnings from type checking (e.g., unconstrained output
349    /// references in ensures clauses, feature_max in verification clauses).
350    pub warnings: Vec<TypeError>,
351}
352
353// ---------------------------------------------------------------------------
354// Built-in type mapping
355// ---------------------------------------------------------------------------
356
357/// Map a built-in type name to its `Type` representation.
358pub(crate) fn builtin_type(name: &str) -> Option<Type> {
359    match name {
360        "Int" => Some(Type::Int),
361        "Nat" => Some(Type::Nat),
362        "Float" => Some(Type::Float),
363        "Bool" => Some(Type::Bool),
364        "String" => Some(Type::String),
365        "Bytes" => Some(Type::Bytes),
366        "Unit" => Some(Type::Unit),
367        "Never" => Some(Type::Never),
368        "U8" => Some(Type::U8),
369        "U16" => Some(Type::U16),
370        "U32" => Some(Type::U32),
371        "U64" => Some(Type::U64),
372        "I8" => Some(Type::I8),
373        "I16" => Some(Type::I16),
374        "I32" => Some(Type::I32),
375        "I64" => Some(Type::I64),
376        "F32" => Some(Type::F32),
377        "F64" => Some(Type::F64),
378        // Generic container types with no type arguments (bare names).
379        // Full `List<Int>` etc. is handled by parse_type_tokens above.
380        "List" => Some(Type::List(Box::new(Type::Unknown))),
381        "Map" => Some(Type::Map(Box::new(Type::Unknown), Box::new(Type::Unknown))),
382        "Set" => Some(Type::Set(Box::new(Type::Unknown))),
383        "Option" => Some(Type::Option(Box::new(Type::Unknown))),
384        "Result" => Some(Type::Result(
385            Box::new(Type::Unknown),
386            Box::new(Type::Unknown),
387        )),
388        "Sequence" => Some(Type::Sequence(Box::new(Type::Unknown))),
389        _ => None,
390    }
391}
392
393// ---------------------------------------------------------------------------
394// Type display (for error messages)
395// ---------------------------------------------------------------------------
396
397impl std::fmt::Display for Type {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        match self {
400            Type::Int => write!(f, "Int"),
401            Type::Nat => write!(f, "Nat"),
402            Type::Float => write!(f, "Float"),
403            Type::Bool => write!(f, "Bool"),
404            Type::String => write!(f, "String"),
405            Type::Bytes => write!(f, "Bytes"),
406            Type::Unit => write!(f, "Unit"),
407            Type::Never => write!(f, "Never"),
408            Type::U8 => write!(f, "U8"),
409            Type::U16 => write!(f, "U16"),
410            Type::U32 => write!(f, "U32"),
411            Type::U64 => write!(f, "U64"),
412            Type::I8 => write!(f, "I8"),
413            Type::I16 => write!(f, "I16"),
414            Type::I32 => write!(f, "I32"),
415            Type::I64 => write!(f, "I64"),
416            Type::F32 => write!(f, "F32"),
417            Type::F64 => write!(f, "F64"),
418            Type::List(t) => write!(f, "List<{t}>"),
419            Type::Map(k, v) => write!(f, "Map<{k}, {v}>"),
420            Type::Set(t) => write!(f, "Set<{t}>"),
421            Type::Option(t) => write!(f, "Option<{t}>"),
422            Type::Result(t, e) => write!(f, "Result<{t}, {e}>"),
423            Type::Sequence(t) => write!(f, "Sequence<{t}>"),
424            Type::Named(n) => write!(f, "{n}"),
425            Type::TypeParam(n) => write!(f, "{n}"),
426            Type::Fn { params, ret } => {
427                write!(f, "fn(")?;
428                for (i, p) in params.iter().enumerate() {
429                    if i > 0 {
430                        write!(f, ", ")?;
431                    }
432                    write!(f, "{p}")?;
433                }
434                write!(f, ") -> {ret}")
435            }
436            Type::Tuple(elems) => {
437                write!(f, "(")?;
438                for (i, t) in elems.iter().enumerate() {
439                    if i > 0 {
440                        write!(f, ", ")?;
441                    }
442                    write!(f, "{t}")?;
443                }
444                // Trailing comma for 1-tuples so display matches Assura syntax (Int,).
445                if elems.len() == 1 {
446                    write!(f, ",")?;
447                }
448                write!(f, ")")
449            }
450            Type::Refined {
451                base,
452                predicate,
453                bound_var,
454            } => {
455                let pred_str = expr_to_string(predicate);
456                if pred_str.is_empty() {
457                    write!(f, "{base}")
458                } else {
459                    write!(f, "{{ {bound_var} : {base} | {pred_str} }}")
460                }
461            }
462            Type::Unknown => write!(f, "Unknown"),
463            Type::Error => write!(f, "<error>"),
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    // ---- is_indeterminate ----
473
474    #[test]
475    fn is_indeterminate_unknown() {
476        assert!(Type::Unknown.is_indeterminate());
477    }
478
479    #[test]
480    fn is_indeterminate_error() {
481        assert!(Type::Error.is_indeterminate());
482    }
483
484    #[test]
485    fn is_indeterminate_concrete_types_return_false() {
486        let concrete = [
487            Type::Int,
488            Type::Nat,
489            Type::Float,
490            Type::Bool,
491            Type::String,
492            Type::Bytes,
493            Type::Unit,
494            Type::Never,
495            Type::U8,
496            Type::U16,
497            Type::U32,
498            Type::U64,
499            Type::I8,
500            Type::I16,
501            Type::I32,
502            Type::I64,
503            Type::F32,
504            Type::F64,
505            Type::List(Box::new(Type::Int)),
506            Type::Map(Box::new(Type::String), Box::new(Type::Int)),
507            Type::Set(Box::new(Type::Nat)),
508            Type::Option(Box::new(Type::Bool)),
509            Type::Result(Box::new(Type::Int), Box::new(Type::String)),
510            Type::Sequence(Box::new(Type::Int)),
511            Type::Named("Foo".into()),
512            Type::TypeParam("T".into()),
513            Type::Fn {
514                params: vec![Type::Int],
515                ret: Box::new(Type::Bool),
516            },
517            Type::Tuple(vec![Type::Int, Type::Bool]),
518            Type::refined_from_str(Type::Int, "x", "x > 0"),
519        ];
520        for ty in &concrete {
521            assert!(!ty.is_indeterminate(), "{ty} should not be indeterminate");
522        }
523    }
524
525    // ---- Display formatting ----
526
527    #[test]
528    fn display_base_types() {
529        assert_eq!(Type::Int.to_string(), "Int");
530        assert_eq!(Type::Nat.to_string(), "Nat");
531        assert_eq!(Type::Float.to_string(), "Float");
532        assert_eq!(Type::Bool.to_string(), "Bool");
533        assert_eq!(Type::String.to_string(), "String");
534        assert_eq!(Type::Bytes.to_string(), "Bytes");
535        assert_eq!(Type::Unit.to_string(), "Unit");
536        assert_eq!(Type::Never.to_string(), "Never");
537    }
538
539    #[test]
540    fn display_fixed_width_integers() {
541        assert_eq!(Type::U8.to_string(), "U8");
542        assert_eq!(Type::U64.to_string(), "U64");
543        assert_eq!(Type::I32.to_string(), "I32");
544        assert_eq!(Type::F64.to_string(), "F64");
545    }
546
547    #[test]
548    fn display_generic_containers() {
549        assert_eq!(Type::List(Box::new(Type::Int)).to_string(), "List<Int>");
550        assert_eq!(
551            Type::Map(Box::new(Type::String), Box::new(Type::Nat)).to_string(),
552            "Map<String, Nat>"
553        );
554        assert_eq!(Type::Set(Box::new(Type::Bool)).to_string(), "Set<Bool>");
555        assert_eq!(
556            Type::Option(Box::new(Type::Float)).to_string(),
557            "Option<Float>"
558        );
559        assert_eq!(
560            Type::Result(Box::new(Type::Int), Box::new(Type::String)).to_string(),
561            "Result<Int, String>"
562        );
563        assert_eq!(
564            Type::Sequence(Box::new(Type::Bytes)).to_string(),
565            "Sequence<Bytes>"
566        );
567    }
568
569    #[test]
570    fn display_fn_type() {
571        let ty = Type::Fn {
572            params: vec![Type::Int, Type::Bool],
573            ret: Box::new(Type::String),
574        };
575        assert_eq!(ty.to_string(), "fn(Int, Bool) -> String");
576    }
577
578    #[test]
579    fn display_fn_no_params() {
580        let ty = Type::Fn {
581            params: vec![],
582            ret: Box::new(Type::Unit),
583        };
584        assert_eq!(ty.to_string(), "fn() -> Unit");
585    }
586
587    #[test]
588    fn display_tuple() {
589        let ty = Type::Tuple(vec![Type::Int, Type::Bool, Type::String]);
590        assert_eq!(ty.to_string(), "(Int, Bool, String)");
591    }
592
593    #[test]
594    fn display_tuple_single_element_trailing_comma() {
595        let ty = Type::Tuple(vec![Type::Int]);
596        assert_eq!(ty.to_string(), "(Int,)");
597    }
598
599    #[test]
600    fn display_refined_with_predicate() {
601        let ty = Type::refined_from_str(Type::Int, "x", "x > 0");
602        assert_eq!(ty.to_string(), "{ x : Int | x > 0 }");
603    }
604
605    #[test]
606    fn display_refined_empty_predicate() {
607        let ty = Type::refined_from_str(Type::Nat, "x", "");
608        // Empty predicate just displays the base type
609        assert_eq!(ty.to_string(), "Nat");
610    }
611
612    #[test]
613    fn display_unknown_and_error() {
614        assert_eq!(Type::Unknown.to_string(), "Unknown");
615        assert_eq!(Type::Error.to_string(), "<error>");
616    }
617
618    #[test]
619    fn display_named_and_type_param() {
620        assert_eq!(Type::Named("MyStruct".into()).to_string(), "MyStruct");
621        assert_eq!(Type::TypeParam("T".into()).to_string(), "T");
622    }
623
624    #[test]
625    fn display_nested_generics() {
626        // List<Option<Int>>
627        let ty = Type::List(Box::new(Type::Option(Box::new(Type::Int))));
628        assert_eq!(ty.to_string(), "List<Option<Int>>");
629    }
630
631    // ---- builtin_type ----
632
633    #[test]
634    fn builtin_type_base_types() {
635        assert_eq!(builtin_type("Int"), Some(Type::Int));
636        assert_eq!(builtin_type("Nat"), Some(Type::Nat));
637        assert_eq!(builtin_type("Float"), Some(Type::Float));
638        assert_eq!(builtin_type("Bool"), Some(Type::Bool));
639        assert_eq!(builtin_type("String"), Some(Type::String));
640        assert_eq!(builtin_type("Bytes"), Some(Type::Bytes));
641        assert_eq!(builtin_type("Unit"), Some(Type::Unit));
642        assert_eq!(builtin_type("Never"), Some(Type::Never));
643    }
644
645    #[test]
646    fn builtin_type_fixed_width() {
647        assert_eq!(builtin_type("U8"), Some(Type::U8));
648        assert_eq!(builtin_type("U16"), Some(Type::U16));
649        assert_eq!(builtin_type("U32"), Some(Type::U32));
650        assert_eq!(builtin_type("U64"), Some(Type::U64));
651        assert_eq!(builtin_type("I8"), Some(Type::I8));
652        assert_eq!(builtin_type("I64"), Some(Type::I64));
653        assert_eq!(builtin_type("F32"), Some(Type::F32));
654        assert_eq!(builtin_type("F64"), Some(Type::F64));
655    }
656
657    #[test]
658    fn builtin_type_generic_containers_bare() {
659        // Bare generic names produce Unknown inner types
660        assert_eq!(
661            builtin_type("List"),
662            Some(Type::List(Box::new(Type::Unknown)))
663        );
664        assert_eq!(
665            builtin_type("Set"),
666            Some(Type::Set(Box::new(Type::Unknown)))
667        );
668        assert_eq!(
669            builtin_type("Option"),
670            Some(Type::Option(Box::new(Type::Unknown)))
671        );
672    }
673
674    #[test]
675    fn builtin_type_unknown_name() {
676        assert_eq!(builtin_type("FooBar"), None);
677        assert_eq!(builtin_type(""), None);
678        assert_eq!(builtin_type("int"), None); // case-sensitive
679    }
680
681    // ---- TypeEnv ----
682
683    #[test]
684    fn type_env_insert_and_lookup() {
685        let mut env = TypeEnv::new();
686        assert!(env.is_empty());
687        assert_eq!(env.len(), 0);
688
689        env.insert("x".into(), Type::Int);
690        assert_eq!(env.lookup("x"), Some(&Type::Int));
691        assert_eq!(env.len(), 1);
692        assert!(!env.is_empty());
693    }
694
695    #[test]
696    fn type_env_insert_overwrites() {
697        let mut env = TypeEnv::new();
698        let prev = env.insert("x".into(), Type::Int);
699        assert!(prev.is_none());
700
701        let prev = env.insert("x".into(), Type::Bool);
702        assert_eq!(prev, Some(Type::Int));
703        assert_eq!(env.lookup("x"), Some(&Type::Bool));
704    }
705
706    #[test]
707    fn type_env_lookup_missing() {
708        let env = TypeEnv::new();
709        assert_eq!(env.lookup("nonexistent"), None);
710    }
711
712    #[test]
713    fn type_env_lookup_field() {
714        let mut env = TypeEnv::new();
715        env.struct_fields.insert(
716            "Point".into(),
717            vec![("x".into(), Type::Float), ("y".into(), Type::Float)],
718        );
719        assert_eq!(env.lookup_field("Point", "x"), Some(&Type::Float));
720        assert_eq!(env.lookup_field("Point", "z"), None);
721        assert_eq!(env.lookup_field("Unknown", "x"), None);
722    }
723
724    // ---- TypeError ----
725
726    #[test]
727    fn type_error_with_context() {
728        let err = TypeError {
729            code: "A03001".into(),
730            message: "type mismatch".into(),
731            span: 10..20,
732            secondary: None,
733            suggestion: None,
734        };
735        let enriched = err.with_context("in function foo");
736        assert_eq!(enriched.message, "type mismatch (in function foo)");
737        assert_eq!(enriched.span, 10..20);
738    }
739
740    #[test]
741    fn type_error_to_diagnostic_with_explicit_suggestion() {
742        let err = TypeError {
743            code: "A03001".into(),
744            message: "type mismatch".into(),
745            span: 10..20,
746            secondary: None,
747            suggestion: Some("use `as Int` to cast".into()),
748        };
749        let diag: assura_diagnostics::Diagnostic = err.into();
750        assert_eq!(diag.code, "A03001");
751        let s = diag.suggestion.expect("should have suggestion");
752        assert_eq!(s.message, "use `as Int` to cast");
753    }
754
755    #[test]
756    fn type_error_to_diagnostic_falls_back_to_catalog() {
757        // A03001 exists in the catalog with a non-empty fix field
758        let err = TypeError {
759            code: "A03001".into(),
760            message: "type mismatch".into(),
761            span: 0..5,
762            secondary: None,
763            suggestion: None,
764        };
765        let diag: assura_diagnostics::Diagnostic = err.into();
766        // The catalog fallback should populate the suggestion
767        let s = diag
768            .suggestion
769            .expect("catalog fallback should produce suggestion");
770        assert!(
771            !s.message.is_empty(),
772            "catalog fix text should not be empty"
773        );
774    }
775
776    /// #903: catalog Help for A03005 must be field-oriented (not "calling a function").
777    #[test]
778    fn a03005_catalog_help_is_field_oriented() {
779        let err = TypeError {
780            code: "A03005".into(),
781            message: "tuple index `2` out of range for type `(Int, Bool)` (arity 2)".into(),
782            span: 0..5,
783            secondary: None,
784            suggestion: None,
785        };
786        let diag: assura_diagnostics::Diagnostic = err.into();
787        let s = diag
788            .suggestion
789            .expect("A03005 catalog should provide Help/suggestion");
790        let help = s.message.to_lowercase();
791        assert!(
792            !help.contains("calling a function"),
793            "A03005 Help must not mention calling a function, got: {}",
794            s.message
795        );
796        assert!(
797            help.contains("field") || help.contains("tuple"),
798            "A03005 Help should be field-oriented, got: {}",
799            s.message
800        );
801    }
802
803    #[test]
804    fn type_error_to_diagnostic_no_suggestion_for_unknown_code() {
805        let err = TypeError {
806            code: "A00000".into(),
807            message: "unknown error".into(),
808            span: 0..1,
809            secondary: None,
810            suggestion: None,
811        };
812        let diag: assura_diagnostics::Diagnostic = err.into();
813        // A00000 is not in the catalog, so no suggestion
814        assert!(diag.suggestion.is_none());
815    }
816
817    // ---- Scoped TypeEnv ----
818
819    #[test]
820    fn typeenv_global_scope_lookup() {
821        let mut env = TypeEnv::new();
822        env.insert("x".into(), Type::Int);
823        assert_eq!(env.lookup("x"), Some(&Type::Int));
824        assert_eq!(env.depth(), 0);
825    }
826
827    #[test]
828    fn typeenv_push_pop_scope() {
829        let mut env = TypeEnv::new();
830        env.insert("x".into(), Type::Int);
831        assert_eq!(env.depth(), 0);
832
833        env.push_scope();
834        assert_eq!(env.depth(), 1);
835        // Can still see outer binding
836        assert_eq!(env.lookup("x"), Some(&Type::Int));
837
838        // Inner binding shadows outer
839        env.insert("x".into(), Type::Bool);
840        assert_eq!(env.lookup("x"), Some(&Type::Bool));
841
842        env.pop_scope();
843        assert_eq!(env.depth(), 0);
844        // Shadowing removed, original type restored
845        assert_eq!(env.lookup("x"), Some(&Type::Int));
846    }
847
848    #[test]
849    fn typeenv_nested_scopes() {
850        let mut env = TypeEnv::new();
851        env.insert("a".into(), Type::Int);
852
853        env.push_scope();
854        env.insert("b".into(), Type::Bool);
855
856        env.push_scope();
857        env.insert("c".into(), Type::String);
858
859        // All visible from innermost scope
860        assert_eq!(env.lookup("a"), Some(&Type::Int));
861        assert_eq!(env.lookup("b"), Some(&Type::Bool));
862        assert_eq!(env.lookup("c"), Some(&Type::String));
863        assert_eq!(env.depth(), 2);
864
865        env.pop_scope();
866        assert!(env.lookup("c").is_none());
867        assert_eq!(env.lookup("b"), Some(&Type::Bool));
868
869        env.pop_scope();
870        assert!(env.lookup("b").is_none());
871        assert_eq!(env.lookup("a"), Some(&Type::Int));
872    }
873
874    #[test]
875    fn typeenv_inner_binding_does_not_leak() {
876        let mut env = TypeEnv::new();
877        env.push_scope();
878        env.insert("local".into(), Type::Nat);
879        assert_eq!(env.lookup("local"), Some(&Type::Nat));
880        env.pop_scope();
881        assert!(env.lookup("local").is_none());
882    }
883
884    #[test]
885    fn typeenv_len_counts_all_scopes() {
886        let mut env = TypeEnv::new();
887        env.insert("x".into(), Type::Int);
888        env.push_scope();
889        env.insert("y".into(), Type::Bool);
890        assert_eq!(env.len(), 2);
891        env.pop_scope();
892        assert_eq!(env.len(), 1);
893    }
894
895    #[test]
896    fn typeenv_is_empty_across_scopes() {
897        let mut env = TypeEnv::new();
898        assert!(env.is_empty());
899        env.push_scope();
900        assert!(env.is_empty());
901        env.insert("x".into(), Type::Int);
902        assert!(!env.is_empty());
903        env.pop_scope();
904        assert!(env.is_empty());
905    }
906
907    #[test]
908    #[should_panic(expected = "cannot pop the global scope")]
909    fn typeenv_pop_global_panics() {
910        let mut env = TypeEnv::new();
911        env.pop_scope(); // should panic
912    }
913
914    #[test]
915    fn typeenv_struct_fields_not_scope_dependent() {
916        let mut env = TypeEnv::new();
917        env.struct_fields
918            .insert("Point".into(), vec![("x".into(), Type::Float)]);
919        env.push_scope();
920        assert_eq!(env.lookup_field("Point", "x"), Some(&Type::Float));
921        env.pop_scope();
922        assert_eq!(env.lookup_field("Point", "x"), Some(&Type::Float));
923    }
924
925    // ---- Refined type with Expr predicate ----
926
927    #[test]
928    fn refined_from_str_creates_expr_raw() {
929        let ty = Type::refined_from_str(Type::Int, "x", "x > 0");
930        if let Type::Refined {
931            base,
932            predicate,
933            bound_var,
934        } = &ty
935        {
936            assert_eq!(**base, Type::Int);
937            assert_eq!(bound_var, "x");
938            // Predicate should be Expr::Raw with split tokens
939            assert!(
940                matches!(&predicate.node, Expr::Raw(tokens) if tokens.len() == 3),
941                "expected 3-token Raw, got {:?}",
942                predicate.node
943            );
944        } else {
945            panic!("expected Refined");
946        }
947    }
948
949    #[test]
950    fn refined_predicate_str_returns_text() {
951        let ty = Type::refined_from_str(Type::Nat, "v", "v >= 0");
952        assert_eq!(ty.predicate_str(), Some("v >= 0".into()));
953    }
954
955    #[test]
956    fn refined_predicate_str_empty_returns_none() {
957        let ty = Type::refined_from_str(Type::Int, "x", "");
958        assert_eq!(ty.predicate_str(), None);
959    }
960
961    #[test]
962    fn refined_display_uses_bound_var() {
963        let ty = Type::refined_from_str(Type::Int, "v", "v > 0");
964        assert_eq!(ty.to_string(), "{ v : Int | v > 0 }");
965    }
966
967    #[test]
968    fn refined_with_structural_expr() {
969        // Construct a Refined type with a real BinOp expression
970        use assura_parser::ast::BinOp;
971        let pred = Spanned::no_span(Expr::BinOp {
972            lhs: Box::new(Spanned::no_span(Expr::Ident("x".into()))),
973            op: BinOp::Gt,
974            rhs: Box::new(Spanned::no_span(Expr::Literal(
975                assura_parser::ast::Literal::Int("0".into()),
976            ))),
977        });
978        let ty = Type::Refined {
979            base: Box::new(Type::Int),
980            predicate: Box::new(pred),
981            bound_var: "x".into(),
982        };
983        // Should display as { x : Int | x > 0 }
984        let s = ty.to_string();
985        assert!(s.contains("x") && s.contains("Int"), "got: {s}");
986    }
987
988    #[test]
989    fn refined_non_refined_type_predicate_str_is_none() {
990        assert_eq!(Type::Int.predicate_str(), None);
991        assert_eq!(Type::Bool.predicate_str(), None);
992    }
993}