diff --git a/.github/workflows/build_all_packages.yml b/.github/workflows/build_all_packages.yml new file mode 100644 index 00000000..66900c39 --- /dev/null +++ b/.github/workflows/build_all_packages.yml @@ -0,0 +1,95 @@ +name: Build All Packages + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + MODULE_TIMEOUT_SECS: 10 + +permissions: + pull-requests: write + +jobs: + build-all-packages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry & build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: build-all-packages-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + build-all-packages-${{ runner.os }}-cargo- + + - name: Build (release) + run: cargo clean && cargo build --release + + - name: Run build_all_packages + id: build_test + run: | + cargo test --release --test build build_all_packages --no-run + start=$(date +%s%N) + cargo test --release --test build build_all_packages -- --exact --ignored --nocapture 2>&1 | tee /tmp/build_output.txt + exit_code=${PIPESTATUS[0]} + end=$(date +%s%N) + elapsed_ms=$(( (end - start) / 1000000 )) + + if [ $elapsed_ms -ge 1000 ]; then + elapsed_s=$(echo "scale=2; $elapsed_ms / 1000" | bc) + echo "elapsed_display=${elapsed_s}s" >> "$GITHUB_OUTPUT" + else + echo "elapsed_display=${elapsed_ms}ms" >> "$GITHUB_OUTPUT" + fi + + results_line=$(grep '^Results:' /tmp/build_output.txt || echo "No results summary found") + echo "results=${results_line}" >> "$GITHUB_OUTPUT" + + exit $exit_code + + - name: Report results on PR + if: github.event_name == 'pull_request' && always() + uses: actions/github-script@v7 + with: + script: | + const elapsed = '${{ steps.build_test.outputs.elapsed_display }}'; + const results = '${{ steps.build_test.outputs.results }}'; + const status = '${{ steps.build_test.outcome }}'; + const icon = status === 'success' ? '✅' : '❌'; + const body = `**Build all packages:** ${icon} ${elapsed}\n\n\`${results}\``; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => + c.user.type === 'Bot' && c.body.startsWith('**Build all packages:**') + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd89f05b..9a4e57e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,12 +28,12 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + key: test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo- + test-${{ runner.os }}-cargo- - name: Build - run: cargo build --release + run: cargo clean && cargo build --release - name: Run tests id: tests diff --git a/.gitignore b/.gitignore index cd208ebe..f6742631 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ Thumbs.db # Test files /test.purs *.purs.bak + +/tests/oa \ No newline at end of file diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 00000000..6fe4279a --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,3096 @@ +//! Abstract Syntax Tree (AST) for PureScript +//! +//! Similar to the CST but with: +//! - Parentheses removed (replaced by inner value) +//! - Operators desugared to function application +//! - Definition sites on key nodes + +use std::collections::{HashMap, HashSet}; + +use crate::cst::{ + self, Associativity, ExportList, FunDep, ImportDecl, ImportList, KindSigSource, ModuleName, + QualifiedIdent, Spanned, +}; +use crate::interner::{self, intern, Symbol}; +use crate::lexer::token::Ident; +use crate::span::Span; +use crate::typechecker::error::TypeError; +use crate::typechecker::registry::{ModuleExports, ModuleRegistry}; + +/// Where a name was defined +#[derive(Debug, Clone, PartialEq)] +pub enum DefinitionSite { + Local(Span), + Imported { module: Ident }, +} + +/// Module +#[derive(Debug, Clone, PartialEq)] +pub struct Module { + pub span: Span, + pub name: Spanned, + pub exports: Option>, + pub imports: Vec, + pub decls: Vec, +} + +/// Top-level declaration +#[derive(Debug, Clone, PartialEq)] +pub enum Decl { + /// Value declaration: foo = bar + Value { + span: Span, + name: Spanned, + binders: Vec, + guarded: GuardedExpr, + where_clause: Vec, + }, + + /// Type signature: foo :: Int -> Int + TypeSignature { + span: Span, + name: Spanned, + ty: TypeExpr, + }, + + /// Data declaration: data Foo a = Bar a | Baz + Data { + span: Span, + name: Spanned, + type_vars: Vec>, + constructors: Vec, + kind_sig: KindSigSource, + is_role_decl: bool, + kind_type: Option>, + type_var_kind_anns: Vec>>, + }, + + /// Type synonym: type Foo = Bar + TypeAlias { + span: Span, + name: Spanned, + type_vars: Vec>, + ty: TypeExpr, + type_var_kind_anns: Vec>>, + }, + + /// Newtype: newtype Foo = Foo Bar + Newtype { + span: Span, + name: Spanned, + type_vars: Vec>, + constructor: Spanned, + ty: TypeExpr, + type_var_kind_anns: Vec>>, + }, + + /// Type class declaration: class Eq a where ... + Class { + span: Span, + constraints: Vec, + name: Spanned, + type_vars: Vec>, + fundeps: Vec, + members: Vec, + is_kind_sig: bool, + kind_type: Option>, + type_var_kind_anns: Vec>>, + }, + + /// Instance declaration: instance Eq Int where ... + Instance { + span: Span, + name: Option>, + constraints: Vec, + class_name: QualifiedIdent, + class_definition_site: DefinitionSite, + types: Vec, + members: Vec, + chain: bool, + }, + + /// Fixity declaration: infixl 6 add as + + Fixity { + span: Span, + associativity: Associativity, + precedence: u8, + target: QualifiedIdent, + target_definition_site: DefinitionSite, + operator: Spanned, + is_type: bool, + }, + + /// Foreign value import: foreign import foo :: Type + Foreign { + span: Span, + name: Spanned, + ty: TypeExpr, + }, + + /// Foreign data import: foreign import data Foo :: Kind + ForeignData { + span: Span, + name: Spanned, + kind: TypeExpr, + }, + + /// Derive instance declaration: derive instance Eq MyType + Derive { + span: Span, + newtype: bool, + name: Option>, + constraints: Vec, + class_name: QualifiedIdent, + class_definition_site: DefinitionSite, + types: Vec, + }, +} + +/// Data constructor in data declaration +#[derive(Debug, Clone, PartialEq)] +pub struct DataConstructor { + pub span: Span, + pub name: Spanned, + pub fields: Vec, +} + +/// Class member (method signature) +#[derive(Debug, Clone, PartialEq)] +pub struct ClassMember { + pub span: Span, + pub name: Spanned, + pub ty: TypeExpr, +} + +/// Guarded expression (for pattern matching) +#[derive(Debug, Clone, PartialEq)] +pub enum GuardedExpr { + Unconditional(Box), + Guarded(Vec), +} + +/// Guard in pattern matching +#[derive(Debug, Clone, PartialEq)] +pub struct Guard { + pub span: Span, + pub patterns: Vec, + pub expr: Box, +} + +/// Pattern in guard +#[derive(Debug, Clone, PartialEq)] +pub enum GuardPattern { + Boolean(Box), + Pattern(Binder, Box), +} + +/// Expression +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + /// Variable: x, Data.Array.head + Var { + span: Span, + name: QualifiedIdent, + definition_site: DefinitionSite, + }, + + /// Constructor: Just, Nothing + Constructor { + span: Span, + name: QualifiedIdent, + definition_site: DefinitionSite, + }, + + /// Literal value + Literal { span: Span, lit: Literal }, + + /// Function application: f x + App { + span: Span, + func: Box, + arg: Box, + }, + + /// Visible type application: f @Type + VisibleTypeApp { + span: Span, + func: Box, + ty: TypeExpr, + }, + + /// Lambda: \x -> x + 1 + Lambda { + span: Span, + binders: Vec, + body: Box, + }, + + /// If-then-else + If { + span: Span, + cond: Box, + then_expr: Box, + else_expr: Box, + }, + + /// Case expression + Case { + span: Span, + exprs: Vec, + alts: Vec, + }, + + /// Let binding + Let { + span: Span, + bindings: Vec, + body: Box, + }, + + /// Do notation + Do { + span: Span, + module: Option, + statements: Vec, + }, + + Ado { + span: Span, + module: Option, + statements: Vec, + result: Box, + }, + + /// Record literal: { x: 1, y: 2 } + Record { + span: Span, + fields: Vec, + }, + + /// Record accessor: rec.field + RecordAccess { + span: Span, + expr: Box, + field: Spanned, + }, + + /// Record update: rec { x = 1 } + RecordUpdate { + span: Span, + expr: Box, + updates: Vec, + }, + + /// Type annotation: expr :: Type + TypeAnnotation { + span: Span, + expr: Box, + ty: TypeExpr, + }, + + /// Typed hole: ?hole + Hole { span: Span, name: Ident }, + + /// Array literal: [1, 2, 3] + Array { span: Span, elements: Vec }, + + /// Negation: -x + Negate { span: Span, expr: Box }, + + /// As-pattern expression: name@pattern + AsPattern { + span: Span, + name: Box, + pattern: Box, + }, +} + +/// Literal values +#[derive(Debug, Clone, PartialEq)] +pub enum Literal { + Int(i64), + Float(f64), + String(String), + Char(char), + Boolean(bool), + Array(Vec), +} + +/// Pattern binder +#[derive(Debug, Clone, PartialEq)] +pub enum Binder { + /// Wildcard: _ + Wildcard { span: Span }, + + /// Variable: x + Var { span: Span, name: Spanned }, + + /// Literal pattern: 42, "foo" + Literal { span: Span, lit: Literal }, + + /// Constructor pattern: Just x (also used for desugared operator patterns) + Constructor { + span: Span, + name: QualifiedIdent, + args: Vec, + definition_site: DefinitionSite, + }, + + /// Record pattern: { x, y } + Record { + span: Span, + fields: Vec, + }, + + /// As-pattern: x@(Just y) + As { + span: Span, + name: Spanned, + binder: Box, + }, + + /// Array pattern: [a, b, c] + Array { span: Span, elements: Vec }, + + /// Type-annotated pattern: (x :: Type) + Typed { + span: Span, + binder: Box, + ty: TypeExpr, + }, +} + +/// Case alternative +#[derive(Debug, Clone, PartialEq)] +pub struct CaseAlternative { + pub span: Span, + pub binders: Vec, + pub result: GuardedExpr, +} + +/// Let binding +#[derive(Debug, Clone, PartialEq)] +pub enum LetBinding { + /// Value binding: x = expr + Value { + span: Span, + binder: Binder, + expr: Expr, + }, + + /// Type signature: x :: Type + Signature { + span: Span, + name: Spanned, + ty: TypeExpr, + }, +} + +/// Do statement +#[derive(Debug, Clone, PartialEq)] +pub enum DoStatement { + /// Bind: x <- action + Bind { + span: Span, + binder: Binder, + expr: Expr, + }, + + /// Let: let x = expr + Let { + span: Span, + bindings: Vec, + }, + + /// Expression statement: action + Discard { span: Span, expr: Expr }, +} + +/// Record field in literal +#[derive(Debug, Clone, PartialEq)] +pub struct RecordField { + pub span: Span, + pub label: Spanned, + pub value: Option, + pub type_ann: Option, + pub is_update: bool, +} + +/// Record update +#[derive(Debug, Clone, PartialEq)] +pub struct RecordUpdate { + pub span: Span, + pub label: Spanned, + pub value: Expr, +} + +/// Record binder field +#[derive(Debug, Clone, PartialEq)] +pub struct RecordBinderField { + pub span: Span, + pub label: Spanned, + pub binder: Option, +} + +/// Type expression +#[derive(Debug, Clone, PartialEq)] +pub enum TypeExpr { + /// Type variable: a + Var { span: Span, name: Spanned }, + + /// Type constructor: Int, Array + Constructor { + span: Span, + name: QualifiedIdent, + definition_site: DefinitionSite, + }, + + /// Type application: Array Int + App { + span: Span, + constructor: Box, + arg: Box, + }, + + /// Function type: Int -> String + Function { + span: Span, + from: Box, + to: Box, + }, + + /// Forall quantification: forall a. a -> a + Forall { + span: Span, + vars: Vec<(Spanned, bool, Option>)>, + ty: Box, + }, + + /// Constrained type: Show a => a -> String + Constrained { + span: Span, + constraints: Vec, + ty: Box, + }, + + /// Record type: { x :: Int, y :: String } + Record { span: Span, fields: Vec }, + + /// Row type: (), (a :: String), ( x :: Int | r ) + Row { + span: Span, + fields: Vec, + tail: Option>, + is_record: bool, + }, + + /// Type hole: ?hole + Hole { span: Span, name: Ident }, + + /// Wildcard type: _ + Wildcard { span: Span }, + + /// Kind annotation: Const Void :: Type -> Type + Kinded { + span: Span, + ty: Box, + kind: Box, + }, + + /// Type-level string literal: "hello" + StringLiteral { span: Span, value: String }, + + /// Type-level integer literal: 42 + IntLiteral { span: Span, value: i64 }, +} + +/// Type constraint (for type classes) +#[derive(Debug, Clone, PartialEq)] +pub struct Constraint { + pub span: Span, + pub class: QualifiedIdent, + pub args: Vec, + pub definition_site: DefinitionSite, +} + +/// Type field in record/row +#[derive(Debug, Clone, PartialEq)] +pub struct TypeField { + pub span: Span, + pub label: Spanned, + pub ty: TypeExpr, +} + +// --- span() impls --- + +impl Decl { + pub fn span(&self) -> Span { + match self { + Decl::Value { span, .. } + | Decl::TypeSignature { span, .. } + | Decl::Data { span, .. } + | Decl::TypeAlias { span, .. } + | Decl::Newtype { span, .. } + | Decl::Class { span, .. } + | Decl::Instance { span, .. } + | Decl::Fixity { span, .. } + | Decl::Foreign { span, .. } + | Decl::ForeignData { span, .. } + | Decl::Derive { span, .. } => *span, + } + } + + pub fn name(&self) -> Option { + match self { + Decl::Value { name, .. } + | Decl::TypeSignature { name, .. } + | Decl::Data { name, .. } + | Decl::TypeAlias { name, .. } + | Decl::Newtype { name, .. } + | Decl::Class { name, .. } => Some(name.value), + Decl::Instance { + name: Some(name), .. + } => Some(name.value), + _ => None, + } + } +} + +impl Expr { + pub fn span(&self) -> Span { + match self { + Expr::Var { span, .. } + | Expr::Constructor { span, .. } + | Expr::Literal { span, .. } + | Expr::App { span, .. } + | Expr::Lambda { span, .. } + | Expr::If { span, .. } + | Expr::Case { span, .. } + | Expr::Let { span, .. } + | Expr::Do { span, .. } + | Expr::Ado { span, .. } + | Expr::Record { span, .. } + | Expr::RecordAccess { span, .. } + | Expr::RecordUpdate { span, .. } + | Expr::TypeAnnotation { span, .. } + | Expr::Hole { span, .. } + | Expr::Array { span, .. } + | Expr::Negate { span, .. } + | Expr::AsPattern { span, .. } + | Expr::VisibleTypeApp { span, .. } => *span, + } + } +} + +impl Binder { + pub fn span(&self) -> Span { + match self { + Binder::Wildcard { span } + | Binder::Var { span, .. } + | Binder::Literal { span, .. } + | Binder::Constructor { span, .. } + | Binder::Record { span, .. } + | Binder::As { span, .. } + | Binder::Array { span, .. } + | Binder::Typed { span, .. } => *span, + } + } +} + +impl TypeExpr { + pub fn span(&self) -> Span { + match self { + TypeExpr::Var { span, .. } + | TypeExpr::Constructor { span, .. } + | TypeExpr::App { span, .. } + | TypeExpr::Function { span, .. } + | TypeExpr::Forall { span, .. } + | TypeExpr::Constrained { span, .. } + | TypeExpr::Record { span, .. } + | TypeExpr::Row { span, .. } + | TypeExpr::Hole { span, .. } + | TypeExpr::Wildcard { span, .. } + | TypeExpr::Kinded { span, .. } + | TypeExpr::StringLiteral { span, .. } + | TypeExpr::IntLiteral { span, .. } => *span, + } + } +} + +impl DoStatement { + pub fn span(&self) -> Span { + match self { + DoStatement::Bind { span, .. } + | DoStatement::Let { span, .. } + | DoStatement::Discard { span, .. } => *span, + } + } +} + +// ===== CST → AST Conversion ===== + +pub fn convert(module: cst::Module, registry: &ModuleRegistry) -> (Module, Vec) { + let mut conv = Converter::from_module(&module, registry); + let decls = module.decls.iter().map(|d| conv.convert_decl(d)).collect(); + let ast = Module { + span: module.span, + name: module.name.clone(), + exports: module.exports.clone(), + imports: module.imports.clone(), + decls, + }; + (ast, conv.errors) +} + +/// Convert a standalone CST expression to an AST expression. +/// Uses a minimal converter with no operator fixity info or definition sites. +/// Suitable for standalone expression inference (e.g. in tests). +pub fn convert_expr(expr: cst::Expr) -> Expr { + let mut conv = Converter::default(); + conv.convert_expr(&expr) +} + +/// Operator in an infix chain: either a named operator/backtick or a complex backtick expression. +enum ChainOp<'a> { + Named(&'a Spanned), + Expr(&'a cst::Expr), +} + +fn chain_op_span(op: &ChainOp) -> Span { + match op { + ChainOp::Named(named) => named.span, + ChainOp::Expr(expr) => expr.span(), + } +} + +struct Converter { + /// Module-level values (vars, constructors, methods) → definition site + values: HashMap, + /// Module-level type constructors → definition site + types: HashMap, + /// Module-level type class names → definition site + classes: HashMap, + + /// Type-level operators: op symbol → target type name + type_operators: HashMap, + /// Type-level operator fixities + type_fixities: HashMap, + /// Value-level operator fixities + value_fixities: HashMap, + /// Value-level operator targets: op symbol → target name (e.g. + → add) + value_operator_targets: HashMap, + /// Operators that alias functions (not constructors) + function_op_aliases: HashSet, + /// Definition sites for operator targets (not user-visible, only for operator desugaring). + /// Maps target names (e.g. `add`) to their definition sites. + operator_target_sites: HashMap, + + /// Local variable scopes (pushed/popped during walk) + local_scopes: Vec>, + + /// Whether we're inside a Parens expression (enables post-rebalance section detection) + in_parens: bool, + + errors: Vec, +} + +impl Default for Converter { + fn default() -> Self { + Converter { + values: HashMap::new(), + types: HashMap::new(), + classes: HashMap::new(), + type_operators: HashMap::new(), + type_fixities: HashMap::new(), + value_fixities: HashMap::new(), + value_operator_targets: HashMap::new(), + function_op_aliases: HashSet::new(), + operator_target_sites: HashMap::new(), + local_scopes: Vec::new(), + in_parens: false, + errors: Vec::new(), + } + } +} + +fn module_name_to_symbol(name: &ModuleName) -> Symbol { + let parts: Vec = name + .parts + .iter() + .map(|p| interner::resolve(*p).unwrap_or_default()) + .collect(); + intern(&parts.join(".")) +} + +fn is_prim_module(name: &ModuleName) -> bool { + name.parts.len() == 1 && interner::resolve(name.parts[0]).unwrap_or_default() == "Prim" +} + +fn is_prim_submodule(name: &ModuleName) -> bool { + name.parts.len() >= 2 && interner::resolve(name.parts[0]).unwrap_or_default() == "Prim" +} + +fn qualified_symbol(module: Symbol, name: Symbol) -> Symbol { + let m = interner::resolve(module).unwrap_or_default(); + let n = interner::resolve(name).unwrap_or_default(); + intern(&format!("{}.{}", m, n)) +} + +impl Converter { + fn from_module(module: &cst::Module, registry: &ModuleRegistry) -> Self { + let mut conv = Converter { + values: HashMap::new(), + types: HashMap::new(), + classes: HashMap::new(), + type_operators: HashMap::new(), + type_fixities: HashMap::new(), + value_fixities: HashMap::new(), + value_operator_targets: HashMap::new(), + function_op_aliases: HashSet::new(), + operator_target_sites: HashMap::new(), + local_scopes: Vec::new(), + in_parens: false, + errors: Vec::new(), + }; + + // 1. Register Prim types (unless module has explicit `import Prim (...)`) + let has_explicit_prim_import = module.imports.iter().any(|imp| { + is_prim_module(&imp.module) && imp.imports.is_some() && imp.qualified.is_none() + }); + if !has_explicit_prim_import { + conv.register_prim(); + } + + // 2. Process imports + conv.process_imports(module, registry); + + // 3. Register local declarations + conv.register_local_decls(&module.decls); + + conv + } + + fn register_prim(&mut self) { + let prim = intern("Prim"); + let site = DefinitionSite::Imported { module: prim }; + for name in &[ + "Int", + "Number", + "String", + "Char", + "Boolean", + "Array", + "Record", + "Function", + "Type", + "Constraint", + "Symbol", + "Row", + ] { + self.types.insert(intern(name), site.clone()); + // Also register with "Prim." qualifier for explicit Prim.Array etc. references + self.types + .insert(qualified_symbol(prim, intern(name)), site.clone()); + } + // `(->)` is the function type constructor. When fully applied via `(->) a b`, + // convert_type_expr normalizes to Type::fun(a, b). + self.types.insert(intern("->"), site.clone()); + // Partial class + self.classes.insert(intern("Partial"), site.clone()); + } + + /// Register types and classes from a Prim submodule import. + fn register_prim_submodule(&mut self, import_decl: &cst::ImportDecl) { + let qualifier = import_decl.qualified.as_ref().map(module_name_to_symbol); + let mod_sym = module_name_to_symbol(&import_decl.module); + let site = DefinitionSite::Imported { module: mod_sym }; + + let sub = if import_decl.module.parts.len() >= 2 { + interner::resolve(import_decl.module.parts[1]).unwrap_or_default() + } else { + return; + }; + + let (type_names, class_names): (&[&str], &[&str]) = match sub.as_str() { + "Boolean" => (&["True", "False"], &[]), + "Coerce" => (&[], &["Coercible"]), + "Int" => (&[], &["Add", "Compare", "Mul", "ToString"]), + "Ordering" => (&["Ordering", "LT", "EQ", "GT"], &[]), + "Row" => (&[], &["Lacks", "Cons", "Nub", "Union"]), + "RowList" => (&["RowList", "Cons", "Nil"], &["RowToList"]), + "Symbol" => (&[], &["Append", "Compare", "Cons"]), + "TypeError" => ( + &["Doc", "Beside", "Above", "Text", "Quote", "QuoteLabel"], + &["Fail", "Warn"], + ), + _ => (&[], &[]), + }; + + // Filter based on import list + let allowed: Option> = match &import_decl.imports { + None => None, // import all + Some(ImportList::Explicit(items)) => Some( + items + .iter() + .map(|i| match i { + cst::Import::Value(n) + | cst::Import::Type(n, _) + | cst::Import::TypeOp(n) + | cst::Import::Class(n) => *n, + }) + .collect(), + ), + Some(ImportList::Hiding(items)) => { + let hidden: HashSet = items + .iter() + .map(|i| match i { + cst::Import::Value(n) + | cst::Import::Type(n, _) + | cst::Import::TypeOp(n) + | cst::Import::Class(n) => *n, + }) + .collect(); + // Build allowed = all names minus hidden + let all_names: HashSet = type_names + .iter() + .chain(class_names.iter()) + .map(|n| intern(n)) + .collect(); + Some(all_names.difference(&hidden).cloned().collect()) + } + }; + + for name in type_names { + let sym = intern(name); + if allowed.as_ref().map_or(true, |s| s.contains(&sym)) { + let key = Self::maybe_qualify(sym, qualifier); + self.types.insert(key, site.clone()); + } + } + for name in class_names { + let sym = intern(name); + if allowed.as_ref().map_or(true, |s| s.contains(&sym)) { + let key = Self::maybe_qualify(sym, qualifier); + self.classes.insert(key, site.clone()); + } + } + } + + fn process_imports(&mut self, module: &cst::Module, registry: &ModuleRegistry) { + for import_decl in &module.imports { + let module_exports = if is_prim_module(&import_decl.module) { + let prim_site = DefinitionSite::Imported { + module: intern("Prim"), + }; + let prim_sym = intern("Prim"); + // Register qualifier if present (e.g. import Prim as P). + if let Some(ref qual) = import_decl.qualified { + let q = module_name_to_symbol(qual); + for name in &[ + "Int", + "Number", + "String", + "Char", + "Boolean", + "Array", + "Record", + "Function", + "Type", + "Constraint", + "Symbol", + "Row", + ] { + self.types + .insert(qualified_symbol(q, intern(name)), prim_site.clone()); + } + self.classes + .insert(qualified_symbol(q, intern("Partial")), prim_site.clone()); + } + // If explicit `import Prim (X, Y)`, register only the listed items. + // register_prim() was skipped for this case, so we must add them here. + if let Some(ImportList::Explicit(items)) = &import_decl.imports { + for item in items { + match item { + cst::Import::Type(name, _) => { + let sym = *name; + self.types.insert(sym, prim_site.clone()); + self.types + .insert(qualified_symbol(prim_sym, sym), prim_site.clone()); + } + cst::Import::Class(name) => { + let sym = *name; + self.classes.insert(sym, prim_site.clone()); + self.classes + .insert(qualified_symbol(prim_sym, sym), prim_site.clone()); + } + cst::Import::Value(name) => { + self.values.insert(*name, prim_site.clone()); + } + cst::Import::TypeOp(name) => { + self.types.insert(*name, prim_site.clone()); + } + } + } + // Always register (->) even for explicit imports + self.types.insert(intern("->"), prim_site.clone()); + } else if let Some(ImportList::Hiding(items)) = &import_decl.imports { + // `import Prim hiding (X, Y)` — register all Prim types/classes + // except the hidden ones. + let hidden: HashSet = items + .iter() + .map(|i| match i { + cst::Import::Value(n) + | cst::Import::Type(n, _) + | cst::Import::TypeOp(n) + | cst::Import::Class(n) => *n, + }) + .collect(); + for name in &[ + "Int", + "Number", + "String", + "Char", + "Boolean", + "Array", + "Record", + "Function", + "Type", + "Constraint", + "Symbol", + "Row", + ] { + let sym = intern(name); + if !hidden.contains(&sym) { + self.types.insert(sym, prim_site.clone()); + self.types + .insert(qualified_symbol(prim_sym, sym), prim_site.clone()); + } + } + if !hidden.contains(&intern("Partial")) { + self.classes.insert(intern("Partial"), prim_site.clone()); + } + self.types.insert(intern("->"), prim_site.clone()); + } + continue; + } else if is_prim_submodule(&import_decl.module) { + // Register Prim submodule types/classes so the AST converter knows about them. + self.register_prim_submodule(import_decl); + continue; + } else { + match registry.lookup(&import_decl.module.parts) { + Some(exports) => exports, + None => { + self.errors.push(TypeError::ModuleNotFound { + span: import_decl.span, + name: module_name_to_symbol(&import_decl.module), + }); + continue; + } + } + }; + + let mod_sym = module_name_to_symbol(&import_decl.module); + let qualifier = import_decl.qualified.as_ref().map(module_name_to_symbol); + let site = DefinitionSite::Imported { module: mod_sym }; + + match &import_decl.imports { + None => { + self.import_all(module_exports, qualifier, &site); + } + Some(ImportList::Explicit(items)) => { + for item in items { + self.import_item(item, module_exports, qualifier, &site); + } + } + Some(ImportList::Hiding(items)) => { + let hidden: HashSet = items + .iter() + .map(|i| match i { + cst::Import::Value(n) + | cst::Import::Type(n, _) + | cst::Import::TypeOp(n) + | cst::Import::Class(n) => *n, + }) + .collect(); + self.import_all_except(module_exports, &hidden, qualifier, &site); + } + } + + // Import fixities, type operators, and operator targets, respecting import filter. + // Collect which value operators and type operators are allowed by this import. + let (allowed_value_ops, allowed_type_ops): ( + Option>, + Option>, + ) = match &import_decl.imports { + None => (None, None), // open import: all allowed + Some(ImportList::Explicit(items)) => { + let mut vops = HashSet::new(); + let mut tops = HashSet::new(); + for item in items { + match item { + cst::Import::Value(n) => { + vops.insert(*n); + } + cst::Import::TypeOp(n) => { + tops.insert(*n); + } + _ => {} + } + } + (Some(vops), Some(tops)) + } + Some(ImportList::Hiding(items)) => { + // Start with all, remove hidden + let hidden_vops: HashSet = items + .iter() + .filter_map(|i| match i { + cst::Import::Value(n) => Some(*n), + _ => None, + }) + .collect(); + let hidden_tops: HashSet = items + .iter() + .filter_map(|i| match i { + cst::Import::TypeOp(n) => Some(*n), + _ => None, + }) + .collect(); + let vops: HashSet = module_exports + .value_fixities + .keys() + .filter(|k| !hidden_vops.contains(&k.name)) + .map(|k| k.name) + .collect(); + let tops: HashSet = module_exports + .type_operators + .keys() + .filter(|k| !hidden_tops.contains(&k.name)) + .map(|k| k.name) + .collect(); + (Some(vops), Some(tops)) + } + }; + + for (op, fixity) in &module_exports.value_fixities { + if allowed_value_ops + .as_ref() + .map_or(true, |s| s.contains(&op.name)) + { + let key = Self::maybe_qualify(op.name, qualifier); + self.value_fixities.insert(key, *fixity); + } + } + for (op, target) in &module_exports.type_operators { + if allowed_type_ops + .as_ref() + .map_or(true, |s| s.contains(&op.name)) + { + let key = Self::maybe_qualify(op.name, qualifier); + self.type_operators.insert(key, target.name); + // Also import the type fixity for this operator (for shunting-yard rebalancing) + if let Some(fixity) = module_exports.type_fixities.get(op) { + self.type_fixities.insert(key, *fixity); + } + // Also register the target type so that type operator desugaring + // (e.g. `a + r` → `App(App(RowApply, a), r)`) can resolve the + // target type constructor. + if !self.types.contains_key(&target.name) { + let target_origin = + Self::type_origin_site(module_exports, target.name, &site); + self.types.insert(target.name, target_origin); + } + } + } + // Only import function_op_aliases when operators are available unqualified. + // `import M as Q` (qualifier, no explicit list) only provides qualified access, + // so operators like `:` from Data.Array shouldn't pollute the unqualified scope. + let has_unqualified_access = qualifier.is_none() || import_decl.imports.is_some(); + for op in &module_exports.function_op_aliases { + if allowed_value_ops + .as_ref() + .map_or(true, |s| s.contains(&op.name)) + { + if has_unqualified_access { + self.function_op_aliases.insert(op.name); + } + // Also register under qualified key so `LL.:` (a function alias) + // is correctly identified even when the unqualified `:` belongs to + // a different module's constructor alias. + if let Some(q) = qualifier { + self.function_op_aliases.insert(qualified_symbol(q, op.name)); + } + } + } + for (op, target) in &module_exports.value_operator_targets { + if allowed_value_ops + .as_ref() + .map_or(true, |s| s.contains(&op.name)) + { + if has_unqualified_access { + self.value_operator_targets.insert(op.name, *target); + } + // Also register under qualified key so `List.:` resolves to the + // correct target even when another import overwrites the unqualified `:`. + if let Some(q) = qualifier { + let qkey = qualified_symbol(q, op.name); + self.value_operator_targets.insert(qkey, *target); + } + // Record the definition site for the operator's target so that + // operator desugaring (e.g. `1 + 2` → `add 1 2`) can produce + // a valid definition_site without requiring `add` to be in `values`. + let target_origin = Self::value_origin_site(module_exports, target.name, &site); + self.operator_target_sites + .insert(target.name, target_origin); + } + } + } + } + + /// Look up the original defining module for a value name, falling back to the + /// importing module's site if no origin is recorded. + fn value_origin_site( + exports: &ModuleExports, + name: Symbol, + fallback: &DefinitionSite, + ) -> DefinitionSite { + exports + .value_origins + .get(&name) + .map(|&origin_mod| DefinitionSite::Imported { module: origin_mod }) + .unwrap_or_else(|| fallback.clone()) + } + + fn type_origin_site( + exports: &ModuleExports, + name: Symbol, + fallback: &DefinitionSite, + ) -> DefinitionSite { + exports + .type_origins + .get(&name) + .map(|&origin_mod| DefinitionSite::Imported { module: origin_mod }) + .unwrap_or_else(|| fallback.clone()) + } + + fn class_origin_site( + exports: &ModuleExports, + name: Symbol, + fallback: &DefinitionSite, + ) -> DefinitionSite { + exports + .class_origins + .get(&name) + .map(|&origin_mod| DefinitionSite::Imported { module: origin_mod }) + .unwrap_or_else(|| fallback.clone()) + } + + fn import_all( + &mut self, + exports: &ModuleExports, + qualifier: Option, + site: &DefinitionSite, + ) { + for name in exports.values.keys() { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::value_origin_site(exports, name.name, site); + self.values.insert(key, origin); + } + for name in exports.data_constructors.keys() { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::type_origin_site(exports, name.name, site); + self.types.insert(key, origin); + } + // Also import type aliases as known types + for name in exports.type_aliases.keys() { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::type_origin_site(exports, name.name, site); + self.types.insert(key, origin); + } + for name in exports.class_param_counts.keys() { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::class_origin_site(exports, name.name, site); + self.classes.insert(key, origin); + } + } + + fn import_all_except( + &mut self, + exports: &ModuleExports, + hidden: &HashSet, + qualifier: Option, + site: &DefinitionSite, + ) { + for name in exports.values.keys() { + if !hidden.contains(&name.name) { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::value_origin_site(exports, name.name, site); + self.values.insert(key, origin); + } + } + for name in exports.data_constructors.keys() { + if !hidden.contains(&name.name) { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::type_origin_site(exports, name.name, site); + self.types.insert(key, origin); + } + } + // Also import type aliases as known types + for name in exports.type_aliases.keys() { + if !hidden.contains(&name.name) { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::type_origin_site(exports, name.name, site); + self.types.insert(key, origin); + } + } + for name in exports.class_param_counts.keys() { + if !hidden.contains(&name.name) { + let key = Self::maybe_qualify(name.name, qualifier); + let origin = Self::class_origin_site(exports, name.name, site); + self.classes.insert(key, origin); + } + } + } + + fn import_item( + &mut self, + item: &cst::Import, + exports: &ModuleExports, + qualifier: Option, + site: &DefinitionSite, + ) { + match item { + cst::Import::Value(name) => { + let key = Self::maybe_qualify(*name, qualifier); + let origin = Self::value_origin_site(exports, *name, site); + self.values.insert(key, origin); + } + cst::Import::Type(name, members) => { + let key = Self::maybe_qualify(*name, qualifier); + let origin = Self::type_origin_site(exports, *name, site); + self.types.insert(key, origin); + // Import constructors if (..) or explicit list + if let Some(members) = members { + let qi = QualifiedIdent { + module: None, + name: *name, + }; + if let Some(ctors) = exports.data_constructors.get(&qi) { + match members { + cst::DataMembers::All => { + for ctor in ctors { + let k = Self::maybe_qualify(ctor.name, qualifier); + let ctor_origin = + Self::value_origin_site(exports, ctor.name, site); + self.values.insert(k, ctor_origin); + } + } + cst::DataMembers::Explicit(names) => { + for n in names { + let k = Self::maybe_qualify(*n, qualifier); + let ctor_origin = Self::value_origin_site(exports, *n, site); + self.values.insert(k, ctor_origin); + } + } + } + } + } + } + cst::Import::TypeOp(name) => { + let key = Self::maybe_qualify(*name, qualifier); + let origin = Self::value_origin_site(exports, *name, site); + self.values.insert(key, origin); + } + cst::Import::Class(name) => { + let key = Self::maybe_qualify(*name, qualifier); + let origin = Self::class_origin_site(exports, *name, site); + self.classes.insert(key, origin); + // Import class methods + for (method_name, _) in &exports.class_methods { + // Check if this method belongs to the imported class + let qi = QualifiedIdent { + module: None, + name: *name, + }; + if exports.class_methods.get(method_name).map(|(cn, _)| cn) == Some(&qi) { + let k = Self::maybe_qualify(method_name.name, qualifier); + let method_origin = + Self::value_origin_site(exports, method_name.name, site); + self.values.insert(k, method_origin); + } + } + } + } + } + + fn register_local_decls(&mut self, decls: &[cst::Decl]) { + // Pass 1: Register all values, types, and classes (so targets exist before fixities) + for decl in decls { + match decl { + cst::Decl::Value { span, name, .. } => { + self.values.insert(name.value, DefinitionSite::Local(*span)); + } + cst::Decl::Data { + span, + name, + constructors, + .. + } => { + self.types.insert(name.value, DefinitionSite::Local(*span)); + for ctor in constructors { + self.values + .insert(ctor.name.value, DefinitionSite::Local(ctor.span)); + } + } + cst::Decl::Newtype { + span, + name, + constructor, + .. + } => { + self.types.insert(name.value, DefinitionSite::Local(*span)); + self.values + .insert(constructor.value, DefinitionSite::Local(*span)); + } + cst::Decl::Class { + span, + name, + members, + .. + } => { + self.classes + .insert(name.value, DefinitionSite::Local(*span)); + for member in members { + self.values + .insert(member.name.value, DefinitionSite::Local(member.span)); + } + } + cst::Decl::TypeAlias { span, name, .. } => { + self.types.insert(name.value, DefinitionSite::Local(*span)); + } + cst::Decl::Foreign { span, name, .. } => { + self.values.insert(name.value, DefinitionSite::Local(*span)); + } + cst::Decl::ForeignData { span, name, .. } => { + self.types.insert(name.value, DefinitionSite::Local(*span)); + } + _ => {} + } + } + + // Pass 2: Process fixity declarations (targets are now registered from pass 1) + for decl in decls { + if let cst::Decl::Fixity { + target, + operator, + is_type, + associativity, + precedence, + .. + } = decl + { + if *is_type { + self.type_operators.insert(operator.value, target.name); + self.type_fixities + .insert(operator.value, (*associativity, *precedence)); + // Ensure the unqualified target type name is in self.types so + // TypeOp desugaring can resolve it. If the target is qualified + // (e.g. TE.Beside from `import Prim.TypeError as TE`), look up + // the qualified key and register under the unqualified name. + if !self.types.contains_key(&target.name) { + if let Some(m) = target.module { + let qualified_key = qualified_symbol(m, target.name); + if let Some(site) = self.types.get(&qualified_key).cloned() { + self.types.insert(target.name, site); + } + } + } + } else { + self.value_fixities + .insert(operator.value, (*associativity, *precedence)); + // Register operator → target mapping + self.value_operator_targets.insert(operator.value, *target); + // Operator inherits the target's definition site + if let Some(target_site) = self.values.get(&target.name).cloned() { + self.values.insert(operator.value, target_site); + } + // Check if target is a function (not a constructor) + let target_str = interner::resolve(target.name).unwrap_or_default(); + if target_str + .chars() + .next() + .map_or(false, |c| c.is_lowercase() || c == '_') + { + self.function_op_aliases.insert( + QualifiedIdent { + module: None, + name: operator.value, + } + .name, + ); + } + } + } + } + } + + fn maybe_qualify(name: Symbol, qualifier: Option) -> Symbol { + match qualifier { + Some(q) => qualified_symbol(q, name), + None => name, + } + } + + // --- Underscore section detection and desugaring --- + + /// Check if a CST expression is an `_` (wildcard used for anonymous functions). + fn is_wildcard(expr: &cst::Expr) -> bool { + matches!(expr, cst::Expr::Wildcard { .. }) + } + + /// Check if a CST expression is a valid underscore section (single-operator with `_` hole). + /// Only valid when `_` is a direct operand of a single Op (no nested Op chain) + /// or a direct argument of App. Multi-operator chains are handled post-rebalancing + /// in convert_op_chain when in_parens is set. + fn has_wildcard(expr: &cst::Expr) -> bool { + match expr { + cst::Expr::Op { left, right, .. } => { + let has_hole = Self::is_wildcard(left) || Self::is_wildcard(right); + // Reject if the non-hole operand is a nested Op (multi-operator chain) + let has_nested_op = matches!(left.as_ref(), cst::Expr::Op { .. }) + || matches!(right.as_ref(), cst::Expr::Op { .. }); + has_hole && !has_nested_op + } + cst::Expr::App { func, arg, .. } => { + // `_{ field = value }` is a record update section, not an operator section wildcard + let is_record_update_section = Self::is_wildcard(func) + && matches!(arg.as_ref(), cst::Expr::Record { fields, .. } + if !fields.is_empty() && fields.iter().all(|f| f.is_update)); + if is_record_update_section { + false + } else { + Self::is_wildcard(func) || Self::is_wildcard(arg) + } + } + cst::Expr::BacktickApp { left, right, .. } => { + Self::is_wildcard(left) || Self::is_wildcard(right) + } + _ => false, + } + } + + /// Desugar an underscore section: `(_ * 1000.0)` → `\$_arg -> mul $_arg 1000.0`. + /// Replaces `_` holes with a fresh variable and wraps in a Lambda. + fn desugar_wildcard_section(&mut self, span: Span, expr: &cst::Expr) -> Expr { + let param_name = intern("$_arg"); + + // Replace all `_` holes in the CST with a variable reference + let replaced = self.replace_underscore_holes(expr, param_name); + + // Push a local scope with the param so resolve_value finds it during body conversion + let mut scope = HashMap::new(); + scope.insert(param_name, span); + self.local_scopes.push(scope); + let body = self.convert_expr(&replaced); + self.local_scopes.pop(); + + Expr::Lambda { + span, + binders: vec![Binder::Var { + span, + name: cst::Spanned { + span, + value: param_name, + }, + }], + body: Box::new(body), + } + } + + /// Replace `_` holes in a CST expression with a variable reference. + fn replace_underscore_holes(&self, expr: &cst::Expr, replacement: Symbol) -> cst::Expr { + if Self::is_wildcard(expr) { + return cst::Expr::Var { + span: expr.span(), + name: QualifiedIdent { + module: None, + name: replacement, + }, + }; + } + match expr { + cst::Expr::Op { + span, + left, + op, + right, + } => cst::Expr::Op { + span: *span, + left: Box::new(self.replace_underscore_holes(left, replacement)), + op: op.clone(), + right: Box::new(self.replace_underscore_holes(right, replacement)), + }, + cst::Expr::App { span, func, arg } => { + // Don't replace the wildcard in `_{ field = value }` — that's a record + // update section marker, not an operator section wildcard. + let is_record_update_section = Self::is_wildcard(func) + && matches!(arg.as_ref(), cst::Expr::Record { fields, .. } + if !fields.is_empty() && fields.iter().all(|f| f.is_update)); + if is_record_update_section { + expr.clone() + } else { + cst::Expr::App { + span: *span, + func: Box::new(self.replace_underscore_holes(func, replacement)), + arg: Box::new(self.replace_underscore_holes(arg, replacement)), + } + } + } + cst::Expr::BacktickApp { + span, + func, + left, + right, + } => cst::Expr::BacktickApp { + span: *span, + func: func.clone(), + left: Box::new(self.replace_underscore_holes(left, replacement)), + right: Box::new(self.replace_underscore_holes(right, replacement)), + }, + other => other.clone(), + } + } + + // --- Definition site resolution --- + + fn resolve_value(&mut self, name: &QualifiedIdent, span: Span) -> DefinitionSite { + // Check local scopes first (innermost first) + if name.module.is_none() { + for scope in self.local_scopes.iter().rev() { + if let Some(&local_span) = scope.get(&name.name) { + return DefinitionSite::Local(local_span); + } + } + } + // Check module scope + let key = match name.module { + Some(m) => qualified_symbol(m, name.name), + None => name.name, + }; + match self.values.get(&key).cloned() { + Some(site) => site, + None => { + self.errors + .push(TypeError::UndefinedVariable { span, name: key }); + DefinitionSite::Local(span) + } + } + } + + /// Resolve the definition site of an operator's target (e.g. `add` for operator `+`). + /// Checks `operator_target_sites` first, then falls back to `values` (for locally + /// defined operators whose targets are already in scope). + fn resolve_operator_target(&self, target_name: Symbol, span: Span) -> DefinitionSite { + if let Some(site) = self.operator_target_sites.get(&target_name) { + return site.clone(); + } + // Fall back to values (e.g. for locally defined operators) + if let Some(site) = self.values.get(&target_name) { + return site.clone(); + } + DefinitionSite::Local(span) + } + + fn resolve_type(&mut self, name: &QualifiedIdent, span: Span) -> DefinitionSite { + let key = match name.module { + Some(m) => qualified_symbol(m, name.name), + None => name.name, + }; + match self.types.get(&key).cloned() { + Some(site) => site, + None => { + // Also check type operators — `(~>)` used as a type constructor is valid + // if `~>` is a known type operator. + if self.type_operators.contains_key(&key) { + return DefinitionSite::Local(span); + } + self.errors.push(TypeError::UnknownName { span, name: key }); + DefinitionSite::Local(span) + } + } + } + + fn resolve_class(&mut self, name: &QualifiedIdent, span: Span) -> DefinitionSite { + let key = match name.module { + Some(m) => qualified_symbol(m, name.name), + None => name.name, + }; + match self.classes.get(&key).cloned() { + Some(site) => site, + None => { + // PureScript reports unknown class names as UnknownName during name + // resolution (the name isn't in scope). UnknownClass is reserved for + // constraint-solver failures where a class can't be found. + self.errors.push(TypeError::UnknownName { span, name: key }); + DefinitionSite::Local(span) + } + } + } + + /// Like resolve_class, but doesn't emit an error if the class isn't found. + /// Used for derive declarations where the class may be handled specially by the typechecker. + fn resolve_class_lenient(&self, name: &QualifiedIdent, span: Span) -> DefinitionSite { + let key = match name.module { + Some(m) => qualified_symbol(m, name.name), + None => name.name, + }; + self.classes + .get(&key) + .cloned() + .unwrap_or(DefinitionSite::Local(span)) + } + + fn push_scope(&mut self) { + self.local_scopes.push(HashMap::new()); + } + + fn pop_scope(&mut self) { + self.local_scopes.pop(); + } + + fn add_local(&mut self, name: Symbol, span: Span) { + if let Some(scope) = self.local_scopes.last_mut() { + scope.insert(name, span); + } + } + + /// Collect all variable names bound by a CST binder (for scope registration) + fn collect_binder_names(binder: &cst::Binder, names: &mut Vec<(Symbol, Span)>) { + match binder { + cst::Binder::Var { name, .. } => { + names.push((name.value, name.span)); + } + cst::Binder::Constructor { args, .. } => { + for arg in args { + Self::collect_binder_names(arg, names); + } + } + cst::Binder::As { name, binder, .. } => { + names.push((name.value, name.span)); + Self::collect_binder_names(binder, names); + } + cst::Binder::Parens { binder, .. } => { + Self::collect_binder_names(binder, names); + } + cst::Binder::Record { fields, .. } => { + for field in fields { + if let Some(b) = &field.binder { + Self::collect_binder_names(b, names); + } else { + // Pun: { x } binds x + names.push((field.label.value, field.label.span)); + } + } + } + cst::Binder::Array { elements, .. } => { + for elem in elements { + Self::collect_binder_names(elem, names); + } + } + cst::Binder::Op { left, right, .. } => { + Self::collect_binder_names(left, names); + Self::collect_binder_names(right, names); + } + cst::Binder::Typed { binder, .. } => { + Self::collect_binder_names(binder, names); + } + cst::Binder::Wildcard { .. } | cst::Binder::Literal { .. } => {} + } + } + + // --- Expression conversion --- + + fn convert_expr(&mut self, expr: &cst::Expr) -> Expr { + match expr { + cst::Expr::Var { span, name } => Expr::Var { + span: *span, + name: *name, + definition_site: self.resolve_value(name, *span), + }, + cst::Expr::Constructor { span, name } => Expr::Constructor { + span: *span, + name: *name, + definition_site: self.resolve_value(name, *span), + }, + cst::Expr::Literal { span, lit } => Expr::Literal { + span: *span, + lit: self.convert_literal(lit), + }, + cst::Expr::App { span, func, arg } => { + // Detect `(expr) { field = val, ... }` — a record update on a parenthesized + // expression. Because `cst::Expr::Parens` is stripped during AST conversion, + // the information that the func was parenthesized would normally be lost. + // Without this check, the record-update peel logic in `infer_app` would + // incorrectly treat `(f x y) { a=1 }` as `f x (y { a=1 })`. + // + // When the func was explicitly parenthesized AND the arg is an all-update + // record, produce `Expr::RecordUpdate` directly so inference never sees the + // ambiguous `App(App(...), Record{is_update})` shape. + if let cst::Expr::Parens { expr: inner, .. } = func.as_ref() { + if let cst::Expr::Record { fields, .. } = arg.as_ref() { + if !fields.is_empty() + && fields.iter().all(|f| f.is_update && f.value.is_some()) + { + let updates: Vec = fields + .iter() + .map(|f| RecordUpdate { + span: f.span, + label: f.label.clone(), + value: self.convert_expr(f.value.as_ref().unwrap()), + }) + .collect(); + return Expr::RecordUpdate { + span: *span, + expr: Box::new(self.convert_expr(inner)), + updates, + }; + } + } + } + Expr::App { + span: *span, + func: Box::new(self.convert_expr(func)), + arg: Box::new(self.convert_expr(arg)), + } + } + cst::Expr::VisibleTypeApp { span, func, ty } => Expr::VisibleTypeApp { + span: *span, + func: Box::new(self.convert_expr(func)), + ty: self.convert_type_expr(ty), + }, + cst::Expr::Lambda { + span, + binders, + body, + } => { + self.push_scope(); + for b in binders { + let mut names = Vec::new(); + Self::collect_binder_names(b, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + let ast_binders: Vec = + binders.iter().map(|b| self.convert_binder(b)).collect(); + let ast_body = self.convert_expr(body); + self.pop_scope(); + Expr::Lambda { + span: *span, + binders: ast_binders, + body: Box::new(ast_body), + } + } + cst::Expr::Op { + span, + left, + op, + right, + } => self.convert_op_chain(*span, left, ChainOp::Named(op), right), + cst::Expr::BacktickApp { + span, + func, + left, + right, + } => self.convert_op_chain(*span, left, ChainOp::Expr(func), right), + cst::Expr::OpParens { span, op } => { + // Use the operator name (not target), same as build_op_app + let paren_op_key = if let Some(m) = op.value.module { + qualified_symbol(m, op.value.name) + } else { + op.value.name + }; + if !self.value_operator_targets.contains_key(&paren_op_key) + && !self.value_operator_targets.contains_key(&op.value.name) { + self.errors.push(TypeError::UndefinedVariable { + span: *span, + name: op.value.name, + }); + } + let def_site = self.resolve_operator_target(op.value.name, *span); + if self.function_op_aliases.contains(&paren_op_key) + || self.function_op_aliases.contains(&op.value.name) { + Expr::Var { + span: *span, + name: op.value, + definition_site: def_site, + } + } else { + Expr::Constructor { + span: *span, + name: op.value, + definition_site: def_site, + } + } + } + cst::Expr::If { + span, + cond, + then_expr, + else_expr, + } => Expr::If { + span: *span, + cond: Box::new(self.convert_expr(cond)), + then_expr: Box::new(self.convert_expr(then_expr)), + else_expr: Box::new(self.convert_expr(else_expr)), + }, + cst::Expr::Case { span, exprs, alts } => { + let ast_exprs: Vec = exprs.iter().map(|e| self.convert_expr(e)).collect(); + let ast_alts: Vec = alts + .iter() + .map(|alt| { + self.push_scope(); + for b in &alt.binders { + let mut names = Vec::new(); + Self::collect_binder_names(b, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + let binders = alt.binders.iter().map(|b| self.convert_binder(b)).collect(); + let result = self.convert_guarded(&alt.result); + self.pop_scope(); + CaseAlternative { + span: alt.span, + binders, + result, + } + }) + .collect(); + Expr::Case { + span: *span, + exprs: ast_exprs, + alts: ast_alts, + } + } + cst::Expr::Let { + span, + bindings, + body, + } => { + self.push_scope(); + // Register let binding names first (for mutual recursion) + for lb in bindings { + if let cst::LetBinding::Value { binder, .. } = lb { + let mut names = Vec::new(); + Self::collect_binder_names(binder, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + } + let ast_bindings: Vec = bindings + .iter() + .map(|lb| self.convert_let_binding(lb)) + .collect(); + let ast_body = self.convert_expr(body); + self.pop_scope(); + Expr::Let { + span: *span, + bindings: ast_bindings, + body: Box::new(ast_body), + } + } + cst::Expr::Do { + span, + module, + statements, + } => { + self.push_scope(); + let ast_stmts: Vec = statements + .iter() + .map(|s| self.convert_do_statement(s)) + .collect(); + self.pop_scope(); + Expr::Do { + span: *span, + module: *module, + statements: ast_stmts, + } + } + cst::Expr::Ado { + span, + module, + statements, + result, + } => { + self.push_scope(); + let ast_stmts: Vec = statements + .iter() + .map(|s| self.convert_do_statement(s)) + .collect(); + let ast_result = self.convert_expr(result); + self.pop_scope(); + Expr::Ado { + span: *span, + module: *module, + statements: ast_stmts, + result: Box::new(ast_result), + } + } + cst::Expr::Record { span, fields } => Expr::Record { + span: *span, + fields: fields + .iter() + .map(|f| self.convert_record_field(f)) + .collect(), + }, + cst::Expr::RecordAccess { span, expr, field } => Expr::RecordAccess { + span: *span, + expr: Box::new(self.convert_expr(expr)), + field: field.clone(), + }, + cst::Expr::RecordUpdate { + span, + expr, + updates, + } => Expr::RecordUpdate { + span: *span, + expr: Box::new(self.convert_expr(expr)), + updates: updates + .iter() + .map(|u| RecordUpdate { + span: u.span, + label: u.label.clone(), + value: self.convert_expr(&u.value), + }) + .collect(), + }, + cst::Expr::Parens { span, expr } => { + // Detect underscore sections: (_ * 1000.0) → \$_arg -> mul $_arg 1000.0 + if Self::has_wildcard(expr) { + self.desugar_wildcard_section(*span, expr) + } else { + // Set in_parens so convert_op_chain can handle post-rebalance sections + // for multi-operator chains like (_ /\ x /\ y) + let prev = self.in_parens; + self.in_parens = true; + let result = self.convert_expr(expr); + self.in_parens = prev; + result + } + } + cst::Expr::TypeAnnotation { span, expr, ty } => Expr::TypeAnnotation { + span: *span, + expr: Box::new(self.convert_expr(expr)), + ty: self.convert_type_expr(ty), + }, + cst::Expr::Hole { span, name } => Expr::Hole { + span: *span, + name: *name, + }, + cst::Expr::Wildcard { span } => Expr::Hole { + span: *span, + name: intern("_"), + }, + cst::Expr::Array { span, elements } => Expr::Array { + span: *span, + elements: elements.iter().map(|e| self.convert_expr(e)).collect(), + }, + cst::Expr::Negate { span, expr } => Expr::Negate { + span: *span, + expr: Box::new(self.convert_expr(expr)), + }, + cst::Expr::AsPattern { + span, + name, + pattern, + } => Expr::AsPattern { + span: *span, + name: Box::new(self.convert_expr(name)), + pattern: Box::new(self.convert_expr(pattern)), + }, + } + } + + // --- Operator chain flattening and rebalancing --- + + fn get_chain_op_fixity(&self, op: &ChainOp) -> (Associativity, u8) { + match op { + ChainOp::Named(named) => { + // For qualified operators, check the qualified key first + if let Some(m) = named.value.module { + let qkey = qualified_symbol(m, named.value.name); + if let Some(fixity) = self.value_fixities.get(&qkey) { + return *fixity; + } + } + self.get_fixity(named.value.name) + } + ChainOp::Expr(_) => (Associativity::Left, 9), // default fixity for complex backtick + } + } + + fn build_chain_op_app( + &mut self, + span: Span, + op: &ChainOp, + converted_expr: &Option, + left: Expr, + right: Expr, + ) -> Expr { + match op { + ChainOp::Named(named) => self.build_op_app(span, named, left, right), + ChainOp::Expr(_) => { + let func = converted_expr.as_ref().unwrap().clone(); + Expr::App { + span, + func: Box::new(Expr::App { + span, + func: Box::new(func), + arg: Box::new(left), + }), + arg: Box::new(right), + } + } + } + } + + fn convert_op_chain( + &mut self, + span: Span, + left: &cst::Expr, + initial_op: ChainOp, + right: &cst::Expr, + ) -> Expr { + // Flatten right-associative chain (following both Op and BacktickApp nodes) + let mut operands: Vec<&cst::Expr> = vec![left]; + let mut operators: Vec = vec![initial_op]; + let mut current = right; + loop { + match current { + cst::Expr::Op { + left: rl, + op: rop, + right: rr, + .. + } => { + operands.push(rl.as_ref()); + operators.push(ChainOp::Named(rop)); + current = rr.as_ref(); + } + cst::Expr::BacktickApp { + left: bl, + func: bf, + right: br, + .. + } => { + operands.push(bl.as_ref()); + operators.push(ChainOp::Expr(bf)); + current = br.as_ref(); + } + _ => break, + } + } + // If the rightmost expression is a TypeAnnotation, extract it. + // In PureScript, `::` has the lowest precedence, so `a op b :: T` means + // `(a op b) :: T`. But our grammar parses `::` within OperatorExpr, so + // `right` of `a op b :: T` becomes `TypeAnnotation { expr: b, ty: T }`. + // We extract the annotation and apply it to the whole chain result. + let mut trailing_annotation: Option<&cst::TypeExpr> = None; + if let cst::Expr::TypeAnnotation { + expr: inner, ty, .. + } = current + { + trailing_annotation = Some(ty); + operands.push(inner.as_ref()); + } else { + operands.push(current); + } + + // Check for `_` holes in operator chains. + // When in_parens is true, defer the error — the section may be valid after rebalancing. + let has_wildcard_operand = operands.iter().any(|o| Self::is_wildcard(o)); + if has_wildcard_operand && !self.in_parens { + for operand in &operands { + if Self::is_wildcard(operand) { + self.errors.push(TypeError::IncorrectAnonymousArgument { + span: operand.span(), + }); + } + } + } + + // Convert all operands + let mut ast_operands: Vec = operands.iter().map(|e| self.convert_expr(e)).collect(); + + // Pre-convert expression operators (BacktickApp func exprs) so we don't re-convert them + let converted_expr_ops: Vec> = operators + .iter() + .map(|op| match op { + ChainOp::Expr(func_expr) => Some(self.convert_expr(func_expr)), + ChainOp::Named(_) => None, + }) + .collect(); + + // Single operator: fast path + if operators.len() == 1 { + let right = ast_operands.pop().unwrap(); + let left = ast_operands.pop().unwrap(); + let result = self.build_chain_op_app(span, &operators[0], &converted_expr_ops[0], left, right); + if let Some(ann_ty) = trailing_annotation { + let ty = self.convert_type_expr(ann_ty); + return Expr::TypeAnnotation { + span, + expr: Box::new(result), + ty, + }; + } + return result; + } + + // Shunting-yard for multiple operators + let mut output: Vec = Vec::new(); + let mut op_stack: Vec = Vec::new(); + + output.push(ast_operands.remove(0)); + + for i in 0..operators.len() { + let (assoc_i, prec_i) = self.get_chain_op_fixity(&operators[i]); + + while let Some(&top_idx) = op_stack.last() { + let (assoc_top, prec_top) = self.get_chain_op_fixity(&operators[top_idx]); + // Check for operator conflicts at the same precedence + if prec_top == prec_i && assoc_top != assoc_i { + // Different associativity at the same precedence → mixed associativity error + self.errors.push(TypeError::MixedAssociativityError { + span: chain_op_span(&operators[i]), + }); + } else if prec_top == prec_i + && assoc_top == Associativity::None + && assoc_i == Associativity::None + { + // Both non-associative at same precedence → non-associative chaining error + if let ChainOp::Named(named) = &operators[i] { + self.errors.push(TypeError::NonAssociativeError { + span: named.span, + op: named.value.name, + }); + } + } + let should_pop = + prec_top > prec_i || (prec_top == prec_i && assoc_i == Associativity::Left); + if should_pop { + op_stack.pop(); + let right = output.pop().unwrap(); + let left = output.pop().unwrap(); + output.push(self.build_chain_op_app(span, &operators[top_idx], &converted_expr_ops[top_idx], left, right)); + } else { + break; + } + } + + op_stack.push(i); + output.push(ast_operands.remove(0)); + } + + // Pop remaining operators + while let Some(top_idx) = op_stack.pop() { + let right = output.pop().unwrap(); + let left = output.pop().unwrap(); + output.push(self.build_chain_op_app(span, &operators[top_idx], &converted_expr_ops[top_idx], left, right)); + } + + let result = output.pop().unwrap(); + + // Post-rebalance section detection: after shunting-yard, check if `_` is a direct + // operand of the top-level operator. This matches PureScript's removeBinaryNoParens. + // After build_op_app, the structure is App(App(op, left), right). + if !has_wildcard_operand || !self.in_parens { + if let Some(ann_ty) = trailing_annotation { + let ty = self.convert_type_expr(ann_ty); + return Expr::TypeAnnotation { + span, + expr: Box::new(result), + ty, + }; + } + return result; + } + + let wildcard_sym = interner::intern("_"); + // Destructure App(App(op, left), right) to check for holes + if let Expr::App { + span: outer_span, + func: outer_func, + arg: right_arg, + } = result + { + if let Expr::App { + span: inner_span, + func: op_func, + arg: left_arg, + } = *outer_func + { + let left_is_hole = + matches!(&*left_arg, Expr::Hole { name, .. } if *name == wildcard_sym); + let right_is_hole = + matches!(&*right_arg, Expr::Hole { name, .. } if *name == wildcard_sym); + if left_is_hole || right_is_hole { + // Valid section after rebalancing — desugar to lambda + let param_name = interner::intern("$_arg"); + let param_var = Box::new(Expr::Var { + span, + name: QualifiedIdent { + module: None, + name: param_name, + }, + definition_site: DefinitionSite::Local(span), + }); + let new_left = if left_is_hole { + param_var.clone() + } else { + left_arg + }; + let new_right = if right_is_hole { param_var } else { right_arg }; + let body = Expr::App { + span: outer_span, + func: Box::new(Expr::App { + span: inner_span, + func: op_func, + arg: new_left, + }), + arg: new_right, + }; + return Expr::Lambda { + span, + binders: vec![Binder::Var { + span, + name: cst::Spanned { + span, + value: param_name, + }, + }], + body: Box::new(body), + }; + } + // _ not a direct operand after rebalancing — invalid section + self.errors + .push(TypeError::IncorrectAnonymousArgument { span }); + return Expr::App { + span: outer_span, + func: Box::new(Expr::App { + span: inner_span, + func: op_func, + arg: left_arg, + }), + arg: right_arg, + }; + } + } + // Shouldn't reach here (convert_op_chain always produces App(App(...))) + // but emit error for safety + self.errors + .push(TypeError::IncorrectAnonymousArgument { span }); + output.pop().unwrap_or(Expr::Hole { + span, + name: wildcard_sym, + }) + } + + fn build_op_app( + &mut self, + span: Span, + op: &Spanned, + left: Expr, + right: Expr, + ) -> Expr { + // For qualified operators (e.g. LL.:), check the qualified key first + let op_key = if let Some(m) = op.value.module { + qualified_symbol(m, op.value.name) + } else { + op.value.name + }; + let op_expr = if self.value_operator_targets.contains_key(&op_key) + || self.value_operator_targets.contains_key(&op.value.name) { + // Declared operator (e.g. +, :, $) + // Use the OPERATOR name (not target name) to avoid conflicts when + // multiple operators map to the same target (e.g. $ → apply, <*> → apply). + // The typechecker env has types registered under operator names. + let def_site = self.resolve_operator_target(op.value.name, op.span); + if self.function_op_aliases.contains(&op_key) + || self.function_op_aliases.contains(&op.value.name) { + Expr::Var { + span: op.span, + name: op.value, + definition_site: def_site, + } + } else { + Expr::Constructor { + span: op.span, + name: op.value, + definition_site: def_site, + } + } + } else { + // Backtick operator (e.g. `implies`, `compare`) — target is the name itself + let def_site = self.resolve_value(&op.value, op.span); + Expr::Var { + span: op.span, + name: op.value, + definition_site: def_site, + } + }; + Expr::App { + span, + func: Box::new(Expr::App { + span, + func: Box::new(op_expr), + arg: Box::new(left), + }), + arg: Box::new(right), + } + } + + fn get_fixity(&self, op_name: Symbol) -> (Associativity, u8) { + self.value_fixities + .get(&op_name) + .copied() + .unwrap_or((Associativity::Left, 9)) + } + + // --- Literal conversion --- + + fn convert_literal(&mut self, lit: &cst::Literal) -> Literal { + match lit { + cst::Literal::Int(n) => Literal::Int(*n), + cst::Literal::Float(f) => Literal::Float(*f), + cst::Literal::String(s) => Literal::String(s.clone()), + cst::Literal::Char(c) => Literal::Char(*c), + cst::Literal::Boolean(b) => Literal::Boolean(*b), + cst::Literal::Array(elems) => { + Literal::Array(elems.iter().map(|e| self.convert_expr(e)).collect()) + } + } + } + + // --- Type expression conversion --- + + fn convert_type_expr(&mut self, ty: &cst::TypeExpr) -> TypeExpr { + match ty { + cst::TypeExpr::Var { span, name } => TypeExpr::Var { + span: *span, + name: name.clone(), + }, + cst::TypeExpr::Constructor { span, name } => TypeExpr::Constructor { + span: *span, + name: *name, + definition_site: self.resolve_type(name, *span), + }, + cst::TypeExpr::App { + span, + constructor, + arg, + } => TypeExpr::App { + span: *span, + constructor: Box::new(self.convert_type_expr(constructor)), + arg: Box::new(self.convert_type_expr(arg)), + }, + cst::TypeExpr::Function { span, from, to } => TypeExpr::Function { + span: *span, + from: Box::new(self.convert_type_expr(from)), + to: Box::new(self.convert_type_expr(to)), + }, + cst::TypeExpr::Forall { span, vars, ty } => TypeExpr::Forall { + span: *span, + vars: vars + .iter() + .map(|(v, visible, kind)| { + ( + v.clone(), + *visible, + kind.as_ref().map(|k| Box::new(self.convert_type_expr(k))), + ) + }) + .collect(), + ty: Box::new(self.convert_type_expr(ty)), + }, + cst::TypeExpr::Constrained { + span, + constraints, + ty, + } => TypeExpr::Constrained { + span: *span, + constraints: constraints + .iter() + .map(|c| self.convert_constraint(c)) + .collect(), + ty: Box::new(self.convert_type_expr(ty)), + }, + cst::TypeExpr::Record { span, fields } => TypeExpr::Record { + span: *span, + fields: fields.iter().map(|f| self.convert_type_field(f)).collect(), + }, + cst::TypeExpr::Row { + span, + fields, + tail, + is_record, + } => TypeExpr::Row { + span: *span, + fields: fields.iter().map(|f| self.convert_type_field(f)).collect(), + tail: tail.as_ref().map(|t| Box::new(self.convert_type_expr(t))), + is_record: *is_record, + }, + cst::TypeExpr::Parens { ty, .. } => self.convert_type_expr(ty), + cst::TypeExpr::Hole { span, name } => TypeExpr::Hole { + span: *span, + name: *name, + }, + cst::TypeExpr::Wildcard { span } => TypeExpr::Wildcard { span: *span }, + cst::TypeExpr::TypeOp { + span: _, + left, + op, + right, + } => { + // Flatten the right-recursive TypeOp chain into operands and operators, + // then use shunting-yard to rebalance based on declared fixity. + let mut operands: Vec<&cst::TypeExpr> = vec![left.as_ref()]; + let mut operators: Vec<&cst::Spanned> = vec![op]; + let mut current: &cst::TypeExpr = right.as_ref(); + loop { + match current { + cst::TypeExpr::TypeOp { left: rl, op: rop, right: rr, .. } => { + operands.push(rl.as_ref()); + operators.push(rop); + current = rr.as_ref(); + } + _ => break, + } + } + operands.push(current); + + // Check for non-associative operator chaining + for i in 0..operators.len().saturating_sub(1) { + let (assoc_l, prec_l) = self.type_fixities.get(&operators[i].value.name) + .copied().unwrap_or((Associativity::Left, 9)); + let (assoc_r, prec_r) = self.type_fixities.get(&operators[i+1].value.name) + .copied().unwrap_or((Associativity::Left, 9)); + if prec_l == prec_r && (assoc_l == Associativity::None || assoc_r == Associativity::None) { + self.errors.push(TypeError::NonAssociativeError { + span: operators[i+1].span, + op: operators[i+1].value.name, + }); + } + } + + // Pre-convert all operands + let converted: Vec = operands.iter().map(|o| self.convert_type_expr(o)).collect(); + + // Resolve all operators to type constructors + let resolved_ops: Vec<(TypeExpr, Span, Associativity, u8)> = operators.iter().map(|op_ref| { + let target = match self.type_operators.get(&op_ref.value.name).copied() { + Some(t) => t, + None => { + self.errors.push(TypeError::UndefinedVariable { + span: op_ref.span, + name: op_ref.value.name, + }); + op_ref.value.name + } + }; + let target_qi = QualifiedIdent { module: None, name: target }; + let def_site = self.resolve_type(&target_qi, op_ref.span); + let ctor = TypeExpr::Constructor { + span: op_ref.span, + name: target_qi, + definition_site: def_site, + }; + let (assoc, prec) = self.type_fixities.get(&op_ref.value.name) + .copied().unwrap_or((Associativity::Left, 9)); + (ctor, op_ref.span, assoc, prec) + }).collect(); + + // Shunting-yard: rebalance based on fixity + let mut output: Vec = Vec::new(); + let mut op_stack: Vec<(TypeExpr, Span, Associativity, u8)> = Vec::new(); + output.push(converted[0].clone()); + + for (i, (ctor, op_span, assoc, prec)) in resolved_ops.into_iter().enumerate() { + while let Some((_, _, _, top_prec)) = op_stack.last() { + let should_pop = match assoc { + Associativity::Left => prec <= *top_prec, + Associativity::Right => prec < *top_prec, + Associativity::None => prec <= *top_prec, + }; + if should_pop { + let (top_ctor, top_span, _, _) = op_stack.pop().unwrap(); + let right_operand = output.pop().unwrap(); + let left_operand = output.pop().unwrap(); + output.push(TypeExpr::App { + span: top_span, + constructor: Box::new(TypeExpr::App { + span: top_span, + constructor: Box::new(top_ctor), + arg: Box::new(left_operand), + }), + arg: Box::new(right_operand), + }); + } else { + break; + } + } + op_stack.push((ctor, op_span, assoc, prec)); + output.push(converted[i + 1].clone()); + } + + // Pop remaining operators + while let Some((ctor, op_span, _, _)) = op_stack.pop() { + let right_operand = output.pop().unwrap(); + let left_operand = output.pop().unwrap(); + output.push(TypeExpr::App { + span: op_span, + constructor: Box::new(TypeExpr::App { + span: op_span, + constructor: Box::new(ctor), + arg: Box::new(left_operand), + }), + arg: Box::new(right_operand), + }); + } + + debug_assert_eq!(output.len(), 1); + output.pop().unwrap() + } + cst::TypeExpr::Kinded { span, ty, kind } => TypeExpr::Kinded { + span: *span, + ty: Box::new(self.convert_type_expr(ty)), + kind: Box::new(self.convert_type_expr(kind)), + }, + cst::TypeExpr::StringLiteral { span, value } => TypeExpr::StringLiteral { + span: *span, + value: value.clone(), + }, + cst::TypeExpr::IntLiteral { span, value } => TypeExpr::IntLiteral { + span: *span, + value: *value, + }, + } + } + + fn convert_type_field(&mut self, field: &cst::TypeField) -> TypeField { + TypeField { + span: field.span, + label: field.label.clone(), + ty: self.convert_type_expr(&field.ty), + } + } + + fn convert_constraint(&mut self, c: &cst::Constraint) -> Constraint { + Constraint { + span: c.span, + class: c.class, + args: c.args.iter().map(|a| self.convert_type_expr(a)).collect(), + definition_site: self.resolve_class(&c.class, c.span), + } + } + + // --- Binder conversion --- + + fn convert_binder(&mut self, binder: &cst::Binder) -> Binder { + match binder { + cst::Binder::Wildcard { span } => Binder::Wildcard { span: *span }, + cst::Binder::Var { span, name } => Binder::Var { + span: *span, + name: name.clone(), + }, + cst::Binder::Literal { span, lit } => Binder::Literal { + span: *span, + lit: self.convert_literal(lit), + }, + cst::Binder::Constructor { span, name, args } => Binder::Constructor { + span: *span, + name: *name, + args: args.iter().map(|a| self.convert_binder(a)).collect(), + definition_site: self.resolve_value(name, *span), + }, + cst::Binder::Record { span, fields } => Binder::Record { + span: *span, + fields: fields + .iter() + .map(|f| RecordBinderField { + span: f.span, + label: f.label.clone(), + binder: f.binder.as_ref().map(|b| self.convert_binder(b)), + }) + .collect(), + }, + cst::Binder::As { span, name, binder } => Binder::As { + span: *span, + name: name.clone(), + binder: Box::new(self.convert_binder(binder)), + }, + cst::Binder::Parens { binder, .. } => self.convert_binder(binder), + cst::Binder::Array { span, elements } => Binder::Array { + span: *span, + elements: elements.iter().map(|e| self.convert_binder(e)).collect(), + }, + cst::Binder::Op { + span, + left, + op, + right, + } => { + // For qualified operators, use the qualified key for lookups + let binder_op_key = if let Some(m) = op.value.module { + qualified_symbol(m, op.value.name) + } else { + op.value.name + }; + // Operators aliasing functions (not constructors) are invalid in binder patterns + if self.function_op_aliases.contains(&binder_op_key) + || self.function_op_aliases.contains(&op.value.name) { + self.errors.push(TypeError::InvalidOperatorInBinder { + span: op.span, + op: op.value.name, + }); + } + // Resolve operator to its target constructor (e.g. `:` → `Cons`) + let left_b = self.convert_binder(left); + let right_b = self.convert_binder(right); + let mut target_name = match self.value_operator_targets.get(&binder_op_key) + .or_else(|| self.value_operator_targets.get(&op.value.name)) { + Some(target) => *target, + None => { + self.errors.push(TypeError::UndefinedVariable { + span: op.span, + name: op.value.name, + }); + op.value + } + }; + // Propagate module qualifier (e.g. A.: → A.Cons) + if target_name.module.is_none() { + target_name.module = op.value.module; + } + let def_site = self.resolve_operator_target(target_name.name, op.span); + Binder::Constructor { + span: *span, + name: target_name, + args: vec![left_b, right_b], + definition_site: def_site, + } + } + cst::Binder::Typed { span, binder, ty } => Binder::Typed { + span: *span, + binder: Box::new(self.convert_binder(binder)), + ty: self.convert_type_expr(ty), + }, + } + } + + // --- Guarded expression conversion --- + + fn convert_guarded(&mut self, guarded: &cst::GuardedExpr) -> GuardedExpr { + match guarded { + cst::GuardedExpr::Unconditional(expr) => { + GuardedExpr::Unconditional(Box::new(self.convert_expr(expr))) + } + cst::GuardedExpr::Guarded(guards) => GuardedExpr::Guarded( + guards + .iter() + .map(|g| { + // Push a scope for pattern guard bindings so that variables + // bound by `| Pat <- expr` are visible in subsequent guards + // and the guard body. + self.push_scope(); + // Pre-collect binder names from pattern guards so they're in + // scope for subsequent boolean guards and the body expression. + for p in &g.patterns { + if let cst::GuardPattern::Pattern(b, _) = p { + let mut names = Vec::new(); + Self::collect_binder_names(b, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + } + let patterns = g + .patterns + .iter() + .map(|p| match p { + cst::GuardPattern::Boolean(e) => { + GuardPattern::Boolean(Box::new(self.convert_expr(e))) + } + cst::GuardPattern::Pattern(b, e) => { + let binder = self.convert_binder(b); + let expr = self.convert_expr(e); + GuardPattern::Pattern(binder, Box::new(expr)) + } + }) + .collect(); + let expr = Box::new(self.convert_expr(&g.expr)); + self.pop_scope(); + Guard { + span: g.span, + patterns, + expr, + } + }) + .collect(), + ), + } + } + + // --- Let binding conversion --- + + fn convert_let_binding(&mut self, lb: &cst::LetBinding) -> LetBinding { + match lb { + cst::LetBinding::Value { span, binder, expr } => LetBinding::Value { + span: *span, + binder: self.convert_binder(binder), + expr: self.convert_expr(expr), + }, + cst::LetBinding::Signature { span, name, ty } => LetBinding::Signature { + span: *span, + name: name.clone(), + ty: self.convert_type_expr(ty), + }, + } + } + + // --- Do statement conversion --- + + fn convert_do_statement(&mut self, stmt: &cst::DoStatement) -> DoStatement { + match stmt { + cst::DoStatement::Bind { span, binder, expr } => { + let ast_expr = self.convert_expr(expr); + // Register binder names AFTER converting the expression + let mut names = Vec::new(); + Self::collect_binder_names(binder, &mut names); + for (n, s) in &names { + self.add_local(*n, *s); + } + let ast_binder = self.convert_binder(binder); + DoStatement::Bind { + span: *span, + binder: ast_binder, + expr: ast_expr, + } + } + cst::DoStatement::Let { span, bindings } => { + // Register names for subsequent statements + for lb in bindings { + if let cst::LetBinding::Value { binder, .. } = lb { + let mut names = Vec::new(); + Self::collect_binder_names(binder, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + } + DoStatement::Let { + span: *span, + bindings: bindings + .iter() + .map(|lb| self.convert_let_binding(lb)) + .collect(), + } + } + cst::DoStatement::Discard { span, expr } => DoStatement::Discard { + span: *span, + expr: self.convert_expr(expr), + }, + } + } + + // --- Record field conversion --- + + fn convert_record_field(&mut self, f: &cst::RecordField) -> RecordField { + RecordField { + span: f.span, + label: f.label.clone(), + value: f.value.as_ref().map(|v| self.convert_expr(v)), + type_ann: f.type_ann.as_ref().map(|t| self.convert_type_expr(t)), + is_update: f.is_update, + } + } + + // --- Declaration conversion --- + + fn convert_decl(&mut self, decl: &cst::Decl) -> Decl { + match decl { + cst::Decl::Value { + span, + name, + binders, + guarded, + where_clause, + } => { + self.push_scope(); + for b in binders { + let mut names = Vec::new(); + Self::collect_binder_names(b, &mut names); + for (n, s) in names { + self.add_local(n, s); + } + } + // Register where clause names and check for overlapping bindings + { + let mut seen: HashMap> = HashMap::new(); + let mut binding_order: Vec = Vec::new(); + for lb in where_clause { + if let cst::LetBinding::Value { + span: lb_span, + binder, + expr, + } = lb + { + let mut names = Vec::new(); + Self::collect_binder_names(binder, &mut names); + for (n, s) in &names { + self.add_local(*n, *s); + } + // Track for overlap detection + if let cst::Binder::Var { name: bname, .. } = binder { + let is_func = matches!(expr, cst::Expr::Lambda { .. }); + seen.entry(bname.value) + .or_default() + .push((*lb_span, is_func)); + binding_order.push(bname.value); + } + } + } + for (name, entries) in &seen { + if entries.len() > 1 { + // All lambdas → function equations (always OK). + // All bare Var binders (non-lambda) → multi-equation guarded + // where-clause definitions, which PureScript also allows. + let all_funcs = entries.iter().all(|(_, is_func)| *is_func); + let all_non_funcs = entries.iter().all(|(_, is_func)| !*is_func); + if !all_funcs && !all_non_funcs { + self.errors.push(TypeError::OverlappingNamesInLet { + spans: entries.iter().map(|(s, _)| *s).collect(), + name: *name, + }); + } else { + let indices: Vec = binding_order + .iter() + .enumerate() + .filter(|(_, n)| **n == *name) + .map(|(i, _)| i) + .collect(); + let is_adjacent = indices.windows(2).all(|w| w[1] == w[0] + 1); + if !is_adjacent { + self.errors.push(TypeError::OverlappingNamesInLet { + spans: entries.iter().map(|(s, _)| *s).collect(), + name: *name, + }); + } + } + } + } + } + let ast_binders = binders.iter().map(|b| self.convert_binder(b)).collect(); + let ast_guarded = self.convert_guarded(guarded); + let ast_where = where_clause + .iter() + .map(|lb| self.convert_let_binding(lb)) + .collect(); + self.pop_scope(); + Decl::Value { + span: *span, + name: name.clone(), + binders: ast_binders, + guarded: ast_guarded, + where_clause: ast_where, + } + } + cst::Decl::TypeSignature { span, name, ty } => Decl::TypeSignature { + span: *span, + name: name.clone(), + ty: self.convert_type_expr(ty), + }, + cst::Decl::Data { + span, + name, + type_vars, + constructors, + kind_sig, + is_role_decl, + kind_type, + type_var_kind_anns, + } => Decl::Data { + span: *span, + name: name.clone(), + type_vars: type_vars.clone(), + constructors: constructors + .iter() + .map(|c| DataConstructor { + span: c.span, + name: c.name.clone(), + fields: c.fields.iter().map(|f| self.convert_type_expr(f)).collect(), + }) + .collect(), + kind_sig: *kind_sig, + is_role_decl: *is_role_decl, + kind_type: kind_type + .as_ref() + .map(|k| Box::new(self.convert_type_expr(k))), + type_var_kind_anns: type_var_kind_anns + .iter() + .map(|a| a.as_ref().map(|k| Box::new(self.convert_type_expr(k)))) + .collect(), + }, + cst::Decl::TypeAlias { + span, + name, + type_vars, + ty, + type_var_kind_anns, + } => Decl::TypeAlias { + span: *span, + name: name.clone(), + type_vars: type_vars.clone(), + ty: self.convert_type_expr(ty), + type_var_kind_anns: type_var_kind_anns + .iter() + .map(|a| a.as_ref().map(|k| Box::new(self.convert_type_expr(k)))) + .collect(), + }, + cst::Decl::Newtype { + span, + name, + type_vars, + constructor, + ty, + type_var_kind_anns, + } => Decl::Newtype { + span: *span, + name: name.clone(), + type_vars: type_vars.clone(), + constructor: constructor.clone(), + ty: self.convert_type_expr(ty), + type_var_kind_anns: type_var_kind_anns + .iter() + .map(|a| a.as_ref().map(|k| Box::new(self.convert_type_expr(k)))) + .collect(), + }, + cst::Decl::Class { + span, + constraints, + name, + type_vars, + fundeps, + members, + is_kind_sig, + kind_type, + type_var_kind_anns, + } => Decl::Class { + span: *span, + constraints: constraints + .iter() + .map(|c| self.convert_constraint(c)) + .collect(), + name: name.clone(), + type_vars: type_vars.clone(), + fundeps: fundeps.clone(), + members: members + .iter() + .map(|m| ClassMember { + span: m.span, + name: m.name.clone(), + ty: self.convert_type_expr(&m.ty), + }) + .collect(), + is_kind_sig: *is_kind_sig, + kind_type: kind_type + .as_ref() + .map(|k| Box::new(self.convert_type_expr(k))), + type_var_kind_anns: type_var_kind_anns + .iter() + .map(|a| a.as_ref().map(|k| Box::new(self.convert_type_expr(k)))) + .collect(), + }, + cst::Decl::Instance { + span, + name, + constraints, + class_name, + types, + members, + chain, + } => Decl::Instance { + span: *span, + name: name.clone(), + constraints: constraints + .iter() + .map(|c| self.convert_constraint(c)) + .collect(), + class_name: *class_name, + class_definition_site: self.resolve_class(class_name, *span), + types: types.iter().map(|t| self.convert_type_expr(t)).collect(), + members: members.iter().map(|d| self.convert_decl(d)).collect(), + chain: *chain, + }, + cst::Decl::Fixity { + span, + associativity, + precedence, + target, + operator, + is_type, + } => { + let target_def = if *is_type { + self.resolve_type(target, *span) + } else { + self.resolve_value(target, *span) + }; + Decl::Fixity { + span: *span, + associativity: *associativity, + precedence: *precedence, + target: *target, + target_definition_site: target_def, + operator: operator.clone(), + is_type: *is_type, + } + } + cst::Decl::Foreign { span, name, ty } => Decl::Foreign { + span: *span, + name: name.clone(), + ty: self.convert_type_expr(ty), + }, + cst::Decl::ForeignData { span, name, kind } => Decl::ForeignData { + span: *span, + name: name.clone(), + kind: self.convert_type_expr(kind), + }, + cst::Decl::Derive { + span, + newtype, + name, + constraints, + class_name, + types, + } => { + // Use lenient class resolution for derive declarations — the typechecker + // handles derive classes specially (e.g. Newtype, Eq, Ord) and they may + // not be in the converter's class map. + let class_definition_site = self.resolve_class_lenient(class_name, *span); + Decl::Derive { + span: *span, + newtype: *newtype, + name: name.clone(), + constraints: constraints + .iter() + .map(|c| self.convert_constraint(c)) + .collect(), + class_name: *class_name, + class_definition_site, + types: types.iter().map(|t| self.convert_type_expr(t)).collect(), + } + } + } + } +} diff --git a/src/ast/decl.rs b/src/ast/decl.rs deleted file mode 100644 index da83b57c..00000000 --- a/src/ast/decl.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::marker::PhantomData; - -/// Declaration AST nodes (Phase 3 - stub for now) -#[derive(Debug, Clone, PartialEq)] -pub enum Decl<'ast> { - /// Placeholder - /// TODO: Add ValueDecl, TypeDecl, etc. in Phase 3 - _Phantom(PhantomData<&'ast ()>), -} diff --git a/src/ast/expr.rs b/src/ast/expr.rs deleted file mode 100644 index 669308ef..00000000 --- a/src/ast/expr.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::ast::span::Span; -use crate::lexer::token::Ident; -use std::marker::PhantomData; - -/// Expression AST nodes (Phase 3 - stub for now) -#[derive(Debug, Clone, PartialEq)] -pub enum Expr<'ast> { - /// Variable reference - Var(Ident, Span), - - /// Integer literal - IntLit(i64, Span), - - /// Placeholder for other expression types - /// TODO: Add Lambda, App, Let, Case, If, etc. in Phase 3 - _Phantom(PhantomData<&'ast ()>), -} diff --git a/src/ast/mod.rs b/src/ast/mod.rs deleted file mode 100644 index 2625d814..00000000 --- a/src/ast/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod span; - -pub use span::{Span, Spanned, SourcePos}; \ No newline at end of file diff --git a/src/ast/module.rs b/src/ast/module.rs deleted file mode 100644 index adc88cbf..00000000 --- a/src/ast/module.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::ast::decl::Decl; -use crate::lexer::token::Ident; - -/// Module structure (Phase 3 - stub for now) -#[derive(Debug, Clone, PartialEq)] -pub struct Module<'ast> { - pub name: Ident, - pub decls: Vec>, -} diff --git a/src/ast/pattern.rs b/src/ast/pattern.rs deleted file mode 100644 index f34c2775..00000000 --- a/src/ast/pattern.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::marker::PhantomData; - -/// Pattern AST nodes (Phase 3 - stub for now) -#[derive(Debug, Clone, PartialEq)] -pub enum Pattern<'ast> { - /// Placeholder - /// TODO: Add VarPat, LitPat, ConsPat, etc. in Phase 3 - _Phantom(PhantomData<&'ast ()>), -} diff --git a/src/ast/types.rs b/src/ast/types.rs deleted file mode 100644 index 9d22adc7..00000000 --- a/src/ast/types.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::marker::PhantomData; - -/// Type AST nodes (Phase 5 - stub for now) -#[derive(Debug, Clone, PartialEq)] -pub enum Type<'ast> { - /// Placeholder - /// TODO: Add TyVar, TyCon, TyApp, TyForall, etc. in Phase 5 - _Phantom(PhantomData<&'ast ()>), -} diff --git a/src/build/error.rs b/src/build/error.rs index 4833f676..33a43683 100644 --- a/src/build/error.rs +++ b/src/build/error.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::ast::span::Span; +use crate::span::Span; use crate::diagnostics::CompilerError; use thiserror::Error; diff --git a/src/build/mod.rs b/src/build/mod.rs index 193aaeac..560d39f0 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -9,13 +9,14 @@ pub mod error; use std::collections::{HashMap, HashSet, VecDeque}; use std::panic::AssertUnwindSafe; use std::path::PathBuf; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::Arc; use std::time::Instant; use crate::cst::{Decl, Module}; use crate::interner::{self, Symbol}; use crate::js_ffi; -use crate::typechecker::check::{self, ModuleRegistry}; +use crate::typechecker::check; +use crate::typechecker::registry::ModuleRegistry; use crate::typechecker::error::TypeError; use crate::typechecker::types::Type; @@ -394,148 +395,113 @@ pub fn build_from_sources_with_options( phase_start.elapsed() ); - // Phase 4: Typecheck in dependency order (parallel by level) + // Phase 4: Typecheck in dependency order (sequential, on a large-stack thread) let total_modules: usize = levels.iter().map(|l| l.len()).sum(); - let num_workers = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(4); log::debug!( - "Phase 4: Typechecking {} modules (parallel, {} workers)", + "Phase 4: Typechecking {} modules (sequential)", total_modules, - num_workers ); let phase_start = Instant::now(); let timeout = options.module_timeout; - - // Process each level concurrently, synchronize between levels. - // Registry is behind RwLock: check_module reads, register writes. - // Use a bounded number of worker threads that pull work from a shared queue. - let rw_registry = RwLock::new(registry); - let results = Mutex::new(Vec::new()); - let errors = Mutex::new(Vec::new()); - let modules_done = std::sync::atomic::AtomicUsize::new(0); - - for (level_idx, level) in levels.iter().enumerate() { - log::debug!(" level {}: {} modules", level_idx, level.len()); - let work_queue = Mutex::new(level.iter().copied()); - std::thread::scope(|s| { - let thread_count = num_workers.min(level.len()); - let handles: Vec<_> = (0..thread_count) - .map(|_| { - let work_queue = &work_queue; - let rw_registry = &rw_registry; - let results = &results; - let errors = &errors; - let parsed = &parsed; - let modules_done = &modules_done; - std::thread::Builder::new() - .stack_size(64 * 1024 * 1024) - .spawn_scoped(s, move || { - loop { - let idx = { - let mut q = work_queue.lock().unwrap(); - q.next() - }; - let Some(idx) = idx else { break }; - let pm = &parsed[idx]; - let tc_start = Instant::now(); - let deadline = timeout.map(|t| tc_start + t); - let check_result = - std::panic::catch_unwind(AssertUnwindSafe(|| { - crate::typechecker::set_deadline(deadline); - let reg = - rw_registry.read().unwrap_or_else(|e| e.into_inner()); - log::debug!(" typechecking {}", pm.module_name); - let result = check::check_module(&pm.module, ®); - log::debug!( - " finished {} ({} type errors) in {:.2?}", - pm.module_name, - result.errors.len(), - tc_start.elapsed() - ); - crate::typechecker::set_deadline(None); - result - })); - let elapsed = tc_start.elapsed(); - let done = modules_done - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - + 1; - match check_result { - Ok(result) => { - log::debug!( - " [{}/{}] ok: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed - ); - rw_registry - .write() - .unwrap_or_else(|e| e.into_inner()) - .register(&pm.module_parts, result.exports); - results.lock().unwrap().push(ModuleResult { - path: pm.path.clone(), - module_name: pm.module_name.clone(), - types: result.types, - type_errors: result.errors, - }); - } - Err(payload) => { - // Distinguish deadline panics from other panics - let is_deadline = - payload.downcast_ref::<&str>().map_or(false, |s| { - *s == "typechecking deadline exceeded" - }) || payload - .downcast_ref::() - .map_or(false, |s| { - s == "typechecking deadline exceeded" - }); - if is_deadline { - log::debug!( - " [{}/{}] timeout: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed - ); - errors.lock().unwrap().push( - BuildError::TypecheckTimeout { - path: pm.path.clone(), - module_name: pm.module_name.clone(), - timeout_secs: timeout.unwrap().as_secs(), - }, - ); - } else { - log::debug!( - " [{}/{}] panic: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed - ); - errors.lock().unwrap().push( - BuildError::TypecheckPanic { - path: pm.path.clone(), - module_name: pm.module_name.clone(), - }, - ); - } - } - } + let mut module_results = Vec::new(); + + std::thread::scope(|s| { + let handle = std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn_scoped(s, || { + let mut done = 0usize; + let mut results = Vec::new(); + let mut errs = Vec::new(); + + for idx in levels.iter().flatten().copied() { + let pm = &parsed[idx]; + let tc_start = Instant::now(); + let deadline = timeout.map(|t| tc_start + t); + let check_result = std::panic::catch_unwind(AssertUnwindSafe(|| { + let mod_sym = crate::interner::intern(&pm.module_name); + let path_str = pm.path.to_string_lossy(); + crate::typechecker::set_deadline(deadline, mod_sym, &path_str); + log::debug!(" typechecking {}", pm.module_name); + let (ast_module, convert_errors) = crate::ast::convert(pm.module.clone(), ®istry); + let mut result = check::check_module(&ast_module, ®istry); + // Prepend AST conversion errors (name resolution failures, overlapping bindings, etc.) + // These are combined with typechecker errors so both are visible. + if !convert_errors.is_empty() { + let mut all_errors = convert_errors; + all_errors.extend(result.errors); + result.errors = all_errors; + } + log::debug!( + " finished {} ({} type errors) in {:.2?}", + pm.module_name, + result.errors.len(), + tc_start.elapsed() + ); + crate::typechecker::set_deadline(None, mod_sym, ""); + result + })); + let elapsed = tc_start.elapsed(); + done += 1; + match check_result { + Ok(result) => { + log::debug!( + " [{}/{}] ok: {} ({:.2?})", + done, + total_modules, + pm.module_name, + elapsed + ); + registry.register(&pm.module_parts, result.exports); + results.push(ModuleResult { + path: pm.path.clone(), + module_name: pm.module_name.clone(), + types: result.types, + type_errors: result.errors, + }); + } + Err(payload) => { + let is_deadline = payload + .downcast_ref::<&str>() + .map_or(false, |s| s.starts_with("typechecking deadline exceeded")) + || payload.downcast_ref::().map_or(false, |s| { + s.starts_with("typechecking deadline exceeded") + }); + if is_deadline { + log::debug!( + " [{}/{}] timeout: {} ({:.2?})", + done, + total_modules, + pm.module_name, + elapsed + ); + errs.push(BuildError::TypecheckTimeout { + path: pm.path.clone(), + module_name: pm.module_name.clone(), + timeout_secs: timeout.unwrap().as_secs(), + }); + } else { + log::debug!( + " [{}/{}] panic: {} ({:.2?})", + done, + total_modules, + pm.module_name, + elapsed + ); + errs.push(BuildError::TypecheckPanic { + path: pm.path.clone(), + module_name: pm.module_name.clone(), + }); } - }) - .expect("failed to spawn typecheck thread") - }) - .collect(); - for handle in handles { - let _ = handle.join(); - } - }); - } - - build_errors.extend(errors.into_inner().unwrap()); - let module_results = results.into_inner().unwrap(); - registry = rw_registry.into_inner().unwrap_or_else(|e| e.into_inner()); + } + } + } + (results, errs) + }) + .expect("failed to spawn typecheck thread"); + let (results, errs) = handle.join().expect("typecheck thread panicked"); + module_results = results; + build_errors.extend(errs); + }); log::debug!( "Phase 4 complete: typechecked {} modules in {:.2?}", module_results.len(), diff --git a/src/codegen/js.rs b/src/codegen/js.rs index 3cdc3f53..c5d42460 100644 --- a/src/codegen/js.rs +++ b/src/codegen/js.rs @@ -10,11 +10,16 @@ use std::collections::{HashMap, HashSet}; use crate::cst::*; use crate::interner::{self, Symbol}; use crate::lexer::token::Ident; -use crate::typechecker::check::{ModuleExports, ModuleRegistry}; +use crate::typechecker::{ModuleExports, ModuleRegistry}; use super::common::{any_name_to_js, ident_to_js, module_name_to_js}; use super::js_ast::*; +/// Create an unqualified QualifiedIdent from a Symbol (for map lookups). +fn unqualified(name: Symbol) -> QualifiedIdent { + QualifiedIdent { module: None, name } +} + /// Context threaded through code generation for a single module. struct CodegenCtx<'a> { /// The module being compiled @@ -32,11 +37,11 @@ struct CodegenCtx<'a> { /// Set of names that are newtypes (newtype constructor erasure) newtype_names: &'a HashSet, /// Mapping from constructor name → (parent_type, type_vars, field_types) - ctor_details: &'a HashMap, Vec)>, + ctor_details: &'a HashMap, Vec)>, /// Data type → constructor names (to determine sum vs product) - data_constructors: &'a HashMap>, + data_constructors: &'a HashMap>, /// Operators that alias functions (not constructors) - function_op_aliases: &'a HashSet, + function_op_aliases: &'a HashSet, /// Names of foreign imports in this module foreign_imports: HashSet, /// Import map: module_parts → JS variable name @@ -292,7 +297,7 @@ fn is_exported(ctx: &CodegenCtx, name: Symbol) -> bool { } Export::Type(_, Some(DataMembers::All)) => { // Check if name is a constructor of this type - if ctx.ctor_details.contains_key(&name) { + if ctx.ctor_details.contains_key(&unqualified(name)) { return true; } } @@ -303,7 +308,7 @@ fn is_exported(ctx: &CodegenCtx, name: Symbol) -> bool { } Export::Class(_) => { // Class methods are exported as values - if ctx.exports.class_methods.contains_key(&name) { + if ctx.exports.class_methods.contains_key(&unqualified(name)) { return true; } } @@ -611,7 +616,7 @@ fn gen_expr(ctx: &CodegenCtx, expr: &Expr) -> JsExpr { Expr::Constructor { name, .. } => { let ctor_name = name.name; // Check if nullary (use .value) or n-ary (use .create) - if let Some((_, _, fields)) = ctx.ctor_details.get(&ctor_name) { + if let Some((_, _, fields)) = ctx.ctor_details.get(&unqualified(ctor_name)) { if fields.is_empty() { // Nullary: Ctor.value let base = gen_qualified_ref_raw(ctx, name); @@ -753,6 +758,17 @@ fn gen_expr(ctx: &CodegenCtx, expr: &Expr) -> JsExpr { // This shouldn't appear in expression position normally gen_expr(ctx, name) } + + Expr::Wildcard { .. } => { + JsExpr::Var("undefined".to_string()) + } + + Expr::BacktickApp { func, left, right, .. } => { + let f = gen_expr(ctx, func); + let l = gen_expr(ctx, left); + let r = gen_expr(ctx, right); + JsExpr::App(Box::new(JsExpr::App(Box::new(f), vec![l])), vec![r]) + } } } @@ -1155,7 +1171,7 @@ fn gen_binder_match( let mut bindings = Vec::new(); // Determine if we need an instanceof check (sum types) - let is_sum = if let Some((parent, _, _)) = ctx.ctor_details.get(&ctor_name) { + let is_sum = if let Some((parent, _, _)) = ctx.ctor_details.get(&unqualified(ctor_name)) { ctx.data_constructors .get(parent) .map_or(false, |ctors| ctors.len() > 1) @@ -1278,7 +1294,7 @@ fn gen_binder_match( let op_name = &op.value; // Check if this is a constructor operator - let is_function_op = ctx.function_op_aliases.contains(&op_name.name); + let is_function_op = ctx.function_op_aliases.contains(&unqualified(op_name.name)); if !is_function_op { // Constructor operator — treat as constructor binder with 2 args diff --git a/src/cst.rs b/src/cst.rs index 015207b9..bf8a2d08 100644 --- a/src/cst.rs +++ b/src/cst.rs @@ -12,7 +12,9 @@ //! - Code formatting tools //! - Refactoring tools -use crate::ast::span::Span; +use std::fmt::Display; + +use crate::span::Span; use crate::lexer::token::Ident; /// Module with full span information @@ -31,6 +33,20 @@ pub struct ModuleName { pub parts: Vec, } +impl Display for ModuleName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.parts + .iter() + .map(|ident| crate::interner::resolve(*ident).unwrap_or_default()) + .collect::>() + .join(".") + ) + } +} + /// Export list #[derive(Debug, Clone, PartialEq)] pub struct ExportList { @@ -50,8 +66,8 @@ pub enum Export { /// Data constructor exports #[derive(Debug, Clone, PartialEq)] pub enum DataMembers { - All, // (..) - Explicit(Vec), // (Foo, Bar) + All, // (..) + Explicit(Vec), // (Foo, Bar) } /// Import declaration @@ -271,22 +287,13 @@ pub enum GuardPattern { #[derive(Debug, Clone, PartialEq)] pub enum Expr { /// Variable: x, Data.Array.head - Var { - span: Span, - name: QualifiedIdent, - }, + Var { span: Span, name: QualifiedIdent }, /// Constructor: Just, Nothing - Constructor { - span: Span, - name: QualifiedIdent, - }, + Constructor { span: Span, name: QualifiedIdent }, /// Literal value - Literal { - span: Span, - lit: Literal, - }, + Literal { span: Span, lit: Literal }, /// Function application: f x App { @@ -380,10 +387,7 @@ pub enum Expr { }, /// Parenthesized expression (preserved in CST) - Parens { - span: Span, - expr: Box, - }, + Parens { span: Span, expr: Box }, /// Type annotation: expr :: Type TypeAnnotation { @@ -392,23 +396,17 @@ pub enum Expr { ty: TypeExpr, }, + /// Wildcard: _ . Sugar for creating lambdas e.g (_ + 1) desugars to \$generated_arg -> $generated_arg + 1, + Wildcard { span: Span }, + /// Typed hole: ?hole - Hole { - span: Span, - name: Ident, - }, + Hole { span: Span, name: Ident }, /// Array literal: [1, 2, 3] - Array { - span: Span, - elements: Vec, - }, + Array { span: Span, elements: Vec }, /// Negation: -x - Negate { - span: Span, - expr: Box, - }, + Negate { span: Span, expr: Box }, /// As-pattern expression: name@pattern (for do-bind conversion to binder) AsPattern { @@ -417,16 +415,60 @@ pub enum Expr { pattern: Box, }, - + /// Complex backtick application: a `f x` b (where backtick expression is not a simple name) + /// Stored separately from App so it can participate in operator chain rebalancing. + BacktickApp { + span: Span, + func: Box, + left: Box, + right: Box, + }, } /// Qualified identifier (potentially with module prefix) -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct QualifiedIdent { pub module: Option, pub name: Ident, } +pub fn qualified_ident(module: &str, name: &str) -> QualifiedIdent { + QualifiedIdent { + module: Some(crate::interner::intern(module)), + name: crate::interner::intern(name), + } +} + +pub fn prim_ident(name: &str) -> QualifiedIdent { + qualified_ident("Prim", name) +} + +pub fn unqualified_ident(name: &str) -> QualifiedIdent { + QualifiedIdent { + module: None, + name: crate::interner::intern(name), + } +} + +impl Display for QualifiedIdent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(module) = &self.module { + write!( + f, + "{}.{}", + crate::interner::resolve(*module).unwrap_or_default(), + crate::interner::resolve(self.name).unwrap_or_default() + ) + } else { + write!( + f, + "{}", + crate::interner::resolve(self.name).unwrap_or_default() + ) + } + } +} + /// Literal values #[derive(Debug, Clone, PartialEq)] pub enum Literal { @@ -442,21 +484,13 @@ pub enum Literal { #[derive(Debug, Clone, PartialEq)] pub enum Binder { /// Wildcard: _ - Wildcard { - span: Span, - }, + Wildcard { span: Span }, /// Variable: x - Var { - span: Span, - name: Spanned, - }, + Var { span: Span, name: Spanned }, /// Literal pattern: 42, "foo" - Literal { - span: Span, - lit: Literal, - }, + Literal { span: Span, lit: Literal }, /// Constructor pattern: Just x Constructor { @@ -479,16 +513,10 @@ pub enum Binder { }, /// Parenthesized pattern - Parens { - span: Span, - binder: Box, - }, + Parens { span: Span, binder: Box }, /// Array pattern: [a, b, c] - Array { - span: Span, - elements: Vec, - }, + Array { span: Span, elements: Vec }, /// Operator pattern: a /\ b, x :| xs Op { @@ -549,10 +577,7 @@ pub enum DoStatement { }, /// Expression statement: action - Discard { - span: Span, - expr: Expr, - }, + Discard { span: Span, expr: Expr }, } /// Record field in literal @@ -587,16 +612,10 @@ pub struct RecordBinderField { #[derive(Debug, Clone, PartialEq)] pub enum TypeExpr { /// Type variable: a - Var { - span: Span, - name: Spanned, - }, + Var { span: Span, name: Spanned }, /// Type constructor: Int, Array - Constructor { - span: Span, - name: QualifiedIdent, - }, + Constructor { span: Span, name: QualifiedIdent }, /// Type application: Array Int App { @@ -628,10 +647,7 @@ pub enum TypeExpr { }, /// Record type: { x :: Int, y :: String } - Record { - span: Span, - fields: Vec, - }, + Record { span: Span, fields: Vec }, /// Row type: (), (a :: String), ( x :: Int | r ) /// `is_record` is true when this came from `{ ... | r }` syntax (a record type), @@ -644,21 +660,13 @@ pub enum TypeExpr { }, /// Parenthesized type - Parens { - span: Span, - ty: Box, - }, + Parens { span: Span, ty: Box }, /// Type hole: ?hole - Hole { - span: Span, - name: Ident, - }, + Hole { span: Span, name: Ident }, /// Wildcard type: _ - Wildcard { - span: Span, - }, + Wildcard { span: Span }, /// Type-level operator application: a ~> b TypeOp { @@ -676,17 +684,10 @@ pub enum TypeExpr { }, /// Type-level string literal: "hello" - StringLiteral { - span: Span, - value: String, - }, + StringLiteral { span: Span, value: String }, /// Type-level integer literal: 42 - IntLiteral { - span: Span, - value: i64, - }, - + IntLiteral { span: Span, value: i64 }, } /// Type constraint (for type classes) @@ -720,7 +721,9 @@ pub fn type_to_constraint(ty: TypeExpr, span: Span) -> Constraint { let mut current = ty; loop { match current { - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { args.push(*arg); current = *constructor; } @@ -739,7 +742,10 @@ pub fn type_to_constraint(ty: TypeExpr, span: Span) -> Constraint { args.reverse(); return Constraint { span, - class: QualifiedIdent { module: None, name: crate::interner::intern("Unknown") }, + class: QualifiedIdent { + module: None, + name: crate::interner::intern("Unknown"), + }, args: { let mut all = vec![other]; all.extend(args); @@ -757,26 +763,27 @@ pub fn type_to_constraint(ty: TypeExpr, span: Span) -> Constraint { /// Returns an error if the expression cannot be represented as a valid binder. pub fn expr_to_binder(expr: Expr) -> Result { match expr { - Expr::Var { span, name } => { - Ok(Binder::Var { - span, - name: Spanned::new(name.name, span), - }) - } - Expr::Constructor { span, name } => { - Ok(Binder::Constructor { span, name, args: vec![] }) - } + Expr::Var { span, name } => Ok(Binder::Var { + span, + name: Spanned::new(name.name, span), + }), + Expr::Constructor { span, name } => Ok(Binder::Constructor { + span, + name, + args: vec![], + }), Expr::Hole { span, name } => { let resolved = crate::interner::resolve(name).unwrap_or_default(); if resolved == "_" { Ok(Binder::Wildcard { span }) } else { - Ok(Binder::Var { span, name: Spanned::new(name, span) }) + Ok(Binder::Var { + span, + name: Spanned::new(name, span), + }) } } - Expr::Literal { span, lit } => { - Ok(Binder::Literal { span, lit }) - } + Expr::Literal { span, lit } => Ok(Binder::Literal { span, lit }), Expr::App { span, func, arg } => { let arg_binder = expr_to_binder(*arg)?; match expr_to_binder(*func)? { @@ -784,50 +791,70 @@ pub fn expr_to_binder(expr: Expr) -> Result { args.push(arg_binder); Ok(Binder::Constructor { span, name, args }) } - _ => { - Err(format!("expected constructor application in binder")) - } + _ => Err(format!("expected constructor application in binder")), } } - Expr::Parens { span, expr } => { - Ok(Binder::Parens { span, binder: Box::new(expr_to_binder(*expr)?) }) - } - Expr::Op { span, left, op, right } => { - Ok(Binder::Op { - span, - left: Box::new(expr_to_binder(*left)?), - op, - right: Box::new(expr_to_binder(*right)?), - }) - } + Expr::Parens { span, expr } => Ok(Binder::Parens { + span, + binder: Box::new(expr_to_binder(*expr)?), + }), + Expr::Op { + span, + left, + op, + right, + } => Ok(Binder::Op { + span, + left: Box::new(expr_to_binder(*left)?), + op, + right: Box::new(expr_to_binder(*right)?), + }), Expr::Record { span, fields } => { - let binder_fields: Result, String> = fields.into_iter().map(|f| { - let binder = f.value.map(expr_to_binder).transpose()?; - Ok(RecordBinderField { - span: f.span, - label: f.label, - binder, + let binder_fields: Result, String> = fields + .into_iter() + .map(|f| { + let binder = f.value.map(expr_to_binder).transpose()?; + Ok(RecordBinderField { + span: f.span, + label: f.label, + binder, + }) }) - }).collect(); - Ok(Binder::Record { span, fields: binder_fields? }) + .collect(); + Ok(Binder::Record { + span, + fields: binder_fields?, + }) } Expr::Array { span, elements } => { - let binders: Result, String> = elements.into_iter().map(expr_to_binder).collect(); - Ok(Binder::Array { span, elements: binders? }) + let binders: Result, String> = + elements.into_iter().map(expr_to_binder).collect(); + Ok(Binder::Array { + span, + elements: binders?, + }) } Expr::TypeAnnotation { span, expr, ty } => { let inner = expr_to_binder(*expr)?; - Ok(Binder::Typed { span, binder: Box::new(inner), ty }) - } - Expr::Negate { expr, .. } => { - match expr_to_binder(*expr)? { - Binder::Literal { span, lit } => Ok(Binder::Literal { span, lit }), - _ => Err(format!("negation in binder must be applied to a literal")), - } + Ok(Binder::Typed { + span, + binder: Box::new(inner), + ty, + }) } - Expr::AsPattern { span, name, pattern } => { + Expr::Negate { expr, .. } => match expr_to_binder(*expr)? { + Binder::Literal { span, lit } => Ok(Binder::Literal { span, lit }), + _ => Err(format!("negation in binder must be applied to a literal")), + }, + Expr::AsPattern { + span, + name, + pattern, + } => { let name_ident = match *name { - Expr::Var { name: qi, span: ns, .. } => Spanned::new(qi.name, ns), + Expr::Var { + name: qi, span: ns, .. + } => Spanned::new(qi.name, ns), _ => return Err(format!("expected variable name in as-pattern")), }; Ok(Binder::As { @@ -838,7 +865,9 @@ pub fn expr_to_binder(expr: Expr) -> Result { } Expr::VisibleTypeApp { span, func, ty } => { let name_ident = match *func { - Expr::Var { name: qi, span: ns, .. } => Spanned::new(qi.name, ns), + Expr::Var { + name: qi, span: ns, .. + } => Spanned::new(qi.name, ns), _ => return Err(format!("expected variable name in as-pattern")), }; Ok(Binder::As { @@ -847,9 +876,8 @@ pub fn expr_to_binder(expr: Expr) -> Result { binder: Box::new(type_to_binder(ty)?), }) } - _other => { - Err(format!("expression cannot be used as a binder")) - } + Expr::Wildcard { span } => Ok(Binder::Wildcard { span }), + _other => Err(format!("expression cannot be used as a binder")), } } @@ -859,18 +887,22 @@ pub fn expr_to_binder(expr: Expr) -> Result { /// the type to be converted back to a binder. pub fn type_to_binder(ty: TypeExpr) -> Result { match ty { - TypeExpr::Var { span, name } => { - Ok(Binder::Var { span, name }) - } - TypeExpr::Constructor { span, name } => { - Ok(Binder::Constructor { span, name, args: vec![] }) - } - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::Var { span, name } => Ok(Binder::Var { span, name }), + TypeExpr::Constructor { span, name } => Ok(Binder::Constructor { + span, + name, + args: vec![], + }), + TypeExpr::App { + constructor, arg, .. + } => { let mut args = vec![type_to_binder(*arg)?]; let mut current = *constructor; loop { match current { - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { args.push(type_to_binder(*arg)?); current = *constructor; } @@ -882,38 +914,41 @@ pub fn type_to_binder(ty: TypeExpr) -> Result { } } } - TypeExpr::Parens { ty, .. } => { - type_to_binder(*ty) - } + TypeExpr::Parens { ty, .. } => type_to_binder(*ty), TypeExpr::Record { span, fields } => { - let binder_fields: Result, String> = fields.into_iter().map(|f| { - let binder = if matches!(f.ty, TypeExpr::Wildcard { .. }) { - None - } else { - Some(type_to_binder(f.ty)?) - }; - Ok(RecordBinderField { - span: f.span, - label: f.label, - binder, + let binder_fields: Result, String> = fields + .into_iter() + .map(|f| { + let binder = if matches!(f.ty, TypeExpr::Wildcard { .. }) { + None + } else { + Some(type_to_binder(f.ty)?) + }; + Ok(RecordBinderField { + span: f.span, + label: f.label, + binder, + }) }) - }).collect(); - Ok(Binder::Record { span, fields: binder_fields? }) - } - TypeExpr::Wildcard { span } => { - Ok(Binder::Wildcard { span }) - } - TypeExpr::TypeOp { span, left, op, right } => { - Ok(Binder::Op { + .collect(); + Ok(Binder::Record { span, - left: Box::new(type_to_binder(*left)?), - op, - right: Box::new(type_to_binder(*right)?), + fields: binder_fields?, }) } - _ => { - Err(format!("type expression cannot be used as a binder")) - } + TypeExpr::Wildcard { span } => Ok(Binder::Wildcard { span }), + TypeExpr::TypeOp { + span, + left, + op, + right, + } => Ok(Binder::Op { + span, + left: Box::new(type_to_binder(*left)?), + op, + right: Box::new(type_to_binder(*right)?), + }), + _ => Err(format!("type expression cannot be used as a binder")), } } @@ -930,7 +965,24 @@ impl Spanned { } } -// Convenience constructors for common patterns +impl Decl { + pub fn span(&self) -> Span { + match self { + Decl::Value { span, .. } + | Decl::TypeSignature { span, .. } + | Decl::Data { span, .. } + | Decl::TypeAlias { span, .. } + | Decl::Newtype { span, .. } + | Decl::Class { span, .. } + | Decl::Instance { span, .. } + | Decl::Fixity { span, .. } + | Decl::Foreign { span, .. } + | Decl::ForeignData { span, .. } + | Decl::Derive { span, .. } => *span, + } + } +} + impl Expr { pub fn span(&self) -> Span { match self { @@ -952,10 +1004,12 @@ impl Expr { | Expr::Parens { span, .. } | Expr::TypeAnnotation { span, .. } | Expr::Hole { span, .. } + | Expr::Wildcard { span, .. } | Expr::Array { span, .. } | Expr::Negate { span, .. } | Expr::AsPattern { span, .. } - | Expr::VisibleTypeApp { span, .. } => *span, + | Expr::VisibleTypeApp { span, .. } + | Expr::BacktickApp { span, .. } => *span, } } } diff --git a/src/diagnostics/error.rs b/src/diagnostics/error.rs index aab01ac3..bb422dd1 100644 --- a/src/diagnostics/error.rs +++ b/src/diagnostics/error.rs @@ -2,7 +2,7 @@ use lalrpop_util::ParseError; use thiserror::Error; -use crate::{Token, ast::Span, lexer::logos_lexer::LexError}; +use crate::{Token, span::Span, lexer::logos_lexer::LexError}; /// Parse errors #[derive(Debug, Error)] diff --git a/src/lexer/layout.rs b/src/lexer/layout.rs index 17ebe618..7f396ae1 100644 --- a/src/lexer/layout.rs +++ b/src/lexer/layout.rs @@ -1,4 +1,4 @@ -use crate::ast::span::Span; +use crate::span::Span; use crate::lexer::SpannedToken; use crate::lexer::token::Token; use crate::lexer::logos_lexer::{RawToken}; @@ -305,11 +305,23 @@ pub fn process_layout(raw_tokens: Vec<(RawToken, Span)>, source: &str) -> Vec expr \n >>> f` = `(case _ of P -> expr) >>> f` + // Exception: if the previous token was an operator, the current token + // is a continuation of the expression (e.g. `A -> a >>>\n b`). + let is_operator_token = matches!(token, Token::Operator(_) | Token::QualifiedOperator(_, _) | Token::Backtick); + if matches!(delim, LayoutDelim::LytOf) && is_operator_token && !last_was_operator { + result.push((Token::RBrace, dummy_span)); + stack.pop(); + // Continue loop to check enclosing blocks + continue; + } // Suppress semicolons in specific contexts: // - `then` when there's a pending `if` (if-then-else continuation) // - `else` when there's a pending `then` (if-then-else continuation) // - any token after else (for "else instance" chains) - // - operators at reference column are continuation lines + // - operators at reference column are continuation lines (non-case blocks) // - -> in case-of blocks (arrow on next line after binder) // - | in case-of blocks (guards at same column as binder) let suppress = (matches!(token, Token::Else) && !then_depths.is_empty()) @@ -317,7 +329,7 @@ pub fn process_layout(raw_tokens: Vec<(RawToken, Span)>, source: &str) -> Vec Result>, LexError> { // Step 2: Layout processing let tokens = process_layout(raw_tokens, source); - // Step 3: Resolve qualified names (merge adjacent UpperIdent.Ident sequences) + // Step 3: Merge adjacent Tilde + Operator tokens (e.g., ~ > → ~>) + // Logos lexes ~ with higher priority than operators, so ~> becomes two tokens. + let tokens = merge_tilde_operators(tokens); + + // Step 4: Resolve qualified names (merge adjacent UpperIdent.Ident sequences) let tokens = resolve_qualified_names(tokens); - // Step 4: Convert to spanned tokens + // Step 5: Convert to spanned tokens Ok(tokens .into_iter() .map(|(tok, span)| Spanned::new(tok, span)) .collect()) } +/// Merge adjacent Tilde tokens with following Operator/Tilde tokens into a single Operator. +/// Logos lexes `~` with priority 2 (above operators at priority 1), so `~>` becomes +/// `Tilde` + `Operator(">")`. This step merges them back into `Operator("~>")`. +fn merge_tilde_operators(tokens: Vec) -> Vec { + let mut result: Vec = Vec::with_capacity(tokens.len()); + let mut i = 0; + + while i < tokens.len() { + if matches!(&tokens[i].0, Token::Tilde) { + let start_span = tokens[i].1; + let mut merged = String::from("~"); + let mut end_span = start_span; + let mut j = i + 1; + + // Consume adjacent Tilde and Operator tokens + while j < tokens.len() && end_span.end == tokens[j].1.start { + match &tokens[j].0 { + Token::Tilde => { + merged.push('~'); + end_span = tokens[j].1; + j += 1; + } + Token::Operator(op) => { + let op_str = interner::resolve(*op).unwrap_or_default(); + merged.push_str(&op_str); + end_span = tokens[j].1; + j += 1; + break; // Operator already consumed remaining chars + } + _ => break, + } + } + + if j > i + 1 { + // Merged: produce a single Operator token + eprintln!("[TILDE_MERGE] Merged {} tokens into Operator(\"{}\")", j - i, merged); + let sym = interner::intern(&merged); + let span = Span::new(start_span.start, end_span.end); + result.push((Token::Operator(sym), span)); + } else { + // Standalone Tilde, keep as-is + result.push(tokens[i].clone()); + } + i = j; + } else { + result.push(tokens[i].clone()); + i += 1; + } + } + + result +} + /// Resolve qualified names by merging adjacent token sequences: /// - UpperIdent.LowerIdent → QualifiedLower /// - UpperIdent.UpperIdent → QualifiedUpper (chained for multi-segment) @@ -89,6 +146,23 @@ fn resolve_qualified_names(tokens: Vec) -> Vec { resolved = true; break; } + // Contextual keywords used as identifiers after module qualifier + Token::As | Token::Hiding => { + let kw_str = match &tokens[j + 1].0 { + Token::As => "as", + Token::Hiding => "hiding", + _ => unreachable!(), + }; + let name = interner::intern(kw_str); + let module_str = module_parts_to_string(&module_parts); + let module_sym = interner::intern(&module_str); + let end_span = tokens[j + 1].1; + let span = Span::new(start_span.start, end_span.end); + result.push((Token::QualifiedLower(module_sym, name), span)); + i = j + 2; + resolved = true; + break; + } _ => break, } } diff --git a/src/lib.rs b/src/lib.rs index 3bca12e5..8afaed6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,10 @@ //! 2. Layout processor for handling indentation-sensitive syntax //! 3. LALRPOP-based parser with declarative grammar +pub mod span; pub mod lexer; -pub mod ast; pub mod cst; +pub mod ast; pub mod parser; pub mod arena; pub mod interner; diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index d66a592f..9adf727b 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -3,7 +3,7 @@ use crate::cst::*; use crate::lexer::Token; -use crate::ast::span::Span; +use crate::span::Span; use crate::lexer::token::Ident; grammar; @@ -286,14 +286,11 @@ GuardExpr: Expr = { } } _ => { - Expr::App { + Expr::BacktickApp { span: Span::new(start, end), - func: Box::new(Expr::App { - span: Span::new(start, end), - func: Box::new(func), - arg: Box::new(left), - }), - arg: Box::new(right), + func: Box::new(func), + left: Box::new(left), + right: Box::new(right), } } } @@ -869,15 +866,13 @@ OperatorExpr: Expr = { } } _ => { - // Complex backtick expression (rare): fall back to App - Expr::App { + // Complex backtick expression (rare): produce BacktickApp so it + // participates in operator chain flattening and shunting-yard rebalancing. + Expr::BacktickApp { span: Span::new(start, end), - func: Box::new(Expr::App { - span: Span::new(start, end), - func: Box::new(func), - arg: Box::new(left), - }), - arg: Box::new(right), + func: Box::new(func), + left: Box::new(left), + right: Box::new(right), } } } @@ -1216,9 +1211,8 @@ AtomicExpr: Expr = { }, // Underscore section: _ in expression position "_" => { - Expr::Hole { - span: Span::new(start, end), - name: crate::interner::intern("_"), + Expr::Wildcard { + span: Span::new(start, end) } }, }; @@ -1451,24 +1445,33 @@ LetBinding: LetBinding = { }, // Guarded binding: f binders | cond = expr | cond2 = expr2 // Per-guard where clauses are handled inside each Guard. + // Desugars to: f = \binders -> case true of _ | guards -> body => { - // For guarded bindings, desugar to a simple value for now - let first_guard = &guards[0]; + let sp = Span::new(start, end); + // Build case alternatives from guards — each guard becomes a case alt with a wildcard binder + let alts: Vec = guards.into_iter().map(|g| { + CaseAlternative { + span: g.span, + binders: vec![Binder::Wildcard { span: sp }], + result: GuardedExpr::Guarded(vec![g]), + } + }).collect(); + let case_expr = Expr::Case { + span: sp, + exprs: vec![Expr::Literal { span: sp, lit: Literal::Boolean(true) }], + alts, + }; let final_expr = if extra_binders.is_empty() { - first_guard.expr.as_ref().clone() + case_expr } else { - let name = match &binder { - Binder::Var { name, .. } => name.clone(), - _ => Spanned::new(crate::interner::intern("_"), Span::new(start, end)), - }; Expr::Lambda { - span: Span::new(start, end), + span: sp, binders: extra_binders, - body: first_guard.expr.clone(), + body: Box::new(case_expr), } }; LetBinding::Value { - span: Span::new(start, end), + span: sp, binder, expr: final_expr, } diff --git a/src/parser/lexer_adapter.rs b/src/parser/lexer_adapter.rs index 2a39562e..20ba5c71 100644 --- a/src/parser/lexer_adapter.rs +++ b/src/parser/lexer_adapter.rs @@ -1,4 +1,4 @@ -use crate::ast::span::Spanned; +use crate::span::Spanned; use crate::lexer::Token; /// Adapter to convert our token stream to LALRPOP's expected format diff --git a/src/ast/span.rs b/src/span.rs similarity index 97% rename from src/ast/span.rs rename to src/span.rs index 46836648..bddc2c44 100644 --- a/src/ast/span.rs +++ b/src/span.rs @@ -10,7 +10,7 @@ pub struct SourcePos { } /// Represents a span in the source code -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Span { /// Start byte offset pub start: usize, diff --git a/src/typechecker/check.rs b/src/typechecker/check.rs index 1579d7e1..c100ebac 100644 --- a/src/typechecker/check.rs +++ b/src/typechecker/check.rs @@ -1,7 +1,14 @@ use std::collections::{HashMap, HashSet}; -use crate::ast::span::Span; -use crate::cst::{Associativity, Binder, DataMembers, Decl, Export, Import, ImportList, KindSigSource, Module, Spanned, TypeExpr}; +use crate::span::Span; +use crate::ast::{ + Binder, Decl, Module, TypeExpr, +}; +use crate::cst::{ + unqualified_ident, qualified_ident, Associativity, DataMembers, + Export, Import, ImportList, KindSigSource, ModuleName, QualifiedIdent, Spanned, +}; +use crate::interner::intern; use crate::interner::Symbol; use crate::typechecker::convert::convert_type_expr; use crate::typechecker::env::Env; @@ -10,8 +17,30 @@ use crate::typechecker::infer::{ check_exhaustiveness, extract_type_con, is_refutable, is_unconditional_for_exhaustiveness, unwrap_binder, InferCtx, }; +use crate::typechecker::registry::{ModuleExports, ModuleRegistry}; use crate::typechecker::types::{Role, Scheme, Type}; +/// Wrap a bare Symbol as an unqualified QualifiedIdent. Only for local identifier, not for imports +#[inline] +fn qi(sym: Symbol) -> QualifiedIdent { + QualifiedIdent { + module: None, + name: sym, + } +} + +#[inline] +fn imported_qi(module: &str, name: Symbol) -> QualifiedIdent { + QualifiedIdent { + module: Some(intern(module)), + name, + } +} + +fn prim_qi(name: Symbol) -> QualifiedIdent { + imported_qi("Prim", name) +} + /// Check for duplicate type arguments in a list of type variables. /// Returns an error if any name appears more than once. fn check_duplicate_type_args(type_vars: &[Spanned], errors: &mut Vec) { @@ -45,26 +74,26 @@ fn check_overlapping_arg_names(decl_span: Span, binders: &[Binder], errors: &mut } /// Collect type constructor references from a CST TypeExpr. -fn collect_type_refs(ty: &crate::cst::TypeExpr, refs: &mut HashSet) { +fn collect_type_refs(ty: &crate::ast::TypeExpr, refs: &mut HashSet) { match ty { - crate::cst::TypeExpr::Constructor { name, .. } => { + crate::ast::TypeExpr::Constructor { name, .. } => { // Only track unqualified references as local alias dependencies. // Qualified refs (e.g. P.Number) point to external modules, not local aliases. if name.module.is_none() { refs.insert(name.name); } } - crate::cst::TypeExpr::App { + crate::ast::TypeExpr::App { constructor, arg, .. } => { collect_type_refs(constructor, refs); collect_type_refs(arg, refs); } - crate::cst::TypeExpr::Function { from, to, .. } => { + crate::ast::TypeExpr::Function { from, to, .. } => { collect_type_refs(from, refs); collect_type_refs(to, refs); } - crate::cst::TypeExpr::Forall { vars, ty, .. } => { + crate::ast::TypeExpr::Forall { vars, ty, .. } => { for (_v, _visible, kind) in vars { if let Some(kind_expr) = kind { collect_type_refs(kind_expr, refs); @@ -72,7 +101,7 @@ fn collect_type_refs(ty: &crate::cst::TypeExpr, refs: &mut HashSet) { } collect_type_refs(ty, refs); } - crate::cst::TypeExpr::Constrained { + crate::ast::TypeExpr::Constrained { constraints, ty, .. } => { for constraint in constraints { @@ -82,23 +111,16 @@ fn collect_type_refs(ty: &crate::cst::TypeExpr, refs: &mut HashSet) { } collect_type_refs(ty, refs); } - crate::cst::TypeExpr::Parens { ty, .. } => { - collect_type_refs(ty, refs); - } - crate::cst::TypeExpr::Kinded { ty, kind, .. } => { + crate::ast::TypeExpr::Kinded { ty, kind, .. } => { collect_type_refs(ty, refs); collect_type_refs(kind, refs); } - crate::cst::TypeExpr::TypeOp { left, right, .. } => { - collect_type_refs(left, refs); - collect_type_refs(right, refs); - } - crate::cst::TypeExpr::Record { fields, .. } => { + crate::ast::TypeExpr::Record { fields, .. } => { for field in fields { collect_type_refs(&field.ty, refs); } } - crate::cst::TypeExpr::Row { fields, tail, .. } => { + crate::ast::TypeExpr::Row { fields, tail, .. } => { for field in fields { collect_type_refs(&field.ty, refs); } @@ -112,7 +134,11 @@ fn collect_type_refs(ty: &crate::cst::TypeExpr, refs: &mut HashSet) { /// Check that all type variables in a TypeExpr are bound. /// Reports UndefinedTypeVariable for any free type variables not in `bound`. -pub(crate) fn collect_type_expr_vars(ty: &TypeExpr, bound: &HashSet, errors: &mut Vec) { +pub(crate) fn collect_type_expr_vars( + ty: &TypeExpr, + bound: &HashSet, + errors: &mut Vec, +) { match ty { TypeExpr::Var { span, name } => { if !bound.contains(&name.value) { @@ -122,7 +148,9 @@ pub(crate) fn collect_type_expr_vars(ty: &TypeExpr, bound: &HashSet, err }); } } - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { collect_type_expr_vars(constructor, bound, errors); collect_type_expr_vars(arg, bound, errors); } @@ -142,7 +170,9 @@ pub(crate) fn collect_type_expr_vars(ty: &TypeExpr, bound: &HashSet, err } collect_type_expr_vars(ty, &inner_bound, errors); } - TypeExpr::Constrained { constraints, ty, .. } => { + TypeExpr::Constrained { + constraints, ty, .. + } => { for c in constraints { for arg in &c.args { collect_type_expr_vars(arg, bound, errors); @@ -150,9 +180,6 @@ pub(crate) fn collect_type_expr_vars(ty: &TypeExpr, bound: &HashSet, err } collect_type_expr_vars(ty, bound, errors); } - TypeExpr::Parens { ty, .. } => { - collect_type_expr_vars(ty, bound, errors); - } TypeExpr::Record { fields, .. } => { for field in fields { collect_type_expr_vars(&field.ty, bound, errors); @@ -170,24 +197,19 @@ pub(crate) fn collect_type_expr_vars(ty: &TypeExpr, bound: &HashSet, err collect_type_expr_vars(ty, bound, errors); collect_type_expr_vars(kind, bound, errors); } - TypeExpr::TypeOp { left, right, .. } => { - collect_type_expr_vars(left, bound, errors); - collect_type_expr_vars(right, bound, errors); - } _ => {} // Constructor, Wildcard, Hole, StringLiteral, IntLiteral } } /// Check if a CST TypeExpr contains `forall` or wildcards (invalid in constraint args). /// Returns the span of the first invalid node found. -fn has_forall_or_wildcard(ty: &TypeExpr) -> Option { +fn has_forall_or_wildcard(ty: &TypeExpr) -> Option { match ty { TypeExpr::Forall { span, .. } => Some(*span), TypeExpr::Wildcard { span, .. } => Some(*span), - TypeExpr::App { constructor, arg, .. } => { - has_forall_or_wildcard(constructor).or_else(|| has_forall_or_wildcard(arg)) - } - TypeExpr::Parens { ty, .. } => has_forall_or_wildcard(ty), + TypeExpr::App { + constructor, arg, .. + } => has_forall_or_wildcard(constructor).or_else(|| has_forall_or_wildcard(arg)), TypeExpr::Function { from, to, .. } => { has_forall_or_wildcard(from).or_else(|| has_forall_or_wildcard(to)) } @@ -199,11 +221,12 @@ fn has_forall_or_wildcard(ty: &TypeExpr) -> Option { fn has_invalid_instance_head_type_expr(ty: &TypeExpr) -> bool { match ty { TypeExpr::Wildcard { .. } | TypeExpr::Hole { .. } => true, - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { has_invalid_instance_head_type_expr(constructor) || has_invalid_instance_head_type_expr(arg) } - TypeExpr::Parens { ty, .. } => has_invalid_instance_head_type_expr(ty), _ => false, } } @@ -212,38 +235,49 @@ fn has_invalid_instance_head_type_expr(ty: &TypeExpr) -> bool { /// Emits UnknownClass for unqualified constraints referencing undefined classes. fn check_constraint_class_names( ty: &TypeExpr, - known_classes: &HashSet, - class_param_counts: &HashMap, + known_classes: &HashSet, + class_param_counts: &HashMap, errors: &mut Vec, ) { match ty { - TypeExpr::Constrained { constraints, ty, .. } => { + TypeExpr::Constrained { + constraints, ty, .. + } => { for constraint in constraints { if constraint.class.module.is_none() - && !known_classes.contains(&constraint.class.name) + && !known_classes.contains(&constraint.class) { errors.push(TypeError::UnknownClass { span: constraint.span, - name: constraint.class.name, + name: constraint.class, }); } // Check constraint arity: the number of type args must match // the class param count. E.g. `Foo a` when `class Foo a b` is an error. // Skip ambiguous classes (usize::MAX = multiple imports with different arities). - if let Some(&expected) = class_param_counts.get(&constraint.class.name) { + if let Some(&expected) = class_param_counts.get(&constraint.class) { if expected != usize::MAX && constraint.args.len() != expected { + // PureScript reports constraint arity mismatches as KindsDoNotUnify + // because class Foo a b has kind Type -> Type -> Constraint, + // and Foo a would have kind Type -> Constraint (not Constraint). + let mut found_kind = Type::kind_constraint(); + for _ in 0..expected.saturating_sub(constraint.args.len()) { + found_kind = Type::Fun( + Box::new(Type::kind_type()), + Box::new(found_kind), + ); + } errors.push(TypeError::KindsDoNotUnify { span: constraint.span, - name: constraint.class.name, - expected, - found: constraint.args.len(), + expected: Type::kind_constraint(), + found: found_kind, }); } } } check_constraint_class_names(ty, known_classes, class_param_counts, errors); } - TypeExpr::Forall { ty, .. } | TypeExpr::Parens { ty, .. } => { + TypeExpr::Forall { ty, .. } => { check_constraint_class_names(ty, known_classes, class_param_counts, errors); } TypeExpr::Function { from, to, .. } => { @@ -256,7 +290,10 @@ fn check_constraint_class_names( /// Check if a type used in an instance head is a type synonym that expands to /// a non-nominal type (record, function). Synonyms expanding to data types are fine. -fn is_non_nominal_instance_head(ty: &Type, type_aliases: &HashMap, Type)>) -> bool { +fn is_non_nominal_instance_head( + ty: &Type, + type_aliases: &HashMap, Type)>, +) -> bool { if !has_synonym_head(ty, type_aliases) { return false; } @@ -271,6 +308,22 @@ fn is_non_nominal_instance_head(ty: &Type, type_aliases: &HashMap b`). +fn is_non_nominal_instance_head_record_only( + ty: &Type, + type_aliases: &HashMap, Type)>, +) -> bool { + if !has_synonym_head(ty, type_aliases) { + return false; + } + let expanded = expand_type_aliases_limited(ty, type_aliases, 0); + match &expanded { + Type::Record(_, Some(_)) => true, + _ => false, + } +} + /// Check if a type contains a record with an open row variable tail. /// E.g., `{ | r }` where `r` is a type variable. fn has_open_record_row(ty: &Type) -> bool { @@ -284,85 +337,416 @@ fn has_open_record_row(ty: &Type) -> bool { } /// Check if a type is non-nominal for derive instance heads. -/// Derive requires a data/newtype constructor — records, functions, and -/// type synonyms expanding to them are all invalid. -fn is_non_nominal_for_derive(ty: &Type, type_aliases: &HashMap, Type)>) -> bool { - if matches!(ty, Type::Record(..) | Type::Fun(..)) { - return true; +/// For plain `derive instance`: records, functions, and synonyms expanding to them +/// are all invalid — derive requires a data/newtype constructor. +/// For `derive newtype instance` (is_newtype=true): only open records (with row +/// variables) are invalid. Closed records and functions are allowed as class +/// parameters (e.g. `derive newtype instance MonadState St (ForEach m a b)` +/// where `St` is a closed record alias). +fn is_non_nominal_for_derive( + ty: &Type, + type_aliases: &HashMap, Type)>, + data_constructors: &HashMap>, + is_newtype: bool, +) -> bool { + if is_newtype { + // derive newtype: only reject open records + if matches!(ty, Type::Record(_, Some(_))) { + return true; + } + } else { + // plain derive: reject any record or function + if matches!(ty, Type::Record(..) | Type::Fun(..)) { + return true; + } + } + // Expand type aliases and check expanded forms + // But skip expansion if the name also exists as a data type (name collision + // from module qualifier stripping — e.g. `Mutex` newtype vs imported alias). + if has_synonym_head(ty, type_aliases) { + let is_also_data_type = match ty { + Type::Con(qi) => data_constructors.contains_key(qi), + Type::App(f, _) => match f.as_ref() { + Type::Con(qi) => data_constructors.contains_key(qi), + _ => false, + }, + _ => false, + }; + if !is_also_data_type { + let expanded = expand_type_aliases_limited(ty, type_aliases, 0); + if is_newtype { + if matches!(&expanded, Type::Record(_, Some(_))) { + return true; + } + } else { + if matches!(&expanded, Type::Record(..) | Type::Fun(..)) { + return true; + } + } + } + } + if is_newtype { + is_non_nominal_instance_head_record_only(ty, type_aliases) + } else { + is_non_nominal_instance_head(ty, type_aliases) } - is_non_nominal_instance_head(ty, type_aliases) } /// Check if the outermost constructor of a type is a known type synonym. fn has_synonym_head(ty: &Type, type_aliases: &HashMap, Type)>) -> bool { match ty { - Type::Con(name) => type_aliases.contains_key(name), + Type::Con(name) => type_aliases.contains_key(&name.name), Type::App(f, _) => has_synonym_head(f, type_aliases), _ => false, } } /// Expand type aliases with a depth limit to prevent stack overflow. -fn expand_type_aliases_limited(ty: &Type, type_aliases: &HashMap, Type)>, depth: u32) -> Type { - if depth > 50 || type_aliases.is_empty() { +/// Uses exact arity matching (args == params) for safety. +pub fn expand_type_aliases_limited( + ty: &Type, + type_aliases: &HashMap, Type)>, + depth: u32, +) -> Type { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner(ty, type_aliases, None, depth, &mut expanding) +} + +/// Expand type aliases with over-saturation support and data-type disambiguation. +/// Look up type constructor arity, falling back to unqualified or name-only match. +/// Needed because alias bodies contain unqualified type references, but +/// type_con_arities stores entries under qualified import keys. +fn lookup_type_con_arity( + arities: &HashMap, + name: &QualifiedIdent, +) -> Option { + // Always return the MAXIMUM arity found across all entries with matching .name. + // This handles the case where an alias body (from another module) contains an + // unqualified Con that refers to a type with arity N, but the consuming module + // also has a local definition with the same name but lower arity + // (e.g. `Data.Options.Options` arity 1 vs local `data Options` arity 0). + // Using the max prevents spurious KindArityMismatch errors when the alias + // body's Con is checked in a context with a lower-arity local definition. + // + // For qualified names (e.g. `Opt.Options`), first try exact match, then fall + // back to unqualified; since there's an exact key we don't need the max trick. + if name.module.is_some() { + arities.get(name).copied() + .or_else(|| arities.get(&QualifiedIdent { module: None, name: name.name }).copied()) + } else { + // Unqualified: return max arity across all entries with matching .name + // (both qualified and unqualified entries in the map). + arities.iter() + .filter(|(k, _)| k.name == name.name) + .map(|(_, &v)| v) + .max() + } +} + +/// Uses `>=` matching: when args > params, extra args are applied to the expanded result. +/// The `type_con_arities` map prevents incorrect expansion when a name is both an alias +/// and a data type (due to module qualifier stripping): if arg count exceeds alias params +/// but fits the data type arity, the alias expansion is skipped. +fn expand_type_aliases_limited_with_arities( + ty: &Type, + type_aliases: &HashMap, Type)>, + type_con_arities: &HashMap, + depth: u32, +) -> Type { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner( + ty, + type_aliases, + Some(type_con_arities), + depth, + &mut expanding, + ) +} + +/// Check if a type contains a Type::Con with the given QualifiedIdent name. +/// Used to determine if an alias expansion result is self-referential. +fn type_contains_con_name(ty: &Type, name: &QualifiedIdent) -> bool { + match ty { + Type::Con(n) => n.name == name.name, + Type::App(f, a) => type_contains_con_name(f, name) || type_contains_con_name(a, name), + Type::Fun(a, b) => type_contains_con_name(a, name) || type_contains_con_name(b, name), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| type_contains_con_name(t, name)) + || tail.as_ref().map_or(false, |t| type_contains_con_name(t, name)) + } + Type::Forall(_, body) => type_contains_con_name(body, name), + _ => false, + } +} + +/// Inner expansion function. +/// When `type_con_arities` is `Some`, over-saturated aliases (args > params) are expanded +/// with extra args applied to the result, but expansion is skipped when the name also +/// exists as a data type with a matching arity (alias/data-type name collision). +/// When `None`, only exact arity matches (args == params) are expanded. +fn expand_type_aliases_limited_inner( + ty: &Type, + type_aliases: &HashMap, Type)>, + type_con_arities: Option<&HashMap>, + depth: u32, + expanding: &mut HashSet, +) -> Type { + if depth > 200 || type_aliases.is_empty() { return ty.clone(); } - let expanded = match ty { - Type::App(f, a) => { - let f2 = expand_type_aliases_limited(f, type_aliases, depth + 1); - let a2 = expand_type_aliases_limited(a, type_aliases, depth + 1); - Type::app(f2, a2) + super::check_deadline(); + + // For App types, collect the full spine first to determine the total arg count. + // This prevents inner App nodes from being independently expanded as aliases + // when they're part of a larger application (e.g., App(App(App(App(App(Con("Codec"), + // ED), a), b), c), d) where Codec has a 1-param alias but is used here as a + // 5-param data type). + if let Type::App(_, _) = ty { + let mut raw_args: Vec<&Type> = Vec::new(); + let mut head = ty; + while let Type::App(f, a) = head { + raw_args.push(a.as_ref()); + head = f.as_ref(); + } + raw_args.reverse(); + + if let Type::Con(name) = head { + if !expanding.contains(name) { + // When the type constructor has a module qualifier (e.g. Codec.Codec), + // prefer the qualified form for alias lookup. If only the unqualified + // name matches an alias but the qualified form doesn't exist, this might + // be a data type that happens to share a name with an alias from a + // different module (e.g. Data.Codec.Codec data type vs Data.Codec.JSON.Codec alias). + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + type_aliases.get(&qualified) + } else { + type_aliases.get(&name.name) + }; + if let Some((params, body)) = alias_entry { + let should_expand = if params.is_empty() { + // Zero-arg alias applied to args: expand head, re-apply args + true + } else if raw_args.len() == params.len() { + // Exactly saturated: always expand + true + } else if raw_args.len() > params.len() && type_con_arities.is_some() { + // Over-saturated: only expand when we have arities for disambiguation. + // Skip if name is also a data type and arg count fits the data type arity. + let arities = type_con_arities.unwrap(); + !lookup_type_con_arity(arities, name) + .map_or(false, |arity| raw_args.len() <= arity) + } else { + false + }; + if should_expand { + let expanded_args: Vec = raw_args + .iter() + .map(|a| { + expand_type_aliases_limited_inner( + a, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ) + }) + .collect(); + let n_sat = params.len(); + if n_sat == 0 { + // Zero-arg alias: expand body, apply all args + expanding.insert(*name); + let expanded_head = expand_type_aliases_limited_inner( + body, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ); + expanding.remove(name); + let mut result = expanded_head; + for arg in expanded_args { + result = Type::app(result, arg); + } + return result; + } + // Saturated or over-saturated: substitute first n_sat args, apply extras + let (sat_args, extra_args) = expanded_args.split_at(n_sat); + let subst: HashMap = params + .iter() + .copied() + .zip(sat_args.iter().cloned()) + .collect(); + let mut result = apply_var_subst(&subst, body); + for extra in extra_args { + result = Type::app(result, extra.clone()); + } + // Only add to expanding set if the substituted result might + // reference this alias (self-referential). For aliases like + // RowApply (body = f a), after substitution the result won't + // contain RowApply, so blocking it prevents legitimate nested + // uses (e.g. OptionsRow body using RowApply again). + let is_self_ref = type_contains_con_name(&result, name); + if is_self_ref { + expanding.insert(*name); + } + let expanded = expand_type_aliases_limited_inner( + &result, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ); + if is_self_ref { + expanding.remove(name); + } + return expanded; + } + } + } } - Type::Fun(a, b) => { - Type::fun( - expand_type_aliases_limited(a, type_aliases, depth + 1), - expand_type_aliases_limited(b, type_aliases, depth + 1), - ) + + // Not an expandable alias — expand each arg independently. + // For the head: if it's a bare Con, don't try to expand it (it's not saturated). + // Otherwise (e.g., nested App, Fun, etc.), recurse into it. + let expanded_args: Vec = raw_args + .iter() + .map(|a| { + expand_type_aliases_limited_inner( + a, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ) + }) + .collect(); + let expanded_head = match head { + Type::Con(_) => head.clone(), + _ => expand_type_aliases_limited_inner( + head, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ), + }; + let mut result = expanded_head; + for arg in expanded_args { + result = Type::app(result, arg); } + return result; + } + + match ty { + Type::Fun(a, b) => Type::fun( + expand_type_aliases_limited_inner( + a, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ), + expand_type_aliases_limited_inner( + b, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ), + ), Type::Record(fields, tail) => { let fields = fields .iter() - .map(|(l, t)| (*l, expand_type_aliases_limited(t, type_aliases, depth + 1))) + .map(|(l, t)| { + ( + *l, + expand_type_aliases_limited_inner( + t, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ), + ) + }) .collect(); - let tail = tail - .as_ref() - .map(|t| Box::new(expand_type_aliases_limited(t, type_aliases, depth + 1))); + let tail = tail.as_ref().map(|t| { + Box::new(expand_type_aliases_limited_inner( + t, + type_aliases, + type_con_arities, + depth + 1, + expanding, + )) + }); Type::Record(fields, tail) } - Type::Forall(vars, body) => { - Type::Forall(vars.clone(), Box::new(expand_type_aliases_limited(body, type_aliases, depth + 1))) - } - _ => ty.clone(), - }; - let mut args = Vec::new(); - let mut head = &expanded; - loop { - match head { - Type::App(f, a) => { - args.push(a.as_ref().clone()); - head = f.as_ref(); - } - _ => break, - } - } - if let Type::Con(name) = head { - if let Some((params, body)) = type_aliases.get(name) { - args.reverse(); - if args.len() >= params.len() { - // Split into saturated args (matching params) and extra args - let (sat_args, extra_args) = args.split_at(params.len()); - let subst: HashMap = - params.iter().copied().zip(sat_args.iter().cloned()).collect(); - let mut result = apply_var_subst(&subst, body); - // Apply any extra args to the expanded body - for arg in extra_args { - result = Type::app(result, arg.clone()); + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(expand_type_aliases_limited_inner( + body, + type_aliases, + type_con_arities, + depth + 1, + expanding, + )), + ), + Type::Con(name) => { + // Zero-arg alias expansion + // Use qualified lookup when a module qualifier is present (matching the App path), + // so that e.g. Border.Evaluated and Style.Evaluated resolve to different aliases + // instead of colliding on the unqualified "Evaluated" key. + let (lookup_key, expand_key) = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + (qualified, qualified_ident(&mod_str, &name_str)) + } else { + (name.name, *name) + }; + if !expanding.contains(&expand_key) { + if let Some((params, body)) = type_aliases.get(&lookup_key) { + if params.is_empty() { + expanding.insert(expand_key); + let result = expand_type_aliases_limited_inner( + body, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ); + expanding.remove(&expand_key); + return result; + } + } + // Do NOT fall back to unqualified lookup when qualified not found. + // Qualified aliases are always stored under their qualified keys during + // import (e.g. Border.Evaluated, Tick.Easing). Falling back to unqualified + // would incorrectly expand data type references (e.g. HATS.Easing) using + // an alias from a different module with the same unqualified name. + // Eta-reduce partially applied aliases (unqualified) + if let Some((params, body)) = type_aliases.get(&lookup_key) { + if let Some(reduced) = eta_reduce_alias(params, body) { + expanding.insert(expand_key); + let result = expand_type_aliases_limited_inner( + &reduced, + type_aliases, + type_con_arities, + depth + 1, + expanding, + ); + expanding.remove(&expand_key); + return result; + } } - return expand_type_aliases_limited(&result, type_aliases, depth + 1); } + ty.clone() } + _ => ty.clone(), } - expanded } /// Check a type for partially applied type synonyms and over-applied type constructors, @@ -370,16 +754,24 @@ fn expand_type_aliases_limited(ty: &Type, type_aliases: &HashMap, Type)>, - type_con_arities: &HashMap, - record_type_aliases: &HashSet, + type_con_arities: &HashMap, + record_type_aliases: &HashSet, span: Span, errors: &mut Vec, ) { // Pre-expansion check: detect record-kind type aliases in row tails // before they get expanded away by expand_type_aliases_limited. check_record_alias_row_tails(ty, record_type_aliases, type_con_arities, span, errors); - let expanded = expand_type_aliases_limited(ty, type_aliases, 0); - check_partially_applied_synonyms_inner(&expanded, type_aliases, type_con_arities, record_type_aliases, span, errors); + let expanded = expand_type_aliases_limited_with_arities(ty, type_aliases, type_con_arities, 0); + check_partially_applied_synonyms_inner( + &expanded, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } /// Pre-expansion check: walk a type and detect record-kind type aliases used @@ -388,29 +780,40 @@ fn check_type_for_partial_synonyms_with_arities( /// `Type::Con("Foo")` with `Type::Record(...)` which is indistinguishable from valid rows. fn check_record_alias_row_tails( ty: &Type, - record_type_aliases: &HashSet, - type_con_arities: &HashMap, + record_type_aliases: &HashSet, + type_con_arities: &HashMap, span: Span, errors: &mut Vec, ) { match ty { Type::Record(fields, tail) => { for (_, t) in fields { - check_record_alias_row_tails(t, record_type_aliases, type_con_arities, span, errors); + check_record_alias_row_tails( + t, + record_type_aliases, + type_con_arities, + span, + errors, + ); } if let Some(t) = tail { if let Type::Con(name) = t.as_ref() { if record_type_aliases.contains(name) { errors.push(TypeError::KindsDoNotUnify { span, - name: *name, - expected: 0, - found: 0, + expected: Type::kind_row_of(Type::kind_type()), + found: Type::kind_type(), }); return; } } - check_record_alias_row_tails(t, record_type_aliases, type_con_arities, span, errors); + check_record_alias_row_tails( + t, + record_type_aliases, + type_con_arities, + span, + errors, + ); } } Type::Fun(a, b) => { @@ -433,10 +836,11 @@ fn check_record_alias_row_tails( fn check_partially_applied_synonyms_inner( ty: &Type, type_aliases: &HashMap, Type)>, - type_con_arities: &HashMap, - record_type_aliases: &HashSet, + type_con_arities: &HashMap, + record_type_aliases: &HashSet, span: Span, errors: &mut Vec, + is_arg: bool, ) { match ty { Type::App(_, _) => { @@ -449,53 +853,124 @@ fn check_partially_applied_synonyms_inner( } // Check if head is a partially or over-applied synonym if let Type::Con(name) = head { - if let Some((params, _)) = type_aliases.get(name) { + // When the name has a module qualifier, use the qualified alias key. + // This prevents confusing a data type (e.g. Codec.Codec) with + // an unrelated alias of the same unqualified name (e.g. CJ.Codec alias). + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + type_aliases.get(&qualified) + } else { + type_aliases.get(&name.name) + }; + if let Some((params, _)) = alias_entry { if args.len() < params.len() { errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); return; } else if args.len() > params.len() { - errors.push(TypeError::KindsDoNotUnify { - span, - name: *name, - expected: params.len(), - found: args.len(), - }); - return; - } - } else if let Some(&arity) = type_con_arities.get(name) { - // Check over-applied data/newtype constructors - if args.len() > arity { - errors.push(TypeError::KindsDoNotUnify { - span, - name: *name, - expected: arity, - found: args.len(), - }); - return; + // If there's also a data type constructor with this name and the + // arg count is valid for it, this is using the data type, not the alias. + // This happens when module qualifiers are stripped and an alias + // shadows a data type (e.g. Data.Codec.JSON.Codec vs Data.Codec.Codec). + let arity_ok = lookup_type_con_arity(type_con_arities, name) + .map_or(false, |arity| args.len() <= arity); + if !arity_ok { + errors.push(TypeError::KindArityMismatch { + span, + name: *name, + expected: params.len(), + found: args.len(), + }); + return; + } } } + // NOTE: Do NOT check over-applied non-alias type constructors here. + // Foreign data types may have result kinds that are type aliases expanding + // to function types (e.g., `UseEffect :: Type -> HookType` where + // `type HookType = HookType' -> HookType'`), allowing more applications + // than the declared arity suggests. The kind checker handles these cases. } else { - check_partially_applied_synonyms_inner(head, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + head, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } - // Recurse into each argument + // Recurse into each argument — pass is_arg=true so bare synonyms + // used as higher-kinded arguments (e.g. `Id` in `ReactAttributesF Id r`) + // are not flagged as partially applied. for arg in args { - check_partially_applied_synonyms_inner(arg, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + arg, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + true, + ); } } Type::Con(name) => { - if let Some((params, _)) = type_aliases.get(name) { + // When this Con appears as an argument to a type application, it may be + // a higher-kinded type argument (e.g. `type Id a = a` passed as `f` in + // `ReactAttributesF f r`). Don't flag these as partially applied. + if is_arg { + return; + } + // Use qualified lookup when the name has a module qualifier, + // to avoid false positives (e.g. DOM.Node matching a different Node alias). + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + type_aliases.get(&qualified) + } else { + type_aliases.get(&name.name) + }; + if let Some((params, _)) = alias_entry { if !params.is_empty() { errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); } } } Type::Fun(a, b) => { - check_partially_applied_synonyms_inner(a, type_aliases, type_con_arities, record_type_aliases, span, errors); - check_partially_applied_synonyms_inner(b, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + a, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); + check_partially_applied_synonyms_inner( + b, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } Type::Record(fields, tail) => { for (_, t) in fields { - check_partially_applied_synonyms_inner(t, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + t, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } if let Some(t) = tail { // Check if the row tail has kind Type instead of Row Type. @@ -508,9 +983,8 @@ fn check_partially_applied_synonyms_inner( if arity == 0 { errors.push(TypeError::KindsDoNotUnify { span, - name: *name, - expected: 0, - found: 0, + expected: Type::kind_row_of(Type::kind_type()), + found: Type::kind_type(), }); return; } @@ -519,123 +993,76 @@ fn check_partially_applied_synonyms_inner( if record_type_aliases.contains(name) { errors.push(TypeError::KindsDoNotUnify { span, - name: *name, - expected: 0, - found: 0, + expected: Type::kind_row_of(Type::kind_type()), + found: Type::kind_type(), }); return; } } - check_partially_applied_synonyms_inner(t, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + t, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } } Type::Forall(_, body) => { - check_partially_applied_synonyms_inner(body, type_aliases, type_con_arities, record_type_aliases, span, errors); + check_partially_applied_synonyms_inner( + body, + type_aliases, + type_con_arities, + record_type_aliases, + span, + errors, + false, + ); } _ => {} } } -/// Check a type expression for type-level operator fixity issues. -/// Detects non-associative operators used in chains and mixed associativity. -fn check_type_op_fixity( - ty: &TypeExpr, - type_fixities: &HashMap, - errors: &mut Vec, -) { - match ty { - TypeExpr::TypeOp { left, op, right, .. } => { - check_type_op_fixity(left, type_fixities, errors); - check_type_op_fixity(right, type_fixities, errors); - // Check if right is also a TypeOp at the same precedence - if let TypeExpr::TypeOp { op: right_op, .. } = right.as_ref() { - let (assoc_l, prec_l) = type_fixities - .get(&op.value.name) - .copied() - .unwrap_or((Associativity::Left, 9)); - let (assoc_r, prec_r) = type_fixities - .get(&right_op.value.name) - .copied() - .unwrap_or((Associativity::Left, 9)); - if prec_l == prec_r { - if assoc_l != assoc_r { - errors.push(TypeError::MixedAssociativityError { - span: op.span, - }); - } else if assoc_l == Associativity::None { - errors.push(TypeError::NonAssociativeError { - span: op.span, - op: op.value.name, - }); - } - } - } - } - TypeExpr::App { constructor, arg, .. } => { - check_type_op_fixity(constructor, type_fixities, errors); - check_type_op_fixity(arg, type_fixities, errors); - } - TypeExpr::Function { from, to, .. } => { - check_type_op_fixity(from, type_fixities, errors); - check_type_op_fixity(to, type_fixities, errors); - } - TypeExpr::Forall { ty, .. } => check_type_op_fixity(ty, type_fixities, errors), - TypeExpr::Constrained { ty, .. } => check_type_op_fixity(ty, type_fixities, errors), - TypeExpr::Parens { ty, .. } => check_type_op_fixity(ty, type_fixities, errors), - TypeExpr::Kinded { ty, kind, .. } => { - check_type_op_fixity(ty, type_fixities, errors); - check_type_op_fixity(kind, type_fixities, errors); - } - TypeExpr::Record { fields, .. } => { - for field in fields { - check_type_op_fixity(&field.ty, type_fixities, errors); - } - } - TypeExpr::Row { fields, tail, .. } => { - for field in fields { - check_type_op_fixity(&field.ty, type_fixities, errors); - } - if let Some(tail) = tail { - check_type_op_fixity(tail, type_fixities, errors); - } - } - _ => {} // Var, Constructor, Wildcard, Hole, StringLiteral, IntLiteral - } -} - /// Detect cycles in type synonym definitions. fn check_type_synonym_cycles( - type_aliases: &HashMap, + type_aliases: &HashMap, errors: &mut Vec, ) { - let alias_names: HashSet = type_aliases.keys().copied().collect(); + let alias_names: HashSet = type_aliases.keys().map(|k| *k).collect(); + + // Build a Symbol-keyed span lookup for error reporting + let alias_spans: HashMap = + type_aliases.iter().map(|(k, (s, _))| (*k, *s)).collect(); // Build adjacency: alias → set of other aliases it references let mut deps: HashMap> = HashMap::new(); - for (&name, (_, ty)) in type_aliases { + for (name, (_, ty)) in type_aliases { let mut refs = HashSet::new(); collect_type_refs(ty, &mut refs); // Only keep references to other aliases refs.retain(|r| alias_names.contains(r)); - deps.insert(name, refs); + deps.insert(*name, refs); } // DFS cycle detection let mut visited: HashSet = HashSet::new(); let mut on_stack: HashSet = HashSet::new(); - for &name in type_aliases.keys() { + for name in type_aliases.keys() { if !visited.contains(&name) { let mut path = Vec::new(); - if let Some(cycle) = dfs_find_cycle(name, &deps, &mut visited, &mut on_stack, &mut path) + if let Some(cycle) = + dfs_find_cycle(*name, &deps, &mut visited, &mut on_stack, &mut path) { - let (span, _) = type_aliases[&name]; + let span = alias_spans[&name]; let cycle_spans: Vec = cycle .iter() - .filter_map(|n| type_aliases.get(n).map(|(s, _)| *s)) + .filter_map(|n| alias_spans.get(n).copied()) .collect(); errors.push(TypeError::CycleInTypeSynonym { - name, + name: name.clone(), span, names_in_cycle: cycle.clone(), spans: cycle_spans, @@ -720,6 +1147,7 @@ fn check_type_class_cycles( } fn collect_binder_vars(binder: &Binder, seen: &mut HashMap>) { + // eprintln!("Collecting binder vars in {:?}, seen so far: {:?}", binder, seen); match binder { Binder::Var { name, .. } => { seen.entry(name.value).or_default().push(name.span); @@ -729,7 +1157,6 @@ fn collect_binder_vars(binder: &Binder, seen: &mut HashMap>) { collect_binder_vars(arg, seen); } } - Binder::Parens { binder, .. } => collect_binder_vars(binder, seen), Binder::As { name, binder, .. } => { seen.entry(name.value).or_default().push(name.span); collect_binder_vars(binder, seen); @@ -740,10 +1167,6 @@ fn collect_binder_vars(binder: &Binder, seen: &mut HashMap>) { collect_binder_vars(elem, seen); } } - Binder::Op { left, right, .. } => { - collect_binder_vars(left, seen); - collect_binder_vars(right, seen); - } Binder::Record { fields, .. } => { for field in fields { if let Some(binder) = &field.binder { @@ -756,104 +1179,6 @@ fn collect_binder_vars(binder: &Binder, seen: &mut HashMap>) { } } -/// Exported information from a type-checked module, available for import by other modules. -#[derive(Debug, Clone, Default)] -pub struct ModuleExports { - /// Value/constructor/method schemes - pub values: HashMap, - /// Class method info: method_name → (class_name, class_type_vars) - pub class_methods: HashMap)>, - /// Data type → constructor names - pub data_constructors: HashMap>, - /// Constructor details: ctor_name → (parent_type, type_vars, field_types) - pub ctor_details: HashMap, Vec)>, - /// Class instances: class_name → [(types, constraints)] - pub instances: HashMap, Vec<(Symbol, Vec)>)>>, - /// Type-level operators: op → target type name - pub type_operators: HashMap, - /// Value-level operator fixities: operator → (associativity, precedence) - pub value_fixities: HashMap, - /// Value-level operators that alias functions (not constructors) - pub function_op_aliases: HashSet, - /// Class methods whose declared type has extra constraints (e.g. `Applicative m =>`). - /// Used for CycleInDeclaration detection across module boundaries. - pub constrained_class_methods: HashSet, - /// Type aliases: alias_name → (params, body_type) - pub type_aliases: HashMap, Type)>, - /// Class definitions: class_name → param_count (for arity checking and orphan detection) - pub class_param_counts: HashMap, - /// Origin tracking: name → original defining module symbol. - /// Used to distinguish re-exports of the same definition from - /// independently defined names that happen to have the same type. - pub value_origins: HashMap, - pub type_origins: HashMap, - pub class_origins: HashMap, - /// Operator → class method target (e.g. `<>` → `append`). - /// Used for tracking deferred constraints on operator usage. - pub operator_class_targets: HashMap, - /// Class functional dependencies: class_name → (type_vars, fundeps as index pairs). - /// Used for fundep-aware orphan instance checking. - pub class_fundeps: HashMap, Vec<(Vec, Vec)>)>, - /// Type constructor arities: type_name → number of type parameters. - /// Used to detect over-applied types after type alias expansion. - pub type_con_arities: HashMap, - /// Roles for each type constructor: type_name → [Role per type parameter]. - pub type_roles: HashMap>, - /// Set of type names declared as newtypes (for Coercible solving). - pub newtype_names: HashSet, - /// Signature constraints for exported functions: name → [(class_name, type_args)]. - pub signature_constraints: HashMap)>>, - /// Type constructor kinds: type_name → kind Type. - /// Used for cross-module kind checking (e.g., detecting kind mismatches - /// between types with the same unqualified name from different modules). - pub type_kinds: HashMap, - /// Functions whose type has Partial in a function parameter position, - /// e.g. `unsafePartial :: (Partial => a) -> a`. These discharge Partial - /// when applied to a partial expression. - pub partial_dischargers: HashSet, -} - -/// Registry of compiled modules, used to resolve imports. -/// -/// Supports layering: a child registry can be created with `with_base()`, -/// which shares an immutable base via `Arc` (zero-copy) and stores new -/// modules in a local overlay. Lookups check the overlay first, then the base. -#[derive(Debug, Clone, Default)] -pub struct ModuleRegistry { - modules: HashMap, ModuleExports>, - base: Option>, -} - -impl ModuleRegistry { - pub fn new() -> Self { - Self::default() - } - - /// Create a child registry layered on top of a shared base. - /// New modules are stored locally; lookups fall through to the base. - pub fn with_base(base: std::sync::Arc) -> Self { - Self { - modules: HashMap::new(), - base: Some(base), - } - } - - pub fn register(&mut self, name: &[Symbol], exports: ModuleExports) { - self.modules.insert(name.to_vec(), exports); - } - - pub fn lookup(&self, name: &[Symbol]) -> Option<&ModuleExports> { - self.modules - .get(name) - .or_else(|| self.base.as_ref().and_then(|b| b.lookup(name))) - } - - pub fn contains(&self, name: &[Symbol]) -> bool { - self.modules.contains_key(name) - || self.base.as_ref().map_or(false, |b| b.contains(name)) - } -} - /// Result of typechecking a module: partial type map + accumulated errors + exports. pub struct CheckResult { pub types: HashMap, @@ -864,77 +1189,68 @@ pub struct CheckResult { // Build the exports for the built-in Prim module. // Prim provides core types (Int, Number, String, Char, Boolean, Array, Function, Record) // and is implicitly imported unqualified in every module. -thread_local! { - static PRIM_EXPORTS_CACHE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; -} +static PRIM_EXPORTS: std::sync::LazyLock = + std::sync::LazyLock::new(prim_exports_inner); -fn prim_exports() -> ModuleExports { - PRIM_EXPORTS_CACHE.with(|cache| { - let mut borrow = cache.borrow_mut(); - if let Some(ref cached) = *borrow { - return cached.clone(); - } - let exports = prim_exports_inner(); - *borrow = Some(exports.clone()); - exports - }) +pub(super) fn prim_exports() -> &'static ModuleExports { + &PRIM_EXPORTS } fn prim_exports_inner() -> ModuleExports { - use crate::interner::intern; let mut exports = ModuleExports::default(); // Register Prim types as known types (empty constructor lists since they're opaque). // This makes them findable by the import system (import_item looks up data_constructors). + // Exports use unqualified keys (like all module exports); import processing + // adds qualification as needed. // Core value types for name in &[ "Int", "Number", "String", "Char", "Boolean", "Array", "Function", "Record", "->", ] { - exports.data_constructors.insert(intern(name), Vec::new()); + exports.data_constructors.insert(unqualified_ident(name), Vec::new()); } // Kind types: Type, Constraint, Symbol, Row for name in &["Type", "Constraint", "Symbol", "Row"] { - exports.data_constructors.insert(intern(name), Vec::new()); + exports.data_constructors.insert(unqualified_ident(name), Vec::new()); } // Type constructor arities for Prim types - exports.type_con_arities.insert(intern("Int"), 0); - exports.type_con_arities.insert(intern("Number"), 0); - exports.type_con_arities.insert(intern("String"), 0); - exports.type_con_arities.insert(intern("Char"), 0); - exports.type_con_arities.insert(intern("Boolean"), 0); - exports.type_con_arities.insert(intern("Array"), 1); - exports.type_con_arities.insert(intern("Record"), 1); - exports.type_con_arities.insert(intern("Function"), 2); - exports.type_con_arities.insert(intern("Type"), 0); - exports.type_con_arities.insert(intern("Constraint"), 0); - exports.type_con_arities.insert(intern("Symbol"), 0); - exports.type_con_arities.insert(intern("Row"), 1); + exports.type_con_arities.insert(unqualified_ident("Int"), 0); + exports.type_con_arities.insert(unqualified_ident("Number"), 0); + exports.type_con_arities.insert(unqualified_ident("String"), 0); + exports.type_con_arities.insert(unqualified_ident("Char"), 0); + exports.type_con_arities.insert(unqualified_ident("Boolean"), 0); + exports.type_con_arities.insert(unqualified_ident("Array"), 1); + exports.type_con_arities.insert(unqualified_ident("Record"), 1); + exports.type_con_arities.insert(unqualified_ident("Function"), 2); + exports.type_con_arities.insert(unqualified_ident("Type"), 0); + exports.type_con_arities.insert(unqualified_ident("Constraint"), 0); + exports.type_con_arities.insert(unqualified_ident("Symbol"), 0); + exports.type_con_arities.insert(unqualified_ident("Row"), 1); // class Partial - exports.instances.insert(intern("Partial"), Vec::new()); - exports.class_param_counts.insert(intern("Partial"), 0); + exports.instances.insert(unqualified_ident("Partial"), Vec::new()); + exports.class_param_counts.insert(unqualified_ident("Partial"), 0); exports } /// Check if a CST ModuleName matches "Prim". -fn is_prim_module(module_name: &crate::cst::ModuleName) -> bool { +pub(super) fn is_prim_module(module_name: &crate::cst::ModuleName) -> bool { module_name.parts.len() == 1 && crate::interner::resolve(module_name.parts[0]).unwrap_or_default() == "Prim" } /// Check if a CST ModuleName is a Prim submodule (e.g. Prim.Coerce, Prim.Row). -fn is_prim_submodule(module_name: &crate::cst::ModuleName) -> bool { +pub(super) fn is_prim_submodule(module_name: &crate::cst::ModuleName) -> bool { module_name.parts.len() >= 2 && crate::interner::resolve(module_name.parts[0]).unwrap_or_default() == "Prim" } /// Build exports for Prim submodules (Prim.Coerce, Prim.Row, Prim.RowList, etc.). /// These are built-in modules with compiler-magic classes and types. -fn prim_submodule_exports(module_name: &crate::cst::ModuleName) -> ModuleExports { - use crate::interner::intern; +pub(super) fn prim_submodule_exports(module_name: &crate::cst::ModuleName) -> ModuleExports { let mut exports = ModuleExports::default(); let sub = if module_name.parts.len() >= 2 { @@ -943,70 +1259,116 @@ fn prim_submodule_exports(module_name: &crate::cst::ModuleName) -> ModuleExports return exports; }; + // Exports use unqualified keys (like all module exports); import processing + // adds qualification as needed. match sub.as_str() { "Boolean" => { // Type-level booleans: True, False - exports.data_constructors.insert(intern("True"), Vec::new()); - exports.data_constructors.insert(intern("False"), Vec::new()); + exports.data_constructors.insert(unqualified_ident("True"), Vec::new()); + exports + .data_constructors + .insert(unqualified_ident("False"), Vec::new()); } "Coerce" => { // class Coercible (no user-visible methods) - exports.instances.insert(intern("Coercible"), Vec::new()); - exports.class_param_counts.insert(intern("Coercible"), 2); + exports.instances.insert(unqualified_ident("Coercible"), Vec::new()); + exports.class_param_counts.insert(unqualified_ident("Coercible"), 2); + // Coercible :: Type -> Type -> Constraint + use crate::typechecker::types::Type as CoerceType; + let ct = CoerceType::kind_type(); + let cc = CoerceType::kind_constraint(); + exports.class_type_kinds.insert(intern("Coercible"), + CoerceType::fun(ct.clone(), CoerceType::fun(ct, cc))); } "Int" => { // Compiler-solved type classes for type-level Ints // class Add (3), class Compare (3), class Mul (3), class ToString (2) for class in &["Add", "Compare", "Mul"] { - exports.instances.insert(intern(class), Vec::new()); - exports.class_param_counts.insert(intern(class), 3); + exports.instances.insert(unqualified_ident(class), Vec::new()); + exports.class_param_counts.insert(unqualified_ident(class), 3); } - exports.instances.insert(intern("ToString"), Vec::new()); - exports.class_param_counts.insert(intern("ToString"), 2); + exports.instances.insert(unqualified_ident("ToString"), Vec::new()); + exports.class_param_counts.insert(unqualified_ident("ToString"), 2); } "Ordering" => { // type Ordering with constructors LT, EQ, GT - exports.data_constructors.insert(intern("Ordering"), vec![intern("LT"), intern("EQ"), intern("GT")]); - exports.data_constructors.insert(intern("LT"), Vec::new()); - exports.data_constructors.insert(intern("EQ"), Vec::new()); - exports.data_constructors.insert(intern("GT"), Vec::new()); + exports.data_constructors.insert( + unqualified_ident("Ordering"), + vec![unqualified_ident("LT"), unqualified_ident("EQ"), unqualified_ident("GT")], + ); + exports.data_constructors.insert(unqualified_ident("LT"), Vec::new()); + exports.data_constructors.insert(unqualified_ident("EQ"), Vec::new()); + exports.data_constructors.insert(unqualified_ident("GT"), Vec::new()); } "Row" => { // classes: Lacks, Cons, Nub, Union for class in &["Lacks", "Cons", "Nub", "Union"] { - exports.instances.insert(intern(class), Vec::new()); - } - exports.class_param_counts.insert(intern("Lacks"), 2); - exports.class_param_counts.insert(intern("Cons"), 4); - exports.class_param_counts.insert(intern("Nub"), 2); - exports.class_param_counts.insert(intern("Union"), 3); + exports.instances.insert(unqualified_ident(class), Vec::new()); + } + exports.class_param_counts.insert(unqualified_ident("Lacks"), 2); + exports.class_param_counts.insert(unqualified_ident("Cons"), 4); + exports.class_param_counts.insert(unqualified_ident("Nub"), 2); + exports.class_param_counts.insert(unqualified_ident("Union"), 3); + + // Export class kinds so they can be registered in class_kinds on import, + // preventing collisions with data types of the same name. + use crate::typechecker::types::Type; + let _k_type = Type::kind_type(); + let k_constraint = Type::kind_constraint(); + let k_symbol = Type::kind_symbol(); + let k_var = intern("k"); + let k_row_k = Type::kind_row_of(Type::Var(k_var)); + + // Union :: forall k. Row k -> Row k -> Row k -> Constraint + exports.class_type_kinds.insert(intern("Union"), + Type::Forall(vec![(k_var, false)], Box::new( + Type::fun(k_row_k.clone(), Type::fun(k_row_k.clone(), Type::fun(k_row_k.clone(), k_constraint.clone()))) + ))); + // Nub :: forall k. Row k -> Row k -> Constraint + exports.class_type_kinds.insert(intern("Nub"), + Type::Forall(vec![(k_var, false)], Box::new( + Type::fun(k_row_k.clone(), Type::fun(k_row_k.clone(), k_constraint.clone())) + ))); + // Lacks :: forall k. Symbol -> Row k -> Constraint + exports.class_type_kinds.insert(intern("Lacks"), + Type::Forall(vec![(k_var, false)], Box::new( + Type::fun(k_symbol, Type::fun(k_row_k.clone(), k_constraint.clone())) + ))); + // Cons :: forall k. Symbol -> k -> Row k -> Row k -> Constraint + exports.class_type_kinds.insert(intern("Cons"), + Type::Forall(vec![(k_var, false)], Box::new( + Type::fun(Type::kind_symbol(), Type::fun(Type::Var(k_var), + Type::fun(k_row_k.clone(), Type::fun(k_row_k, k_constraint)))) + ))); } "RowList" => { // type RowList with constructors Cons, Nil; class RowToList - exports.data_constructors.insert(intern("RowList"), vec![intern("Cons"), intern("Nil")]); - exports.data_constructors.insert(intern("Cons"), Vec::new()); - exports.data_constructors.insert(intern("Nil"), Vec::new()); - exports.instances.insert(intern("RowToList"), Vec::new()); - exports.class_param_counts.insert(intern("RowToList"), 2); + exports + .data_constructors + .insert(unqualified_ident("RowList"), vec![unqualified_ident("Cons"), unqualified_ident("Nil")]); + exports.data_constructors.insert(unqualified_ident("Cons"), Vec::new()); + exports.data_constructors.insert(unqualified_ident("Nil"), Vec::new()); + exports.instances.insert(unqualified_ident("RowToList"), Vec::new()); + exports.class_param_counts.insert(unqualified_ident("RowToList"), 2); } "Symbol" => { // classes: Append, Compare, Cons for class in &["Append", "Compare", "Cons"] { - exports.instances.insert(intern(class), Vec::new()); + exports.instances.insert(unqualified_ident(class), Vec::new()); } - exports.class_param_counts.insert(intern("Append"), 3); - exports.class_param_counts.insert(intern("Compare"), 3); - exports.class_param_counts.insert(intern("Cons"), 3); + exports.class_param_counts.insert(unqualified_ident("Append"), 3); + exports.class_param_counts.insert(unqualified_ident("Compare"), 3); + exports.class_param_counts.insert(unqualified_ident("Cons"), 3); } "TypeError" => { // classes: Fail, Warn; type constructors: Text, Beside, Above, Quote, QuoteLabel for class in &["Fail", "Warn"] { - exports.instances.insert(intern(class), Vec::new()); + exports.instances.insert(unqualified_ident(class), Vec::new()); } - exports.class_param_counts.insert(intern("Fail"), 1); - exports.class_param_counts.insert(intern("Warn"), 1); + exports.class_param_counts.insert(unqualified_ident("Fail"), 1); + exports.class_param_counts.insert(unqualified_ident("Warn"), 1); for ty in &["Doc", "Text", "Beside", "Above", "Quote", "QuoteLabel"] { - exports.data_constructors.insert(intern(ty), Vec::new()); + exports.data_constructors.insert(unqualified_ident(ty), Vec::new()); } } _ => { @@ -1037,9 +1399,17 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { fn collect_vars(ty: &Type, vars: &mut HashSet) { match ty { - Type::Var(v) => { vars.insert(*v); } - Type::Fun(a, b) => { collect_vars(a, vars); collect_vars(b, vars); } - Type::App(f, a) => { collect_vars(f, vars); collect_vars(a, vars); } + Type::Var(v) => { + vars.insert(*v); + } + Type::Fun(a, b) => { + collect_vars(a, vars); + collect_vars(b, vars); + } + Type::App(f, a) => { + collect_vars(f, vars); + collect_vars(a, vars); + } Type::Forall(bound, body) => { let mut inner_vars = HashSet::new(); collect_vars(body, &mut inner_vars); @@ -1050,8 +1420,12 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { } } Type::Record(fields, tail) => { - for (_, t) in fields { collect_vars(t, vars); } - if let Some(t) = tail { collect_vars(t, vars); } + for (_, t) in fields { + collect_vars(t, vars); + } + if let Some(t) = tail { + collect_vars(t, vars); + } } _ => {} } @@ -1066,7 +1440,7 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { // Check for App(App(Function, a), b) if let Type::App(ff, a) = &f { if let Type::Con(name) = ff.as_ref() { - if *name == function_sym { + if name.name == function_sym { return Type::fun(a.as_ref().clone(), b); } } @@ -1077,12 +1451,12 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { normalize_function(*a, function_sym), normalize_function(*b, function_sym), ), - Type::Forall(vars, body) => Type::Forall( - vars, - Box::new(normalize_function(*body, function_sym)), - ), + Type::Forall(vars, body) => { + Type::Forall(vars, Box::new(normalize_function(*body, function_sym))) + } Type::Record(fields, tail) => { - let fields = fields.into_iter() + let fields = fields + .into_iter() .map(|(l, t)| (l, normalize_function(t, function_sym))) .collect(); let tail = tail.map(|t| Box::new(normalize_function(*t, function_sym))); @@ -1095,7 +1469,8 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { // Instantiate forall first let ty = match ty { Type::Forall(vars, body) => { - let subst: HashMap = vars.iter() + let subst: HashMap = vars + .iter() .map(|&(v, _)| (v, Type::Unif(ctx.state.fresh_var()))) .collect(); apply_var_subst(&subst, &body) @@ -1112,7 +1487,8 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { if free_vars.is_empty() { ty } else { - let subst: HashMap = free_vars.into_iter() + let subst: HashMap = free_vars + .into_iter() .map(|v| (v, Type::Unif(ctx.state.fresh_var()))) .collect(); apply_var_subst(&subst, &ty) @@ -1122,11 +1498,10 @@ fn instantiate_all_vars(ctx: &mut InferCtx, ty: Type) -> Type { /// Extract the head type constructor name from a CST TypeExpr, /// peeling through type applications and parentheses. /// E.g. `Maybe Int` → Some("Maybe"), `(Foo a b)` → Some("Foo") -fn extract_head_constructor(ty: &crate::cst::TypeExpr) -> Option { +fn extract_head_constructor(ty: &crate::ast::TypeExpr) -> Option { match ty { - crate::cst::TypeExpr::Constructor { name, .. } => Some(name.name), - crate::cst::TypeExpr::App { constructor, .. } => extract_head_constructor(constructor), - crate::cst::TypeExpr::Parens { ty, .. } => extract_head_constructor(ty), + crate::ast::TypeExpr::Constructor { name, .. } => Some(*name), + crate::ast::TypeExpr::App { constructor, .. } => extract_head_constructor(constructor), _ => None, } } @@ -1137,8 +1512,8 @@ fn extract_head_constructor(ty: &crate::cst::TypeExpr) -> Option { // forward references and mutual recursion are handled properly. /// Collect references to top-level value names from an expression. -fn collect_expr_refs(expr: &crate::cst::Expr, top: &HashSet, refs: &mut HashSet) { - use crate::cst::Expr; +fn collect_expr_refs(expr: &crate::ast::Expr, top: &HashSet, refs: &mut HashSet) { + use crate::ast::Expr; match expr { Expr::Var { name, .. } if name.module.is_none() => { if top.contains(&name.name) { @@ -1151,32 +1526,27 @@ fn collect_expr_refs(expr: &crate::cst::Expr, top: &HashSet, refs: &mut } Expr::VisibleTypeApp { func, .. } => collect_expr_refs(func, top, refs), Expr::Lambda { body, .. } => collect_expr_refs(body, top, refs), - Expr::Op { left, op, right, .. } => { - collect_expr_refs(left, top, refs); - if op.value.module.is_none() && top.contains(&op.value.name) { - refs.insert(op.value.name); - } - collect_expr_refs(right, top, refs); - } - Expr::OpParens { op, .. } => { - if op.value.module.is_none() && top.contains(&op.value.name) { - refs.insert(op.value.name); - } - } - Expr::If { cond, then_expr, else_expr, .. } => { + Expr::If { + cond, + then_expr, + else_expr, + .. + } => { collect_expr_refs(cond, top, refs); collect_expr_refs(then_expr, top, refs); collect_expr_refs(else_expr, top, refs); } Expr::Case { exprs, alts, .. } => { - for e in exprs { collect_expr_refs(e, top, refs); } + for e in exprs { + collect_expr_refs(e, top, refs); + } for alt in alts { collect_guarded_refs(&alt.result, top, refs); } } Expr::Let { bindings, body, .. } => { for b in bindings { - if let crate::cst::LetBinding::Value { expr, .. } = b { + if let crate::ast::LetBinding::Value { expr, .. } = b { collect_expr_refs(expr, top, refs); } } @@ -1185,15 +1555,19 @@ fn collect_expr_refs(expr: &crate::cst::Expr, top: &HashSet, refs: &mut Expr::Do { statements, .. } | Expr::Ado { statements, .. } => { for stmt in statements { match stmt { - crate::cst::DoStatement::Bind { expr, .. } => collect_expr_refs(expr, top, refs), - crate::cst::DoStatement::Let { bindings, .. } => { + crate::ast::DoStatement::Bind { expr, .. } => { + collect_expr_refs(expr, top, refs) + } + crate::ast::DoStatement::Let { bindings, .. } => { for b in bindings { - if let crate::cst::LetBinding::Value { expr, .. } = b { + if let crate::ast::LetBinding::Value { expr, .. } = b { collect_expr_refs(expr, top, refs); } } } - crate::cst::DoStatement::Discard { expr, .. } => collect_expr_refs(expr, top, refs), + crate::ast::DoStatement::Discard { expr, .. } => { + collect_expr_refs(expr, top, refs) + } } } if let Expr::Ado { result, .. } = expr { @@ -1202,23 +1576,30 @@ fn collect_expr_refs(expr: &crate::cst::Expr, top: &HashSet, refs: &mut } Expr::Record { fields, .. } => { for f in fields { - if let Some(v) = &f.value { collect_expr_refs(v, top, refs); } + if let Some(v) = &f.value { + collect_expr_refs(v, top, refs); + } } } Expr::RecordAccess { expr, .. } => collect_expr_refs(expr, top, refs), Expr::RecordUpdate { expr, updates, .. } => { collect_expr_refs(expr, top, refs); - for u in updates { collect_expr_refs(&u.value, top, refs); } + for u in updates { + collect_expr_refs(&u.value, top, refs); + } } - Expr::Parens { expr, .. } => collect_expr_refs(expr, top, refs), Expr::TypeAnnotation { expr, .. } => collect_expr_refs(expr, top, refs), Expr::Array { elements, .. } => { - for e in elements { collect_expr_refs(e, top, refs); } + for e in elements { + collect_expr_refs(e, top, refs); + } } Expr::Negate { expr, .. } => collect_expr_refs(expr, top, refs), Expr::Literal { lit, .. } => { - if let crate::cst::Literal::Array(elems) = lit { - for e in elems { collect_expr_refs(e, top, refs); } + if let crate::ast::Literal::Array(elems) = lit { + for e in elems { + collect_expr_refs(e, top, refs); + } } } Expr::AsPattern { name, pattern, .. } => { @@ -1230,15 +1611,19 @@ fn collect_expr_refs(expr: &crate::cst::Expr, top: &HashSet, refs: &mut } /// Collect references from a guarded expression (unconditional or guarded). -fn collect_guarded_refs(guarded: &crate::cst::GuardedExpr, top: &HashSet, refs: &mut HashSet) { +fn collect_guarded_refs( + guarded: &crate::ast::GuardedExpr, + top: &HashSet, + refs: &mut HashSet, +) { match guarded { - crate::cst::GuardedExpr::Unconditional(e) => collect_expr_refs(e, top, refs), - crate::cst::GuardedExpr::Guarded(guards) => { + crate::ast::GuardedExpr::Unconditional(e) => collect_expr_refs(e, top, refs), + crate::ast::GuardedExpr::Guarded(guards) => { for g in guards { for p in &g.patterns { match p { - crate::cst::GuardPattern::Boolean(e) => collect_expr_refs(e, top, refs), - crate::cst::GuardPattern::Pattern(_, e) => collect_expr_refs(e, top, refs), + crate::ast::GuardPattern::Boolean(e) => collect_expr_refs(e, top, refs), + crate::ast::GuardPattern::Pattern(_, e) => collect_expr_refs(e, top, refs), } } collect_expr_refs(&g.expr, top, refs); @@ -1251,10 +1636,15 @@ fn collect_guarded_refs(guarded: &crate::cst::GuardedExpr, top: &HashSet fn collect_decl_refs(decls: &[&Decl], top: &HashSet) -> HashSet { let mut refs = HashSet::new(); for decl in decls { - if let Decl::Value { guarded, where_clause, .. } = decl { + if let Decl::Value { + guarded, + where_clause, + .. + } = decl + { collect_guarded_refs(guarded, top, &mut refs); for wb in where_clause { - if let crate::cst::LetBinding::Value { expr, .. } = wb { + if let crate::ast::LetBinding::Value { expr, .. } = wb { collect_expr_refs(expr, top, &mut refs); } } @@ -1265,10 +1655,7 @@ fn collect_decl_refs(decls: &[&Decl], top: &HashSet) -> HashSet /// Compute strongly connected components using Tarjan's algorithm. /// Returns SCCs in reverse topological order (leaves first). -fn tarjan_scc( - nodes: &[Symbol], - edges: &HashMap>, -) -> Vec> { +pub fn tarjan_scc(nodes: &[Symbol], edges: &HashMap>) -> Vec> { let n = nodes.len(); let idx_of: HashMap = nodes.iter().enumerate().map(|(i, s)| (*s, i)).collect(); @@ -1301,7 +1688,18 @@ fn tarjan_scc( for dep in deps { if let Some(&w) = idx_of.get(dep) { if index[w] == usize::MAX { - strongconnect(w, nodes, edges, idx_of, index_counter, stack, on_stack, index, lowlink, sccs); + strongconnect( + w, + nodes, + edges, + idx_of, + index_counter, + stack, + on_stack, + index, + lowlink, + sccs, + ); lowlink[v] = lowlink[v].min(lowlink[w]); } else if on_stack[w] { lowlink[v] = lowlink[v].min(index[w]); @@ -1315,7 +1713,9 @@ fn tarjan_scc( while let Some(w) = stack.pop() { on_stack[w] = false; scc.push(nodes[w]); - if w == v { break; } + if w == v { + break; + } } sccs.push(scc); } @@ -1323,7 +1723,18 @@ fn tarjan_scc( for i in 0..n { if index[i] == usize::MAX { - strongconnect(i, nodes, edges, &idx_of, &mut index_counter, &mut stack, &mut on_stack, &mut index, &mut lowlink, &mut sccs); + strongconnect( + i, + nodes, + edges, + &idx_of, + &mut index_counter, + &mut stack, + &mut on_stack, + &mut index, + &mut lowlink, + &mut sccs, + ); } } @@ -1337,21 +1748,25 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut ctx = InferCtx::new(); ctx.module_mode = true; let mut env = Env::new(); - let mut signatures: HashMap = HashMap::new(); + let mut signatures: HashMap = HashMap::new(); let mut result_types: HashMap = HashMap::new(); let mut errors: Vec = Vec::new(); // Track class info for instance checking // Each instance stores (type_args, constraints) where constraints are (class_name, constraint_type_args) - let mut instances: HashMap, Vec<(Symbol, Vec)>)>> = HashMap::new(); + let mut instances: HashMap, Vec<(QualifiedIdent, Vec)>)>> = + HashMap::new(); // Track locally-defined instance heads for overlap checking // Stores (converted types, had_kind_annotations, CST types) for each instance - let mut local_instance_heads: HashMap, bool, Vec)>> = HashMap::new(); + let mut local_instance_heads: HashMap< + Symbol, + Vec<(Vec, bool, Vec)>, + > = HashMap::new(); // Track classes that have instance chains (else keyword). // Used during deferred constraint checking to detect ambiguous chain resolution. - let mut chained_classes: HashSet = HashSet::new(); + let mut chained_classes: HashSet = HashSet::new(); // Track locally-defined names for export computation let mut local_values: HashMap = HashMap::new(); @@ -1368,17 +1783,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // newtype_names is now on ctx.newtype_names (shared via ModuleExports for Coercible) // Track type alias definitions for cycle detection - let mut type_alias_defs: HashMap = HashMap::new(); + let mut type_alias_defs: HashMap = HashMap::new(); // Track class definitions for superclass cycle detection: name → (span, superclass class names) let mut class_defs: HashMap)> = HashMap::new(); // Track superclass constraints per class for instance validation: // class name → (class type var names, superclass constraints as (class_name, type_args)) - let mut class_superclasses: HashMap, Vec<(Symbol, Vec)>)> = HashMap::new(); + let mut class_superclasses: HashMap, Vec<(QualifiedIdent, Vec)>)> = HashMap::new(); // Track class type parameter counts for instance arity validation. - let mut class_param_counts: HashMap = HashMap::new(); + let mut class_param_counts: HashMap = HashMap::new(); // Track kind signatures for orphan detection: name → span let mut kind_sigs: HashMap = HashMap::new(); @@ -1392,21 +1807,69 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Pre-scan: Collect locally-defined type and class names for orphan instance detection. // An instance is orphan if neither the class nor any type in the instance head is locally defined. - let local_type_names: HashSet = module.decls.iter().filter_map(|d| match d { - Decl::Data { name, .. } | Decl::Newtype { name, .. } | Decl::TypeAlias { name, .. } | Decl::ForeignData { name, .. } => Some(name.value), - _ => None, - }).collect(); - let local_class_names: HashSet = module.decls.iter().filter_map(|d| match d { - Decl::Class { name, is_kind_sig, .. } if !*is_kind_sig => Some(name.value), - _ => None, - }).collect(); + let local_type_names: HashSet = module + .decls + .iter() + .filter_map(|d| match d { + Decl::Data { name, .. } + | Decl::Newtype { name, .. } + | Decl::TypeAlias { name, .. } + | Decl::ForeignData { name, .. } => Some(name.value), + _ => None, + }) + .collect(); + // Data/newtype names only (excludes type aliases) — used for orphan instance checks + // where type aliases should be treated as transparent (expanded to their underlying type). + let local_data_type_names: HashSet = module + .decls + .iter() + .filter_map(|d| match d { + Decl::Data { name, .. } + | Decl::Newtype { name, .. } + | Decl::ForeignData { name, .. } => Some(name.value), + _ => None, + }) + .collect(); + // Concrete locally-defined data/newtype names only — excludes foreign data types. + // Used in derive position checking to allow locally-defined types that haven't + // been processed yet in Pass 1 declaration order to be treated as covariant. + // Foreign data types are excluded because they're abstract and have unknown variance. + let local_concrete_type_names: HashSet = module + .decls + .iter() + .filter_map(|d| match d { + Decl::Data { name, .. } | Decl::Newtype { name, .. } => Some(name.value), + _ => None, + }) + .collect(); + let local_class_names: HashSet = module + .decls + .iter() + .filter_map(|d| match d { + Decl::Class { + name, is_kind_sig, .. + } if !*is_kind_sig => Some(name.value), + _ => None, + }) + .collect(); // Track locally-registered instances for superclass validation: (span, class_name, inst_types) let mut registered_instances: Vec<(Span, Symbol, Vec)> = Vec::new(); // Deferred instance method bodies: checked after Pass 1.5 so foreign imports and fixity are available. // Tuple: (method_name, span, binders, guarded, where_clause, expected_type, scoped_vars, given_classes) - let mut deferred_instance_methods: Vec<(Symbol, Span, &[Binder], &crate::cst::GuardedExpr, &[crate::cst::LetBinding], Option, HashSet, HashSet)> = Vec::new(); + let mut deferred_instance_methods: Vec<( + Symbol, + Span, + &[Binder], + &crate::ast::GuardedExpr, + &[crate::ast::LetBinding], + Option, + HashSet, + HashSet, + usize, // instance_id: groups methods from the same instance + )> = Vec::new(); + let mut next_instance_id: usize = 0; // Instance method groups: each entry is the list of method names for one instance. // Used for CycleInDeclaration detection among sibling methods. let mut instance_method_groups: Vec> = Vec::new(); @@ -1414,12 +1877,26 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Import Prim unqualified. Prim is implicitly available in all modules, // BUT if the module has an explicit `import Prim (...)` or `import Prim hiding (...)`, // skip the automatic full import and let process_imports handle the selective import. - let has_explicit_prim_import = module.imports.iter().any(|imp| { - is_prim_module(&imp.module) && imp.imports.is_some() && imp.qualified.is_none() - }); + let has_explicit_prim_import = module + .imports + .iter() + .any(|imp| is_prim_module(&imp.module) && imp.imports.is_some() && imp.qualified.is_none()); if !has_explicit_prim_import { let prim = prim_exports(); - import_all(&prim, &mut env, &mut ctx, &mut instances, None); + import_all(None, prim, &mut env, &mut ctx, None, &HashSet::new(), &HashSet::new()); + // Also register Prim type_con_arities with "Prim." qualifier so explicit + // Prim.Array, Prim.Int etc. references work in source code. + let prim_sym = intern("Prim"); + for name in prim.type_con_arities.keys() { + ctx.type_con_arities.insert(QualifiedIdent { module: Some(prim_sym), name: name.name }, *prim.type_con_arities.get(name).unwrap()); + } + // Import Prim instances (instances now handled centrally, not in import_all) + for (class_name, insts) in &prim.instances { + instances + .entry(*class_name) + .or_default() + .extend(insts.iter().cloned()); + } // Also register Prim's class_param_counts so Partial etc. are known classes for (class_name, count) in &prim.class_param_counts { class_param_counts.entry(*class_name).or_insert(*count); @@ -1438,13 +1915,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Pre-populate class param counts from imported class methods and class definitions. for (_method, (class_name, tvs)) in &ctx.class_methods { - class_param_counts.entry(*class_name).or_insert(tvs.len()); + class_param_counts + .entry(*class_name) + .or_insert(tvs.len()); } // Also populate from explicitly exported class_param_counts (catches classes without methods) for import_decl in &module.imports { + super::check_deadline(); let prim_sub; let module_exports = if is_prim_module(&import_decl.module) { - Some(&prim_exports()) + Some(prim_exports()) } else if is_prim_submodule(&import_decl.module) { prim_sub = prim_submodule_exports(&import_decl.module); Some(&prim_sub) @@ -1454,7 +1934,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if let Some(exports) = module_exports { for (class_name, count) in &exports.class_param_counts { match class_param_counts.entry(*class_name) { - std::collections::hash_map::Entry::Vacant(e) => { e.insert(*count); } + std::collections::hash_map::Entry::Vacant(e) => { + e.insert(*count); + } std::collections::hash_map::Entry::Occupied(e) => { // If same name has different arity from another module, // mark as ambiguous by using usize::MAX (won't match any real arity) @@ -1465,38 +1947,48 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } for (class_name, fd) in &exports.class_fundeps { - ctx.class_fundeps.entry(*class_name).or_insert_with(|| fd.clone()); + ctx.class_fundeps + .entry(qi(*class_name)) + .or_insert_with(|| fd.clone()); } } } - // Mark known_types as active (non-empty) so convert_type_expr performs - // unknown type checking. Use a sentinel that can't collide with real names. - ctx.known_types.insert(crate::interner::intern("$module_scope_active")); - - // Pre-scan: Register all locally declared type names so they are known + // Pre-scan: Register all locally declared type names for type_con_arities // before any type expressions are converted. This mirrors PureScript's // non-order-dependent module scoping for types. for decl in &module.decls { match decl { - Decl::Data { name, type_vars, kind_sig, .. } => { - ctx.known_types.insert(name.value); + Decl::Data { + name, + type_vars, + kind_sig, + .. + } => { // Only set arity for real data declarations, not standalone kind signatures // (e.g. `type Id :: forall k. k -> k` is parsed as Data with type_vars=[]) if *kind_sig == crate::cst::KindSigSource::None { - ctx.type_con_arities.insert(name.value, type_vars.len()); + ctx.type_con_arities.insert(qi(name.value), type_vars.len()); } } - Decl::Newtype { name, type_vars, .. } => { - ctx.known_types.insert(name.value); - ctx.type_con_arities.insert(name.value, type_vars.len()); + Decl::Newtype { + name, type_vars, .. + } => { + ctx.type_con_arities.insert(qi(name.value), type_vars.len()); } - Decl::ForeignData { name, .. } => { - ctx.known_types.insert(name.value); - // Foreign data arity is unknown without kind annotation; skip + Decl::ForeignData { name, kind, .. } => { + // Compute arity from kind annotation by counting arrows. + // e.g. `foreign import data Stream :: Row Effect -> Type` has arity 1. + fn count_kind_arrows(te: &TypeExpr) -> usize { + match te { + TypeExpr::Function { to, .. } => 1 + count_kind_arrows(to), + TypeExpr::Forall { ty, .. } => count_kind_arrows(ty), + _ => 0, + } + } + ctx.type_con_arities.insert(qi(name.value), count_kind_arrows(kind)); } Decl::TypeAlias { name, span, .. } => { - ctx.known_types.insert(name.value); // Type synonyms re-defining an explicitly imported type name are a ScopeConflict. // Data/newtype declarations are allowed to shadow imports. if explicitly_imported_types.contains(&name.value) { @@ -1520,7 +2012,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for decl in &module.decls { match decl { - Decl::Data { span, name, constructors, kind_sig, is_role_decl, .. } => { + Decl::Data { + span, + name, + constructors, + kind_sig, + is_role_decl, + .. + } => { // Kind signatures and role declarations don't count as type declarations if *kind_sig == KindSigSource::None && !*is_role_decl { // Check type name conflicts @@ -1551,7 +2050,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { new_kind: "data constructor", existing_kind, }); - } else if let Some((existing_kind, _)) = declared_classes.get(&ctor.name.value) { + } else if let Some((existing_kind, _)) = + declared_classes.get(&ctor.name.value) + { errors.push(TypeError::DeclConflict { span: ctor.span, name: ctor.name.value, @@ -1559,12 +2060,18 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { existing_kind, }); } else { - declared_ctors.insert(ctor.name.value, ("data constructor", ctor.span)); + declared_ctors + .insert(ctor.name.value, ("data constructor", ctor.span)); } } } } - Decl::Newtype { span, name, constructor, .. } => { + Decl::Newtype { + span, + name, + constructor, + .. + } => { // Check type name if let Some((existing_kind, _)) = declared_types.get(&name.value) { errors.push(TypeError::DeclConflict { @@ -1592,7 +2099,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { new_kind: "data constructor", existing_kind, }); - } else if let Some((existing_kind, _)) = declared_classes.get(&constructor.value) { + } else if let Some((existing_kind, _)) = + declared_classes.get(&constructor.value) + { errors.push(TypeError::DeclConflict { span: *span, name: constructor.value, @@ -1622,7 +2131,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { declared_types.insert(name.value, ("type", *span)); } } - Decl::Class { span, name, is_kind_sig, .. } => { + Decl::Class { + span, + name, + is_kind_sig, + .. + } => { if !*is_kind_sig { if let Some((existing_kind, _)) = declared_classes.get(&name.value) { // DuplicateTypeClass is handled separately — skip here @@ -1651,9 +2165,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + let _module_start = std::time::Instant::now(); // Pass 0: Collect fixity declarations and check for duplicates. - let mut seen_value_ops: HashMap> = HashMap::new(); - let mut seen_type_ops: HashMap> = HashMap::new(); + let mut seen_value_ops: HashMap> = HashMap::new(); + let mut seen_type_ops: HashMap> = HashMap::new(); let mut type_fixities: HashMap = HashMap::new(); for decl in &module.decls { if let Decl::Fixity { @@ -1668,14 +2183,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { { if *is_type { seen_type_ops.entry(operator.value).or_default().push(*span); - ctx.type_operators.insert(operator.value, target.name); + ctx.type_operators.insert(qi(operator.value), *target); type_fixities.insert(operator.value, (*associativity, *precedence)); } else { seen_value_ops .entry(operator.value) .or_default() .push(*span); - ctx.value_fixities.insert(operator.value, (*associativity, *precedence)); + ctx.value_fixities + .insert(operator.value, (*associativity, *precedence)); } } } @@ -1696,33 +2212,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } - // Check type-level operator fixity in all type expressions - if !type_fixities.is_empty() { - for decl in &module.decls { - match decl { - Decl::TypeSignature { ty, .. } => { - check_type_op_fixity(ty, &type_fixities, &mut errors); - } - Decl::Data { constructors, .. } => { - for ctor in constructors { - for field_ty in &ctor.fields { - check_type_op_fixity(field_ty, &type_fixities, &mut errors); - } - } - } - Decl::TypeAlias { ty, .. } => { - check_type_op_fixity(ty, &type_fixities, &mut errors); - } - Decl::Foreign { ty, .. } => { - check_type_op_fixity(ty, &type_fixities, &mut errors); - } - _ => {} - } - } - } - // Clone so we don't hold an immutable borrow on ctx across mutable uses. let type_ops = ctx.type_operators.clone(); + // Symbol-keyed version for kind:: functions which still use Symbol maps // Build set of known class names for constraint validation (from all sources). // Note: we do NOT include instances.keys() here because instances propagate @@ -1730,32 +2222,45 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // should only be in scope if it's actually imported. E.g. Prim.Row.Cons // instances leak through the registry, but using `Cons` in a constraint // requires `import Prim.Row (class Cons)`. - let mut known_classes: HashSet = class_param_counts.keys().copied().collect(); + let mut known_classes: HashSet = class_param_counts.keys().copied().collect(); for (_, (class_name, _)) in &ctx.class_methods { known_classes.insert(*class_name); } for name in &local_class_names { - known_classes.insert(*name); + known_classes.insert(qi(*name)); } // ===== Kind Pass: Infer and check kinds for all type declarations ===== - let mut saved_type_kinds: HashMap = HashMap::new(); + let saved_type_kinds: HashMap; + let saved_class_kinds: HashMap; { use crate::typechecker::kind::{self, KindState}; let mut ks = KindState::new(); - // Register imported type kinds from qualified imports for cross-module kind checking. - // This enables detecting kind mismatches between types with the same unqualified name - // from different modules (e.g., LibA.DemoKind ≠ LibB.DemoKind). - // Only qualified imports are registered — unqualified imports use fresh kind vars - // from infer_kind, which avoids contaminating the quantification check with - // instantiated forall kind vars. + // Build mapping from import qualifier aliases to canonical (full) module names. + // This is used to canonicalize kind constructor qualifiers so that the same type + // imported via different aliases (e.g. `import M as K` and `import M as Subject`) + // produces identical kind representations. for import_decl in &module.imports { - let qualifier = match import_decl.qualified.as_ref() { - Some(q) => module_name_to_symbol(q), - None => continue, // Skip unqualified imports - }; + if let Some(q) = &import_decl.qualified { + let alias = module_name_to_symbol(q); + let canonical = module_name_to_symbol(&import_decl.module); + ks.qualifier_to_canonical.insert(alias, canonical); + } + } + // Sync to the kind UnifyState so module qualifier resolution works during kind unification. + ks.state.qualifier_to_canonical = ks.qualifier_to_canonical.clone(); + + // Register imported type kinds for cross-module kind checking. + // Both qualified and unqualified imports are registered so that the kind + // checker can determine kinds from imported type constructors (e.g., + // SlotStorage's kind annotation constraining `slot :: (Type -> Type) -> Type -> Type`). + // Qualified imports are additionally registered under their qualified name + // for disambiguation (e.g., LibA.DemoKind ≠ LibB.DemoKind). + for import_decl in &module.imports { + super::check_deadline(); + let qualifier = import_decl.qualified.as_ref().map(|q| module_name_to_symbol(q)); let prim_sub; let module_exports = if is_prim_module(&import_decl.module) { @@ -1768,15 +2273,100 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { Some(exports) => exports, None => continue, } - }; - - let exported_type_names: HashSet = module_exports.type_kinds.keys().copied().collect(); - - for (&type_name, kind) in &module_exports.type_kinds { - // Qualify Con references in the kind to use the import qualifier - let qualified_kind = qualify_kind_refs(kind, qualifier, &exported_type_names); - let qualified_name = qualified_symbol(qualifier, type_name); - ks.register_type(qualified_name, qualified_kind); + }; + + let exported_type_names: HashSet = + module_exports.type_kinds.keys().copied().collect(); + + // Compute which type names are actually imported (respecting explicit lists). + // For `import M (Foo, Bar)`, only register kinds for Foo and Bar. + // For `import M` or `import M hiding (...)`, register all (or filtered). + let allowed_type_names: Option> = match &import_decl.imports { + Some(crate::cst::ImportList::Explicit(items)) => { + let names: HashSet = items.iter().filter_map(|item| match item { + crate::cst::Import::Type(name, _) => Some(*name), + crate::cst::Import::Class(name) => Some(*name), + _ => None, + }).collect(); + Some(names) + } + Some(crate::cst::ImportList::Hiding(items)) => { + let hidden: HashSet = items.iter().filter_map(|item| match item { + crate::cst::Import::Type(name, _) => Some(*name), + crate::cst::Import::Class(name) => Some(*name), + _ => None, + }).collect(); + let names: HashSet = exported_type_names.iter() + .filter(|n| !hidden.contains(n)) + .copied() + .collect(); + Some(names) + } + None => None, // import everything + }; + + for (&type_name, kind) in &module_exports.type_kinds { + // Skip types not in the explicit import list + if let Some(ref allowed) = allowed_type_names { + if !allowed.contains(&type_name) { + continue; + } + } + + if let Some(q) = qualifier { + // Qualify Con references in the kind to use the import qualifier + let qualified_kind = qualify_kind_refs(kind, q, &exported_type_names); + let qualified_name = qualified_symbol(q, type_name); + ks.register_type(qualified_name, qualified_kind); + } else { + // Register under the bare name only for unqualified imports. + ks.register_type(type_name, kind.clone()); + } + } + // Import class kinds separately so they don't get overwritten by + // data types with the same name (e.g., class Error vs data Error). + for (&type_name, kind) in &module_exports.class_type_kinds { + if let Some(ref allowed) = allowed_type_names { + if !allowed.contains(&type_name) { + continue; + } + } + if let Some(q) = qualifier { + let qualified_kind = qualify_kind_refs(kind, q, &exported_type_names); + let qualified_name = qualified_symbol(q, type_name); + ks.class_kinds.insert(qualified_name, qualified_kind); + } else { + ks.class_kinds.insert(type_name, kind.clone()); + } + } + // Also register type alias kinds under qualified names so that + // qualified references (e.g. CJ.Codec) find the alias's kind + // rather than falling back to a data type with the same unqualified name + // but different arity (e.g. Data.Codec's 5-param `data Codec`). + // Register type alias kinds so the kind checker knows their arities. + // For qualified imports, register under the qualified name; + // for unqualified imports, register under the bare name. + for (alias_name, (params, _body)) in &module_exports.type_aliases { + // Skip aliases not in the explicit import list + if let Some(ref allowed) = allowed_type_names { + if !allowed.contains(&alias_name.name) { + continue; + } + } + let reg_name = if let Some(q) = qualifier { + qualified_symbol(q, alias_name.name) + } else { + alias_name.name + }; + // Don't overwrite if already registered from type_kinds + if ks.type_kinds.get(®_name).is_none() { + // Build kind: ?k1 -> ?k2 -> ... -> ?kN -> ?k_result + let mut kind = ks.fresh_kind_var(); + for _ in 0..params.len() { + kind = Type::fun(ks.fresh_kind_var(), kind); + } + ks.register_type(reg_name, kind); + } } } @@ -1792,7 +2382,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if let Some(e) = kind::check_visible_dependent_quantification(kind) { errors.push(e); } - let k = kind::convert_kind_expr(kind); + let k = ks.convert_kind_expr_canonical(kind); ks.register_type(name.value, k); } } @@ -1801,12 +2391,20 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // used in kind annotations (e.g., `data P (a :: Id Type)`) are expanded during // kind unification. Also register already-known type aliases from imports. for (&alias_name, (params, body)) in &ctx.state.type_aliases { - ks.state.type_aliases.insert(alias_name, (params.clone(), body.clone())); + ks.state + .type_aliases + .insert(alias_name, (params.clone(), body.clone())); } for decl in &module.decls { - if let Decl::TypeAlias { name, type_vars, ty, .. } = decl { + if let Decl::TypeAlias { + name, + type_vars, + ty, + .. + } = decl + { let var_syms: Vec = type_vars.iter().map(|tv| tv.value).collect(); - if let Ok(body) = convert_type_expr(ty, &type_ops, &ctx.known_types) { + if let Ok(body) = convert_type_expr(ty, &type_ops) { ks.state.type_aliases.insert(name.value, (var_syms, body)); } } @@ -1815,26 +2413,42 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Collect standalone kind signatures: name → kind Type let mut standalone_kinds: HashMap = HashMap::new(); for decl in &module.decls { - if let Decl::Data { name, kind_sig, kind_type: Some(kind_ty), .. } = decl { + if let Decl::Data { + name, + kind_sig, + kind_type: Some(kind_ty), + .. + } = decl + { if *kind_sig != KindSigSource::None { // Check for quantification failures in the standalone kind sig - if let Some(e) = kind::check_standalone_kind_quantification(&mut ks, kind_ty, &type_ops) { + if let Some(e) = + kind::check_standalone_kind_quantification(&mut ks, kind_ty, &type_ops) + { errors.push(e); } - let k = kind::convert_kind_expr(kind_ty); + let k = ks.convert_kind_expr_canonical(kind_ty); standalone_kinds.insert(name.value, k.clone()); // Pre-register so other declarations can reference this type's kind ks.register_type(name.value, k); } } - if let Decl::Class { is_kind_sig: true, name, kind_type: Some(kind_ty), .. } = decl { + if let Decl::Class { + is_kind_sig: true, + name, + kind_type: Some(kind_ty), + .. + } = decl + { // Check for quantification failures in the standalone kind sig - if let Some(e) = kind::check_standalone_kind_quantification(&mut ks, kind_ty, &type_ops) { + if let Some(e) = + kind::check_standalone_kind_quantification(&mut ks, kind_ty, &type_ops) + { errors.push(e); } - let k = kind::convert_kind_expr(kind_ty); + let k = ks.convert_kind_expr_canonical(kind_ty); standalone_kinds.insert(name.value, k.clone()); - ks.register_type(name.value, k); + ks.register_class_kind(name.value, k); } } @@ -1843,9 +2457,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut pre_assigned: HashMap = HashMap::new(); for decl in &module.decls { match decl { - Decl::Data { name, kind_sig, is_role_decl, .. } - if *kind_sig == KindSigSource::None && !*is_role_decl => - { + Decl::Data { + name, + kind_sig, + is_role_decl, + .. + } if *kind_sig == KindSigSource::None && !*is_role_decl => { if !standalone_kinds.contains_key(&name.value) { let fresh = ks.fresh_kind_var(); pre_assigned.insert(name.value, fresh.clone()); @@ -1866,11 +2483,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ks.register_type(name.value, fresh); } } - Decl::Class { name, is_kind_sig, .. } if !*is_kind_sig => { + Decl::Class { + name, is_kind_sig, .. + } if !*is_kind_sig => { if !standalone_kinds.contains_key(&name.value) { let fresh = ks.fresh_kind_var(); pre_assigned.insert(name.value, fresh.clone()); - ks.register_type(name.value, fresh); + ks.register_class_kind(name.value, fresh); } } _ => {} @@ -1892,13 +2511,48 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { map }; - for decl in &module.decls { + // Build SCC-ordered declaration list: declarations that participate in kind + // inference are processed in SCC topological order (dependencies first) to ensure + // kinds are resolved before use. Other declarations keep their source order. + let scc_order: HashMap = { + let mut m = HashMap::new(); + for (i, scc) in sccs.iter().enumerate() { + for &name in scc { + m.insert(name, i); + } + } + m + }; + let mut decl_indices: Vec = (0..module.decls.len()).collect(); + decl_indices.sort_by_key(|&i| { + let decl = &module.decls[i]; + let name = match decl { + Decl::Data { name, kind_sig, is_role_decl, .. } + if *kind_sig == KindSigSource::None && !*is_role_decl => Some(name.value), + Decl::Newtype { name, .. } => Some(name.value), + Decl::TypeAlias { name, .. } => Some(name.value), + Decl::Class { name, is_kind_sig, .. } if !*is_kind_sig => Some(name.value), + _ => None, + }; + // SCC-participating decls get their SCC index; others go to the end + name.and_then(|n| scc_order.get(&n).copied()).unwrap_or(usize::MAX) + }); + + for &decl_idx in &decl_indices { + let decl = &module.decls[decl_idx]; // Set binding group for the current declaration's SCC let decl_name = match decl { - Decl::Data { name, kind_sig, is_role_decl, .. } if *kind_sig == KindSigSource::None && !*is_role_decl => Some(name.value), + Decl::Data { + name, + kind_sig, + is_role_decl, + .. + } if *kind_sig == KindSigSource::None && !*is_role_decl => Some(name.value), Decl::Newtype { name, .. } => Some(name.value), Decl::TypeAlias { name, .. } => Some(name.value), - Decl::Class { name, is_kind_sig, .. } if !*is_kind_sig => Some(name.value), + Decl::Class { + name, is_kind_sig, .. + } if !*is_kind_sig => Some(name.value), _ => None, }; if let Some(dn) = decl_name { @@ -1908,19 +2562,51 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } match decl { - Decl::Data { span, name, type_vars, constructors, kind_sig, is_role_decl, kind_type: _, type_var_kind_anns } => { + Decl::Data { + span, + name, + type_vars, + constructors, + kind_sig, + is_role_decl, + kind_type: _, + type_var_kind_anns, + } => { if *kind_sig != KindSigSource::None || *is_role_decl { continue; } // Check type var kind annotations for partially applied synonyms let mut has_pas = false; for ann in type_var_kind_anns.iter().flatten() { - if let Err(e) = kind::check_type_expr_partial_synonym(ann, &ks.state.type_aliases, &type_ops) { + if let Err(e) = kind::check_type_expr_partial_synonym( + ann, + &ks.state.type_aliases, + &type_ops, + ) { errors.push(e); has_pas = true; } } - if has_pas { continue; } + if has_pas { + continue; + } + // Check constructor field types for directly partially applied synonyms + // (e.g. `data X = Y S` where `type S a = D a`). + for ctor in constructors.iter() { + for field in &ctor.fields { + if let Some(e) = check_field_partially_applied_synonym( + field, + &ks.state.type_aliases, + &type_ops, + ) { + errors.push(e); + has_pas = true; + } + } + } + if has_pas { + continue; + } match kind::infer_data_kind( &mut ks, @@ -1939,11 +2625,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { errors.push(e); } // Also check with skolemized kinds for data types - let field_refs: Vec<&crate::cst::TypeExpr> = constructors.iter() - .flat_map(|c| c.fields.iter()) - .collect(); + let field_refs: Vec<&crate::ast::TypeExpr> = + constructors.iter().flat_map(|c| c.fields.iter()).collect(); if let Some(e) = kind::check_body_against_standalone_kind( - &mut ks, standalone, type_vars, &field_refs, name.value, *span, &type_ops, + &mut ks, + standalone, + type_vars, + &field_refs, + name.value, + *span, + &type_ops, ) { errors.push(e); } @@ -1956,16 +2647,43 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { Err(e) => errors.push(e), } } - Decl::Newtype { span, name, type_vars, ty, type_var_kind_anns, .. } => { + Decl::Newtype { + span, + name, + type_vars, + ty, + type_var_kind_anns, + .. + } => { // Check type var kind annotations for partially applied synonyms let mut has_pas = false; for ann in type_var_kind_anns.iter().flatten() { - if let Err(e) = kind::check_type_expr_partial_synonym(ann, &ks.state.type_aliases, &type_ops) { + if let Err(e) = kind::check_type_expr_partial_synonym( + ann, + &ks.state.type_aliases, + &type_ops, + ) { errors.push(e); has_pas = true; } } - if has_pas { continue; } + if has_pas { + continue; + } + // Check if the newtype field is directly a partially applied synonym + // (e.g. `newtype N = N S` where `type S a = D a` — S is missing its argument). + // Must run BEFORE kind inference to produce the specific + // PartiallyAppliedSynonym error instead of the generic KindsDoNotUnify. + // Only check the outermost type — nested partial apps (like `F ((~>) Array)`) + // should be reported as KindsDoNotUnify by the kind checker. + if let Some(e) = check_field_partially_applied_synonym( + ty, + &ks.state.type_aliases, + &type_ops, + ) { + errors.push(e); + continue; + } match kind::infer_newtype_kind( &mut ks, name.value, @@ -1983,7 +2701,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Also check with skolemized kinds to detect over-constraining if let Some(e) = kind::check_body_against_standalone_kind( - &mut ks, standalone, type_vars, &[ty], name.value, *span, &type_ops, + &mut ks, + standalone, + type_vars, + &[ty], + name.value, + *span, + &type_ops, ) { errors.push(e); } @@ -1996,16 +2720,28 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { Err(e) => errors.push(e), } } - Decl::TypeAlias { span, name, type_vars, ty, type_var_kind_anns } => { + Decl::TypeAlias { + span, + name, + type_vars, + ty, + type_var_kind_anns, + } => { // Check type var kind annotations for partially applied synonyms let mut has_pas = false; for ann in type_var_kind_anns.iter().flatten() { - if let Err(e) = kind::check_type_expr_partial_synonym(ann, &ks.state.type_aliases, &type_ops) { + if let Err(e) = kind::check_type_expr_partial_synonym( + ann, + &ks.state.type_aliases, + &type_ops, + ) { errors.push(e); has_pas = true; } } - if has_pas { continue; } + if has_pas { + continue; + } match kind::infer_type_alias_kind( &mut ks, name.value, @@ -2016,32 +2752,62 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { &type_ops, ) { Ok(inferred) => { - if let Some(pre) = pre_assigned.get(&name.value) { + if let Some(standalone) = standalone_kinds.get(&name.value) { + // Unify with standalone kind signature + let inst = kind::instantiate_kind(&mut ks, standalone); + if let Err(e) = ks.unify_kinds(*span, &inst, &inferred) { + errors.push(e); + } + } else if let Some(pre) = pre_assigned.get(&name.value) { // Silently ignore kind unification failures for aliases let _ = ks.unify_kinds(*span, pre, &inferred); + // Register the inferred kind so importing modules + // can use it for kind checking. + let zonked_kind = ks.zonk_kind(inferred); + ks.register_type(name.value, zonked_kind); + } else { + let zonked_kind = ks.zonk_kind(inferred); + ks.register_type(name.value, zonked_kind); } } Err(e) => errors.push(e), } + // Clear any deferred quantification checks that accumulated + // from foralls inside the alias body (e.g. `type Foo r = ∀ s. ...`). + // These don't need quantification checking — alias bodies are + // transparent and their forall kind vars are constrained by usage. + // Without this, they leak into the next data/newtype's check. + ks.deferred_quantification_checks.clear(); } - Decl::Class { span, name, type_vars, members, is_kind_sig, type_var_kind_anns, .. } => { - if *is_kind_sig { continue; } + Decl::Class { + span, + name, + type_vars, + members, + is_kind_sig, + type_var_kind_anns, + .. + } => { + if *is_kind_sig { + continue; + } // Check type var kind annotations for partially applied synonyms let mut has_pas = false; for ann in type_var_kind_anns.iter().flatten() { - if let Err(e) = kind::check_type_expr_partial_synonym(ann, &ks.state.type_aliases, &type_ops) { + if let Err(e) = kind::check_type_expr_partial_synonym( + ann, + &ks.state.type_aliases, + &type_ops, + ) { errors.push(e); has_pas = true; } } - if has_pas { continue; } + if has_pas { + continue; + } match kind::infer_class_kind( - &mut ks, - name.value, - type_vars, - members, - *span, - &type_ops, + &mut ks, name.value, type_vars, members, *span, &type_ops, ) { Ok(inferred) => { if let Some(pre) = pre_assigned.get(&name.value) { @@ -2060,39 +2826,166 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Pass C: Kind-check usage sites — type signatures and instance heads. - // Uses a temporary KindState per check to avoid cross-contamination. - for decl in &module.decls { - match decl { - Decl::TypeSignature { ty, .. } => { - // Check kind annotations inside the type for partially applied synonyms - if let Err(e) = kind::check_kind_annotations_for_partial_synonym(ty, &ks.state.type_aliases, &type_ops) { - errors.push(e); - } else if let Err(e) = kind::check_type_expr_kind(&mut ks, ty, &type_ops) { - errors.push(e); + // We use the main `ks` directly (no temp state clone). This is safe because + // lookup_type_fresh freshens all unsolved vars into new IDs, so Pass C's + // kind unification can never solve the original vars from Passes A/B. + // Save deferred quantification checks from Passes A/B since Pass C will + // add its own (from forall vars in type annotations) which are not relevant. + let saved_deferred = std::mem::take(&mut ks.deferred_quantification_checks); + let saved_class_param_kinds = ks.class_param_kind_types.clone(); + { + let empty_var_kinds: HashMap = HashMap::new(); + let k_type = Type::kind_type(); + for decl in &module.decls { + match decl { + Decl::TypeSignature { ty, .. } => { + if let Err(e) = kind::check_kind_annotations_for_partial_synonym( + ty, + &ks.state.type_aliases, + &type_ops, + ) { + errors.push(e); + } else { + match kind::infer_kind(&mut ks, ty, &empty_var_kinds, &type_ops, None) { + Ok(kind) => { + let zonked = ks.zonk_kind(kind); + if zonked != k_type && !matches!(zonked, Type::Unif(_)) { + errors.push(TypeError::ExpectedType { + span: ty.span(), + found: zonked, + }); + } + } + Err(e) => errors.push(e), + } + } } - } - Decl::Instance { span, class_name, types, .. } | - Decl::Derive { span, class_name, types, .. } => { - if let Err(e) = kind::check_instance_head_kinds(&mut ks, class_name.name, types, *span, &type_ops) { - errors.push(e); + Decl::Instance { + span, + class_name, + types, + .. } + | Decl::Derive { + span, + class_name, + types, + .. + } => { + let class_kind = match ks.lookup_class_kind_fresh(class_name.name) { + Some(k) => kind::instantiate_kind(&mut ks, &k), + None => continue, + }; + let mut remaining_kind = class_kind; + let mut had_error = false; + for ty_expr in types.iter() { + let arg_kind = match kind::infer_kind(&mut ks, ty_expr, &empty_var_kinds, &type_ops, None) { + Ok(k) => k, + Err(e) => { errors.push(e); had_error = true; break; } + }; + let result_kind = ks.fresh_kind_var(); + let expected = Type::fun(arg_kind, result_kind.clone()); + if let Err(e) = ks.unify_kinds(*span, &expected, &remaining_kind) { + errors.push(e); had_error = true; break; + } + remaining_kind = result_kind; + } + let _ = had_error; + } + Decl::Value { + binders, + guarded, + where_clause, + .. + } => { + let mut type_exprs = Vec::new(); + for b in binders.iter() { + kind::collect_type_exprs_from_binder(b, &mut type_exprs); + } + kind::collect_type_exprs_from_guarded(guarded, &mut type_exprs); + for lb in where_clause.iter() { + kind::collect_type_exprs_from_let_binding(lb, &mut type_exprs); + } + for te in type_exprs { + match kind::infer_kind(&mut ks, te, &empty_var_kinds, &type_ops, None) { + Ok(kind) => { + let zonked = ks.zonk_kind(kind); + if zonked != k_type && !matches!(zonked, Type::Unif(_)) { + errors.push(TypeError::ExpectedType { + span: te.span(), + found: zonked, + }); + } + } + Err(e) => errors.push(e), + } + } + } + _ => {} } - Decl::Value { binders, guarded, where_clause, .. } => { - errors.extend(kind::check_value_decl_kinds(&mut ks, binders, guarded, where_clause, &type_ops)); - } - _ => {} } } + // Restore deferred quantification checks from Passes A/B, discarding + // any checks accumulated during Pass C (those are for usage-site foralls + // which don't need quantification checking). + ks.deferred_quantification_checks = saved_deferred; + ks.class_param_kind_types = saved_class_param_kinds; + + // Run deferred quantification checks now that ALL kind vars are maximally + // constrained. This catches forall vars with ambiguous kinds (e.g. + // `data P = P (forall a. Proxy a)` where a's kind is undetermined). + // Running at the end avoids false positives from forall vars that reference + // types whose kinds aren't yet fully inferred during per-declaration checking. + if let Some(err) = ks.check_deferred_quantification() { + errors.push(err); + } // Save kind information for post-inference kind checking. // Zonk kinds using the kind pass state to resolve solved vars. - saved_type_kinds = ks.type_kinds.iter() - .map(|(&name, kind)| (name, ks.state.zonk(kind.clone()))) + saved_type_kinds = ks + .type_kinds + .iter() + .map(|(&name, kind)| (qi(name), ks.state.zonk(kind.clone()))) .collect(); + saved_class_kinds = ks + .class_kinds + .iter() + .map(|(&name, kind)| (qi(name), ks.state.zonk(kind.clone()))) + .collect(); + } + + let module_name = module.name.value + .parts + .iter() + .map(|p| crate::interner::resolve(*p).unwrap_or_default()) + .collect::>() + .join("."); + let timed_module = std::env::var("TIME_MODULE").unwrap_or_default(); + // macro for only when when module name is the module name from env var + macro_rules! timed_pass { + ($pass_num:expr, $pass_desc:expr, $span:expr) => { + if timed_module == module_name { + eprintln!("[TIMING] {} pass {} ({}) at {} {:?}", module_name, $pass_num, $pass_desc, $span, _module_start.elapsed()); + } + }; } + timed_pass!(0, "start", ""); + // Pre-scan: collect newtype names so derive statements that appear before + // their corresponding newtype declaration (common in PureScript) work correctly. + for decl in &module.decls { + if let Decl::Newtype { name, .. } = decl { + ctx.newtype_names.insert(qi(name.value)); + } + } // Pass 1: Collect type signatures and data constructors for decl in &module.decls { + super::check_deadline(); + let decl_name = match decl.name() { + Some(n) => crate::interner::resolve(n).unwrap_or_default(), + None => "".to_string(), + }; + timed_pass!(1, format!("start decl {}", decl_name), decl.span()); match decl { Decl::TypeSignature { span, name, ty } => { if signatures.contains_key(&name.value) { @@ -2108,28 +3001,37 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Check for Partial in function parameter (discharges Partial constraint) if has_partial_in_function_param(ty) { - ctx.partial_dischargers.insert(name.value); + ctx.partial_dischargers.insert(qi(name.value)); } // Check for undefined type variables (all vars must be bound by forall) collect_type_expr_vars(ty, &HashSet::new(), &mut errors); // Validate constraint class names in the type signature check_constraint_class_names(ty, &known_classes, &class_param_counts, &mut errors); - match convert_type_expr(ty, &type_ops, &ctx.known_types) { + match convert_type_expr(ty, &type_ops) { Ok(converted) => { // Check for partially applied synonyms in type signature - check_type_for_partial_synonyms_with_arities(&converted, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + &converted, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); // Replace wildcard `_` with fresh unification variables so // signatures like `main :: Effect _` work correctly. let converted = ctx.instantiate_wildcards(&converted); signatures.insert(name.value, (*span, converted)); // Extract constraints from the type signature for propagation // to call sites (e.g., Lacks "x" r => ...) - let sig_constraints = extract_type_signature_constraints(ty, &type_ops, &ctx.known_types); + let sig_constraints = + extract_type_signature_constraints(ty, &type_ops); if !sig_constraints.is_empty() { // Check for Fail constraints — these are deliberately unsatisfiable // and should always produce NoInstanceFound at the definition site. for (class_name, type_args) in &sig_constraints { - let cn = crate::interner::resolve(*class_name).unwrap_or_default(); + let cn = + crate::interner::resolve(class_name.name).unwrap_or_default(); if cn == "Fail" { errors.push(TypeError::NoInstanceFound { span: *span, @@ -2138,7 +3040,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } } - ctx.signature_constraints.insert(name.value, sig_constraints); + ctx.signature_constraints + .insert(qi(name.value), sig_constraints); } } Err(e) => errors.push(e), @@ -2171,7 +3074,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let type_var_syms: Vec = type_vars.iter().map(|tv| tv.value).collect(); // Build the result type: TypeName tv1 tv2 ... - let mut result_type = Type::Con(name.value); + let mut result_type = Type::Con(qi(name.value)); for &tv in &type_var_syms { result_type = Type::app(result_type, Type::Var(tv)); } @@ -2179,16 +3082,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Register constructors for exhaustiveness checking. // Skip if this is a kind/role annotation (empty constructors) and // the type already has constructors registered (e.g. from a Newtype). - let ctor_names: Vec = constructors.iter().map(|c| c.name.value).collect(); - if !ctor_names.is_empty() || !ctx.data_constructors.contains_key(&name.value) { - ctx.data_constructors.insert(name.value, ctor_names); + let ctor_names: Vec = + constructors.iter().map(|c| qi(c.name.value)).collect(); + if !ctor_names.is_empty() || !ctx.data_constructors.contains_key(&qi(name.value)) { + ctx.data_constructors.insert(qi(name.value), ctor_names); } for ctor in constructors { // Reject type wildcards in data constructor fields for f in &ctor.fields { if let Some(wc_span) = find_wildcard_span(f) { - errors.push(TypeError::WildcardInTypeDefinition { span: wc_span }); + errors.push(TypeError::SyntaxError { span: wc_span }); } } @@ -2196,7 +3100,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let field_results: Vec> = ctor .fields .iter() - .map(|f| convert_type_expr(f, &type_ops, &ctx.known_types)) + .map(|f| convert_type_expr(f, &type_ops)) .collect(); // If any field type fails, record the error and skip this constructor @@ -2218,13 +3122,20 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check for partially applied synonyms in field types for field_ty in &field_types { - check_type_for_partial_synonyms_with_arities(field_ty, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + field_ty, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); } // Save field types for nested exhaustiveness checking ctx.ctor_details.insert( - ctor.name.value, - (name.value, type_var_syms.clone(), field_types.clone()), + qi(ctor.name.value), + (qi(name.value), type_var_syms.clone(), field_types.clone()), ); let mut ctor_ty = result_type.clone(); @@ -2235,7 +3146,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Wrap in Forall if there are type variables // Data constructor type vars are always visible for VTA if !type_var_syms.is_empty() { - ctor_ty = Type::Forall(type_var_syms.iter().map(|&v| (v, true)).collect(), Box::new(ctor_ty)); + ctor_ty = Type::Forall( + type_var_syms.iter().map(|&v| (v, true)).collect(), + Box::new(ctor_ty), + ); } let scheme = Scheme::mono(ctor_ty); @@ -2257,35 +3171,49 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { check_duplicate_type_args(type_vars, &mut errors); // Track as a newtype for derive validation and Coercible solving - ctx.newtype_names.insert(name.value); + ctx.newtype_names.insert(qi(name.value)); // Register constructor for exhaustiveness checking ctx.data_constructors - .insert(name.value, vec![constructor.value]); + .insert(qi(name.value), vec![qi(constructor.value)]); let type_var_syms: Vec = type_vars.iter().map(|tv| tv.value).collect(); - let mut result_type = Type::Con(name.value); + let mut result_type = Type::Con(qi(name.value)); for &tv in &type_var_syms { result_type = Type::app(result_type, Type::Var(tv)); } - match convert_type_expr(ty, &type_ops, &ctx.known_types) { + match convert_type_expr(ty, &type_ops) { Ok(field_ty) => { // Check for partially applied synonyms in field type - check_type_for_partial_synonyms_with_arities(&field_ty, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + &field_ty, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); // Save field type for nested exhaustiveness checking ctx.ctor_details.insert( - constructor.value, - (name.value, type_var_syms.clone(), vec![field_ty.clone()]), + qi(constructor.value), + ( + qi(name.value), + type_var_syms.clone(), + vec![field_ty.clone()], + ), ); let mut ctor_ty = Type::fun(field_ty, result_type); // Newtype constructor type vars are always visible for VTA if !type_var_syms.is_empty() { - ctor_ty = Type::Forall(type_var_syms.iter().map(|&v| (v, true)).collect(), Box::new(ctor_ty)); + ctor_ty = Type::Forall( + type_var_syms.iter().map(|&v| (v, true)).collect(), + Box::new(ctor_ty), + ); } let scheme = Scheme::mono(ctor_ty); @@ -2310,9 +3238,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { collect_type_expr_vars(ty, &HashSet::new(), &mut errors); // Reject constraints in foreign import types if let Some(c_span) = has_any_constraint(ty) { - errors.push(TypeError::ConstraintInForeignImport { span: c_span }); + errors.push(TypeError::SyntaxError { span: c_span }); } - match convert_type_expr(ty, &type_ops, &ctx.known_types) { + match convert_type_expr(ty, &type_ops) { Ok(converted) => { let scheme = Scheme::mono(converted); env.insert_scheme(name.value, scheme.clone()); @@ -2333,7 +3261,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } => { // Track kind signatures vs real definitions for orphan detection if *is_kind_sig { - kind_sigs.entry(name.value).or_insert((*span, KindSigSource::Class)); + kind_sigs + .entry(name.value) + .or_insert((*span, KindSigSource::Class)); } else { has_real_definition.insert(name.value); has_class_def.insert(name.value); @@ -2357,46 +3287,62 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for constraint in constraints { for arg in &constraint.args { if let Some(bad_span) = has_forall_or_wildcard(arg) { - errors.push(TypeError::InvalidConstraintArgument { span: bad_span }); + errors + .push(TypeError::SyntaxError { span: bad_span }); } } } // Track superclass constraints with converted type args for instance validation let tvs: Vec = type_vars.iter().map(|tv| tv.value).collect(); - let mut sc_constraints = Vec::new(); + let mut sc_constraints: Vec<(QualifiedIdent, Vec)> = Vec::new(); for constraint in constraints { let mut sc_args = Vec::new(); let mut ok = true; for arg in &constraint.args { - match convert_type_expr(arg, &type_ops, &ctx.known_types) { + match convert_type_expr(arg, &type_ops) { Ok(ty) => sc_args.push(ty), - Err(_) => { ok = false; break; } + Err(_) => { + ok = false; + break; + } } } if ok { - sc_constraints.push((constraint.class.name, sc_args)); + sc_constraints.push((constraint.class, sc_args)); } } - class_superclasses.insert(name.value, (tvs.clone(), sc_constraints)); + class_superclasses.insert(qi(name.value), (tvs.clone(), sc_constraints)); // Store functional dependencies as index pairs for orphan checking if !fundeps.is_empty() { - let fd_indices: Vec<(Vec, Vec)> = fundeps.iter().filter_map(|fd| { - let lhs: Vec = fd.lhs.iter().filter_map(|v| tvs.iter().position(|tv| tv == v)).collect(); - let rhs: Vec = fd.rhs.iter().filter_map(|v| tvs.iter().position(|tv| tv == v)).collect(); - if lhs.len() == fd.lhs.len() && rhs.len() == fd.rhs.len() { - Some((lhs, rhs)) - } else { - None - } - }).collect(); - ctx.class_fundeps.insert(name.value, (tvs.clone(), fd_indices)); + let fd_indices: Vec<(Vec, Vec)> = fundeps + .iter() + .filter_map(|fd| { + let lhs: Vec = fd + .lhs + .iter() + .filter_map(|v| tvs.iter().position(|tv| tv == v)) + .collect(); + let rhs: Vec = fd + .rhs + .iter() + .filter_map(|v| tvs.iter().position(|tv| tv == v)) + .collect(); + if lhs.len() == fd.lhs.len() && rhs.len() == fd.rhs.len() { + Some((lhs, rhs)) + } else { + None + } + }) + .collect(); + ctx.class_fundeps + .insert(qi(name.value), (tvs.clone(), fd_indices)); } // Track class type parameter count for arity checking - class_param_counts.insert(name.value, type_vars.len()); - known_classes.insert(name.value); + class_param_counts.insert(qi(name.value), type_vars.len()); + known_classes.insert(qi(name.value)); } // Check for duplicate type arguments @@ -2412,13 +3358,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Check that superclass is a known class if constraint.class.module.is_none() { - let sc_known = class_param_counts.contains_key(&constraint.class.name) - || instances.contains_key(&constraint.class.name) + let sc_known = class_param_counts.contains_key(&constraint.class) + || instances.contains_key(&constraint.class) || local_class_names.contains(&constraint.class.name); if !sc_known { errors.push(TypeError::UnknownClass { span: *span, - name: constraint.class.name, + name: constraint.class, }); } } @@ -2434,20 +3380,29 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if has_any_constraint(&member.ty).is_some() { ctx.constrained_class_methods.insert(member.name.value); } - match convert_type_expr(&member.ty, &type_ops, &ctx.known_types) { + match convert_type_expr(&member.ty, &type_ops) { Ok(member_ty) => { // Class header type vars are always visible for VTA let scheme_ty = if !type_var_syms.is_empty() { - Type::Forall(type_var_syms.iter().map(|&v| (v, true)).collect(), Box::new(member_ty)) + Type::Forall( + type_var_syms.iter().map(|&v| (v, true)).collect(), + Box::new(member_ty), + ) } else { member_ty }; let scheme = Scheme::mono(scheme_ty); env.insert_scheme(member.name.value, scheme.clone()); local_values.insert(member.name.value, scheme.clone()); + // Save the canonical scheme so instance method expected-type + // lookup can use it without being affected by later value + // imports that shadow the same name in the env. + ctx.class_method_schemes.insert(member.name.value, scheme.clone()); // Track that this method belongs to this class - ctx.class_methods - .insert(member.name.value, (name.value, type_var_syms.clone())); + ctx.class_methods.insert( + qi(member.name.value), + (qi(name.value), type_var_syms.clone()), + ); } Err(e) => errors.push(e), } @@ -2484,7 +3439,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut inst_types = Vec::new(); let mut inst_ok = true; for ty_expr in types { - match convert_type_expr(ty_expr, &type_ops, &ctx.known_types) { + match convert_type_expr(ty_expr, &type_ops) { Ok(ty) => inst_types.push(ty), Err(e) => { errors.push(e); @@ -2495,11 +3450,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Check instance arity matches class parameter count if inst_ok { - if let Some(&expected_count) = class_param_counts.get(&class_name.name) { - if inst_types.len() != expected_count { + if let Some(&expected_count) = class_param_counts.get(class_name) { + if expected_count != usize::MAX && inst_types.len() != expected_count { errors.push(TypeError::ClassInstanceArityMismatch { span: *span, - class_name: class_name.name, + class_name: *class_name, expected: expected_count, found: inst_types.len(), }); @@ -2520,11 +3475,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } // Check for non-nominal types in instance heads: type synonyms that - // expand to open records (with row variables) or functions are invalid. - // Synonyms expanding to closed records are fine (they're nominal). + // expand to open records are invalid. Function type aliases (e.g. + // Fn1 a b = a -> b) are allowed since they serve as nominal types + // in practice (the real compiler uses foreign import data for these). if inst_ok { for inst_ty in &inst_types { - if is_non_nominal_instance_head(inst_ty, &ctx.state.type_aliases) { + if is_non_nominal_instance_head_record_only(inst_ty, &ctx.state.type_aliases) { errors.push(TypeError::InvalidInstanceHead { span: *span }); inst_ok = false; break; @@ -2534,7 +3490,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check for partially applied synonyms in instance types if inst_ok { for inst_ty in &inst_types { - check_type_for_partial_synonyms_with_arities(inst_ty, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + inst_ty, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); } } // Validate constraint arguments: reject forall and wildcards @@ -2542,22 +3505,25 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for constraint in constraints { for arg in &constraint.args { if let Some(bad_span) = has_forall_or_wildcard(arg) { - errors.push(TypeError::InvalidConstraintArgument { span: bad_span }); + errors + .push(TypeError::SyntaxError { span: bad_span }); inst_ok = false; break; } } - if !inst_ok { break; } + if !inst_ok { + break; + } } } // Convert constraints (e.g., `A a =>` part) - let mut inst_constraints = Vec::new(); + let mut inst_constraints: Vec<(QualifiedIdent, Vec)> = Vec::new(); if inst_ok { for constraint in constraints { let mut c_args = Vec::new(); let mut c_ok = true; for arg in &constraint.args { - match convert_type_expr(arg, &type_ops, &ctx.known_types) { + match convert_type_expr(arg, &type_ops) { Ok(ty) => c_args.push(ty), Err(e) => { errors.push(e); @@ -2568,7 +3534,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } if c_ok { - inst_constraints.push((constraint.class.name, c_args)); + inst_constraints.push((constraint.class, c_args)); } } } @@ -2577,15 +3543,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut vars = Vec::new(); fn collect_vars_from_type(ty: &Type, vars: &mut Vec) { match ty { - Type::Var(v) => { if !vars.contains(v) { vars.push(*v); } } + Type::Var(v) => { + if !vars.contains(v) { + vars.push(*v); + } + } Type::Fun(a, b) | Type::App(a, b) => { collect_vars_from_type(a, vars); collect_vars_from_type(b, vars); } Type::Forall(_, body) => collect_vars_from_type(body, vars), Type::Record(fields, tail) => { - for (_, t) in fields { collect_vars_from_type(t, vars); } - if let Some(t) = tail { collect_vars_from_type(t, vars); } + for (_, t) in fields { + collect_vars_from_type(t, vars); + } + if let Some(t) = tail { + collect_vars_from_type(t, vars); + } } _ => {} } @@ -2598,15 +3572,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { vars }; // Check if the class is known (either via param counts or instances) - let class_known = class_param_counts.contains_key(&class_name.name) - || instances.contains_key(&class_name.name) + let class_known = class_param_counts.contains_key(&class_name) + || instances.contains_key(&class_name) || local_class_names.contains(&class_name.name); // If the class doesn't exist at all, report it if inst_ok && !class_known && class_name.module.is_none() { errors.push(TypeError::UnknownClass { span: *span, - name: class_name.name, + name: *class_name, }); } @@ -2618,12 +3592,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let class_is_local = local_class_names.contains(&class_name.name); if !class_is_local { let is_orphan = check_orphan_with_fundeps( - &inst_types, &class_name.name, &ctx.class_fundeps, &local_type_names, + &inst_types, + &class_name, + &ctx.class_fundeps, + &local_type_names, ); if is_orphan { errors.push(TypeError::OrphanInstance { span: *span, - class_name: class_name.name, + class_name: *class_name, }); } } @@ -2633,16 +3610,18 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Row/record types can only appear at positions that are fully determined // by other positions via functional dependencies. if inst_ok { - let has_row: Vec = inst_types.iter() + let has_row: Vec = inst_types + .iter() .map(|ty| type_contains_record(ty)) .collect(); if has_row.iter().any(|&x| x) { - let covering_sets = if let Some((_, fds)) = ctx.class_fundeps.get(&class_name.name) { - compute_covering_sets(inst_types.len(), fds) - } else { - // No fundeps: the only covering set is all positions - vec![(0..inst_types.len()).collect()] - }; + let covering_sets = + if let Some((_, fds)) = ctx.class_fundeps.get(class_name) { + compute_covering_sets(inst_types.len(), fds) + } else { + // No fundeps: the only covering set is all positions + vec![(0..inst_types.len()).collect()] + }; for (idx, &is_row) in has_row.iter().enumerate() { if is_row { // Row type is invalid if this position is in ANY covering set @@ -2659,11 +3638,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Build substitution from class type vars → instance types for method type checking. // Must be computed before inst_types is moved into instances. let inst_subst: HashMap = if inst_ok { - let class_tvs: Option<&Vec> = ctx.class_methods.values() - .find(|(cn, _)| *cn == class_name.name) + let class_tvs: Option<&Vec> = ctx + .class_methods + .values() + .find(|(cn, _)| *cn == *class_name) .map(|(_, tvs)| tvs); if let Some(tvs) = class_tvs { - tvs.iter().zip(inst_types.iter()).map(|(tv, ty)| (*tv, ty.clone())).collect() + tvs.iter() + .zip(inst_types.iter()) + .map(|(tv, ty)| (*tv, ty.clone())) + .collect() } else { HashMap::new() } @@ -2688,16 +3672,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if type_exprs_alpha_eq_list(types, existing_cst) { errors.push(TypeError::OverlappingInstances { span: *span, - class_name: class_name.name, + class_name: *class_name, type_args: inst_types.clone(), }); found_overlap = true; break; } - } else if instance_heads_overlap(&inst_types, existing_types, &ctx.state.type_aliases) { + } else if instance_heads_overlap( + &inst_types, + existing_types, + &ctx.state.type_aliases, + &local_data_type_names, + ) { errors.push(TypeError::OverlappingInstances { span: *span, - class_name: class_name.name, + class_name: *class_name, type_args: inst_types.clone(), }); found_overlap = true; @@ -2716,16 +3705,24 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { && !local_class_names.contains(&class_name.name) && inst_types.iter().all(|t| !type_has_vars(t)) { - if let Some(imported) = instances.get(&class_name.name) { + if let Some(imported) = lookup_instances(&instances, &class_name) { for (existing_types, _) in imported { // Skip if the imported instance uses a type constructor with the // same name as a locally-defined type — they're actually different // types from different modules that happen to share a short name. let imported_shadows_local = existing_types.iter().any(|t| { - fn has_local_con(ty: &Type, locals: &HashSet) -> bool { + fn has_local_con( + ty: &Type, + locals: &HashSet, + ) -> bool { match ty { - Type::Con(n) => locals.contains(n), - Type::App(f, a) => has_local_con(f, locals) || has_local_con(a, locals), + Type::Con(n) => { + n.module == None && locals.contains(&n.name) + } + Type::App(f, a) => { + has_local_con(f, locals) + || has_local_con(a, locals) + } _ => false, } } @@ -2734,10 +3731,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if imported_shadows_local { continue; } - if instance_heads_overlap(&inst_types, existing_types, &ctx.state.type_aliases) { + if instance_heads_overlap( + &inst_types, + existing_types, + &ctx.state.type_aliases, + &local_data_type_names, + ) { errors.push(TypeError::OverlappingInstances { span: *span, - class_name: class_name.name, + class_name: *class_name, type_args: inst_types.clone(), }); break; @@ -2751,31 +3753,45 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .or_default() .push((inst_types.clone(), has_kind_ann, types.clone())); registered_instances.push((*span, class_name.name, inst_types.clone())); + // Store instances with unqualified class name key. + // Class names may have import alias qualifiers (e.g. Filterable.Filterable) + // but internal maps should use unqualified keys. + let unqual_class = qi(class_name.name); instances - .entry(class_name.name) + .entry(unqual_class) .or_default() .push((inst_types, inst_constraints)); if *is_chain { - chained_classes.insert(class_name.name); - ctx.chained_classes.insert(class_name.name); + chained_classes.insert(unqual_class); + ctx.chained_classes.insert(unqual_class); } } // Check for missing/extraneous class members in this instance { // Collect method names expected for this class - let expected_methods: Vec = ctx.class_methods.iter() - .filter(|(_, (cn, _))| *cn == class_name.name) - .map(|(method, _)| *method) + let expected_methods: Vec = ctx + .class_methods + .iter() + .filter(|(_, (cn, _))| *cn == *class_name) + .map(|(method, _)| method.name) .collect(); // Collect method names provided in this instance let mut provided_methods: HashSet = HashSet::new(); let mut provided_method_spans: HashMap> = HashMap::new(); for member_decl in members.iter() { - if let Decl::Value { name: mname, span: mspan, .. } = member_decl { + if let Decl::Value { + name: mname, + span: mspan, + .. + } = member_decl + { provided_methods.insert(mname.value); - provided_method_spans.entry(mname.value).or_default().push(*mspan); + provided_method_spans + .entry(mname.value) + .or_default() + .push(*mspan); } } @@ -2816,25 +3832,32 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !expected_methods.is_empty() && !provided_methods.is_empty() { for method_name in &provided_methods { if !expected_methods.contains(method_name) { - errors.push(TypeError::ExtraneousClassMember { - span: *span, - class_name: class_name.name, - member_name: *method_name, - }); + // Only report as extraneous if this method is not a known + // class method at all. When two classes define the same method + // name (e.g. NumExpr.add and DataDSL.add), the class_methods + // map may be overwritten, causing expected_methods to miss the + // method. In that case, skip the error. + let is_known_class_method = ctx.class_methods.contains_key(&qi(*method_name)); + if !is_known_class_method { + errors.push(TypeError::ExtraneousClassMember { + span: *span, + class_name: *class_name, + member_name: *method_name, + }); + } } } // Check for missing members (expected but not provided) - let missing: Vec<(Symbol, Type)> = expected_methods.iter() + let missing: Vec<(Symbol, Type)> = expected_methods + .iter() .filter(|m| !provided_methods.contains(m)) - .filter_map(|m| { - env.lookup(*m).map(|scheme| (*m, scheme.ty.clone())) - }) + .filter_map(|m| env.lookup(*m).map(|scheme| (*m, scheme.ty.clone()))) .collect(); if !missing.is_empty() { errors.push(TypeError::MissingClassMember { span: *span, - class_name: class_name.name, + class_name: *class_name, members: missing, }); } @@ -2842,13 +3865,20 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Validate instance type signatures and detect orphans - let expected_methods: HashSet = ctx.class_methods.iter() - .filter(|(_, (cn, _))| *cn == class_name.name) - .map(|(method, _)| *method) + let expected_methods: HashSet = ctx + .class_methods + .iter() + .filter(|(_, (cn, _))| *cn == *class_name) + .map(|(method, _)| method.name) .collect(); for member_decl in members.iter() { - if let Decl::TypeSignature { name: sig_name, span: sig_span, .. } = member_decl { + if let Decl::TypeSignature { + name: sig_name, + span: sig_span, + .. + } = member_decl + { if !expected_methods.contains(&sig_name.value) { // Orphan type declaration inside instance — not a class method errors.push(TypeError::OrphanTypeSignature { @@ -2856,9 +3886,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { name: sig_name.value, }); } else if inst_ok && !inst_subst.is_empty() { - // Check that instance method signature matches the class-derived type - if let Some(scheme) = env.lookup(sig_name.value) { - let class_ty = scheme.ty.clone(); + // Check that instance method signature matches the class-derived type. + // Use class_method_schemes (not env.lookup) so that an explicit value + // import like `import Data.Array (foldl)` doesn't shadow the class + // method's canonical type here. + let canon = ctx.class_method_schemes.get(&sig_name.value) + .map(|s| s.ty.clone()) + .or_else(|| env.lookup(sig_name.value).map(|s| s.ty.clone())); + if let Some(class_ty) = canon { // Strip outer forall (class type vars) and substitute let inner = match &class_ty { Type::Forall(_, body) => (**body).clone(), @@ -2867,9 +3902,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let expected_ty = apply_var_subst(&inst_subst, &inner); // Convert the instance signature type if let Decl::TypeSignature { ty, .. } = member_decl { - if let Ok(sig_ty) = convert_type_expr(ty, &type_ops, &ctx.known_types) { + if let Ok(sig_ty) = + convert_type_expr(ty, &type_ops) + { // Unify the declared instance sig with the class-derived type - if let Err(e) = ctx.state.unify(*sig_span, &sig_ty, &expected_ty) { + if let Err(e) = + ctx.state.unify(*sig_span, &sig_ty, &expected_ty) + { errors.push(e); } } @@ -2885,15 +3924,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut inst_scoped_vars: HashSet = HashSet::new(); fn collect_free_vars_inst(ty: &Type, vars: &mut HashSet) { match ty { - Type::Var(v) => { vars.insert(*v); } + Type::Var(v) => { + vars.insert(*v); + } Type::Fun(a, b) | Type::App(a, b) => { collect_free_vars_inst(a, vars); collect_free_vars_inst(b, vars); } Type::Forall(_, body) => collect_free_vars_inst(body, vars), Type::Record(fields, tail) => { - for (_, t) in fields { collect_free_vars_inst(t, vars); } - if let Some(t) = tail { collect_free_vars_inst(t, vars); } + for (_, t) in fields { + collect_free_vars_inst(t, vars); + } + if let Some(t) = tail { + collect_free_vars_inst(t, vars); + } } _ => {} } @@ -2903,6 +3948,34 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } inst_scoped_vars.extend(constraint_scoped_vars.iter().copied()); + // Collect instance method type signatures for scoped type variables. + // When a method has an explicit annotation like `compare1 :: forall a. ...`, + // the forall-bound vars should be in scope in where-clause annotations. + let mut method_sig_vars: HashMap> = HashMap::new(); + for member_decl in members { + if let Decl::TypeSignature { name, ty, .. } = member_decl { + let mut vars = HashSet::new(); + fn collect_forall_vars_from_type_expr(ty: &TypeExpr, vars: &mut HashSet) { + match ty { + TypeExpr::Forall { vars: forall_vars, ty, .. } => { + for (v, _, _) in forall_vars { + vars.insert(v.value); + } + collect_forall_vars_from_type_expr(ty, vars); + } + TypeExpr::Constrained { ty, .. } => { + collect_forall_vars_from_type_expr(ty, vars); + } + _ => {} + } + } + collect_forall_vars_from_type_expr(ty, &mut vars); + if !vars.is_empty() { + method_sig_vars.insert(name.value, vars); + } + } + } + // Collect instance method bodies for deferred checking (after foreign imports // and fixity declarations are processed, so all values are in scope) let mut method_names: Vec = Vec::new(); @@ -2916,12 +3989,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .. } = member_decl { - // Compute the expected type for 0-binder methods from class definition. - // Only for 0-binder methods: with binders, pre-inserted monomorphic - // values and env shadowing can cause false unification failures. - let expected_ty = if inst_ok && !inst_subst.is_empty() && binders.is_empty() { - if let Some(scheme) = env.lookup(name.value) { - let class_ty = scheme.ty.clone(); + // Compute the expected type for instance methods from class definition. + // Use class_method_schemes (not env.lookup) so that an explicit value + // import like `import Data.Array (foldl)` doesn't shadow the class + // method's canonical type used for instance checking. + let expected_ty = if inst_ok && !inst_subst.is_empty() + { + let canon = ctx.class_method_schemes.get(&name.value) + .map(|s| s.ty.clone()) + .or_else(|| env.lookup(name.value).map(|s| s.ty.clone())); + if let Some(class_ty) = canon { // Strip outer forall (class type vars) let inner = match &class_ty { Type::Forall(_, body) => (**body).clone(), @@ -2940,9 +4017,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { None }; - let inst_given_classes: HashSet = constraints.iter() - .map(|c| c.class.name) - .collect(); + // Include forall-bound vars from the method's explicit type annotation + let mut method_scoped = inst_scoped_vars.clone(); + if let Some(sig_vars) = method_sig_vars.get(&name.value) { + method_scoped.extend(sig_vars); + } + + let inst_given_classes: HashSet = + constraints.iter().map(|c| c.class).collect(); method_names.push(name.value); deferred_instance_methods.push(( name.value, @@ -2951,11 +4033,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { guarded, where_clause as &[_], expected_ty, - inst_scoped_vars.clone(), + method_scoped, inst_given_classes, + next_instance_id, )); } } + next_instance_id += 1; if method_names.len() > 1 { instance_method_groups.push(method_names); } @@ -2980,14 +4064,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Reject type wildcards in type alias bodies if let Some(wc_span) = find_wildcard_span(ty) { - errors.push(TypeError::WildcardInTypeDefinition { span: wc_span }); + errors.push(TypeError::SyntaxError { span: wc_span }); } - // Convert and register type alias for expansion during unification - match convert_type_expr(ty, &type_ops, &ctx.known_types) { + // Convert and register type alias for expansion during unification. + match convert_type_expr(ty, &type_ops) { Ok(body_ty) => { // Check for partially applied synonyms in the body - check_type_for_partial_synonyms_with_arities(&body_ty, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + &body_ty, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); let params: Vec = type_vars.iter().map(|tv| tv.value).collect(); // Check that free type variables in the body are all declared parameters let param_set: HashSet = params.iter().copied().collect(); @@ -3003,7 +4094,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ctx.state.type_aliases.insert(name.value, (params, body_ty)); // Track if this is a record-kind alias (body is { } syntax, kind Type) if matches!(ty, TypeExpr::Record { .. }) { - ctx.record_type_aliases.insert(name.value); + ctx.record_type_aliases.insert(qi(name.value)); } } Err(e) => { @@ -3022,7 +4113,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { has_data_def.insert(name.value); // Register foreign data types in data_constructors so they can be imported // as types (e.g. `import Data.Unit (Unit)`). They have no constructors. - ctx.data_constructors.insert(name.value, Vec::new()); + ctx.data_constructors.insert(qi(name.value), Vec::new()); } Decl::Derive { span, @@ -3033,24 +4124,33 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .. } => { // Check if the class exists - let derive_class_known = class_param_counts.contains_key(&class_name.name) - || instances.contains_key(&class_name.name) + let derive_class_known = class_param_counts.contains_key(class_name) + || instances.contains_key(class_name) || local_class_names.contains(&class_name.name); if !derive_class_known && class_name.module.is_none() { errors.push(TypeError::UnknownClass { span: *span, - name: class_name.name, + name: *class_name, }); } // Check for invalid instance heads: bare record/row/function types - // at the top level of type arguments (wildcards are ok in derive, e.g. Newtype T _) + // at the top level of type arguments (wildcards are ok in derive, e.g. Newtype T _). + // For `derive newtype instance`, records and functions are allowed as class + // parameters (e.g. `derive newtype instance MonadAsk {} TestM`). + // For plain `derive instance`, records and functions are invalid. for ty_expr in types { - let is_record_or_row = matches!( - ty_expr, - TypeExpr::Record { .. } | TypeExpr::Row { .. } | TypeExpr::Function { .. } - ) || matches!(ty_expr, TypeExpr::Parens { ty, .. } if matches!(ty.as_ref(), TypeExpr::Record { .. } | TypeExpr::Row { .. } | TypeExpr::Function { .. })); - if is_record_or_row { + let is_invalid = if *newtype { + // derive newtype: only reject bare row types + matches!(ty_expr, TypeExpr::Row { .. }) + } else { + // derive: reject records, rows, and functions + matches!( + ty_expr, + TypeExpr::Record { .. } | TypeExpr::Row { .. } | TypeExpr::Function { .. } + ) + }; + if is_invalid { errors.push(TypeError::InvalidInstanceHead { span: *span }); break; } @@ -3060,14 +4160,31 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Try the last arg first (for multi-param classes like FunctorWithIndex Int NonEmptyArray, // the newtype is the last arg), then fall back to any arg with a constructor head // (e.g. `derive instance Newtype (Pair Int Int) _` where last arg is wildcard). - let target_type_name = types.last().and_then(|t| extract_head_constructor(t)) + let target_type_name = types + .last() + .and_then(|t| extract_head_constructor(t)) .or_else(|| types.iter().rev().find_map(|t| extract_head_constructor(t))); + // ExpectedWildcard: derive instance Newtype X String + // where the second arg should be a wildcard (_), not a concrete type. + let newtype_ident = crate::interner::intern("Newtype"); + if class_name.name == newtype_ident && types.len() >= 2 { + if !matches!(types.last(), Some(TypeExpr::Wildcard { .. })) { + errors.push(TypeError::ExpectedWildcard { + span: *span, + name: class_name.clone(), + }); + } + } + if let Some(target_name) = target_type_name { + let is_newtype = ctx.newtype_names.contains(&target_name) + || ctx.newtype_names.iter().any(|n| n.name == target_name.name); + // InvalidNewtypeInstance: derive instance Newtype X _ // where X is not actually a newtype - let newtype_sym = crate::interner::intern("Newtype"); - if class_name.name == newtype_sym && !ctx.newtype_names.contains(&target_name) { + if class_name.name == newtype_ident && !is_newtype + { errors.push(TypeError::InvalidNewtypeInstance { span: *span, name: target_name, @@ -3076,7 +4193,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // InvalidNewtypeDerivation: derive newtype instance SomeClass X // where X is not actually a newtype - if *newtype && !ctx.newtype_names.contains(&target_name) { + if *newtype && !is_newtype { errors.push(TypeError::InvalidNewtypeDerivation { span: *span, name: target_name, @@ -3087,19 +4204,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // where X's inner type is a bare type variable (e.g. `newtype X a = X a`). // Only when the target is unapplied (bare constructor), because when // applied (e.g. `N S`), the type var is substituted with concrete type. - if *newtype && ctx.newtype_names.contains(&target_name) { + if *newtype && is_newtype { let target_is_bare = types.iter().any(|t| { - matches!(t, TypeExpr::Constructor { name, .. } if name.name == target_name) + matches!(t, TypeExpr::Constructor { name, .. } if *name == target_name) }); if target_is_bare { if let Some(ctors) = ctx.data_constructors.get(&target_name) { if let Some(ctor_name) = ctors.first() { - if let Some((_, _, field_types)) = ctx.ctor_details.get(ctor_name) { + if let Some((_, _, field_types)) = + ctx.ctor_details.get(ctor_name) + { if let Some(inner_ty) = field_types.first() { if matches!(inner_ty, Type::Var(_)) { errors.push(TypeError::InvalidNewtypeInstance { span: *span, - name: target_name, + name: target_name.clone(), }); } } @@ -3113,7 +4232,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // — there's no target type to be a newtype errors.push(TypeError::InvalidNewtypeInstance { span: *span, - name: class_name.name, + name: class_name.clone(), }); } @@ -3121,7 +4240,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut inst_types = Vec::new(); let mut inst_ok = true; for ty_expr in types { - match convert_type_expr(ty_expr, &type_ops, &ctx.known_types) { + match convert_type_expr(ty_expr, &type_ops) { Ok(ty) => inst_types.push(ty), Err(_) => { inst_ok = false; @@ -3131,11 +4250,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Check derived instance arity matches class parameter count if inst_ok { - if let Some(&expected_count) = class_param_counts.get(&class_name.name) { - if inst_types.len() != expected_count { + if let Some(&expected_count) = class_param_counts.get(&class_name) { + if expected_count != usize::MAX && inst_types.len() != expected_count { errors.push(TypeError::ClassInstanceArityMismatch { span: *span, - class_name: class_name.name, + class_name: *class_name, expected: expected_count, found: inst_types.len(), }); @@ -3146,36 +4265,64 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check for partially applied synonyms in derived instance types if inst_ok { for inst_ty in &inst_types { - check_type_for_partial_synonyms_with_arities(inst_ty, &ctx.state.type_aliases, &ctx.type_con_arities, &ctx.record_type_aliases, *span, &mut errors); + check_type_for_partial_synonyms_with_arities( + inst_ty, + &ctx.state.type_aliases, + &ctx.type_con_arities, + &ctx.record_type_aliases, + *span, + &mut errors, + ); } } // Check for non-nominal types in derived instance heads (records, functions, // or type synonyms expanding to them). Derive requires a data/newtype. + // For derive newtype, records and functions are allowed as class parameters. if inst_ok { for inst_ty in &inst_types { - if is_non_nominal_for_derive(inst_ty, &ctx.state.type_aliases) { + if is_non_nominal_for_derive(inst_ty, &ctx.state.type_aliases, &ctx.data_constructors, *newtype) { errors.push(TypeError::InvalidInstanceHead { span: *span }); inst_ok = false; break; } } } - // Orphan check for derived instances: expand type aliases first, - // then check if any type constructor in the instance head is locally defined. - // With functional dependencies, every covering set must have a local type. + // Orphan check for derived instances. + // First check unexpanded types — if any type constructor in the instance head + // is locally defined (data/newtype), it's not orphan. This prevents false + // positives when imported type aliases share the same unqualified name as + // a locally-defined data type (e.g. `Mutex` newtype vs imported `Mutex` alias). + // Only fall through to expanded checking if unexpanded check doesn't find a local type. if inst_ok && class_name.module.is_none() { let class_is_local = local_class_names.contains(&class_name.name); if !class_is_local { - let expanded: Vec = inst_types.iter() - .map(|t| expand_type_aliases(t, &ctx.state.type_aliases)) - .collect(); - let is_orphan = check_orphan_with_fundeps( - &expanded, &class_name.name, &ctx.class_fundeps, &local_type_names, + // Check unexpanded types first, using only data/newtype names + // (not type aliases, which are transparent for orphan checking). + let is_orphan_unexpanded = check_orphan_with_fundeps( + &inst_types, + &class_name, + &ctx.class_fundeps, + &local_data_type_names, ); + // Only expand and re-check if unexpanded check says orphan + let is_orphan = if is_orphan_unexpanded { + let expanded: Vec = inst_types + .iter() + .map(|t| expand_type_aliases(t, &ctx.state.type_aliases)) + .collect(); + check_orphan_with_fundeps( + &expanded, + &class_name, + &ctx.class_fundeps, + &local_type_names, + ) + } else { + false + }; if is_orphan { errors.push(TypeError::OrphanInstance { span: *span, - class_name: class_name.name, + class_name: *class_name, }); } } @@ -3204,9 +4351,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }; if let Some(ctors) = ctx.data_constructors.get(&target_name) { 'open_row_check: for ctor in ctors { - if let Some((_, type_vars, field_types)) = ctx.ctor_details.get(ctor) { + if let Some((_, type_vars, field_types)) = + ctx.ctor_details.get(ctor) + { // Build substitution from data-decl type vars to instance args - let subst: HashMap = type_vars.iter() + let subst: HashMap = type_vars + .iter() .zip(derive_args.iter()) .map(|(v, t)| (*v, t.clone())) .collect(); @@ -3215,7 +4365,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if has_open_record_row(&concrete) { errors.push(TypeError::NoInstanceFound { span: *span, - class_name: class_name.name, + class_name: class_name.clone(), type_args: inst_types.clone(), }); inst_ok = false; @@ -3275,7 +4425,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for constraint in constraints { // Extract type vars from constraint args (e.g. `Functor f` → f → [Functor]) for arg in &constraint.args { - if let crate::cst::TypeExpr::Var { name, .. } = arg { + if let crate::ast::TypeExpr::Var { name, .. } = arg { tyvar_classes .entry(name.value) .or_default() @@ -3304,13 +4454,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if let Some(ctors) = ctx.data_constructors.get(&target_name) { 'ctor_check: for ctor in ctors { - if let Some((_, type_vars, field_types)) = ctx.ctor_details.get(ctor) { + if let Some((_, type_vars, field_types)) = + ctx.ctor_details.get(ctor) + { // Build field substitution: map data-decl vars to instance args - let num_derived = var_checks.iter().map(|(off, _)| off + 1).max().unwrap_or(1); - let num_non_derived = type_vars.len().saturating_sub(num_derived); + let num_derived = var_checks + .iter() + .map(|(off, _)| off + 1) + .max() + .unwrap_or(1); + let num_non_derived = + type_vars.len().saturating_sub(num_derived); let mut field_subst: HashMap = HashMap::new(); - for i in 0..std::cmp::min(num_non_derived, derive_subst.len()) { - field_subst.insert(type_vars[i], derive_subst[i].clone()); + for i in + 0..std::cmp::min(num_non_derived, derive_subst.len()) + { + field_subst + .insert(type_vars[i], derive_subst[i].clone()); } for &(offset, want_covariant) in &var_checks { @@ -3319,14 +4479,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } let var = type_vars[type_vars.len() - 1 - offset]; for field_ty in field_types { - let expanded_ty = expand_type_aliases(field_ty, &ctx.state.type_aliases); + let expanded_ty = expand_type_aliases( + field_ty, + &ctx.state.type_aliases, + ); let subst_ty = if field_subst.is_empty() { expanded_ty } else { apply_var_subst(&field_subst, &expanded_ty) }; if type_var_occurs_in(var, &subst_ty) { - if !check_derive_position( + let pos_result = check_derive_position( &subst_ty, var, true, // start in positive position @@ -3336,8 +4499,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { &tyvar_classes, &ctx.ctor_details, &ctx.data_constructors, + &local_concrete_type_names, 0, - ) { + ); + if !pos_result { errors.push(TypeError::CannotDeriveInvalidConstructorArg { span: *span, }); @@ -3354,13 +4519,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } - let mut inst_constraints = Vec::new(); + let mut inst_constraints: Vec<(QualifiedIdent, Vec)> = Vec::new(); if inst_ok { for constraint in constraints { let mut c_args = Vec::new(); let mut c_ok = true; for arg in &constraint.args { - match convert_type_expr(arg, &type_ops, &ctx.known_types) { + match convert_type_expr(arg, &type_ops) { Ok(ty) => c_args.push(ty), Err(_) => { c_ok = false; @@ -3370,14 +4535,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } if c_ok { - inst_constraints.push((constraint.class.name, c_args)); + inst_constraints.push((constraint.class, c_args)); } } } if inst_ok { registered_instances.push((*span, class_name.name, inst_types.clone())); instances - .entry(class_name.name) + .entry(qi(class_name.name)) .or_default() .push((inst_types, inst_constraints)); } @@ -3386,6 +4551,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Handled in Pass 2 } } + timed_pass!(1, format!("end decl {}", decl_name), decl.span()); } // Check for orphan kind declarations (kind sig without matching definition) @@ -3416,7 +4582,6 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { match kind { TypeExpr::Function { to, .. } => 1 + count_kind_arity(to), TypeExpr::Forall { ty, .. } => count_kind_arity(ty), - TypeExpr::Parens { ty, .. } => count_kind_arity(ty), _ => 0, } } @@ -3429,7 +4594,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut prev_was_role_for: Option = None; for decl in &module.decls { match decl { - Decl::Data { name, type_vars, is_role_decl: true, kind_sig, .. } => { + Decl::Data { + name, + type_vars, + is_role_decl: true, + kind_sig, + .. + } => { if *kind_sig != KindSigSource::None { prev_decl = None; prev_was_role_for = None; @@ -3466,13 +4637,19 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } else { // Valid role declaration — store the roles - let roles: Vec = type_vars.iter().map(|tv| { - match crate::interner::resolve(tv.value).unwrap_or_default().as_str() { - "nominal" => Role::Nominal, - "representational" => Role::Representational, - "phantom" | _ => Role::Phantom, - } - }).collect(); + let roles: Vec = type_vars + .iter() + .map(|tv| { + match crate::interner::resolve(tv.value) + .unwrap_or_default() + .as_str() + { + "nominal" => Role::Nominal, + "representational" => Role::Representational, + "phantom" | _ => Role::Phantom, + } + }) + .collect(); ctx.type_roles.insert(role_name, roles); } } @@ -3486,7 +4663,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { prev_was_role_for = Some(role_name); prev_decl = None; } - Decl::Data { name, type_vars, is_role_decl: false, kind_sig, .. } => { + Decl::Data { + name, + type_vars, + is_role_decl: false, + kind_sig, + .. + } => { if *kind_sig == KindSigSource::None { prev_decl = Some((name.value, "data", type_vars.len())); } else { @@ -3494,7 +4677,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } prev_was_role_for = None; } - Decl::Newtype { name, type_vars, .. } => { + Decl::Newtype { + name, type_vars, .. + } => { prev_decl = Some((name.value, "data", type_vars.len())); prev_was_role_for = None; } @@ -3503,11 +4688,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { prev_decl = Some((name.value, "foreign", arity)); prev_was_role_for = None; } - Decl::TypeAlias { name, type_vars, .. } => { + Decl::TypeAlias { + name, type_vars, .. + } => { prev_decl = Some((name.value, "synonym", type_vars.len())); prev_was_role_for = None; } - Decl::Class { name, type_vars, .. } => { + Decl::Class { + name, type_vars, .. + } => { prev_decl = Some((name.value, "class", type_vars.len())); prev_was_role_for = None; } @@ -3527,10 +4716,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Also collect CST field types to scan for constrained vars (constraints are // stripped during convert_type_expr, but affect role inference — any type var // in a constraint position must be nominal). - let mut type_cst_fields: HashMap> = HashMap::new(); + let mut type_cst_fields: HashMap> = HashMap::new(); for decl in &module.decls { match decl { - Decl::Data { name, type_vars, constructors, kind_sig, is_role_decl, .. } => { + Decl::Data { + name, + type_vars, + constructors, + kind_sig, + is_role_decl, + .. + } => { if *is_role_decl || *kind_sig != KindSigSource::None { continue; } @@ -3539,28 +4735,38 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let ctor_fields: Vec> = constructors .iter() .map(|c| { - c.fields.iter().filter_map(|f| { - cst_fields.push(f); - convert_type_expr(f, &type_ops, &ctx.known_types).ok() - }).collect() + c.fields + .iter() + .filter_map(|f| { + cst_fields.push(f); + convert_type_expr(f, &type_ops).ok() + }) + .collect() }) .collect(); type_cst_fields.insert(name.value, cst_fields); type_ctor_fields.insert(name.value, (tvs, ctor_fields)); } - Decl::Newtype { name, type_vars, ty, .. } => { + Decl::Newtype { + name, + type_vars, + ty, + .. + } => { let tvs: Vec = type_vars.iter().map(|tv| tv.value).collect(); - if let Ok(field_ty) = convert_type_expr(ty, &type_ops, &ctx.known_types) { + if let Ok(field_ty) = convert_type_expr(ty, &type_ops) { type_cst_fields.insert(name.value, vec![ty]); type_ctor_fields.insert(name.value, (tvs, vec![vec![field_ty]])); } } Decl::ForeignData { name, kind, .. } => { // Foreign types without role declarations default to Nominal - // (conservative: we don't know internal structure of foreign types) + // (matches PureScript behavior: foreign types are opaque, so all + // type params are assumed Nominal for safety) let arity = count_kind_arity(kind); if arity > 0 && !ctx.type_roles.contains_key(&name.value) { - ctx.type_roles.insert(name.value, vec![Role::Nominal; arity]); + ctx.type_roles + .insert(name.value, vec![Role::Nominal; arity]); } } _ => {} @@ -3577,7 +4783,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // least-restrictive fixed point. for (type_name, (type_vars, _)) in &type_ctor_fields { if !declared_role_types.contains(type_name) { - ctx.type_roles.insert(*type_name, vec![Role::Phantom; type_vars.len()]); + ctx.type_roles + .insert(*type_name, vec![Role::Phantom; type_vars.len()]); } } @@ -3593,7 +4800,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Also mark type vars in constraint positions as nominal if let Some(cst_fields) = type_cst_fields.get(type_name) { for field_te in cst_fields { - mark_constrained_vars_nominal_cst(field_te, type_vars, &mut inferred, &HashSet::new()); + mark_constrained_vars_nominal_cst( + field_te, + type_vars, + &mut inferred, + &HashSet::new(), + ); } } let existing = ctx.type_roles.get(type_name); @@ -3618,14 +4830,25 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut inferred = infer_roles_from_fields(type_vars, ctor_fields, &ctx.type_roles); if let Some(cst_fields) = type_cst_fields.get(type_name) { for field_te in cst_fields { - mark_constrained_vars_nominal_cst(field_te, type_vars, &mut inferred, &HashSet::new()); + mark_constrained_vars_nominal_cst( + field_te, + type_vars, + &mut inferred, + &HashSet::new(), + ); } } for (decl_role, inferred_role) in declared.iter().zip(inferred.iter()) { if *decl_role < *inferred_role { // Find the span for this type's role declaration for d in &module.decls { - if let Decl::Data { name, is_role_decl: true, kind_sig, .. } = d { + if let Decl::Data { + name, + is_role_decl: true, + kind_sig, + .. + } = d + { if *kind_sig == KindSigSource::None && name.value == *type_name { errors.push(TypeError::RoleMismatch { span: name.span, @@ -3646,10 +4869,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check for cycles in kind declarations (data kind sigs and foreign data kinds) { - let mut kind_decls: HashMap = HashMap::new(); + let mut kind_decls: HashMap = HashMap::new(); for decl in &module.decls { match decl { - Decl::Data { name, kind_sig, kind_type: Some(kind_ty), .. } if *kind_sig != KindSigSource::None => { + Decl::Data { + name, + kind_sig, + kind_type: Some(kind_ty), + .. + } if *kind_sig != KindSigSource::None => { kind_decls.insert(name.value, (name.span, kind_ty)); } Decl::ForeignData { name, kind, .. } => { @@ -3671,7 +4899,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for &name in kind_decls.keys() { if !visited.contains(&name) { let mut path = Vec::new(); - if let Some(cycle) = dfs_find_cycle(name, &deps, &mut visited, &mut on_stack, &mut path) { + if let Some(cycle) = + dfs_find_cycle(name, &deps, &mut visited, &mut on_stack, &mut path) + { let (span, _) = kind_decls[&name]; let cycle_spans: Vec = cycle .iter() @@ -3712,6 +4942,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + // Copy type_con_arities to UnifyState for alias/data-type disambiguation in try_expand_alias. + ctx.state.type_con_arities = ctx.type_con_arities.clone(); + // Pre-compute which aliases are transitively self-referential (e.g., Codec → Codec' → Codec). + // This prevents infinite re-expansion loops during unification. + ctx.state.compute_self_referential_aliases(); // Pass 1.5: Process value-level fixity declarations whose targets are already // in local_values or env (class methods, data constructors, imported values). // This must happen before Pass 2 so operators like `==`, `<`, `+`, `/\` are available. @@ -3733,11 +4968,22 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { env.insert_scheme(operator.value, scheme.clone()); local_values.insert(operator.value, scheme); } + } else if let Some(m) = target.module { + // Try qualified name (e.g. `infixl 9 S.compose as <.` where + // compose is imported as `import Control.Semigroupoid as S`) + let qualified = qualified_symbol(m, target.name); + if let Some(scheme) = env.lookup(qualified).cloned() { + if ctx.state.free_unif_vars(&scheme.ty).is_empty() { + env.insert_scheme(operator.value, scheme.clone()); + local_values.insert(operator.value, scheme); + } + } } // If the target is a data constructor, register the operator→constructor mapping // so exhaustiveness checking recognizes operator patterns (e.g. `:` for `Cons`). - if ctx.ctor_details.contains_key(&target.name) { - ctx.ctor_details.insert(operator.value, ctx.ctor_details[&target.name].clone()); + if ctx.ctor_details.contains_key(&target) { + ctx.ctor_details + .insert(qi(operator.value), ctx.ctor_details[&target].clone()); } } } @@ -3779,19 +5025,26 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Local fixity declarations override imported ones, so we process all local // fixities and either add (function target) or remove (constructor target). for decl in &module.decls { - if let Decl::Fixity { target, operator, is_type: false, .. } = decl { - if ctx.ctor_details.contains_key(&target.name) { + if let Decl::Fixity { + target, + operator, + is_type: false, + .. + } = decl + { + if ctx.ctor_details.contains_key(&target) { // Constructor target: remove any inherited function alias flag - ctx.function_op_aliases.remove(&operator.value); + ctx.function_op_aliases.remove(&qi(operator.value)); } else { - ctx.function_op_aliases.insert(operator.value); + ctx.function_op_aliases.insert(qi(operator.value)); } // Track operator → class method target for deferred constraint tracking. // Local fixity overrides imported mapping, so remove if new target isn't a class method. - if ctx.class_methods.contains_key(&target.name) { - ctx.operator_class_targets.insert(operator.value, target.name); + if ctx.class_methods.contains_key(&target) { + ctx.operator_class_targets + .insert(qi(operator.value), *target); } else { - ctx.operator_class_targets.remove(&operator.value); + ctx.operator_class_targets.remove(&qi(operator.value)); } } } @@ -3808,17 +5061,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // We also exclude names that exist as top-level values in the module, // since the RHS refers to the top-level function, not the sibling method // (e.g. `chooseInt = chooseInt` delegates to a top-level function). - let top_level_values: HashSet = module.decls.iter().filter_map(|d| { - match d { + let top_level_values: HashSet = module + .decls + .iter() + .filter_map(|d| match d { Decl::Value { name, .. } | Decl::TypeSignature { name, .. } => Some(name.value), Decl::Foreign { name, .. } => Some(name.value), _ => None, - } - }).collect(); + }) + .collect(); let mut cycle_methods: HashSet = HashSet::new(); for group in &instance_method_groups { let sibling_set: HashSet = group.iter().copied().collect(); - for (name, span, binders, guarded, _where, _expected, _scoped, _given) in &deferred_instance_methods { + for (name, span, binders, guarded, _where, _expected, _scoped, _given, _inst_id) in + &deferred_instance_methods + { if !sibling_set.contains(name) || !binders.is_empty() { continue; } @@ -3829,7 +5086,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { continue; } // Check if the application head is a sibling method name - let head_is_sibling = |expr: &crate::cst::Expr| -> bool { + let head_is_sibling = |expr: &crate::ast::Expr| -> bool { if let Some(head) = expr_app_head_name(expr) { sibling_set.contains(&head) && !top_level_values.contains(&head) } else { @@ -3837,8 +5094,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } }; let is_cycle = match guarded { - crate::cst::GuardedExpr::Unconditional(expr) => head_is_sibling(expr), - crate::cst::GuardedExpr::Guarded(guards) => { + crate::ast::GuardedExpr::Unconditional(expr) => head_is_sibling(expr), + crate::ast::GuardedExpr::Guarded(guards) => { guards.iter().any(|g| head_is_sibling(&g.expr)) } }; @@ -3855,7 +5112,25 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Now that foreign imports, fixity declarations, and value signatures have been // processed, all values are available in env for instance method checking. - for (name, span, binders, guarded, where_clause, expected_ty, inst_scoped, inst_given) in &deferred_instance_methods { + + // Pre-compute which instance methods have at least one equation with no inherently + // partial binders (e.g. a catch-all). For multi-equation methods like: + // uniqueId (Key []) = ... -- has [] array binder (inherently partial) + // uniqueId e = ... -- catch-all (not partial) + // we should NOT emit a Partial error because the method as a whole is exhaustive. + // Keyed by (instance_id, method_name) to avoid cross-instance interference. + let mut inst_methods_with_total_eq: HashSet<(usize, Symbol)> = HashSet::new(); + for (name, _span, binders, _guarded, _where, _expected, _scoped, _given, inst_id) in + &deferred_instance_methods + { + if !binders.iter().any(|b| contains_inherently_partial_binder(b)) { + inst_methods_with_total_eq.insert((*inst_id, *name)); + } + } + + for (name, span, binders, guarded, where_clause, expected_ty, inst_scoped, inst_given, inst_id) in + &deferred_instance_methods + { let prev_scoped = ctx.scoped_type_vars.clone(); let prev_given = ctx.given_class_names.clone(); ctx.scoped_type_vars.extend(inst_scoped); @@ -3874,13 +5149,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } ctx.scoped_type_vars = prev_scoped; ctx.given_class_names = prev_given; + // Clear non-exhaustive state from instance method processing + // to prevent leaking into subsequent declarations. + ctx.has_partial_lambda = false; + ctx.non_exhaustive_errors.clear(); // Check for non-exhaustive patterns in instance methods. // Array and literal binders are always refutable (can never be exhaustive // because you can't enumerate all array lengths or literal values). // These require a Partial constraint which instances don't provide. - if binders.iter().any(|b| contains_inherently_partial_binder(b)) { - let partial_sym = crate::interner::intern("Partial"); + // Skip if another equation for the same method (in the same instance) + // has no partial binders (catch-all). + if !inst_methods_with_total_eq.contains(&(*inst_id, *name)) + && binders + .iter() + .any(|b| contains_inherently_partial_binder(b)) + { + let partial_sym = unqualified_ident("Partial"); errors.push(TypeError::NoInstanceFound { span: *span, class_name: partial_sym, @@ -3889,6 +5174,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + timed_pass!(1, "done", ""); // Pass 2: Group value declarations by name and check them let mut value_groups: Vec<(Symbol, Vec<&Decl>)> = Vec::new(); let mut seen_values: HashMap = HashMap::new(); @@ -3947,15 +5233,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let refs = collect_decl_refs(decls, &top_names); dep_edges.insert(*name, refs); } - // Compute SCCs via Tarjan (returns leaves-first = correct processing order) let node_order: Vec = value_groups.iter().map(|(n, _)| *n).collect(); let sccs = tarjan_scc(&node_order, &dep_edges); - // Build lookup: name → index in value_groups - let group_idx: HashMap = - value_groups.iter().enumerate().map(|(i, (n, _))| (*n, i)).collect(); - + let group_idx: HashMap = value_groups + .iter() + .enumerate() + .map(|(i, (n, _))| (*n, i)) + .collect(); // Process each SCC in dependency order for scc in &sccs { super::check_deadline(); @@ -3964,7 +5250,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { true } else { let name = scc[0]; - dep_edges.get(&name).map_or(false, |refs| refs.contains(&name)) + dep_edges + .get(&name) + .map_or(false, |refs| refs.contains(&name)) }; // Cycle detection: check for non-function (0-arity) value bindings in cyclic SCCs. @@ -3978,14 +5266,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } else { // Single-member SCC: cyclic only if self-referencing let name = scc[0]; - dep_edges.get(&name).map_or(false, |refs| refs.contains(&name)) + dep_edges + .get(&name) + .map_or(false, |refs| refs.contains(&name)) }; if is_cyclic { // For each member with 0 explicit binders, check if the body // contains a strict (not under lambda) reference to any SCC member. let scc_set: HashSet = scc.iter().copied().collect(); - let mut non_func_members: Vec<(Symbol, crate::ast::span::Span)> = Vec::new(); + let mut non_func_members: Vec<(Symbol, crate::span::Span)> = Vec::new(); for &name in scc { if let Some(&idx) = group_idx.get(&name) { let (_, decls) = &value_groups[idx]; @@ -3995,7 +5285,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { continue; } let has_binders = decls.iter().any(|d| { - if let Decl::Value { binders, .. } = d { !binders.is_empty() } else { false } + if let Decl::Value { binders, .. } = d { + !binders.is_empty() + } else { + false + } }); if has_binders { continue; // Function with explicit arguments — OK @@ -4003,7 +5297,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check if the body is directly a reference to an SCC member let has_strict_cycle = decls.iter().any(|d| { if let Decl::Value { guarded, .. } = d { - if let crate::cst::GuardedExpr::Unconditional(expr) = guarded { + if let crate::ast::GuardedExpr::Unconditional(expr) = guarded { is_direct_var_ref(expr, &scc_set) } else { false @@ -4013,7 +5307,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } }); if has_strict_cycle { - let span = if let Decl::Value { span, .. } = decls[0] { *span } else { crate::ast::span::Span { start: 0, end: 0 } }; + let span = if let Decl::Value { span, .. } = decls[0] { + *span + } else { + crate::span::Span { start: 0, end: 0 } + }; non_func_members.push((name, span)); } } @@ -4022,7 +5320,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !non_func_members.is_empty() { // Report cycle for the first non-function member let (name, span) = non_func_members[0]; - let others: Vec<(Symbol, crate::ast::span::Span)> = non_func_members[1..].to_vec(); + let others: Vec<(Symbol, crate::span::Span)> = + non_func_members[1..].to_vec(); errors.push(TypeError::CycleInDeclaration { name, span, @@ -4033,7 +5332,6 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } } - // For mutual recursion: pre-insert all unsignatured values so // forward references within the SCC resolve correctly. let mut scc_pre_vars: HashMap = HashMap::new(); @@ -4060,11 +5358,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for &scc_name in scc { let idx = group_idx[&scc_name]; let (name, decls) = &value_groups[idx]; + let qualified = qi(*name); let sig = signatures.get(name).map(|(_, ty)| ty); // Check for duplicate value declarations: multiple equations with 0 binders if decls.len() > 1 { - let zero_arity_spans: Vec = decls + let zero_arity_spans: Vec = decls .iter() .filter_map(|d| { if let Decl::Value { span, binders, .. } = d { @@ -4089,7 +5388,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check for overlapping argument names in each equation for decl in decls { - if let Decl::Value { span, binders, .. } = decl { + if let Decl::Value { + span, + binders, + .. + } = decl + { if !binders.is_empty() { check_overlapping_arg_names(*span, binders, &mut errors); } @@ -4152,24 +5456,35 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Compare constraint solver: check new Compare constraints // against the function's own signature constraints using // graph-based transitive reasoning. - if let Some(sig_constraints) = ctx.signature_constraints.get(name).cloned() { - let compare_sym = crate::interner::intern("Compare"); + if let Some(sig_constraints) = + ctx.signature_constraints.get(&qualified).cloned() + { // Build relations from the function's own signature constraints let mut relations: Vec<(Type, Type, &str)> = Vec::new(); for (class_name_c, args) in &sig_constraints { - if *class_name_c == compare_sym && args.len() == 3 { + if is_compare(&class_name_c) && args.len() == 3 { if let Type::Con(ordering) = &args[2] { - let ord_str = crate::interner::resolve(*ordering).unwrap_or_default(); + let ord_str = + crate::interner::resolve(ordering.name) + .unwrap_or_default(); let ord_static: &str = match ord_str.as_str() { "LT" => "LT", "EQ" => "EQ", "GT" => "GT", _ => continue, }; - relations.push((args[0].clone(), args[1].clone(), ord_static)); + // Expand type aliases so e.g. Common.NegOne becomes TypeInt(-1) + let lhs = expand_type_aliases_limited(&args[0], &ctx.state.type_aliases, 10); + let rhs = expand_type_aliases_limited(&args[1], &ctx.state.type_aliases, 10); + relations.push(( + lhs, + rhs, + ord_static, + )); } } } + if !relations.is_empty() { // Collect all concrete integers from both given and wanted // Compare constraints (for mkFacts-style ordering). @@ -4183,7 +5498,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } for i in constraint_start..ctx.deferred_constraints.len() { let (_, c_class_i, _) = ctx.deferred_constraints[i]; - if c_class_i != compare_sym { continue; } + if !is_compare(&c_class_i) { + continue; + } for arg in &ctx.deferred_constraints[i].2 { let z = ctx.state.zonk(arg.clone()); if matches!(z, Type::TypeInt(_)) { @@ -4193,17 +5510,30 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } for i in constraint_start..ctx.deferred_constraints.len() { let (c_span, c_class, _) = ctx.deferred_constraints[i]; - if c_class != compare_sym { continue; } - let zonked: Vec = ctx.deferred_constraints[i].2.iter() + if !is_compare(&c_class) { + continue; + } + let zonked: Vec = ctx.deferred_constraints[i] + .2 + .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); - if zonked.len() != 3 { continue; } + if zonked.len() != 3 { + continue; + } // Only use given relations from the signature (NOT wanted // constraints, which would be circular reasoning). // Pass extra concrete ints for mkFacts-style ordering. - if let Some(solved) = solve_compare_graph(&relations, &extra_ints, &zonked[0], &zonked[1]) { + if let Some(solved) = solve_compare_graph( + &relations, + &extra_ints, + &zonked[0], + &zonked[1], + ) { let result = Type::Con(solved); - if let Err(e) = ctx.state.unify(c_span, &zonked[2], &result) { + if let Err(e) = + ctx.state.unify(c_span, &zonked[2], &result) + { errors.push(e); } } else { @@ -4218,14 +5548,18 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } } + // Lacks constraint solver: check that body-generated // Lacks constraints with type variables are entailed by // the function's signature constraints. { - let lacks_sym = crate::interner::intern("Lacks"); + let lacks_sym = unqualified_ident("Lacks"); // Collect given Lacks constraints from signature - let sig_lacks: Vec<(Type, Type)> = if let Some(sig_constraints) = ctx.signature_constraints.get(name) { - sig_constraints.iter() + let sig_lacks: Vec<(Type, Type)> = if let Some(sig_constraints) = + ctx.signature_constraints.get(&qualified) + { + sig_constraints + .iter() .filter(|(cn, args)| *cn == lacks_sym && args.len() == 2) .map(|(_, args)| (args[0].clone(), args[1].clone())) .collect() @@ -4234,21 +5568,32 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }; for i in constraint_start..ctx.deferred_constraints.len() { let (c_span, c_class, _) = ctx.deferred_constraints[i]; - if c_class != lacks_sym { continue; } - let zonked: Vec = ctx.deferred_constraints[i].2.iter() + if c_class != lacks_sym { + continue; + } + let zonked: Vec = ctx.deferred_constraints[i] + .2 + .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); - if zonked.len() != 2 { continue; } + if zonked.len() != 2 { + continue; + } let has_type_vars = zonked.iter().any(|t| contains_type_var(t)); - if !has_type_vars { continue; } + if !has_type_vars { + continue; + } // Decompose: Lacks label (fields | tail) → Lacks label tail let (label, row_tail) = match &zonked[1] { Type::Record(fields, Some(tail)) => { // Check label is not in known fields if let Type::TypeString(label_sym) = &zonked[0] { - let label_str = crate::interner::resolve(*label_sym).unwrap_or_default(); + let label_str = + crate::interner::resolve(*label_sym) + .unwrap_or_default(); let has_label = fields.iter().any(|(f, _)| { - crate::interner::resolve(*f).unwrap_or_default() == label_str.as_str() + crate::interner::resolve(*f).unwrap_or_default() + == label_str.as_str() }); if has_label { // Label IS in the row — Lacks fails @@ -4266,9 +5611,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }; // After decomposition, check if the reduced Lacks is given // by the function's signature constraints. - let is_given = sig_lacks.iter().any(|(sl, sr)| { - *sl == label && *sr == row_tail - }); + let is_given = sig_lacks + .iter() + .any(|(sl, sr)| *sl == label && *sr == row_tail); if !is_given { // Check if the tail is a bare type variable (from forall). // If so, and there's no matching given Lacks constraint, @@ -4287,43 +5632,70 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // with type variables using role-based decomposition and // the function's own given Coercible constraints. { - let coercible_sym = crate::interner::intern("Coercible"); - let newtype_sym = crate::interner::intern("Newtype"); - let coercible_givens: Vec<(Type, Type)> = ctx.signature_constraints.get(name) + let coercible_ident: QualifiedIdent = + unqualified_ident("Coercible"); + let newtype_ident = unqualified_ident("Newtype"); + let type_equals_ident = unqualified_ident("TypeEquals"); + let coercible_givens: Vec<(Type, Type)> = ctx + .signature_constraints + .get(&qualified.clone()) .map(|constraints| { - constraints.iter() - .filter(|(cn, args)| *cn == coercible_sym && args.len() == 2) + constraints + .iter() + .filter(|(cn, args)| { + // Coercible a b directly, or TypeEquals a b + // (since Coercible is a superclass of TypeEquals) + (*cn == coercible_ident || *cn == type_equals_ident) + && args.len() == 2 + }) .map(|(_, args)| (args[0].clone(), args[1].clone())) .collect() }) .unwrap_or_default(); // Only trust Newtype constraints blindly (superclass relation). // Coercible givens are handled through proper interaction. - let has_newtype_givens = ctx.signature_constraints.get(name) + let has_newtype_givens = ctx + .signature_constraints + .get(&qualified.clone()) .map(|constraints| { - constraints.iter().any(|(cn, _)| *cn == newtype_sym) + constraints.iter().any(|(cn, _)| *cn == newtype_ident) }) .unwrap_or(false); for i in constraint_start..ctx.deferred_constraints.len() { let (c_span, c_class, _) = ctx.deferred_constraints[i]; - if c_class != coercible_sym { continue; } - let zonked: Vec = ctx.deferred_constraints[i].2.iter() + if c_class != coercible_ident { + continue; + } + let zonked: Vec = ctx.deferred_constraints[i] + .2 + .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); - if zonked.len() != 2 { continue; } + if zonked.len() != 2 { + continue; + } // Only handle constraints with type variables here // (concrete constraints are handled in Pass 3) let has_type_vars = zonked.iter().any(|t| contains_type_var(t)); - if !has_type_vars { continue; } + if !has_type_vars { + continue; + } // Skip if constraint contains unsolved unif vars — they may // be resolved later, so we can't definitively fail here. - let has_unif_vars = zonked.iter().any(|t| !ctx.state.free_unif_vars(t).is_empty()); - if has_unif_vars { continue; } + let has_unif_vars = zonked + .iter() + .any(|t| !ctx.state.free_unif_vars(t).is_empty()); + if has_unif_vars { + continue; + } match try_solve_coercible_with_interactions( - &zonked[0], &zonked[1], + &zonked[0], + &zonked[1], &coercible_givens, - &ctx.type_roles, &ctx.newtype_names, - &ctx.state.type_aliases, &ctx.ctor_details, + &ctx.type_roles, + &ctx.newtype_names, + &ctx.state.type_aliases, + &ctx.ctor_details, ) { CoercibleResult::Solved => {} result => { @@ -4356,7 +5728,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } CoercibleResult::KindMismatch => { - errors.push(TypeError::KindMismatch { + errors.push(TypeError::KindsDoNotUnify { span: c_span, expected: zonked[0].clone(), found: zonked[1].clone(), @@ -4384,16 +5756,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // without type annotation that would generalize constrained vars if is_cyclic { if let Some(err) = check_cannot_generalize_recursive( - &mut ctx.state, &env, &ctx.deferred_constraints, - &ctx.op_deferred_constraints, *name, *span, &zonked, + &mut ctx.state, + &ctx.deferred_constraints, + &ctx.op_deferred_constraints, + *name, + *span, + &zonked, ) { errors.push(err); } } // Check for ambiguous type variables: constraint vars not in the type if let Some(err) = check_ambiguous_type_variables( - &mut ctx.state, &ctx.deferred_constraints, - constraint_start, *span, &zonked, + &mut ctx.state, + &ctx.deferred_constraints, + constraint_start, + *span, + &zonked, ) { errors.push(err); } @@ -4407,11 +5786,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // The flag is set during infer_guarded when a pattern guard // doesn't cover all constructors. We also need the overall // function/case to lack an unconditional fallback. - if !partial_names.contains(name) && ctx.has_non_exhaustive_pattern_guards { + if !partial_names.contains(name) + && ctx.has_non_exhaustive_pattern_guards + { if !is_unconditional_for_exhaustiveness(guarded) { errors.push(TypeError::NoInstanceFound { span: *span, - class_name: crate::interner::intern("Partial"), + class_name: unqualified_ident("Partial"), type_args: vec![], }); } @@ -4422,13 +5803,19 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Unlike guard patterns, partial lambdas always require Partial // regardless of the enclosing function's guard structure. if !partial_names.contains(name) && ctx.has_partial_lambda { - errors.push(TypeError::NoInstanceFound { - span: *span, - class_name: crate::interner::intern("Partial"), - type_args: vec![], - }); + // Prefer specific NonExhaustivePattern errors from case expressions + if !ctx.non_exhaustive_errors.is_empty() { + errors.extend(ctx.non_exhaustive_errors.drain(..)); + } else { + errors.push(TypeError::NoInstanceFound { + span: *span, + class_name: unqualified_ident("Partial"), + type_args: vec![], + }); + } } ctx.has_partial_lambda = false; + ctx.non_exhaustive_errors.clear(); result_types.insert(*name, zonked); } @@ -4469,15 +5856,18 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !arity_ok { continue; } - // Set scoped type vars from multi-equation function's signature let prev_scoped_multi = ctx.scoped_type_vars.clone(); if let Some(sig_ty) = sig { fn collect_sig_vars(ty: &Type, vars: &mut HashSet) { match ty { - Type::Var(v) => { vars.insert(*v); } + Type::Var(v) => { + vars.insert(*v); + } Type::Forall(bv, body) => { - for &(v, _) in bv { vars.insert(v); } + for &(v, _) in bv { + vars.insert(v); + } collect_sig_vars(body, vars); } Type::Fun(a, b) | Type::App(a, b) => { @@ -4485,8 +5875,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { collect_sig_vars(b, vars); } Type::Record(fields, tail) => { - for (_, t) in fields { collect_sig_vars(t, vars); } - if let Some(t) = tail { collect_sig_vars(t, vars); } + for (_, t) in fields { + collect_sig_vars(t, vars); + } + if let Some(t) = tail { + collect_sig_vars(t, vars); + } } _ => {} } @@ -4505,8 +5899,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }, None => Type::Unif(ctx.state.fresh_var()), }; - let mut group_failed = false; + // Track partial lambda/exhaustiveness across all equations. + // Save and restore between equations to prevent leakage, but + // accumulate: if ANY equation sets the flag, keep it. + let mut any_partial_lambda = false; + let mut all_non_exhaustive_errors: Vec = Vec::new(); for decl in decls { if let Decl::Value { span, @@ -4516,6 +5914,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .. } = decl { + // Clear flags before each equation so they don't leak + ctx.has_partial_lambda = false; + ctx.non_exhaustive_errors.clear(); // Pass func_ty as expected so binders get correct types // from the signature (including rank-2 types like forall r). let expected_sig = if sig.is_some() { Some(&func_ty) } else { None }; @@ -4540,14 +5941,22 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { group_failed = true; } } + // Accumulate flags from this equation + if ctx.has_partial_lambda { + any_partial_lambda = true; + } + all_non_exhaustive_errors.extend(ctx.non_exhaustive_errors.drain(..)); } } + // Restore accumulated flags for the post-equation checks + ctx.has_partial_lambda = any_partial_lambda; + ctx.non_exhaustive_errors = all_non_exhaustive_errors; if !group_failed { let first_span = if let Decl::Value { span, .. } = decls[0] { *span } else { - crate::ast::span::Span::new(0, 0) + crate::span::Span::new(0, 0) }; if let Err(e) = ctx.state.unify(first_span, &self_ty, &func_ty) { errors.push(e); @@ -4555,37 +5964,56 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Inline Coercible solver for multi-equation declarations { - let coercible_sym = crate::interner::intern("Coercible"); - let newtype_sym = crate::interner::intern("Newtype"); - let coercible_givens: Vec<(Type, Type)> = ctx.signature_constraints.get(name) + let coercible_ident = unqualified_ident("Coercible"); + let newtype_ident = unqualified_ident("Newtype"); // probably not quite correct + let coercible_givens: Vec<(Type, Type)> = ctx + .signature_constraints + .get(&qi(*name)) .map(|constraints| { - constraints.iter() - .filter(|(cn, args)| *cn == coercible_sym && args.len() == 2) + constraints + .iter() + .filter(|(cn, args)| *cn == coercible_ident && args.len() == 2) .map(|(_, args)| (args[0].clone(), args[1].clone())) .collect() }) .unwrap_or_default(); - let has_newtype_givens = ctx.signature_constraints.get(name) + let has_newtype_givens = ctx + .signature_constraints + .get(&qualified) .map(|constraints| { - constraints.iter().any(|(cn, _)| *cn == newtype_sym) + constraints.iter().any(|(cn, _)| *cn == newtype_ident) }) .unwrap_or(false); for i in constraint_start..ctx.deferred_constraints.len() { let (c_span, c_class, _) = ctx.deferred_constraints[i]; - if c_class != coercible_sym { continue; } - let zonked: Vec = ctx.deferred_constraints[i].2.iter() + if c_class != coercible_ident { + continue; + } + let zonked: Vec = ctx.deferred_constraints[i] + .2 + .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); - if zonked.len() != 2 { continue; } + if zonked.len() != 2 { + continue; + } let has_type_vars = zonked.iter().any(|t| contains_type_var(t)); - if !has_type_vars { continue; } - let all_unif = matches!((&zonked[0], &zonked[1]), (Type::Unif(_), Type::Unif(_))); - if all_unif { continue; } + if !has_type_vars { + continue; + } + let all_unif = + matches!((&zonked[0], &zonked[1]), (Type::Unif(_), Type::Unif(_))); + if all_unif { + continue; + } match try_solve_coercible_with_interactions( - &zonked[0], &zonked[1], + &zonked[0], + &zonked[1], &coercible_givens, - &ctx.type_roles, &ctx.newtype_names, - &ctx.state.type_aliases, &ctx.ctor_details, + &ctx.type_roles, + &ctx.newtype_names, + &ctx.state.type_aliases, + &ctx.ctor_details, ) { CoercibleResult::Solved => {} result => { @@ -4609,14 +6037,16 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } CoercibleResult::DepthExceeded => { - errors.push(TypeError::PossiblyInfiniteCoercibleInstance { - span: c_span, - class_name: c_class, - type_args: zonked, - }); + errors.push( + TypeError::PossiblyInfiniteCoercibleInstance { + span: c_span, + class_name: c_class, + type_args: zonked, + }, + ); } CoercibleResult::KindMismatch => { - errors.push(TypeError::KindMismatch { + errors.push(TypeError::KindsDoNotUnify { span: c_span, expected: zonked[0].clone(), found: zonked[1].clone(), @@ -4643,16 +6073,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check CannotGeneralizeRecursiveFunction if is_cyclic { if let Some(err) = check_cannot_generalize_recursive( - &mut ctx.state, &env, &ctx.deferred_constraints, - &ctx.op_deferred_constraints, *name, first_span, &zonked, + &mut ctx.state, + &ctx.deferred_constraints, + &ctx.op_deferred_constraints, + *name, + first_span, + &zonked, ) { errors.push(err); } } // Check for ambiguous type variables if let Some(err) = check_ambiguous_type_variables( - &mut ctx.state, &ctx.deferred_constraints, - constraint_start, first_span, &zonked, + &mut ctx.state, + &ctx.deferred_constraints, + constraint_start, + first_span, + &zonked, ) { errors.push(err); } @@ -4671,7 +6108,6 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { &mut errors, ); } - // Check for non-exhaustive pattern guards (multi-equation). // The flag is set during infer_guarded when pattern guards // don't cover all constructors. We also need the overall @@ -4687,7 +6123,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !has_fallback { errors.push(TypeError::NoInstanceFound { span: first_span, - class_name: crate::interner::intern("Partial"), + class_name: unqualified_ident("Partial"), type_args: vec![], }); } @@ -4695,14 +6131,22 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ctx.has_non_exhaustive_pattern_guards = false; // Check for partial lambdas (multi-equation). - if !partial_names.contains(name) && ctx.has_partial_lambda { - errors.push(TypeError::NoInstanceFound { - span: first_span, - class_name: crate::interner::intern("Partial"), - type_args: vec![], - }); + // Skip for multi-equation functions (first_arity > 0) because + // individual equation binders are expected to be partial — the + // overall exhaustiveness is checked by check_multi_eq_exhaustiveness. + if first_arity == 0 && !partial_names.contains(name) && ctx.has_partial_lambda { + if !ctx.non_exhaustive_errors.is_empty() { + errors.extend(ctx.non_exhaustive_errors.drain(..)); + } else { + errors.push(TypeError::NoInstanceFound { + span: first_span, + class_name: unqualified_ident("Partial"), + type_args: vec![], + }); + } } ctx.has_partial_lambda = false; + ctx.non_exhaustive_errors.clear(); result_types.insert(*name, zonked); } @@ -4718,19 +6162,30 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Deferred generalization for mutual recursion SCC if is_mutual { for cv in &checked_values { - let cv_span = value_groups.iter() + let cv_span = value_groups + .iter() .find(|(n, _)| *n == cv.name) .and_then(|(_, decls)| decls.first()) - .and_then(|d| if let Decl::Value { span, .. } = d { Some(*span) } else { None }) - .unwrap_or(crate::ast::span::Span::new(0, 0)); + .and_then(|d| { + if let Decl::Value { span, .. } = d { + Some(*span) + } else { + None + } + }) + .unwrap_or(crate::span::Span::new(0, 0)); let scheme = if let Some(sig_ty) = &cv.sig { Scheme::mono(ctx.state.zonk(sig_ty.clone())) } else { let zonked = ctx.state.zonk(cv.ty.clone()); // Check CannotGeneralizeRecursiveFunction for mutual recursion if let Some(err) = check_cannot_generalize_recursive( - &mut ctx.state, &env, &ctx.deferred_constraints, - &ctx.op_deferred_constraints, cv.name, cv_span, &zonked, + &mut ctx.state, + &ctx.deferred_constraints, + &ctx.op_deferred_constraints, + cv.name, + cv_span, + &zonked, ) { errors.push(err); } @@ -4775,27 +6230,40 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !local_class_names.contains(class_name) { continue; } - if let Some((class_tvs, sc_constraints)) = class_superclasses.get(class_name) { + if let Some((class_tvs, sc_constraints)) = class_superclasses.get(&qi(class_name.clone())) { if class_tvs.len() == inst_types.len() { - let subst: HashMap = class_tvs.iter().copied() + let subst: HashMap = class_tvs + .iter() + .copied() .zip(inst_types.iter().cloned()) .collect(); for (sc_class, sc_args) in sc_constraints { // Only check superclasses that are locally defined or have // zero instances (imported superclasses may have instances // our resolution can't match, e.g. Profunctor Function). - let sc_is_local = local_class_names.contains(sc_class); + let sc_is_local = sc_class.module == None; let sc_has_no_instances = !instances.contains_key(sc_class) || instances.get(sc_class).map_or(true, |v| v.is_empty()); if !sc_is_local && !sc_has_no_instances { continue; } - let concrete_args: Vec = sc_args.iter() - .map(|t| apply_var_subst(&subst, t)) - .collect(); + let concrete_args: Vec = + sc_args.iter().map(|t| apply_var_subst(&subst, t)).collect(); let has_vars = concrete_args.iter().any(|t| contains_type_var(t)); - let has_unif = concrete_args.iter().any(|t| !ctx.state.free_unif_vars(t).is_empty()); - if !has_vars && !has_unif && !has_matching_instance_depth(&instances, &ctx.state.type_aliases, sc_class, &concrete_args, 0) { + let has_unif = concrete_args + .iter() + .any(|t| !ctx.state.free_unif_vars(t).is_empty()); + if !has_vars + && !has_unif + && !has_matching_instance_depth( + &instances, + &ctx.state.type_aliases, + sc_class, + &concrete_args, + 0, + Some(&ctx.type_con_arities), + ) + { errors.push(TypeError::NoInstanceFound { span: *span, class_name: *sc_class, @@ -4816,21 +6284,47 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); + let all_pure_unif = zonked_args.iter().all(|t| matches!(t, Type::Unif(_))); let has_type_vars = zonked_args.iter().any(|t| contains_type_var(t)); - let class_has_instances = instances.get(class_name) + let class_has_instances = lookup_instances(&instances, class_name) .map_or(false, |insts| !insts.is_empty()); if !class_has_instances { // Only fire when at least one arg is concrete (not all purely unsolved unif vars) // and there are no polymorphic type variables. If all args are unsolved, the // constraint may be satisfied at a downstream call site. if !all_pure_unif && !has_type_vars { - errors.push(TypeError::NoInstanceFound { - span: *span, - class_name: *class_name, - type_args: zonked_args, - }); + // Skip compiler-magic classes that are resolved without explicit instances + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); + let is_magic = matches!( + class_str.as_str(), + "Partial" + | "Warn" + | "Coercible" + | "IsSymbol" + | "Fail" + | "Union" + | "Cons" + | "Lacks" + | "RowToList" + | "Nub" + | "CompareSymbol" + | "Append" + | "Compare" + | "Add" + | "Mul" + | "ToString" + | "Reflectable" + | "Reifiable" + ); + if !is_magic { + errors.push(TypeError::NoInstanceFound { + span: *span, + class_name: *class_name, + type_args: zonked_args, + }); + } } continue; } @@ -4840,9 +6334,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // matches like Same (Proxy t) (Proxy Int) where the chain can't determine // which instance to use. if has_type_vars && chained_classes.contains(class_name) { - let has_structured_arg = zonked_args.iter().any(|t| matches!(t, Type::App(_, _) | Type::Record(_, _) | Type::Fun(_, _))); - if has_structured_arg { - if let Some(known) = instances.get(class_name) { + let has_structured_arg = zonked_args + .iter() + .any(|t| matches!(t, Type::App(_, _) | Type::Record(_, _) | Type::Fun(_, _))); + // Skip if the structured args themselves contain unif vars — + // these constraints are not yet resolved enough for chain ambiguity checking + let structured_args_have_unif = zonked_args.iter().any(|t| { + matches!(t, Type::App(_, _) | Type::Record(_, _) | Type::Fun(_, _)) + && !ctx.state.free_unif_vars(t).is_empty() + }); + // Skip when all args are purely polymorphic (no concrete type constructors). + // Fully polymorphic constraints like `ToRecordObj codecsRL { | codecs } { | values }` + // can't be resolved at the definition site — they'll be satisfied by callers. + let has_any_concrete = zonked_args.iter().any(|t| type_has_concrete_con(t)); + if has_structured_arg && !structured_args_have_unif && has_any_concrete { + if let Some(known) = lookup_instances(&instances, class_name) { match check_chain_ambiguity(known, &zonked_args) { ChainResult::Resolved => {} ChainResult::Ambiguous | ChainResult::NoMatch => { @@ -4856,7 +6362,6 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } } - } // Pass 2.75: Solve type-level constraints (ToString, Add, Mul). @@ -4867,8 +6372,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut solved_any = false; for i in 0..ctx.deferred_constraints.len() { let (span, class_name, _) = ctx.deferred_constraints[i]; - let class_str = crate::interner::resolve(class_name).unwrap_or_default(); - let zonked_args: Vec = ctx.deferred_constraints[i].2.iter() + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); + let zonked_args: Vec = ctx.deferred_constraints[i] + .2 + .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); match class_str.as_str() { @@ -4883,7 +6390,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } "Add" if zonked_args.len() == 3 => { - if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) { + if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) + { let result = Type::TypeInt(a.wrapping_add(*b)); if let Err(e) = ctx.state.unify(span, &zonked_args[2], &result) { errors.push(e); @@ -4893,7 +6401,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } "Mul" if zonked_args.len() == 3 => { - if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) { + if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) + { let result = Type::TypeInt(a.wrapping_mul(*b)); if let Err(e) = ctx.state.unify(span, &zonked_args[2], &result) { errors.push(e); @@ -4903,13 +6412,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } "Compare" if zonked_args.len() == 3 => { - if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) { + if let (Type::TypeInt(a), Type::TypeInt(b)) = (&zonked_args[0], &zonked_args[1]) + { let ordering_str = match a.cmp(b) { std::cmp::Ordering::Less => "LT", std::cmp::Ordering::Equal => "EQ", std::cmp::Ordering::Greater => "GT", }; - let result = Type::Con(crate::interner::intern(ordering_str)); + let result = Type::Con(qi(intern(ordering_str))); if let Err(e) = ctx.state.unify(span, &zonked_args[2], &result) { errors.push(e); } else { @@ -4936,6 +6446,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + timed_pass!(2, "done", ""); // Pass 3: Check deferred type class constraints for (span, class_name, type_args) in &ctx.deferred_constraints { super::check_deadline(); @@ -4947,9 +6458,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Skip if any arg still contains unsolved unification variables or type variables // (polymorphic usage — no concrete instance needed). // We check deeply since unif vars can be nested inside App, e.g. Show ((?1 ?2) ?2). - let has_unsolved = zonked_args.iter().any(|t| { - !ctx.state.free_unif_vars(t).is_empty() || contains_type_var(t) - }); + let has_unsolved = zonked_args + .iter() + .any(|t| !ctx.state.free_unif_vars(t).is_empty() || contains_type_var(t)); if has_unsolved { // For classes with instance chains: check for ambiguous chain resolution. @@ -4965,10 +6476,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // they shouldn't be checked for chain ambiguity at the definition site. // The actual ambiguity (e.g. TLShow (S i)) is caught in Pass 2.5 via // sig_deferred_constraints when the function is called with concrete args. - let is_given = ctx.signature_constraints.values() + let is_given = ctx + .signature_constraints + .values() .any(|constraints| constraints.iter().any(|(cn, _)| cn == class_name)); if !is_given { - if let Some(known) = instances.get(class_name) { + if let Some(known) = lookup_instances(&instances, class_name) { let has_concrete_instance = known.iter().any(|(inst_types, _)| { inst_types.iter().any(|t| !matches!(t, Type::Var(_))) }); @@ -4990,15 +6503,39 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // can't be resolved. Only when at least one arg is a structured type // (App/Record/Fun) — bare Var/Unif/Con args alone shouldn't trigger this. let all_pure_unif = zonked_args.iter().all(|t| matches!(t, Type::Unif(_))); - let has_structured_arg = zonked_args.iter().any(|t| matches!(t, Type::App(_, _) | Type::Record(_, _) | Type::Fun(_, _))); - if chained_classes.contains(class_name) && !all_bare_vars && !all_pure_unif && has_structured_arg { + let has_structured_arg = zonked_args + .iter() + .any(|t| matches!(t, Type::App(_, _) | Type::Record(_, _) | Type::Fun(_, _))); + if chained_classes.contains(class_name) + && !all_bare_vars + && !all_pure_unif + && has_structured_arg + { + // Skip if the class is "given" by an enclosing function's type signature. + // These constraints are polymorphic and will be satisfied by the caller. + let is_given = ctx + .signature_constraints + .values() + .any(|constraints| constraints.iter().any(|(cn, _)| cn == class_name)); + if is_given { + continue; + } + // When args contain forall-bound type variables (Type::Var), use chain-aware // ambiguity checking. This properly handles cases like Inject g (Either f g) // where an earlier instance in the chain is "not Apart" (could match if g=f) // but our exact matcher says no-match and skips to a later instance. let has_type_vars = zonked_args.iter().any(|t| contains_type_var(t)); if has_type_vars { - if let Some(known) = instances.get(class_name) { + // If there are also unsolved unification variables, skip the check — + // we can't determine chain ambiguity with partially-known types. + let has_unif_vars = zonked_args + .iter() + .any(|t| !ctx.state.free_unif_vars(t).is_empty()); + if has_unif_vars { + continue; + } + if let Some(known) = lookup_instances(&instances, class_name) { match check_chain_ambiguity(known, &zonked_args) { ChainResult::Resolved => {} ChainResult::Ambiguous | ChainResult::NoMatch => { @@ -5011,7 +6548,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } } else { - match check_instance_depth(&instances, &ctx.state.type_aliases, class_name, &zonked_args, 0) { + // If any arg is a bare unif var, instance resolution can't + // match it against constructor-headed instance patterns (like + // RL.Cons/RL.Nil in RowList-based classes). Defer — the unif + // var may be resolved by another constraint (e.g., RowToList). + let has_bare_unif = zonked_args.iter().any(|t| matches!(t, Type::Unif(_))); + if has_bare_unif { + continue; + } + match check_instance_depth( + &instances, + &ctx.state.type_aliases, + class_name, + &zonked_args, + 0, + Some(&known_classes), + Some(&ctx.type_con_arities), + ) { InstanceResult::Match => {} InstanceResult::NoMatch => { errors.push(TypeError::NoInstanceFound { @@ -5027,16 +6580,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { type_args: zonked_args, }); } + InstanceResult::UnknownClass(unknown) => { + errors.push(TypeError::UnknownClass { + span: *span, + name: unknown, + }); + } } } continue; } { - let class_str = crate::interner::resolve(*class_name).unwrap_or_default(); + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); let has_type_vars = zonked_args.iter().any(|t| contains_type_var(t)); if class_str == "Coercible" && zonked_args.len() == 2 && !has_type_vars { - let both_have_unif = zonked_args.iter() + let both_have_unif = zonked_args + .iter() .all(|t| !ctx.state.free_unif_vars(t).is_empty()); if both_have_unif { continue; @@ -5074,7 +6634,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } CoercibleResult::KindMismatch => { - errors.push(TypeError::KindMismatch { + errors.push(TypeError::KindsDoNotUnify { span: *span, expected: zonked_args[0].clone(), found: zonked_args[1].clone(), @@ -5087,22 +6647,62 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check if the class has zero registered instances — if so, the constraint // is guaranteed unsatisfiable regardless of what the unsolved vars become. - // We fire when either: - // 1. All args are pure unsolved unif vars (completely unconstrained), OR - // 2. The constraint has no type variables (only concrete types + unif vars), - // meaning it's not from a polymorphic context like `forall a. Foo a => ...` - let class_has_instances = instances.get(class_name) + // Only fire when concrete types (no type variables) are present — constraints + // from polymorphic contexts like `forall a. Foo a => ...` are satisfied by callers. + // When all args are pure unsolved unif vars, only report if the constraint is NOT + // "given" by a type signature (i.e., it arose from the body, not the declared type). + // Given constraints (from signatures) will be discharged by callers, even if zero + // instances are visible locally (instances may exist in downstream modules). + let class_has_instances = instances + .get(class_name) .map_or(false, |insts| !insts.is_empty()); let all_pure_unif = zonked_args.iter().all(|t| matches!(t, Type::Unif(_))); let has_type_vars = zonked_args.iter().any(|t| contains_type_var(t)); - if !class_has_instances && (all_pure_unif || !has_type_vars) { - let class_str = crate::interner::resolve(*class_name).unwrap_or_default(); + // Check if any arg contains unsolved unif vars (mixed with concrete types). + // When mixed unif vars are present, the constraint may be satisfiable through + // superclass constraints not yet tracked in signature_constraints. + // But reject pure-unif constraints (all args unknown) with zero instances. + let has_mixed_unif = !all_pure_unif && zonked_args.iter().any(|t| !ctx.state.free_unif_vars(t).is_empty()); + let is_given = ctx + .signature_constraints + .values() + .any(|constraints| constraints.iter().any(|(cn, _)| cn == class_name)); + // Also treat constraints as "given" if all their unif vars were generalized + // in a let/where binding (e.g., `where bind = ibind` generalizes the class + // method's constraint vars — they belong to the polymorphic scheme, not the + // outer context, so the constraint is satisfied by callers). + let all_generalized = all_pure_unif && zonked_args.iter().all(|t| { + if let Type::Unif(id) = t { + ctx.state.generalized_vars.contains(id) + } else { + false + } + }); + if !class_has_instances && !has_type_vars && !has_mixed_unif + && (!all_pure_unif || (!is_given && !all_generalized)) + { + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); // Skip compiler-magic classes that are resolved without explicit instances - let is_magic = matches!(class_str.as_str(), - "Partial" | "Warn" | "Coercible" | "IsSymbol" | "Fail" - | "Union" | "Cons" | "Lacks" | "RowToList" | "Nub" - | "CompareSymbol" | "Append" | "Compare" | "Add" | "Mul" - | "ToString" | "Reflectable" | "Reifiable" + let is_magic = matches!( + class_str.as_str(), + "Partial" + | "Warn" + | "Coercible" + | "IsSymbol" + | "Fail" + | "Union" + | "Cons" + | "Lacks" + | "RowToList" + | "Nub" + | "CompareSymbol" + | "Append" + | "Compare" + | "Add" + | "Mul" + | "ToString" + | "Reflectable" + | "Reifiable" ); if !is_magic { errors.push(TypeError::NoInstanceFound { @@ -5119,8 +6719,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // not by explicit instances. Without this, fully-resolved Add/Mul/ToString // constraints would fail instance resolution since they have no instances. { - let class_str = crate::interner::resolve(*class_name).unwrap_or_default(); - if matches!(class_str.as_str(), "Add" | "Mul" | "ToString" | "Compare" | "Nub") { + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); + if matches!( + class_str.as_str(), + "Add" | "Mul" | "ToString" | "Compare" | "Nub" + ) { continue; } } @@ -5128,7 +6731,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Coercible solver: handle Coercible constraints with role-based decomposition. // Only solve when no type variables remain (polymorphic constraints are deferred). if !has_unsolved { - let class_str = crate::interner::resolve(*class_name).unwrap_or_default(); + // TODO: check module + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); if class_str == "Coercible" && zonked_args.len() == 2 { match solve_coercible( &zonked_args[0], @@ -5163,7 +6767,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } CoercibleResult::KindMismatch => { - errors.push(TypeError::KindMismatch { + errors.push(TypeError::KindsDoNotUnify { span: *span, expected: zonked_args[0].clone(), found: zonked_args[1].clone(), @@ -5184,7 +6788,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { name: *class_name, }); } else { - match check_instance_depth(&instances, &ctx.state.type_aliases, class_name, &zonked_args, 0) { + match check_instance_depth( + &instances, + &ctx.state.type_aliases, + class_name, + &zonked_args, + 0, + Some(&known_classes), + Some(&ctx.type_con_arities), + ) { InstanceResult::Match => { // Kind-check the constraint type against the class's kind signature. // This catches cases like IxFunctor (Indexed Array) where the class @@ -5193,15 +6805,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if type_args.len() == 1 { if let Type::Unif(param_id) = &type_args[0] { if let Some(app_args) = ctx.class_param_app_args.get(param_id) { - let zonked_app_args: Vec = app_args.iter() - .map(|t| ctx.state.zonk(t.clone())) - .collect(); + let zonked_app_args: Vec = + app_args.iter().map(|t| ctx.state.zonk(t.clone())).collect(); if let Err(e) = check_class_param_kind_consistency( *span, *class_name, &zonked_args[0], &zonked_app_args, &saved_type_kinds, + &saved_class_kinds, ) { errors.push(e); } @@ -5223,6 +6835,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { type_args: zonked_args, }); } + InstanceResult::UnknownClass(unknown) => { + errors.push(TypeError::UnknownClass { + span: *span, + name: unknown, + }); + } } } } @@ -5235,13 +6853,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); - let has_unsolved = zonked_args.iter().any(|t| { - !ctx.state.free_unif_vars(t).is_empty() || contains_type_var(t) - }); + let has_unsolved = zonked_args + .iter() + .any(|t| !ctx.state.free_unif_vars(t).is_empty() || contains_type_var(t)); if has_unsolved { continue; } - if let InstanceResult::DepthExceeded = check_instance_depth(&instances, &ctx.state.type_aliases, class_name, &zonked_args, 0) { + if let InstanceResult::DepthExceeded = check_instance_depth( + &instances, + &ctx.state.type_aliases, + class_name, + &zonked_args, + 0, + None, + Some(&ctx.type_con_arities), + ) { errors.push(TypeError::PossiblyInfiniteInstance { span: *span, class_name: *class_name, @@ -5292,10 +6918,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } else if let Some(crate::cst::DataMembers::Explicit(ctors)) = members { // Check that each listed constructor actually belongs to this type - let valid_ctors = ctx.data_constructors.get(name); + let valid_ctors = ctx.data_constructors.get(&qi(*name)); for ctor in ctors { - let is_valid = valid_ctors - .map_or(false, |cs| cs.contains(ctor)); + let is_valid = valid_ctors.map_or(false, |cs| cs.contains(&qi(*ctor))); if !is_valid { errors.push(TypeError::UnkownExport { span: export_list.span, @@ -5308,12 +6933,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // T() (empty list) is valid — opaque type export. if !ctors.is_empty() { if let Some(all_ctors) = valid_ctors { - let exported_set: std::collections::HashSet<_> = ctors.iter().copied().collect(); + let exported_set: std::collections::HashSet = + ctors.iter().map(|c| qi(*c)).collect(); for ctor in all_ctors { if !exported_set.contains(ctor) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: *name, + exported: qi(*name), dependency: *ctor, }); } @@ -5340,7 +6966,13 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut value_op_targets: HashMap = HashMap::new(); let mut type_op_targets: HashMap = HashMap::new(); for decl in &module.decls { - if let Decl::Fixity { target, operator, is_type, .. } = decl { + if let Decl::Fixity { + target, + operator, + is_type, + .. + } = decl + { if *is_type { type_op_targets.insert(operator.value, target.name); } else { @@ -5350,21 +6982,33 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Transitive export checks: class members require their class, and vice versa - let exported_values: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::Value(n) => Some(*n), _ => None }) + let exported_values: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::Value(n) => Some(*n), + _ => None, + }) .collect(); - let exported_classes: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::Class(n) => Some(*n), _ => None }) + let exported_classes: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::Class(n) => Some(*n), + _ => None, + }) .collect(); // Check: exporting a class member without its class for &val in &exported_values { - if let Some((class_name, _)) = ctx.class_methods.get(&val) { + if let Some((class_name, _)) = ctx.class_methods.get(&qi(val)) { // Only check locally-defined classes (not imported ones) - if declared_classes.contains(class_name) && !exported_classes.contains(class_name) { + if declared_classes.contains(&class_name.name) && !exported_classes.contains(&class_name.name) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: val, + exported: qi(val), dependency: *class_name, }); } @@ -5374,12 +7018,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check: exporting a class without its members for &cls in &exported_classes { for (method, (class_name, _)) in &ctx.class_methods { - if *class_name == cls && !exported_values.contains(method) { + if *class_name == qi(cls) && !exported_values.contains(&method.name) { // Only check locally-defined class methods - if local_values.contains_key(method) { + if local_values.contains_key(&method.name) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: cls, + exported: qi(cls), dependency: *method, }); break; // One error per class is enough @@ -5391,13 +7035,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check: exporting a class without its superclasses (transitive) let declared_class_set: HashSet = declared_classes.iter().copied().collect(); for &cls in &exported_classes { - if let Some((_, sc_constraints)) = class_superclasses.get(&cls) { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { for (sc_class, _) in sc_constraints { // Only check locally-defined superclasses - if declared_class_set.contains(sc_class) && !exported_classes.contains(sc_class) { + if sc_class.module == None && declared_class_set.contains(&sc_class.name) && !exported_classes.contains(&sc_class.name) + { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: cls, + exported: qi(cls), dependency: *sc_class, }); } @@ -5408,17 +7053,22 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Check: exporting a value operator without its target function (local defs only) for &val in &exported_values { if let Some(&target) = value_op_targets.get(&val) { - if ctx.ctor_details.contains_key(&target) { + // Only check locally-defined constructors, not imported ones + let is_local_ctor = ctx.ctor_details.contains_key(&qi(target)) + && local_values.contains_key(&target); + if is_local_ctor { // Operator aliases a data constructor — check that the constructor // is exported through its parent type's constructor list. + let target_qi = qi(target); let ctor_exported = export_list.value.exports.iter().any(|e| { if let Export::Type(ty_name, Some(members)) = e { - let type_ctors = ctx.data_constructors.get(ty_name); - let has_this_ctor = type_ctors.map_or(false, |cs| cs.contains(&target)); - has_this_ctor && match members { - crate::cst::DataMembers::All => true, - crate::cst::DataMembers::Explicit(cs) => cs.contains(&target), - } + let type_ctors = ctx.data_constructors.get(&qi(*ty_name)); + let has_this_ctor = type_ctors.map_or(false, |cs| cs.contains(&target_qi)); + has_this_ctor + && match members { + crate::cst::DataMembers::All => true, + crate::cst::DataMembers::Explicit(cs) => cs.contains(&target), + } } else { false } @@ -5426,28 +7076,38 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !ctor_exported { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: val, - dependency: target, + exported: qi(val), + dependency: qi(target), }); } - } else if local_values.contains_key(&target) - && !exported_values.contains(&target) - { + } else if local_values.contains_key(&target) && !exported_values.contains(&target) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: val, - dependency: target, + exported: qi(val), + dependency: qi(target), }); } } } // Check: exporting a type operator without its target type (local defs only) - let exported_types: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::Type(n, _) => Some(*n), _ => None }) + let exported_types: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::Type(n, _) => Some(*n), + _ => None, + }) .collect(); - let exported_type_ops: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::TypeOp(n) => Some(*n), _ => None }) + let exported_type_ops: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::TypeOp(n) => Some(*n), + _ => None, + }) .collect(); let declared_type_set: HashSet<&Symbol> = declared_types.iter().collect(); for &op in &exported_type_ops { @@ -5455,8 +7115,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if declared_type_set.contains(&target) && !exported_types.contains(&target) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: op, - dependency: target, + exported: qi(op), + dependency: qi(target), }); } } @@ -5471,8 +7131,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if declared_type_set.contains(dep) && !exported_types.contains(dep) { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: ty_name, - dependency: *dep, + exported: qi(ty_name), + dependency: qi(*dep), }); break; } @@ -5485,18 +7145,20 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for export in &export_list.value.exports { if let Export::Type(ty_name, Some(crate::cst::DataMembers::All)) = export { // This type is exported with all constructors — check field types - if let Some(ctors) = ctx.data_constructors.get(ty_name) { + if let Some(ctors) = ctx.data_constructors.get(&qi(*ty_name)) { 'ctor_loop: for ctor in ctors { if let Some((_, _, field_types)) = ctx.ctor_details.get(ctor) { for field_ty in field_types { let mut referenced = Vec::new(); collect_type_constructors(field_ty, &mut referenced); for dep in &referenced { - if declared_type_set.contains(dep) && !exported_types.contains(dep) { + if declared_type_set.contains(dep) + && !exported_types.contains(dep) + { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: *ty_name, - dependency: *dep, + exported: qi(*ty_name), + dependency: qi(*dep), }); break 'ctor_loop; } @@ -5513,17 +7175,24 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for export in &export_list.value.exports { if let Export::Type(ty_name, _) = export { for decl in &module.decls { - if let Decl::Data { name, type_var_kind_anns, .. } = decl { + if let Decl::Data { + name, + type_var_kind_anns, + .. + } = decl + { if name.value == *ty_name { for kind_ann in type_var_kind_anns.iter().flatten() { let mut kind_refs = HashSet::new(); collect_type_refs(kind_ann, &mut kind_refs); for dep in &kind_refs { - if declared_type_set.contains(dep) && !exported_types.contains(dep) { + if declared_type_set.contains(dep) + && !exported_types.contains(dep) + { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: *ty_name, - dependency: *dep, + exported: qi(*ty_name), + dependency: qi(*dep), }); } } @@ -5535,13 +7204,36 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } - // Check: exporting a value whose type references a locally-defined type that is not exported + // Check: exporting a value whose type references a locally-defined type that is not exported. + // Skip type aliases — PureScript doesn't require type aliases to be explicitly exported + // for the transitive export check on values (aliases are transparent). if let Some(ref export_list) = module.exports { - let exp_vals: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::Value(n) => Some(*n), _ => None }) + let exp_vals: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::Value(n) => Some(*n), + _ => None, + }) + .collect(); + let exp_types: HashSet = export_list + .value + .exports + .iter() + .filter_map(|e| match e { + Export::Type(n, _) => Some(*n), + _ => None, + }) .collect(); - let exp_types: HashSet = export_list.value.exports.iter() - .filter_map(|e| match e { Export::Type(n, _) => Some(*n), _ => None }) + // Collect local type alias names to exclude from the check + let local_type_aliases: HashSet = module + .decls + .iter() + .filter_map(|d| match d { + Decl::TypeAlias { name, .. } => Some(name.value), + _ => None, + }) .collect(); for &val in &exp_vals { if let Some(scheme) = local_values.get(&val) { @@ -5549,12 +7241,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut referenced_types = Vec::new(); collect_type_constructors(&zonked, &mut referenced_types); for ty_name in &referenced_types { - // Only flag local types that are not exported - if declared_types.contains(ty_name) && !exp_types.contains(ty_name) { + // Only flag local non-alias types that are not exported + if declared_types.contains(ty_name) + && !exp_types.contains(ty_name) + && !local_type_aliases.contains(ty_name) + { errors.push(TypeError::TransitiveExportError { span: export_list.span, - exported: val, - dependency: *ty_name, + exported: qi(val), + dependency: qi(*ty_name), }); break; // One error per value is enough } @@ -5569,40 +7264,53 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let local_type_set: HashSet = declared_types.iter().copied().collect(); let local_class_set: HashSet = declared_classes.iter().copied().collect(); - let mut export_data_constructors: HashMap> = HashMap::new(); - let mut export_ctor_details: HashMap, Vec)> = HashMap::new(); + let mut export_data_constructors: HashMap> = HashMap::new(); + let mut export_ctor_details: HashMap, Vec)> = HashMap::new(); for type_name in &declared_types { - if let Some(ctors) = ctx.data_constructors.get(type_name) { - export_data_constructors.insert(*type_name, ctors.clone()); + if let Some(ctors) = ctx.data_constructors.get(&qi(*type_name)) { + export_data_constructors.insert(qi(*type_name), ctors.clone()); for ctor in ctors { - if let Some(details) = ctx.ctor_details.get(ctor) { - export_ctor_details.insert(*ctor, details.clone()); + if let Some((parent, tvs, fields)) = ctx.ctor_details.get(ctor) { + // Expand type aliases in field types so downstream modules + // can resolve them even without importing the alias names + // (e.g. NutF wraps Nut' which is a local alias for Entity Int (Node payload)). + let expanded_fields: Vec = fields.iter() + .map(|f| expand_type_aliases(f, &ctx.state.type_aliases)) + .collect(); + export_ctor_details.insert(*ctor, (*parent, tvs.iter().map(|s| qi(*s)).collect(), expanded_fields)); } } } } - // Also export ctor_details for operator aliases (e.g. `:|` for `NonEmpty`). + // Also export ctor_details for operator aliases (e.g. `:|` for `NonEmpty`, `:` for `Cons`). // These are registered during fixity processing but not in data_constructors. - for (name, details) in &ctx.ctor_details { - if local_values.contains_key(name) && !export_ctor_details.contains_key(name) { - export_ctor_details.insert(*name, details.clone()); + // Include both locally-defined and imported operators so downstream modules can + // resolve operator aliases for exhaustiveness checking. + for (name, (parent, tvs, fields)) in &ctx.ctor_details { + if !export_ctor_details.contains_key(name) { + let expanded_fields: Vec = fields.iter() + .map(|f| expand_type_aliases(f, &ctx.state.type_aliases)) + .collect(); + export_ctor_details.insert(*name, (*parent, tvs.iter().map(|s| qi(*s)).collect(), expanded_fields)); } } - let mut export_class_methods: HashMap)> = HashMap::new(); + let mut export_class_methods: HashMap)> = HashMap::new(); for (method, (class_name, tvs)) in &ctx.class_methods { - if local_class_set.contains(class_name) { - export_class_methods.insert(*method, (*class_name, tvs.clone())); + if local_class_set.contains(&class_name.name) { + export_class_methods.insert(*method, (*class_name, tvs.iter().map(|s| qi(*s)).collect())); } } // Register locally-defined class names as types in data_constructors so they // participate in ExportConflict detection (classes are types in PureScript). for class_name in &declared_classes { - export_data_constructors.entry(*class_name).or_insert_with(Vec::new); + export_data_constructors + .entry(qi(*class_name)) + .or_insert_with(Vec::new); } - let mut export_instances: HashMap, Vec<(Symbol, Vec)>)>> = + let mut export_instances: HashMap, Vec<(QualifiedIdent, Vec)>)>> = HashMap::new(); for (class_name, insts) in &instances { // Export all instances (both for local and imported classes) since instances @@ -5610,9 +7318,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { export_instances.insert(*class_name, insts.clone()); } - let mut export_type_operators: HashMap = HashMap::new(); - let mut export_value_fixities: HashMap = HashMap::new(); - let mut export_function_op_aliases: HashSet = HashSet::new(); + let mut export_type_operators: HashMap = HashMap::new(); + let mut export_type_fixities: HashMap = HashMap::new(); + let mut export_value_fixities: HashMap = HashMap::new(); + let mut export_function_op_aliases: HashSet = HashSet::new(); + let mut export_value_operator_targets: HashMap = HashMap::new(); for decl in &module.decls { if let Decl::Fixity { associativity, @@ -5624,32 +7334,59 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } = decl { if *is_type { - export_type_operators.insert(operator.value, target.name); + export_type_operators.insert(qi(operator.value), qi(target.name)); + export_type_fixities.insert(qi(operator.value), (*associativity, *precedence)); } else { - export_value_fixities.insert(operator.value, (*associativity, *precedence)); + export_value_fixities.insert(qi(operator.value), (*associativity, *precedence)); + export_value_operator_targets.insert(qi(operator.value), qi(target.name)); // Track operators that alias functions (not constructors) - if !ctx.ctor_details.contains_key(&target.name) { - export_function_op_aliases.insert(operator.value); + if !ctx.ctor_details.contains_key(&qi(target.name)) { + export_function_op_aliases.insert(qi(operator.value)); } } } } + // Pre-compute self-referential alias set (as QualifiedIdent) for export expansion. + // Self-referential aliases like `type Thread = { state :: ShowRef Thread, ... }` must + // not be expanded during export to prevent cross-module double-expansion. + let self_ref_qis: HashSet = ctx.state.self_referential_aliases + .iter() + .map(|s| qi(*s)) + .collect(); + // Collect type aliases for export, pre-expanding bodies so importing modules // don't need transitive access to aliases used in the bodies. // Use the depth-limited variant to avoid infinite recursion on cyclic aliases // (e.g. `type Effect = Effect` re-exports). - let export_type_aliases: HashMap, Type)> = ctx.state.type_aliases.iter() + let export_type_aliases: HashMap, Type)> = ctx + .state + .type_aliases + .iter() .map(|(name, (params, body))| { - let expanded_body = expand_type_aliases_limited(body, &ctx.state.type_aliases, 0); - (*name, (params.clone(), expanded_body)) + let mut expanding = self_ref_qis.clone(); + let expanded_body = expand_type_aliases_limited_inner(body, &ctx.state.type_aliases, Some(&ctx.type_con_arities), 0, &mut expanding); + (qi(*name), (params.iter().map(|p| qi(*p)).collect(), expanded_body)) }) .collect(); // Expand type aliases in all exported values so importing modules don't - // need access to module-local aliases like `type Size = Int`. + // need transitive access to aliases like `type DriverStateRec = { component :: ComponentSpec ... }`. + // Use the arities-aware variant to handle over-saturated aliases like `Except e a` + // where `Except` has 1 param but appears with 2 args. + // Pre-seed the expanding set with self-referential aliases to prevent cross-module + // double-expansion (e.g. `type Thread = { state :: ShowRef Thread, ... }` would + // be expanded at export time, then again at import time, creating ever-deeper types). for scheme in local_values.values_mut() { scheme.ty = ctx.state.zonk(scheme.ty.clone()); + let mut expanding = self_ref_qis.clone(); + scheme.ty = expand_type_aliases_limited_inner( + &scheme.ty, + &ctx.state.type_aliases, + Some(&ctx.type_con_arities), + 0, + &mut expanding, + ); } // Build origin maps: all locally-defined names have origin = this module @@ -5661,49 +7398,113 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { value_origins.insert(*name, current_mod_sym); } for name in export_data_constructors.keys() { - type_origins.insert(*name, current_mod_sym); + type_origins.insert(name.name, current_mod_sym); + } + // Also include origins for imported types that may appear in exported value schemes. + // Without this, types like `Response` (from JS.Fetch.Response, imported by JS.Fetch + // but not in its explicit export list) wouldn't have origins, preventing downstream + // modules from canonicalizing them to avoid local alias collisions. + for import_decl in &module.imports { + if is_prim_module(&import_decl.module) || is_prim_submodule(&import_decl.module) { + continue; + } + if let Some(mod_exports) = registry.lookup(&import_decl.module.parts) { + for (&name, &origin) in &mod_exports.type_origins { + type_origins.entry(name).or_insert(origin); + } + // Also propagate value_origins and class_origins from imports. + // Without this, re-exported values (like Tuple constructor from Prelude) + // default to source_mod_sym, incorrectly marking them as locally defined + // and causing false export conflicts. + for (&name, &origin) in &mod_exports.value_origins { + value_origins.entry(name).or_insert(origin); + } + for (&name, &origin) in &mod_exports.class_origins { + class_origins.entry(name).or_insert(origin); + } + } } for class_name in &declared_classes { class_origins.insert(*class_name, current_mod_sym); } for (_, (class_name, _)) in &export_class_methods { - class_origins.insert(*class_name, current_mod_sym); + class_origins.insert(class_name.name, current_mod_sym); } - + // Type aliases in local_values were already expanded at lines 7148-7158 using + // expand_type_aliases_limited_inner (which handles qualified names correctly). + // Do NOT re-expand here: expand_type_aliases uses unqualified lookup which would + // incorrectly expand data type references (e.g. HATS.Easing) as aliases. let mut module_exports = ModuleExports { - values: local_values, + values: local_values.iter().map(|(&k, v)| { + (qi(k), Scheme { forall_vars: v.forall_vars.clone(), ty: v.ty.clone() }) + }).collect(), class_methods: export_class_methods, data_constructors: export_data_constructors, ctor_details: export_ctor_details, instances: export_instances, type_operators: export_type_operators, + type_fixities: export_type_fixities, value_fixities: export_value_fixities, function_op_aliases: export_function_op_aliases, - constrained_class_methods: ctx.constrained_class_methods.clone(), + value_operator_targets: export_value_operator_targets, + constrained_class_methods: ctx.constrained_class_methods.iter().map(|s| qi(*s)).collect(), type_aliases: export_type_aliases, class_param_counts: class_param_counts.clone(), value_origins, type_origins, class_origins, - operator_class_targets: ctx.operator_class_targets.clone(), - class_fundeps: ctx.class_fundeps.clone(), - type_con_arities: ctx.type_con_arities.clone(), + operator_class_targets: ctx.operator_class_targets.iter().map(|(k, v)| (k.name, v.name)).collect(), + class_fundeps: ctx.class_fundeps.iter().map(|(k, v)| (k.name, v.clone())).collect(), + type_con_arities: ctx.type_con_arities.iter() + .filter(|(k, _)| k.module.is_none()) + .map(|(k, v)| (*k, *v)) + .collect(), type_roles: ctx.type_roles.clone(), - newtype_names: ctx.newtype_names.clone(), + newtype_names: ctx.newtype_names.iter().map(|n| n.name).collect(), signature_constraints: ctx.signature_constraints.clone(), - partial_dischargers: ctx.partial_dischargers.clone(), - type_kinds: saved_type_kinds.iter() - .filter(|(name, _)| local_type_names.contains(name)) - .map(|(&name, kind)| (name, generalize_kind_for_export(kind))) + partial_dischargers: ctx.partial_dischargers.iter().map(|n| n.name).collect(), + self_referential_aliases: ctx.state.self_referential_aliases.clone(), + type_kinds: saved_type_kinds + .iter() + .filter(|(name, _)| local_type_names.contains(&name.name)) + .map(|(name, kind)| { + let generalized = generalize_kind_for_export(kind); + // Strip import-alias module qualifiers from exported kinds so downstream + // modules can add their own qualifiers via qualify_kind_refs. + (name.name, strip_kind_qualifiers(&generalized)) + }) + .collect(), + class_type_kinds: saved_class_kinds + .iter() + .map(|(name, kind)| { + let generalized = generalize_kind_for_export(kind); + (name.name, strip_kind_qualifiers(&generalized)) + }) .collect(), }; + // Ensure operator targets (e.g. Tuple for /\) are included in exported values and + // ctor_details, even when the target was imported rather than locally defined. + for (_op, target) in &module_exports.value_operator_targets.clone() { + if !module_exports.values.contains_key(target) { + if let Some(scheme) = env.lookup(target.name) { + module_exports.values.insert(*target, scheme.clone()); + } + } + if !module_exports.ctor_details.contains_key(target) { + if let Some(details) = ctx.ctor_details.get(target) { + module_exports.ctor_details.insert(*target, (details.0, details.1.iter().map(|s| qi(*s)).collect(), details.2.clone())); + } + } + } + // Post-inference kind validation: check that inferred types are kind-consistent. // This catches kind mismatches like `TProxy "apple"` where TProxy expects Type but // "apple" has kind Symbol, which occur when type variables are instantiated to // type-level literals during inference. // Only check types that contain type-level literals, since those are the main // cases where kind mismatches arise from type inference. + let saved_type_kinds_sym: HashMap = saved_type_kinds.iter().map(|(k, v)| (k.name, v.clone())).collect(); if !saved_type_kinds.is_empty() { fn contains_type_literal(ty: &Type) -> bool { match ty { @@ -5719,13 +7520,21 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } for (&_name, ty) in &result_types { - if !contains_type_literal(ty) { continue; } + if !contains_type_literal(ty) { + continue; + } // Find span for this declaration - let decl_span = module.decls.iter().find_map(|d| match d { - Decl::Value { name: n, span, .. } if n.value == _name => Some(*span), - _ => None, - }).unwrap_or(Span::new(0, 0)); - if let Err(e) = crate::typechecker::kind::check_inferred_type_kind(ty, &saved_type_kinds, decl_span) { + let decl_span = module + .decls + .iter() + .find_map(|d| match d { + Decl::Value { name: n, span, .. } if n.value == _name => Some(*span), + _ => None, + }) + .unwrap_or(Span::new(0, 0)); + if let Err(e) = + crate::typechecker::kind::check_inferred_type_kind(ty, &saved_type_kinds_sym, decl_span) + { errors.push(e); } } @@ -5735,8 +7544,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // (e.g., `"foo" -> String` when a polykinded type variable was unified with "foo"). for (ty, span) in &ctx.deferred_kind_checks { let zonked = ctx.state.zonk(ty.clone()); - if !contains_type_literal(&zonked) { continue; } - if let Err(e) = crate::typechecker::kind::check_inferred_type_kind(&zonked, &saved_type_kinds, *span) { + if !contains_type_literal(&zonked) { + continue; + } + if let Err(e) = crate::typechecker::kind::check_inferred_type_kind( + &zonked, + &saved_type_kinds_sym, + *span, + ) { errors.push(e); } } @@ -5754,10 +7569,43 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { &module.imports, &module.name.value, &mut errors, + &ctx.scope_conflicts, ); } + // Resolve import-qualifier-prefixed type constructors in exported schemes to their + // canonical (full module path) form. This prevents import-local qualifiers like + // `CoreResponse.Response` from leaking into other modules' scopes. + { + let mut qualifier_map: HashMap = HashMap::new(); + for import_decl in &module.imports { + if let Some(ref alias) = import_decl.qualified { + let mod_sym = module_name_to_symbol(&import_decl.module); + let alias_sym = module_name_to_symbol(alias); + qualifier_map.insert(alias_sym, mod_sym); + } + } + if !qualifier_map.is_empty() { + for scheme in module_exports.values.values_mut() { + scheme.ty = resolve_type_qualifiers(&scheme.ty, &qualifier_map); + } + for details in module_exports.ctor_details.values_mut() { + for ty in &mut details.2 { + *ty = resolve_type_qualifiers(ty, &qualifier_map); + } + } + // NOTE: Do NOT resolve type qualifiers in instance types. + // Instance matching uses type_con_qi_eq which is lenient about + // module qualifiers. Canonicalizing instance types (e.g., + // List.List → Data.List.Types.List) breaks matching against + // local import aliases (e.g., List.List in the importing module). + for alias in module_exports.type_aliases.values_mut() { + alias.1 = resolve_type_qualifiers(&alias.1, &qualifier_map); + } + } + } + CheckResult { types: result_types, errors, @@ -5765,6 +7613,53 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } +/// Check if a constructor field type is directly a partially applied type synonym. +/// Only checks the outermost type expression (counts args at the top-level App chain). +/// Nested partial applications (e.g. `F ((~>) Array)`) are left for the kind checker +/// to report as KindsDoNotUnify. +fn check_field_partially_applied_synonym( + te: &crate::ast::TypeExpr, + type_aliases: &HashMap, Type)>, + type_ops: &HashMap, +) -> Option { + use crate::ast::TypeExpr; + // Count top-level App args and find the head + let mut head = te; + let mut arg_count = 0usize; + while let TypeExpr::App { constructor, .. } = head { + arg_count += 1; + head = constructor.as_ref(); + } + // Check if head is a type synonym (directly or via type operator) + let alias_sym = match head { + TypeExpr::Constructor { name, .. } => { + if type_aliases.contains_key(&name.name) { + Some(name.name) + } else { + type_ops.get(name).and_then(|target| { + if type_aliases.contains_key(&target.name) { + Some(target.name) + } else { + None + } + }) + } + } + _ => None, + }; + if let Some(sym) = alias_sym { + if let Some((params, _)) = type_aliases.get(&sym) { + if arg_count < params.len() { + return Some(TypeError::PartiallyAppliedSynonym { + span: te.span(), + name: QualifiedIdent { module: None, name: sym }, + }); + } + } + } + None +} + /// Create a qualified symbol by combining a module alias with a name. fn qualified_symbol(module: Symbol, name: Symbol) -> Symbol { let mod_str = crate::interner::resolve(module).unwrap_or_default(); @@ -5807,7 +7702,10 @@ fn collect_kind_unif_ids(kind: &Type, out: &mut Vec) -> Type { +fn replace_unif_with_var( + kind: &Type, + subst: &HashMap, +) -> Type { match kind { Type::Unif(id) => { if let Some(&sym) = subst.get(id) { @@ -5824,10 +7722,9 @@ fn replace_unif_with_var(kind: &Type, subst: &HashMap Type::Forall( - vars.clone(), - Box::new(replace_unif_with_var(body, subst)), - ), + Type::Forall(vars, body) => { + Type::Forall(vars.clone(), Box::new(replace_unif_with_var(body, subst))) + } _ => kind.clone(), } } @@ -5837,8 +7734,17 @@ fn replace_unif_with_var(kind: &Type, subst: &HashMap) -> Type { match kind { - Type::Con(name) if exported_types.contains(name) => { - Type::Con(qualified_symbol(qualifier, *name)) + Type::Con(name) => { + // Don't qualify Prim kind names — these are built-in kinds, not module-specific types. + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + if matches!(name_str.as_str(), "Type" | "Constraint" | "Symbol" | "Row" | "Int") { + return kind.clone(); + } + if name.module.is_none() && exported_types.contains(&name.name) { + Type::Con(QualifiedIdent { module: Some(qualifier), name: name.name }) + } else { + kind.clone() + } } Type::Fun(a, b) => Type::fun( qualify_kind_refs(a, qualifier, exported_types), @@ -5856,6 +7762,32 @@ fn qualify_kind_refs(kind: &Type, qualifier: Symbol, exported_types: &HashSet Type { + match kind { + Type::Con(name) if name.module.is_some() => { + Type::Con(qi(name.name)) + } + Type::Fun(a, b) => Type::fun( + strip_kind_qualifiers(a), + strip_kind_qualifiers(b), + ), + Type::App(a, b) => Type::app( + strip_kind_qualifiers(a), + strip_kind_qualifiers(b), + ), + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(strip_kind_qualifiers(body)), + ), + _ => kind.clone(), + } +} + /// Convert a ModuleName to a single symbol (joining parts with '.'). fn module_name_to_symbol(module_name: &crate::cst::ModuleName) -> Symbol { let parts: Vec = module_name @@ -5867,13 +7799,48 @@ fn module_name_to_symbol(module_name: &crate::cst::ModuleName) -> Symbol { } /// Optionally qualify a name: if qualifier is Some, prefix with "Q.", otherwise return as-is. -fn maybe_qualify(name: Symbol, qualifier: Option) -> Symbol { +fn maybe_qualify_symbol(name: Symbol, qualifier: Option) -> Symbol { match qualifier { Some(q) => qualified_symbol(q, name), None => name, } } +fn maybe_qualify_qualified_ident( + ident: QualifiedIdent, + qualifier: Option, +) -> QualifiedIdent { + match qualifier { + Some(q) => QualifiedIdent { + module: Some(q), + name: ident.name, + }, + None => ident, + } +} + +type InstanceMap = HashMap, Vec<(QualifiedIdent, Vec)>)>>; + +/// Look up instances for a class, falling back to unqualified name if needed. +/// Instance entries are stored under the exporting module's key (typically unqualified), +/// but constraints may reference the class through a qualified import (e.g. `Row.Nub`). +fn lookup_instances<'a>( + instances: &'a InstanceMap, + class_name: &QualifiedIdent, +) -> Option<&'a Vec<(Vec, Vec<(QualifiedIdent, Vec)>)>> { + instances.get(class_name).or_else(|| { + if class_name.module.is_some() { + // Qualified lookup failed — try unqualified + instances.get(&QualifiedIdent { module: None, name: class_name.name }) + } else { + // Unqualified lookup failed — search for any qualified variant with same name + instances.iter() + .find(|(k, _)| k.name == class_name.name) + .map(|(_, v)| v) + } + }) +} + /// Process all import declarations, bringing imported names into scope. /// Returns the set of explicitly imported type names (for scope conflict detection /// with local type declarations). @@ -5882,13 +7849,44 @@ fn process_imports( registry: &ModuleRegistry, env: &mut Env, ctx: &mut InferCtx, - instances: &mut HashMap, Vec<(Symbol, Vec)>)>>, + instances: &mut HashMap, Vec<(QualifiedIdent, Vec)>)>>, errors: &mut Vec, ) -> HashSet { let mut explicitly_imported_types: HashSet = HashSet::new(); // Build Prim exports once so explicit `import Prim` / `import Prim as P` resolves. let prim = prim_exports(); + // Track which modules' instances have already been imported to avoid redundant dedup work. + // Each module's exports contain all transitive instances, so we only need to import + // instances from each unique module once. + let mut imported_instance_modules: HashSet = HashSet::new(); + + // Pre-scan local type alias names so import processing can detect collisions. + // This is needed because local aliases aren't registered until Pass 1, but we need + // to know about them during import processing to qualify conflicting imported types. + let local_type_alias_names: HashSet = module.decls.iter() + .filter_map(|d| match d { + Decl::TypeAlias { name, .. } => Some(name.value), + _ => None, + }) + .collect(); + + // Pre-scan local data/newtype/foreign data type names so import processing can + // avoid registering imported type aliases that collide with local data types. + // Without this, `type Thread = { ... }` (imported alias) overwrites the local + // `newtype Thread = T Thread.Thread` in type_aliases, causing instance heads + // like `Show Thread` to be incorrectly alias-expanded to a record type. + let local_data_type_names: HashSet = module.decls.iter() + .filter_map(|d| match d { + Decl::Data { name, kind_sig, is_role_decl, .. } + if *kind_sig == crate::cst::KindSigSource::None && !is_role_decl => + Some(name.value), + Decl::Newtype { name, .. } => Some(name.value), + Decl::ForeignData { name, .. } => Some(name.value), + _ => None, + }) + .collect(); + // Track import origins for scope conflict detection. // Maps (possibly qualified) name → (origin module symbol, is_explicit). // A scope conflict occurs when a name is imported from two different origin modules @@ -5896,10 +7894,11 @@ fn process_imports( let mut import_origins: HashMap = HashMap::new(); for import_decl in &module.imports { + super::check_deadline(); // Handle Prim submodules (Prim.Coerce, Prim.Row, etc.) as built-in let prim_sub; let module_exports = if is_prim_module(&import_decl.module) { - &prim + prim } else if is_prim_submodule(&import_decl.module) { prim_sub = prim_submodule_exports(&import_decl.module); &prim_sub @@ -5926,22 +7925,27 @@ fn process_imports( let imported_names: Vec = match (&import_decl.imports, qualifier) { (None, Some(q)) => { // import M as Q — qualified names - module_exports.values.keys() - .map(|n| maybe_qualify(*n, Some(q))) + module_exports + .values + .keys() + .map(|n| maybe_qualify_symbol(n.name, Some(q))) .collect() } (None, None) => { // import M — all unqualified values - module_exports.values.keys().copied().collect() - } - (Some(ImportList::Explicit(items)), _) => { - items.iter().map(|i| maybe_qualify(import_name(i), qualifier)).collect() + module_exports.values.keys().map(|n| n.name).collect() } + (Some(ImportList::Explicit(items)), _) => items + .iter() + .map(|i| maybe_qualify_symbol(import_name(i), qualifier)) + .collect(), (Some(ImportList::Hiding(items)), _) => { let hidden: HashSet = items.iter().map(|i| import_name(i)).collect(); - module_exports.values.keys().copied() - .filter(|n| !hidden.contains(n)) - .map(|n| maybe_qualify(n, qualifier)) + module_exports + .values + .keys() + .filter(|n| !hidden.contains(&n.name)) + .map(|n| maybe_qualify_symbol(n.name, qualifier)) .collect() } }; @@ -5953,7 +7957,7 @@ fn process_imports( // For qualified imports, extract unqualified name for origin lookup let name_str = crate::interner::resolve(*name).unwrap_or_default(); if let Some(pos) = name_str.find('.') { - crate::interner::intern(&name_str[pos+1..]) + crate::interner::intern(&name_str[pos + 1..]) } else { *name } @@ -5982,10 +7986,26 @@ fn process_imports( } } + // Import instances once per unique module. In PureScript, type class instances are + // globally visible — importing any item from a module imports all its instances. + // Module-level dedup avoids redundant O(n²) per-instance comparison for reimports. + if imported_instance_modules.insert(mod_sym) { + for (class_name, insts) in &module_exports.instances { + let existing = instances.entry(*class_name).or_default(); + for inst in insts { + if !existing.iter().any(|e| e.0 == inst.0) { + existing.push(inst.clone()); + } + } + } + // Import pre-computed self-referential aliases to avoid recomputing from scratch. + ctx.state.self_referential_aliases.extend(&module_exports.self_referential_aliases); + } + match &import_decl.imports { None => { // import M — everything unqualified; import M as Q — everything qualified only - import_all(module_exports, env, ctx, instances, qualifier); + import_all(Some(import_decl.module.clone()), module_exports, env, ctx, qualifier, &local_type_alias_names, &local_data_type_names); } Some(ImportList::Explicit(items)) => { // import M (x) — listed items unqualified @@ -5998,6 +8018,7 @@ fn process_imports( } } import_item( + &import_decl.module, item, module_exports, env, @@ -6008,22 +8029,10 @@ fn process_imports( errors, ); } - // Always import all instances from the module. - // In PureScript, type class instances are globally visible — - // importing any item from a module imports all its instances. - // Deduplicate to avoid combinatorial explosion in constraint checking. - for (class_name, insts) in &module_exports.instances { - let existing = instances.entry(*class_name).or_default(); - for inst in insts { - if !existing.iter().any(|e| e.0 == inst.0) { - existing.push(inst.clone()); - } - } - } } Some(ImportList::Hiding(items)) => { let hidden: HashSet = items.iter().map(|i| import_name(i)).collect(); - import_all_except(module_exports, &hidden, env, ctx, instances, qualifier); + import_all_except(module_exports, &hidden, env, ctx, instances, qualifier, &local_data_type_names); } } } @@ -6031,83 +8040,263 @@ fn process_imports( explicitly_imported_types } + +/// Resolve import-qualifier-prefixed type constructors to canonical module names. +/// E.g., `Con(CoreResponse.Response)` → `Con(JS.Fetch.Response.Response)` when +/// `CoreResponse` maps to `JS.Fetch.Response` in the qualifier map. +fn resolve_type_qualifiers(ty: &Type, qualifier_map: &HashMap) -> Type { + match ty { + Type::Con(name) => { + if let Some(q) = name.module { + if let Some(&canonical) = qualifier_map.get(&q) { + return Type::Con(QualifiedIdent { module: Some(canonical), name: name.name }); + } + } + ty.clone() + } + Type::Fun(a, b) => Type::fun( + resolve_type_qualifiers(a, qualifier_map), + resolve_type_qualifiers(b, qualifier_map), + ), + Type::App(f, a) => Type::app( + resolve_type_qualifiers(f, qualifier_map), + resolve_type_qualifiers(a, qualifier_map), + ), + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(resolve_type_qualifiers(body, qualifier_map)), + ), + Type::Record(fields, tail) => { + let fields = fields.iter() + .map(|(l, t)| (*l, resolve_type_qualifiers(t, qualifier_map))) + .collect(); + let tail = tail.as_ref() + .map(|t| Box::new(resolve_type_qualifiers(t, qualifier_map))); + Type::Record(fields, tail) + } + _ => ty.clone(), + } +} + +/// Qualify unqualified type constructor names in a type using canonical module names. +/// For each unqualified `Con(X)` that has an entry in `canonical_origins`, replace with +/// `Con(CanonicalModule.X)`. This prevents local type aliases from incorrectly expanding +/// imported type constructors that share the same name. +fn canonicalize_type_cons(ty: &Type, canonical_origins: &HashMap) -> Type { + match ty { + Type::Con(name) => { + if name.module.is_none() { + if let Some(&origin) = canonical_origins.get(&name.name) { + return Type::Con(QualifiedIdent { module: Some(origin), name: name.name }); + } + } + ty.clone() + } + Type::Fun(a, b) => Type::fun( + canonicalize_type_cons(a, canonical_origins), + canonicalize_type_cons(b, canonical_origins), + ), + Type::App(f, a) => Type::app( + canonicalize_type_cons(f, canonical_origins), + canonicalize_type_cons(a, canonical_origins), + ), + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(canonicalize_type_cons(body, canonical_origins)), + ), + Type::Record(fields, tail) => { + let fields = fields.iter() + .map(|(l, t)| (*l, canonicalize_type_cons(t, canonical_origins))) + .collect(); + let tail = tail.as_ref() + .map(|t| Box::new(canonicalize_type_cons(t, canonical_origins))); + Type::Record(fields, tail) + } + _ => ty.clone(), + } +} + +fn canonicalize_scheme_type_cons(scheme: &Scheme, canonical_origins: &HashMap) -> Scheme { + Scheme { + forall_vars: scheme.forall_vars.clone(), + ty: canonicalize_type_cons(&scheme.ty, canonical_origins), + } +} + /// Import all names from a module's exports. /// If `qualifier` is Some, env entries are stored with qualified keys (e.g. "Q.foo"). /// Internal maps (class_methods, data_constructors, etc.) are always unqualified. fn import_all( + _from: Option, exports: &ModuleExports, env: &mut Env, ctx: &mut InferCtx, - instances: &mut HashMap, Vec<(Symbol, Vec)>)>>, qualifier: Option, + local_type_alias_names: &HashSet, + local_data_type_names: &HashSet, ) { - // Import class method info first so we can detect conflicts + // For qualified imports, qualify imported type constructors defined in the source + // module to prevent local alias collisions within this module's scope. + // E.g., `import JS.Fetch.Response as CoreResponse` qualifies `Con(Response)` to + // `Con(CoreResponse.Response)` so local `type Response = { ... }` won't expand it. + // IMPORTANT: only qualify types that actually collide with local type aliases, + // otherwise instance resolution breaks (instances use unqualified names). + let defined_types: Option<(HashSet, Symbol)> = qualifier.and_then(|q| { + let mod_sym = _from.as_ref().map(module_name_to_symbol)?; + let dt: HashSet = exports.type_origins.iter() + .filter(|(_, &origin)| origin == mod_sym) + .filter(|(&name, _)| ctx.state.type_aliases.contains_key(&name) || local_type_alias_names.contains(&name)) + .map(|(&name, _)| name) + .collect(); + if dt.is_empty() { None } else { Some((dt, q)) } + }); + + // Also canonicalize unqualified type names that collide with existing local aliases. + // This handles re-exported types: `Con(Response)` from JS.Fetch (where Response + // originates from JS.Fetch.Response) becomes `Con(JS.Fetch.Response.Response)`. + let canonical_origins: Option> = { + let mut origins: HashMap = HashMap::new(); + for (&name, &origin) in &exports.type_origins { + if ctx.state.type_aliases.contains_key(&name) || local_type_alias_names.contains(&name) { + origins.insert(name, origin); + } + } + if origins.is_empty() { None } else { Some(origins) } + }; + + // Import class method info first so we can detect conflicts. + // For qualified imports (import M as Q), only insert under the qualified key + // so we don't pollute the unqualified class_methods map. This prevents + // `import Prelude as Prelude` from re-registering `top` as a class method + // after `import Prelude hiding (top)` correctly hid it. for (name, info) in &exports.class_methods { - ctx.class_methods.insert(*name, info.clone()); + let key = maybe_qualify_qualified_ident(*name, qualifier); + ctx.class_methods.insert(key, (info.0, info.1.iter().map(|s| s.name).collect())); + // Populate class_method_schemes so instance expected-type lookups use the canonical + // class type even if the method name later gets shadowed in env by another import. + if let Some(scheme) = exports.values.get(name) { + ctx.class_method_schemes.entry(name.name).or_insert_with(|| scheme.clone()); + } } for (name, scheme) in &exports.values { // Don't let a non-class value overwrite a class method's env entry. // E.g. Data.Function.apply must not shadow Control.Apply.apply. // Only applies to unqualified imports — qualified names (Q.foo) can't conflict. - if qualifier.is_none() && ctx.class_methods.contains_key(name) && !exports.class_methods.contains_key(name) { + if qualifier.is_none() + && ctx.class_methods.contains_key(name) + && !exports.class_methods.contains_key(name) + { continue; } - env.insert_scheme(maybe_qualify(*name, qualifier), scheme.clone()); + // Apply two fixes to prevent alias expansion collisions: + // 1. For qualified imports, qualify type cons defined in source module with qualifier + // 2. Canonicalize type cons that collide with existing local aliases + let mut scheme = scheme.clone(); + if let Some((dt, q)) = &defined_types { + scheme = Scheme { + forall_vars: scheme.forall_vars, + ty: canonicalize_type_cons(&scheme.ty, &dt.iter().map(|&n| (n, *q)).collect()), + }; + } + if let Some(co) = &canonical_origins { + scheme = canonicalize_scheme_type_cons(&scheme, co); + } + env.insert_scheme(maybe_qualify_symbol(name.name, qualifier), scheme); } for (name, ctors) in &exports.data_constructors { ctx.data_constructors.insert(*name, ctors.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); + if let Some(q) = qualifier { + ctx.data_constructors.insert(QualifiedIdent { module: Some(q), name: name.name }, ctors.clone()); + } } for (name, details) in &exports.ctor_details { - ctx.ctor_details.insert(*name, details.clone()); - } - for (name, insts) in &exports.instances { - let existing = instances.entry(*name).or_default(); - for inst in insts { - if !existing.contains(inst) { - existing.push(inst.clone()); + let entry = (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone()); + if let Some(q) = qualifier { + // Qualified import: store under qualified key (e.g. M.Leaf) + ctx.ctor_details.insert(QualifiedIdent { module: Some(q), name: name.name }, entry.clone()); + // Also store unqualified, but don't overwrite existing entries from explicit imports + // (e.g. don't let Map's Leaf(0 args) overwrite Tree's Leaf(1 arg)) + let unqualified = QualifiedIdent { module: None, name: name.name }; + if !ctx.ctor_details.contains_key(&unqualified) { + ctx.ctor_details.insert(unqualified, entry); } + } else { + ctx.ctor_details.insert(*name, entry); } } + // Instances are imported centrally in process_imports with module-level dedup. for (op, target) in &exports.type_operators { ctx.type_operators.insert(*op, *target); } for (op, fixity) in &exports.value_fixities { - ctx.value_fixities.insert(*op, *fixity); + ctx.value_fixities.insert(op.name, *fixity); } for op in &exports.function_op_aliases { ctx.function_op_aliases.insert(*op); } + // For constructor operators (not function aliases), also import the target + // constructor's scheme under its target name, because Binder::Constructor + // uses the target name (e.g. `:|` → `NonEmpty`, `:` → `Cons`). + // Function operator targets (e.g. `$` → `apply`) are NOT imported under their + // target names to avoid collisions (Data.Function.apply vs Control.Apply.apply). + for (op, target) in &exports.value_operator_targets { + if !exports.function_op_aliases.contains(op) { + if let Some(scheme) = exports.values.get(target) { + env.insert_scheme(maybe_qualify_symbol(target.name, qualifier), scheme.clone()); + } + } + } for (op, target) in &exports.operator_class_targets { - ctx.operator_class_targets.insert(*op, *target); + ctx.operator_class_targets.insert(maybe_qualify_qualified_ident(qi(*op), qualifier), maybe_qualify_qualified_ident(qi(*target), qualifier)); } for name in &exports.constrained_class_methods { - ctx.constrained_class_methods.insert(*name); + ctx.constrained_class_methods.insert(name.name); } for (name, alias) in &exports.type_aliases { - ctx.state.type_aliases.insert(*name, alias.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); + let sym_params: Vec = alias.0.iter().map(|p| p.name).collect(); + // For qualified imports (import M as Q), don't overwrite existing unqualified + // type aliases. This prevents a re-exported 0-param alias (e.g. `Parents = Array Parent_`) + // from clobbering a correctly-imported 1-param alias of the same name. + // Also don't register under unqualified key if it collides with a locally-defined + // data/newtype name — this prevents `type Thread = { ... }` (imported alias) from + // overwriting the local `newtype Thread` during alias expansion. + let collides_with_local_data = local_data_type_names.contains(&name.name); + if !collides_with_local_data && (qualifier.is_none() || !ctx.state.type_aliases.contains_key(&name.name)) { + ctx.state.type_aliases.insert(name.name, (sym_params.clone(), alias.1.clone())); + } + let qualified_name = maybe_qualify_symbol(name.name, qualifier); + // Also store under qualified key so alias expansion can disambiguate + // when multiple modules export the same alias name with different bodies. + if qualifier.is_some() { + ctx.state.type_aliases.insert(qualified_name, (sym_params, alias.1.clone())); + ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(*name, qualifier)); + } } for (name, arity) in &exports.type_con_arities { - ctx.type_con_arities.insert(*name, *arity); + ctx.type_con_arities.insert(maybe_qualify_qualified_ident(*name, qualifier), *arity); } for (name, roles) in &exports.type_roles { ctx.type_roles.insert(*name, roles.clone()); } for name in &exports.newtype_names { - ctx.newtype_names.insert(*name); + ctx.newtype_names.insert(maybe_qualify_qualified_ident(qi(*name), qualifier)); } for name in &exports.partial_dischargers { - ctx.partial_dischargers.insert(*name); + ctx.partial_dischargers.insert(maybe_qualify_qualified_ident(qi(*name), qualifier)); } for (name, constraints) in &exports.signature_constraints { // Only import Coercible constraints from other modules (other constraints // are handled locally via extract_type_signature_constraints on CST types) - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); + let coercible_only: Vec<_> = constraints + .iter() + .filter(|(cn, _)| crate::interner::resolve(cn.name).unwrap_or_default() == "Coercible") + .cloned() + .collect(); if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + ctx.signature_constraints + .entry(maybe_qualify_qualified_ident(*name, qualifier)) + .or_default() + .extend(coercible_only); } } } @@ -6115,18 +8304,20 @@ fn import_all( /// Import a single item from a module's exports. /// If `qualifier` is Some, env entries are stored with qualified keys. fn import_item( + _module_name: &ModuleName, item: &Import, exports: &ModuleExports, env: &mut Env, ctx: &mut InferCtx, - instances: &mut HashMap, Vec<(Symbol, Vec)>)>>, + _instances: &mut HashMap, Vec<(QualifiedIdent, Vec)>)>>, qualifier: Option, - import_span: crate::ast::span::Span, + import_span: crate::span::Span, errors: &mut Vec, ) { match item { Import::Value(name) => { - if exports.values.get(name).is_none() && exports.class_methods.get(name).is_none() { + let name_qi = qi(*name); + if exports.values.get(&name_qi).is_none() && exports.class_methods.get(&name_qi).is_none() { errors.push(TypeError::UnknownImport { span: import_span, name: *name, @@ -6134,103 +8325,152 @@ fn import_item( return; } // Import class method info first if applicable - if let Some((class_name, tvs)) = exports.class_methods.get(name) { - ctx.class_methods.insert(*name, (*class_name, tvs.clone())); + if let Some((class_name, tvs)) = exports.class_methods.get(&name_qi) { + ctx.class_methods.insert(name_qi, (*class_name, tvs.iter().map(|s| s.name).collect())); } - if let Some(scheme) = exports.values.get(name) { + if let Some(scheme) = exports.values.get(&name_qi) { // Explicit imports always win — the user specifically asked for this value. - // (The class method shadow check only applies to bulk import_all.) - env.insert_scheme(maybe_qualify(*name, qualifier), scheme.clone()); - } - // Also import instances if this is a class method - if let Some((class_name, _)) = exports.class_methods.get(name) { - // Import instances for the method's class so constraints can be resolved - if let Some(insts) = exports.instances.get(class_name) { - instances - .entry(*class_name) - .or_default() - .extend(insts.clone()); - } + // Values are already alias-expanded at export time. + env.insert_scheme(maybe_qualify_symbol(*name, qualifier), scheme.clone()); } + // Instances are imported centrally in process_imports with module-level dedup. // Import fixity if this is an operator - if let Some(fixity) = exports.value_fixities.get(name) { + if let Some(fixity) = exports.value_fixities.get(&name_qi) { ctx.value_fixities.insert(*name, *fixity); } - if exports.function_op_aliases.contains(name) { - ctx.function_op_aliases.insert(*name); + if exports.function_op_aliases.contains(&name_qi) { + ctx.function_op_aliases.insert(name_qi); } if let Some(target) = exports.operator_class_targets.get(name) { - ctx.operator_class_targets.insert(*name, *target); + ctx.operator_class_targets.insert(qi(*name), qi(*target)); } - if exports.constrained_class_methods.contains(name) { + if exports.constrained_class_methods.contains(&name_qi) { ctx.constrained_class_methods.insert(*name); } // Import ctor_details if this is a constructor alias (e.g. `:|` for `NonEmpty`) - if let Some(details) = exports.ctor_details.get(name) { - ctx.ctor_details.insert(*name, details.clone()); + if let Some(details) = exports.ctor_details.get(&name_qi) { + ctx.ctor_details.insert(name_qi, (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone())); } // Import signature constraints for Coercible propagation (only Coercible) - if let Some(constraints) = exports.signature_constraints.get(name) { - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); + if let Some(constraints) = exports.signature_constraints.get(&name_qi) { + let coercible_only: Vec<_> = constraints + .iter() + .filter(|(cn, _)| { + crate::interner::resolve(cn.name).unwrap_or_default() == "Coercible" + }) + .cloned() + .collect(); if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + ctx.signature_constraints + .entry(name_qi) + .or_default() + .extend(coercible_only); } } // Import partial discharger info (functions with Partial in param position) if exports.partial_dischargers.contains(name) { - ctx.partial_dischargers.insert(maybe_qualify(*name, qualifier)); + ctx.partial_dischargers + .insert(maybe_qualify_qualified_ident(qi(*name), qualifier)); + } + // Import ctor_details if the operator targets a constructor (e.g. `:` → Cons) + // Use the TARGET name as key since Binder::Constructor uses the target name + if let Some(target) = exports.value_operator_targets.get(&name_qi) { + if let Some(details) = exports.ctor_details.get(target) { + ctx.ctor_details.insert(*target, (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone())); + } + // For constructor operators, also import the target constructor's scheme + // under its target name (e.g. `:|` → import `NonEmpty` constructor scheme) + if !exports.function_op_aliases.contains(&name_qi) { + if let Some(scheme) = exports.values.get(target) { + env.insert_scheme(maybe_qualify_symbol(target.name, qualifier), scheme.clone()); + } + } } } Import::Type(name, members) => { - if let Some(ctors) = exports.data_constructors.get(name) { - ctx.data_constructors.insert(*name, ctors.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); - if let Some(arity) = exports.type_con_arities.get(name) { - ctx.type_con_arities.insert(*name, *arity); + let name_qi = qi(*name); + if let Some(ctors) = exports.data_constructors.get(&name_qi) { + ctx.data_constructors.insert(name_qi, ctors.clone()); + if let Some(q) = qualifier { + ctx.data_constructors.insert(QualifiedIdent { module: Some(q), name: name_qi.name }, ctors.clone()); + } + if let Some(arity) = exports.type_con_arities.get(&name_qi) { + ctx.type_con_arities.insert(name_qi, *arity); } if let Some(roles) = exports.type_roles.get(name) { ctx.type_roles.insert(*name, roles.clone()); } if exports.newtype_names.contains(name) { - ctx.newtype_names.insert(*name); + ctx.newtype_names.insert(name_qi); } - let import_ctors: Vec = match members { + let import_ctors: Vec = match members { Some(DataMembers::All) => ctors.clone(), Some(DataMembers::Explicit(listed)) => { // Validate that each listed constructor actually exists for ctor_name in listed { - if !ctors.contains(ctor_name) { + if !ctors.iter().any(|c| c.name == *ctor_name) { errors.push(TypeError::UnknownImportDataConstructor { span: import_span, name: *ctor_name, }); } } - listed.clone() + listed.iter().map(|n| qi(*n)).collect() } None => Vec::new(), // Just the type, no constructors }; for ctor in &import_ctors { if let Some(scheme) = exports.values.get(ctor) { - env.insert_scheme(maybe_qualify(*ctor, qualifier), scheme.clone()); - } - if let Some(details) = exports.ctor_details.get(ctor) { - ctx.ctor_details.insert(*ctor, details.clone()); + env.insert_scheme(maybe_qualify_symbol(ctor.name, qualifier), scheme.clone()); + } + } + // Import ctor_details for ALL constructors when at least some are imported, + // so the exhaustiveness checker can resolve operator aliases. + // e.g. `import Data.List (List(Nil), (:))` needs Cons ctor_details + // to match `:` against `Cons` during exhaustiveness checking. + // But DON'T import ctor_details for type-only imports (members=None), + // as the Coercible solver uses ctor_details availability to check + // constructor accessibility for newtype unwrapping. + if members.is_some() { + for ctor in ctors { + if let Some(details) = exports.ctor_details.get(ctor) { + let entry = (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone()); + if let Some(q) = qualifier { + // Qualified import: store under qualified key + ctx.ctor_details.insert(QualifiedIdent { module: Some(q), name: ctor.name }, entry.clone()); + // Store under unqualified key only if no existing entry + let unqualified = QualifiedIdent { module: None, name: ctor.name }; + if !ctx.ctor_details.contains_key(&unqualified) { + ctx.ctor_details.insert(unqualified, entry); + } + } else { + ctx.ctor_details.insert(*ctor, entry); + } + } } } // Also import the type alias if one exists with the same name // (kind signatures create data_constructors entries for type aliases) - if let Some(alias) = exports.type_aliases.get(name) { - ctx.state.type_aliases.insert(*name, alias.clone()); + if let Some(alias) = exports.type_aliases.get(&name_qi) { + let sym_alias = (alias.0.iter().map(|p| p.name).collect(), alias.1.clone()); + ctx.state.type_aliases.insert(*name, sym_alias.clone()); + if let Some(q) = qualifier { + let qualified_name = maybe_qualify_symbol(*name, Some(q)); + ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(name_qi, Some(q))); + } } - } else if let Some(alias) = exports.type_aliases.get(name) { + } else if let Some(alias) = exports.type_aliases.get(&name_qi) { // Type alias import - ctx.state.type_aliases.insert(*name, alias.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); + let sym_alias = (alias.0.iter().map(|p| p.name).collect(), alias.1.clone()); + ctx.state.type_aliases.insert(*name, sym_alias.clone()); + if qualifier.is_some() { + let qualified_name = maybe_qualify_symbol(*name, qualifier); + ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(name_qi, qualifier)); + } } else { errors.push(TypeError::UnknownImport { span: import_span, @@ -6239,11 +8479,12 @@ fn import_item( } } Import::Class(name) => { + let name_qi = qi(*name); // Check if the class exists in the exports: it may have methods, // instances, or be a constraint-only class (no methods, e.g. `class (A a, B a) <= C a`). - let has_class = exports.class_methods.values().any(|(cn, _)| cn == name) - || exports.instances.get(name).is_some() - || exports.class_param_counts.contains_key(name); + let has_class = exports.class_methods.values().any(|(cn, _)| cn.name == *name) + || exports.instances.get(&name_qi).is_some() + || exports.class_param_counts.contains_key(&name_qi); if !has_class { errors.push(TypeError::UnknownImport { span: import_span, @@ -6252,26 +8493,30 @@ fn import_item( return; } for (method_name, (class_name, tvs)) in &exports.class_methods { - if class_name == name { + if class_name.name == *name { ctx.class_methods - .insert(*method_name, (*class_name, tvs.clone())); + .insert(*method_name, (*class_name, tvs.iter().map(|s| s.name).collect())); if exports.constrained_class_methods.contains(method_name) { - ctx.constrained_class_methods.insert(*method_name); + ctx.constrained_class_methods.insert(method_name.name); + } + // Also populate class_method_schemes so instance expected-type + // lookups can use the canonical class type even if the method + // name gets shadowed in env by a later value import. + if let Some(scheme) = exports.values.get(method_name) { + ctx.class_method_schemes.insert(method_name.name, scheme.clone()); } } } - if let Some(insts) = exports.instances.get(name) { - instances.entry(*name).or_default().extend(insts.clone()); - } + // Instances are imported centrally in process_imports with module-level dedup. } Import::TypeOp(name) => { - if let Some(target) = exports.type_operators.get(name) { - ctx.type_operators.insert(*name, *target); - // Also add the target type to known_types so it passes validation in convert_type_expr - ctx.known_types.insert(*target); + let name_qi = qi(*name); + if let Some(target) = exports.type_operators.get(&name_qi) { + ctx.type_operators.insert(name_qi, *target); // Import the target's type alias definition if it exists if let Some(alias) = exports.type_aliases.get(target) { - ctx.state.type_aliases.insert(*target, alias.clone()); + let _arity = alias.0.len(); + ctx.state.type_aliases.insert(target.name, (alias.0.iter().map(|p| p.name).collect(), alias.1.clone())); } } else { errors.push(TypeError::UnknownImport { @@ -6290,75 +8535,99 @@ fn import_all_except( hidden: &HashSet, env: &mut Env, ctx: &mut InferCtx, - instances: &mut HashMap, Vec<(Symbol, Vec)>)>>, + _instances: &mut HashMap, Vec<(QualifiedIdent, Vec)>)>>, qualifier: Option, + local_data_type_names: &HashSet, ) { // Import class method info first so we can detect conflicts for (name, info) in &exports.class_methods { - if !hidden.contains(name) { - ctx.class_methods.insert(*name, info.clone()); + if !hidden.contains(&name.name) { + ctx.class_methods.insert(*name, (info.0, info.1.iter().map(|s| s.name).collect())); } } for (name, scheme) in &exports.values { - if !hidden.contains(name) { + if !hidden.contains(&name.name) { // Don't let a non-class value overwrite a class method's env entry. // Only applies to unqualified imports — qualified names (Q.foo) can't conflict. - if qualifier.is_none() && ctx.class_methods.contains_key(name) && !exports.class_methods.contains_key(name) { + if qualifier.is_none() + && ctx.class_methods.contains_key(name) + && !exports.class_methods.contains_key(name) + { continue; } - env.insert_scheme(maybe_qualify(*name, qualifier), scheme.clone()); + // Values are already alias-expanded at export time. + env.insert_scheme(maybe_qualify_symbol(name.name, qualifier), scheme.clone()); } } for (name, ctors) in &exports.data_constructors { - if !hidden.contains(name) { + if !hidden.contains(&name.name) { ctx.data_constructors.insert(*name, ctors.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); + if let Some(q) = qualifier { + ctx.data_constructors.insert(QualifiedIdent { module: Some(q), name: name.name }, ctors.clone()); + } for ctor in ctors { - if !hidden.contains(ctor) { + if !hidden.contains(&ctor.name) { if let Some(details) = exports.ctor_details.get(ctor) { - ctx.ctor_details.insert(*ctor, details.clone()); + ctx.ctor_details.insert(*ctor, (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone())); } } } } } - for (name, insts) in &exports.instances { - let existing = instances.entry(*name).or_default(); - for inst in insts { - if !existing.contains(inst) { - existing.push(inst.clone()); - } - } - } + // Instances are imported centrally in process_imports with module-level dedup. for (op, target) in &exports.type_operators { - if !hidden.contains(op) { + if !hidden.contains(&op.name) { ctx.type_operators.insert(*op, *target); } } for (op, fixity) in &exports.value_fixities { - if !hidden.contains(op) { - ctx.value_fixities.insert(*op, *fixity); + if !hidden.contains(&op.name) { + ctx.value_fixities.insert(op.name, *fixity); } } for op in &exports.function_op_aliases { - if !hidden.contains(op) { + if !hidden.contains(&op.name) { ctx.function_op_aliases.insert(*op); } } + // For constructor operators, also import the target constructor's scheme + for (op, target) in &exports.value_operator_targets { + if !hidden.contains(&op.name) && !exports.function_op_aliases.contains(op) { + if let Some(scheme) = exports.values.get(target) { + env.insert_scheme(maybe_qualify_symbol(target.name, qualifier), scheme.clone()); + } + } + } for (op, target) in &exports.operator_class_targets { if !hidden.contains(op) { - ctx.operator_class_targets.insert(*op, *target); + ctx.operator_class_targets.insert(maybe_qualify_qualified_ident(qi(*op), qualifier), maybe_qualify_qualified_ident(qi(*target), qualifier)); } } for name in &exports.constrained_class_methods { - if !hidden.contains(name) { - ctx.constrained_class_methods.insert(*name); + if !hidden.contains(&name.name) { + ctx.constrained_class_methods.insert(name.name); } } for (name, alias) in &exports.type_aliases { - if !hidden.contains(name) { - ctx.state.type_aliases.insert(*name, alias.clone()); - ctx.known_types.insert(maybe_qualify(*name, qualifier)); + if !hidden.contains(&name.name) { + let sym_alias: (Vec, Type) = (alias.0.iter().map(|p| p.name).collect(), alias.1.clone()); + // type_con_arities is not updated here — alias arities come from type_aliases + // For qualified imports, don't overwrite existing unqualified aliases. + // Also skip if the name collides with a locally-defined data/newtype. + let collides_with_local_data = local_data_type_names.contains(&name.name); + if !collides_with_local_data && (qualifier.is_none() || !ctx.state.type_aliases.contains_key(&name.name)) { + ctx.state.type_aliases.insert(name.name, sym_alias.clone()); + } + if qualifier.is_some() { + let qualified_name = maybe_qualify_symbol(name.name, qualifier); + ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(*name, qualifier)); + } + } + } + for (name, arity) in &exports.type_con_arities { + if !hidden.contains(&name.name) { + ctx.type_con_arities.insert(maybe_qualify_qualified_ident(*name, qualifier), *arity); } } // Roles, newtype info, and signature constraints are always imported (non-hideable) @@ -6366,20 +8635,26 @@ fn import_all_except( ctx.type_roles.insert(*name, roles.clone()); } for name in &exports.newtype_names { - ctx.newtype_names.insert(*name); + ctx.newtype_names.insert(maybe_qualify_qualified_ident(qi(*name), qualifier)); } for name in &exports.partial_dischargers { if !hidden.contains(name) { - ctx.partial_dischargers.insert(maybe_qualify(*name, qualifier)); + ctx.partial_dischargers + .insert(maybe_qualify_qualified_ident(qi(*name), qualifier)); } } for (name, constraints) in &exports.signature_constraints { - if !hidden.contains(name) { - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); + if !hidden.contains(&name.name) { + let coercible_only: Vec<_> = constraints + .iter() + .filter(|(cn, _)| crate::interner::resolve(cn.name).unwrap_or_default() == "Coercible") + .cloned() + .collect(); if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + ctx.signature_constraints + .entry(maybe_qualify_qualified_ident(*name, qualifier)) + .or_default() + .extend(coercible_only); } } } @@ -6424,17 +8699,26 @@ fn build_import_filter( let mut type_ops: HashSet = HashSet::new(); for imp in imports { match imp { - crate::cst::Import::Value(name) => { values.insert(*name); } + crate::cst::Import::Value(name) => { + values.insert(*name); + // Importing an operator also imports its target value into the env + // so the typechecker can look up its type (AST desugars `1 + 2` to `add 1 2`). + // The AST converter gates user-visible scoping separately. + if let Some(target) = mod_exports.value_operator_targets.get(&qi(*name)) { + values.insert(target.name); + } + } crate::cst::Import::Type(name, members) => { types.insert(*name); // Importing Type(..) also imports its constructors as values if let Some(crate::cst::DataMembers::All) = members { - if let Some(ctors) = mod_exports.data_constructors.get(name) { + if let Some(ctors) = mod_exports.data_constructors.get(&qi(*name)) { for ctor in ctors { - values.insert(*ctor); + values.insert(ctor.name); } } - } else if let Some(crate::cst::DataMembers::Explicit(ctor_names)) = members { + } else if let Some(crate::cst::DataMembers::Explicit(ctor_names)) = members + { for ctor in ctor_names { values.insert(*ctor); } @@ -6444,15 +8728,22 @@ fn build_import_filter( classes.insert(*name); // Importing a class also imports all its methods for (method_name, (class_name, _)) in &mod_exports.class_methods { - if *class_name == *name { - values.insert(*method_name); + if class_name.name == *name { + values.insert(method_name.name); } } } - crate::cst::Import::TypeOp(name) => { type_ops.insert(*name); } + crate::cst::Import::TypeOp(name) => { + type_ops.insert(*name); + } } } - ImportFilter { values: Some(values), types: Some(types), classes: Some(classes), type_ops: Some(type_ops) } + ImportFilter { + values: Some(values), + types: Some(types), + classes: Some(classes), + type_ops: Some(type_ops), + } } Some(crate::cst::ImportList::Hiding(imports)) => { // For hiding, build exclusion sets and invert to "everything except hidden" @@ -6462,16 +8753,19 @@ fn build_import_filter( let mut hidden_type_ops: HashSet = HashSet::new(); for imp in imports { match imp { - crate::cst::Import::Value(name) => { hidden_values.insert(*name); } + crate::cst::Import::Value(name) => { + hidden_values.insert(*name); + } crate::cst::Import::Type(name, members) => { hidden_types.insert(*name); if let Some(crate::cst::DataMembers::All) = members { - if let Some(ctors) = mod_exports.data_constructors.get(name) { + if let Some(ctors) = mod_exports.data_constructors.get(&qi(*name)) { for ctor in ctors { - hidden_values.insert(*ctor); + hidden_values.insert(ctor.name); } } - } else if let Some(crate::cst::DataMembers::Explicit(ctor_names)) = members { + } else if let Some(crate::cst::DataMembers::Explicit(ctor_names)) = members + { for ctor in ctor_names { hidden_values.insert(*ctor); } @@ -6480,26 +8774,48 @@ fn build_import_filter( crate::cst::Import::Class(name) => { hidden_classes.insert(*name); for (method_name, (class_name, _)) in &mod_exports.class_methods { - if *class_name == *name { - hidden_values.insert(*method_name); + if class_name.name == *name { + hidden_values.insert(method_name.name); } } } - crate::cst::Import::TypeOp(name) => { hidden_type_ops.insert(*name); } + crate::cst::Import::TypeOp(name) => { + hidden_type_ops.insert(*name); + } } } // Build allowed sets = everything in mod_exports minus hidden - let values: HashSet = mod_exports.values.keys().copied() - .filter(|n| !hidden_values.contains(n)).collect(); - let types: HashSet = mod_exports.data_constructors.keys().copied() - .chain(mod_exports.type_aliases.keys().copied()) - .filter(|n| !hidden_types.contains(n)).collect(); - let classes: HashSet = mod_exports.class_methods.values() - .map(|(c, _)| *c) - .filter(|n| !hidden_classes.contains(n)).collect(); - let type_ops: HashSet = mod_exports.type_operators.keys().copied() - .filter(|n| !hidden_type_ops.contains(n)).collect(); - ImportFilter { values: Some(values), types: Some(types), classes: Some(classes), type_ops: Some(type_ops) } + let values: HashSet = mod_exports + .values + .keys() + .map(|n| n.name) + .filter(|n| !hidden_values.contains(n)) + .collect(); + let types: HashSet = mod_exports + .data_constructors + .keys() + .map(|n| n.name) + .chain(mod_exports.type_aliases.keys().map(|n| n.name)) + .filter(|n| !hidden_types.contains(n)) + .collect(); + let classes: HashSet = mod_exports + .class_methods + .values() + .map(|(c, _)| c.name) + .filter(|n| !hidden_classes.contains(n)) + .collect(); + let type_ops: HashSet = mod_exports + .type_operators + .keys() + .map(|n| n.name) + .filter(|n| !hidden_type_ops.contains(n)) + .collect(); + ImportFilter { + values: Some(values), + types: Some(types), + classes: Some(classes), + type_ops: Some(type_ops), + } } } } @@ -6508,13 +8824,14 @@ fn build_import_filter( fn filter_exports( all: &ModuleExports, export_list: &crate::cst::ExportList, - export_span: crate::ast::span::Span, + export_span: crate::span::Span, _local_types: &HashSet, _local_classes: &HashSet, registry: &ModuleRegistry, imports: &[crate::cst::ImportDecl], current_module: &crate::cst::ModuleName, errors: &mut Vec, + _scope_conflicts: &HashSet, ) -> ModuleExports { let mut result = ModuleExports::default(); @@ -6522,58 +8839,98 @@ fn filter_exports( // When two different re-export modules contribute the same name, it's only a conflict // if the names have different origins (i.e. independently defined in different modules). // Re-exporting the same definition through different paths is allowed (ModuleExportDupes). - let mut value_origins: HashMap = HashMap::new(); - let mut type_origins: HashMap = HashMap::new(); - let mut class_origins: HashMap = HashMap::new(); + // We also track the import qualifier to distinguish ScopeConflict (same qualifier) from + // ExportConflict (different qualifiers). + // Each entry stores (origin_module, import_qualifier, is_locally_defined_in_source). + // The is_locally_defined flag indicates whether the name was defined in the source module + // (origin == source) vs. re-exported through it. Conflicts are only genuine when BOTH + // names are locally defined in different modules. Re-exported names may trace to the same + // definition but through different import paths, which is not a conflict. + let mut value_origins: HashMap, bool)> = HashMap::new(); + let mut type_origins: HashMap, bool)> = HashMap::new(); + let mut class_origins: HashMap, bool)> = HashMap::new(); for export in &export_list.exports { match export { Export::Value(name) => { - if let Some(scheme) = all.values.get(name) { - result.values.insert(*name, scheme.clone()); + let name_qi = qi(*name); + if let Some(scheme) = all.values.get(&name_qi) { + result.values.insert(name_qi, scheme.clone()); } // Also export class method info if applicable - if let Some(info) = all.class_methods.get(name) { - result.class_methods.insert(*name, info.clone()); + if let Some(info) = all.class_methods.get(&name_qi) { + result.class_methods.insert(name_qi, info.clone()); } // Also export fixity if applicable - if let Some(fixity) = all.value_fixities.get(name) { - result.value_fixities.insert(*name, *fixity); + if let Some(fixity) = all.value_fixities.get(&name_qi) { + result.value_fixities.insert(name_qi, *fixity); } - if all.function_op_aliases.contains(name) { - result.function_op_aliases.insert(*name); + if all.function_op_aliases.contains(&name_qi) { + result.function_op_aliases.insert(name_qi); } if let Some(target) = all.operator_class_targets.get(name) { result.operator_class_targets.insert(*name, *target); } - if all.constrained_class_methods.contains(name) { - result.constrained_class_methods.insert(*name); + if all.constrained_class_methods.contains(&name_qi) { + result.constrained_class_methods.insert(name_qi); } // Also export ctor_details if this is a constructor alias (e.g. `:|`) - if let Some(details) = all.ctor_details.get(name) { - result.ctor_details.insert(*name, details.clone()); + if let Some(details) = all.ctor_details.get(&name_qi) { + result.ctor_details.insert(name_qi, details.clone()); + } + // Also export operator target mapping (e.g. + → add) and the target's value scheme + if let Some(target) = all.value_operator_targets.get(&name_qi) { + result.value_operator_targets.insert(name_qi, target.clone()); + // Include the target value's scheme so importers can look up its type + // (AST desugars `1 + 2` to `add 1 2`, which needs `add`'s type). + if let Some(target_scheme) = all.values.get(target) { + result.values.insert(*target, target_scheme.clone()); + } + // Also export target's ctor_details if it's a constructor + if let Some(details) = all.ctor_details.get(target) { + result.ctor_details.insert(*target, details.clone()); + } + } + // Export signature constraints for Coercible propagation + if let Some(constraints) = all.signature_constraints.get(&name_qi) { + result.signature_constraints.insert(name_qi, constraints.clone()); + } + // Export partial discharger info + if all.partial_dischargers.contains(name) { + result.partial_dischargers.insert(*name); } } Export::Type(name, members) => { - if let Some(ctors) = all.data_constructors.get(name) { - let export_ctors: Vec = match members { + let name_qi = qi(*name); + if let Some(ctors) = all.data_constructors.get(&name_qi) { + let export_ctors: Vec = match members { Some(DataMembers::All) => ctors.clone(), - Some(DataMembers::Explicit(listed)) => listed.clone(), + Some(DataMembers::Explicit(listed)) => listed.iter().map(|n| qi(*n)).collect(), None => { // Don't overwrite existing constructor list with empty // (handles `module X (A(..), A)` where second A has no members) - if !result.data_constructors.contains_key(name) { - result.data_constructors.insert(*name, Vec::new()); + if !result.data_constructors.contains_key(&name_qi) { + result.data_constructors.insert(name_qi, Vec::new()); } // Still need to export type aliases below - if let Some(alias) = all.type_aliases.get(name) { - result.type_aliases.insert(*name, alias.clone()); + if let Some(alias) = all.type_aliases.get(&name_qi) { + result.type_aliases.insert(name_qi, alias.clone()); + } + // Export type kind, arities, and roles + if let Some(kind) = all.type_kinds.get(name) { + result.type_kinds.insert(*name, kind.clone()); + } + if let Some(arity) = all.type_con_arities.get(&name_qi) { + result.type_con_arities.insert(name_qi, *arity); + } + if let Some(roles) = all.type_roles.get(name) { + result.type_roles.insert(*name, roles.clone()); } continue; } }; - result.data_constructors.insert(*name, export_ctors.clone()); + result.data_constructors.insert(name_qi, export_ctors.clone()); for ctor in &export_ctors { if let Some(scheme) = all.values.get(ctor) { @@ -6585,16 +8942,29 @@ fn filter_exports( } } // Also export type aliases with this name - if let Some(alias) = all.type_aliases.get(name) { - result.type_aliases.insert(*name, alias.clone()); + if let Some(alias) = all.type_aliases.get(&name_qi) { + result.type_aliases.insert(name_qi, alias.clone()); + } + // Also export type kind + if let Some(kind) = all.type_kinds.get(name) { + result.type_kinds.insert(*name, kind.clone()); + } + // Also export type con arities + if let Some(arity) = all.type_con_arities.get(&name_qi) { + result.type_con_arities.insert(name_qi, *arity); + } + // Also export type roles + if let Some(roles) = all.type_roles.get(name) { + result.type_roles.insert(*name, roles.clone()); } } Export::Class(name) => { + let name_qi = qi(*name); // Export class metadata (for constraint generation) but NOT methods as values. // In PureScript, `module M (class C) where` only exports the class — // methods must be listed separately: `module M (class C, methodName) where`. for (method_name, (class_name, tvs)) in &all.class_methods { - if class_name == name { + if class_name.name == *name { result .class_methods .insert(*method_name, (*class_name, tvs.clone())); @@ -6604,20 +8974,24 @@ fn filter_exports( } } // Export instances for this class - if let Some(insts) = all.instances.get(name) { - result.instances.insert(*name, insts.clone()); + if let Some(insts) = all.instances.get(&name_qi) { + result.instances.insert(name_qi, insts.clone()); } // Export class param count (needed for orphan detection and arity checking) - if let Some(count) = all.class_param_counts.get(name) { - result.class_param_counts.insert(*name, *count); + if let Some(count) = all.class_param_counts.get(&name_qi) { + result.class_param_counts.insert(name_qi, *count); } if let Some(fd) = all.class_fundeps.get(name) { result.class_fundeps.insert(*name, fd.clone()); } } Export::TypeOp(name) => { - if let Some(target) = all.type_operators.get(name) { - result.type_operators.insert(*name, *target); + let name_qi = qi(*name); + if let Some(target) = all.type_operators.get(&name_qi) { + result.type_operators.insert(name_qi, *target); + } + if let Some(fixity) = all.type_fixities.get(&name_qi) { + result.type_fixities.insert(name_qi, *fixity); } } Export::Module(mod_name) => { @@ -6640,6 +9014,9 @@ fn filter_exports( for (name, target) in &all.type_operators { result.type_operators.insert(*name, *target); } + for (name, fixity) in &all.type_fixities { + result.type_fixities.insert(*name, *fixity); + } for (name, fixity) in &all.value_fixities { result.value_fixities.insert(*name, *fixity); } @@ -6649,6 +9026,9 @@ fn filter_exports( for (op, target) in &all.operator_class_targets { result.operator_class_targets.insert(*op, *target); } + for (op, target) in &all.value_operator_targets { + result.value_operator_targets.insert(*op, target.clone()); + } for name in &all.constrained_class_methods { result.constrained_class_methods.insert(*name); } @@ -6664,20 +9044,23 @@ fn filter_exports( continue; } // Re-export everything from the named module. - // `module X` in the export list matches either: - // - an import whose module name equals X (e.g. `import Data.Foo`) - // - an import whose qualified alias equals X (e.g. `import Prim.Ordering as PO` matches `module PO`) + // `module X` in the export list matches an import whose *effective qualifier* equals X. + // The effective qualifier is the alias if present, otherwise the module name. + // e.g. `import Data.Foo` has effective qualifier `Data.Foo` + // e.g. `import Data.Foo as Foo` has effective qualifier `Foo` + // So `module Data.Foo` matches the first but NOT the second. let reexport_mod_sym = module_name_to_symbol(mod_name); for import_decl in imports { - let matches_module = module_name_to_symbol(&import_decl.module) == reexport_mod_sym; - let matches_alias = import_decl.qualified.as_ref() - .map(|q| module_name_to_symbol(q) == reexport_mod_sym) - .unwrap_or(false); - if matches_module || matches_alias { + let effective_qualifier = import_decl + .qualified + .as_ref() + .map(|q| module_name_to_symbol(q)) + .unwrap_or_else(|| module_name_to_symbol(&import_decl.module)); + if effective_qualifier == reexport_mod_sym { // Look up from registry; also check Prim submodules let prim_sub; let full_exports = if is_prim_module(&import_decl.module) { - Some(&prim_exports()) + Some(prim_exports()) } else if is_prim_submodule(&import_decl.module) { prim_sub = prim_submodule_exports(&import_decl.module); Some(&prim_sub) @@ -6693,50 +9076,86 @@ fn filter_exports( // in conflict detection, but all items are re-exported. let filter = build_import_filter(import_decl, mod_exports); + // The import qualifier determines whether a conflict is + // a ScopeConflict (same qualifier) or ExportConflict (different qualifiers). + let import_qual = import_decl.qualified.as_ref().map(|q| module_name_to_symbol(q)); + // Check for conflicts: class methods for (name, info) in &mod_exports.class_methods { let (class_name, _) = info; - let imported = filter.classes.as_ref() - .map_or(true, |allowed| allowed.contains(class_name)); + let imported = filter + .classes + .as_ref() + .map_or(true, |allowed| allowed.contains(&class_name.name)); if imported { - // Determine origin: use source module's origin if available, - // otherwise the source module itself defined it - let origin = mod_exports.class_origins.get(class_name) + let origin = mod_exports + .class_origins + .get(&class_name.name) .copied() .unwrap_or(source_mod_sym); - if let Some(prev_origin) = class_origins.get(class_name) { - if *prev_origin != origin { - errors.push(TypeError::ExportConflict { - span: export_span, - name: *class_name, - }); + let is_local_def = origin == source_mod_sym; + if let Some(&(prev_origin, prev_qual, prev_local)) = class_origins.get(&class_name.name) { + // Only flag genuine conflicts: both names must be locally + // defined in their respective source modules. Re-exported + // names through different paths are likely the same definition. + if prev_origin != origin && prev_local && is_local_def { + if prev_qual == import_qual { + errors.push(TypeError::ScopeConflict { + span: export_span, + name: class_name.name, + }); + } else { + errors.push(TypeError::ExportConflict { + span: export_span, + name: class_name.name, + }); + } } } else { - class_origins.insert(*class_name, origin); + class_origins.insert(class_name.name, (origin, import_qual, is_local_def)); } } result.class_methods.insert(*name, info.clone()); } for (name, scheme) in &mod_exports.values { // Don't let a non-class value overwrite a class method's entry - if result.class_methods.contains_key(name) && !mod_exports.class_methods.contains_key(name) { + if result.class_methods.contains_key(name) + && !mod_exports.class_methods.contains_key(name) + { continue; } - let origin = mod_exports.value_origins.get(name) + let origin = mod_exports + .value_origins + .get(&name.name) .copied() .unwrap_or(source_mod_sym); - let imported = filter.values.as_ref() - .map_or(true, |allowed| allowed.contains(name)); + let imported = filter + .values + .as_ref() + .map_or(true, |allowed| allowed.contains(&name.name)); if imported { - if let Some(prev_origin) = value_origins.get(name) { - if *prev_origin != origin { - errors.push(TypeError::ExportConflict { - span: export_span, - name: *name, - }); + let is_local_def = origin == source_mod_sym; + if let Some(&(prev_origin, prev_qual, prev_local)) = value_origins.get(&name.name) { + if prev_origin != origin && prev_local && is_local_def { + let both_are_class_methods = + mod_exports.class_methods.contains_key(name) + && result.class_methods.contains_key(name); + if !both_are_class_methods { + if prev_qual == import_qual { + errors.push(TypeError::ScopeConflict { + span: export_span, + name: name.name, + }); + } else { + errors.push(TypeError::ExportConflict { + span: export_span, + name: name.name, + }); + } + } } } else { - value_origins.insert(*name, origin); + value_origins.insert(name.name, (origin, import_qual, is_local_def)); } } if imported { @@ -6744,21 +9163,33 @@ fn filter_exports( } } for (name, ctors) in &mod_exports.data_constructors { - let imported = filter.types.as_ref() - .map_or(true, |allowed| allowed.contains(name)); + let imported = filter + .types + .as_ref() + .map_or(true, |allowed| allowed.contains(&name.name)); if imported { - let origin = mod_exports.type_origins.get(name) + let origin = mod_exports + .type_origins + .get(&name.name) .copied() .unwrap_or(source_mod_sym); - if let Some(prev_origin) = type_origins.get(name) { - if *prev_origin != origin { - errors.push(TypeError::ExportConflict { - span: export_span, - name: *name, - }); + let is_local_def = origin == source_mod_sym; + if let Some(&(prev_origin, prev_qual, prev_local)) = type_origins.get(&name.name) { + if prev_origin != origin && prev_local && is_local_def { + if prev_qual == import_qual { + errors.push(TypeError::ScopeConflict { + span: export_span, + name: name.name, + }); + } else { + errors.push(TypeError::ExportConflict { + span: export_span, + name: name.name, + }); + } } } else { - type_origins.insert(*name, origin); + type_origins.insert(name.name, (origin, import_qual, is_local_def)); } } result.data_constructors.insert(*name, ctors.clone()); @@ -6767,26 +9198,40 @@ fn filter_exports( result.ctor_details.insert(*name, details.clone()); } for (name, target) in &mod_exports.type_operators { - let imported = filter.type_ops.as_ref() - .map_or(true, |allowed| allowed.contains(name)); + let imported = filter + .type_ops + .as_ref() + .map_or(true, |allowed| allowed.contains(&name.name)); if imported { - // Use value_origins for type operators too - let origin = mod_exports.value_origins.get(name) + let origin = mod_exports + .value_origins + .get(&name.name) .copied() .unwrap_or(source_mod_sym); - if let Some(prev_origin) = value_origins.get(name) { - if *prev_origin != origin { - errors.push(TypeError::ExportConflict { - span: export_span, - name: *name, - }); + let is_local_def = origin == source_mod_sym; + if let Some(&(prev_origin, prev_qual, prev_local)) = value_origins.get(&name.name) { + if prev_origin != origin && prev_local && is_local_def { + if prev_qual == import_qual { + errors.push(TypeError::ScopeConflict { + span: export_span, + name: name.name, + }); + } else { + errors.push(TypeError::ExportConflict { + span: export_span, + name: name.name, + }); + } } } else { - value_origins.insert(*name, origin); + value_origins.insert(name.name, (origin, import_qual, is_local_def)); } } result.type_operators.insert(*name, *target); } + for (name, fixity) in &mod_exports.type_fixities { + result.type_fixities.insert(*name, *fixity); + } for (name, fixity) in &mod_exports.value_fixities { result.value_fixities.insert(*name, *fixity); } @@ -6796,6 +9241,9 @@ fn filter_exports( for (op, target) in &mod_exports.operator_class_targets { result.operator_class_targets.insert(*op, *target); } + for (op, target) in &mod_exports.value_operator_targets { + result.value_operator_targets.insert(*op, target.clone()); + } for name in &mod_exports.constrained_class_methods { result.constrained_class_methods.insert(*name); } @@ -6808,6 +9256,23 @@ fn filter_exports( for (name, fd) in &mod_exports.class_fundeps { result.class_fundeps.insert(*name, fd.clone()); } + for (name, kind) in &mod_exports.type_kinds { + // Don't overwrite existing type_kinds entries — an explicit + // Export::Type may have already set the correct kind (e.g. + // a 1-param alias kind), and a `module X` re-export from a + // different module may carry a data type with the same + // unqualified name but a different kind. + result.type_kinds.entry(*name).or_insert_with(|| kind.clone()); + } + for (name, kind) in &mod_exports.class_type_kinds { + result.class_type_kinds.entry(*name).or_insert_with(|| kind.clone()); + } + for (name, arity) in &mod_exports.type_con_arities { + result.type_con_arities.insert(*name, *arity); + } + for (name, roles) in &mod_exports.type_roles { + result.type_roles.insert(*name, roles.clone()); + } } } } @@ -6829,26 +9294,43 @@ fn filter_exports( // For locally-exported names (Export::Value/Type/Class), use all's origins. // For re-exported names (Export::Module), use the origins we tracked. for (name, origin) in &all.value_origins { - if result.values.contains_key(name) { + if result.values.contains_key(&qi(*name)) { result.value_origins.entry(*name).or_insert(*origin); } } for (name, origin) in &all.type_origins { - if result.data_constructors.contains_key(name) { + if result.data_constructors.contains_key(&qi(*name)) { result.type_origins.entry(*name).or_insert(*origin); } } + // Also propagate type_origins for types that appear in exported value schemes + // but aren't in data_constructors. This covers cases like `fetchWithOptions :: ... + // Promise Response` where Response is a foreign import data type from another module + // that isn't directly exported as a type. + { + let mut scheme_type_names: HashSet = HashSet::new(); + for scheme in result.values.values() { + collect_unqualified_type_cons(&scheme.ty, &mut scheme_type_names); + } + for name in &scheme_type_names { + if !result.type_origins.contains_key(name) { + if let Some(origin) = all.type_origins.get(name) { + result.type_origins.insert(*name, *origin); + } + } + } + } for (name, origin) in &all.class_origins { result.class_origins.entry(*name).or_insert(*origin); } // Also include origins from re-exported modules - for (name, origin) in &value_origins { + for (name, (origin, _, _)) in &value_origins { result.value_origins.entry(*name).or_insert(*origin); } - for (name, origin) in &type_origins { + for (name, (origin, _, _)) in &type_origins { result.type_origins.entry(*name).or_insert(*origin); } - for (name, origin) in &class_origins { + for (name, (origin, _, _)) in &class_origins { result.class_origins.entry(*name).or_insert(*origin); } @@ -6862,6 +9344,28 @@ fn filter_exports( result } +/// Collect unqualified type constructor names from a type. +/// Used to find type names in exported value schemes that need origin tracking. +fn collect_unqualified_type_cons(ty: &Type, out: &mut HashSet) { + match ty { + Type::Con(name) if name.module.is_none() => { out.insert(name.name); } + Type::Fun(a, b) => { + collect_unqualified_type_cons(a, out); + collect_unqualified_type_cons(b, out); + } + Type::App(f, a) => { + collect_unqualified_type_cons(f, out); + collect_unqualified_type_cons(a, out); + } + Type::Forall(_, body) => collect_unqualified_type_cons(body, out), + Type::Record(fields, tail) => { + for (_, t) in fields { collect_unqualified_type_cons(t, out); } + if let Some(t) = tail { collect_unqualified_type_cons(t, out); } + } + _ => {} + } +} + /// Check exhaustiveness for multi-equation function definitions. /// Peels `func_ty` to extract parameter types, then for each binder position, /// checks if all constructors of the corresponding type are covered. @@ -6874,16 +9378,42 @@ fn filter_exports( fn contains_inherently_partial_binder(binder: &Binder) -> bool { match unwrap_binder(binder) { Binder::Array { .. } => true, - Binder::Record { fields, .. } => { - fields.iter().any(|f| { - f.binder.as_ref().map_or(false, |b| contains_inherently_partial_binder(b)) - }) - } + Binder::Record { fields, .. } => fields.iter().any(|f| { + f.binder + .as_ref() + .map_or(false, |b| contains_inherently_partial_binder(b)) + }), Binder::Constructor { args, .. } => { args.iter().any(|b| contains_inherently_partial_binder(b)) } - Binder::Op { left, right, .. } => { - contains_inherently_partial_binder(left) || contains_inherently_partial_binder(right) + _ => false, + } +} + +/// Check if a binder is irrefutable when considering single-constructor types. +/// A constructor pattern like `Path trace` is irrefutable if `Path` is the only +/// constructor of its type (newtype or single-ctor data) and all its args are +/// also irrefutable (wildcards/vars, or recursively single-ctor patterns). +/// This is used by the array-binder check to avoid false Partial errors for +/// patterns like `dashed (Path []) = ... ; dashed (Path trace) = ...`. +fn is_single_ctor_irrefutable(binder: &Binder, ctx: &InferCtx) -> bool { + match unwrap_binder(binder) { + Binder::Wildcard { .. } | Binder::Var { .. } => true, + Binder::Constructor { name, args, .. } => { + // Check if this constructor's type has exactly one constructor + if let Some((parent_type, _, _)) = ctx.ctor_details.get(name) { + if let Some(ctors) = ctx.data_constructors.get(parent_type) { + if ctors.len() == 1 { + return args.iter().all(|a| is_single_ctor_irrefutable(a, ctx)); + } + } + } + false + } + Binder::Record { fields, .. } => { + fields.iter().all(|f| { + f.binder.as_ref().map_or(true, |b| is_single_ctor_irrefutable(b, ctx)) + }) } _ => false, } @@ -6891,7 +9421,7 @@ fn contains_inherently_partial_binder(binder: &Binder) -> bool { fn check_multi_eq_exhaustiveness( ctx: &InferCtx, - span: crate::ast::span::Span, + span: crate::span::Span, func_ty: &Type, arity: usize, decls: &[&Decl], @@ -6916,10 +9446,13 @@ fn check_multi_eq_exhaustiveness( // (can never cover all cases since arrays have infinite possible lengths). // If any position has array binders without a wildcard/var fallback, it needs Partial. let has_irrefutable_at_position = decls.iter().any(|decl| { - if let Decl::Value { binders, guarded, .. } = decl { + if let Decl::Value { + binders, guarded, .. + } = decl + { if is_unconditional_for_exhaustiveness(guarded) { if let Some(binder) = binders.get(idx) { - return !is_refutable(binder); + return !is_refutable(binder) || is_single_ctor_irrefutable(binder, ctx); } } } @@ -6928,19 +9461,30 @@ fn check_multi_eq_exhaustiveness( if !has_irrefutable_at_position { let has_array_binder = decls.iter().any(|decl| { if let Decl::Value { binders, .. } = decl { - binders.get(idx).map_or(false, |b| contains_inherently_partial_binder(b)) + binders + .get(idx) + .map_or(false, |b| contains_inherently_partial_binder(b)) } else { false } }); if has_array_binder { - let partial_sym = crate::interner::intern("Partial"); - errors.push(TypeError::NoInstanceFound { - span, - class_name: partial_sym, - type_args: vec![], - }); - return; + // Only emit Partial for array binders when the type at this position + // is NOT a known ADT. For ADTs, the constructor exhaustiveness check + // below handles coverage correctly — an array binder inside a + // constructor (e.g., `Node [x, y] _`) is harmless if another equation + // catches all cases for that constructor (e.g., `Node arr w`). + let is_known_adt = extract_type_con(param_ty) + .map_or(false, |tn| ctx.data_constructors.contains_key(&tn)); + if !is_known_adt { + let partial_sym = crate::interner::intern("Partial"); + errors.push(TypeError::NoInstanceFound { + span, + class_name: qi(partial_sym), + type_args: vec![], + }); + return; + } } } @@ -6989,10 +9533,10 @@ fn check_value_decl( ctx: &mut InferCtx, env: &Env, _name: Symbol, - span: crate::ast::span::Span, + span: crate::span::Span, binders: &[Binder], - guarded: &crate::cst::GuardedExpr, - where_clause: &[crate::cst::LetBinding], + guarded: &crate::ast::GuardedExpr, + where_clause: &[crate::ast::LetBinding], expected: Option<&Type>, ) -> Result { // Set scoped type variables from the expected type. @@ -7002,7 +9546,9 @@ fn check_value_decl( if let Some(ty) = expected { fn collect_all_type_vars(ty: &Type, vars: &mut std::collections::HashSet) { match ty { - Type::Var(v) => { vars.insert(*v); } + Type::Var(v) => { + vars.insert(*v); + } Type::Forall(bound_vars, body) => { for &(v, _) in bound_vars { vars.insert(v); @@ -7030,7 +9576,16 @@ fn check_value_decl( } collect_all_type_vars(ty, &mut ctx.scoped_type_vars); } - let result = check_value_decl_inner(ctx, env, _name, span, binders, guarded, where_clause, expected); + let result = check_value_decl_inner( + ctx, + env, + _name, + span, + binders, + guarded, + where_clause, + expected, + ); ctx.scoped_type_vars = prev_scoped; result } @@ -7039,16 +9594,17 @@ fn check_value_decl_inner( ctx: &mut InferCtx, env: &Env, _name: Symbol, - span: crate::ast::span::Span, + span: crate::span::Span, binders: &[Binder], - guarded: &crate::cst::GuardedExpr, - where_clause: &[crate::cst::LetBinding], + guarded: &crate::ast::GuardedExpr, + where_clause: &[crate::ast::LetBinding], expected: Option<&Type>, ) -> Result { // Reject bare `_` as the entire body — it's not a valid anonymous argument context. if binders.is_empty() { - if let crate::cst::GuardedExpr::Unconditional(body) = guarded { - if matches!(body.as_ref(), crate::cst::Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_") { + if let crate::ast::GuardedExpr::Unconditional(body) = guarded { + if matches!(body.as_ref(), crate::ast::Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_") + { return Err(TypeError::IncorrectAnonymousArgument { span }); } } @@ -7069,8 +9625,8 @@ fn check_value_decl_inner( // Pass the FULL type (with Forall) to check_against — it will instantiate // the forall vars with fresh unif vars, keeping them flexible. if let Some(sig_ty) = expected { - if let crate::cst::GuardedExpr::Unconditional(body) = guarded { - if matches!(body.as_ref(), crate::cst::Expr::Lambda { .. }) { + if let crate::ast::GuardedExpr::Unconditional(body) = guarded { + if matches!(body.as_ref(), crate::ast::Expr::Lambda { .. }) { let body_ty = ctx.check_against(&local_env, body, sig_ty)?; return Ok(body_ty); } @@ -7167,6 +9723,41 @@ fn contains_type_var(ty: &Type) -> bool { } } +/// Like `contains_type_var` but also considers unification variables (Type::Unif) +/// as "unknown" types. Used in instance constraint checking where unsolved unif vars +/// should be treated optimistically (the constraint may be satisfiable once resolved). +fn contains_type_var_or_unif(ty: &Type) -> bool { + match ty { + Type::Var(_) | Type::Unif(_) => true, + Type::Fun(a, b) => contains_type_var_or_unif(a) || contains_type_var_or_unif(b), + Type::App(f, a) => contains_type_var_or_unif(f) || contains_type_var_or_unif(a), + Type::Forall(_, body) => contains_type_var_or_unif(body), + Type::Record(fields, rest) => { + fields.iter().any(|(_, t)| contains_type_var_or_unif(t)) + || rest.as_ref().is_some_and(|r| contains_type_var_or_unif(r)) + } + _ => false, + } +} + +/// Check if a type contains any concrete type constructors (Type::Con). +/// Used to distinguish fully polymorphic constraints (all args are type vars/records +/// with type var tails) from constraints with concrete type constructors that can +/// potentially be resolved by instance matching. +fn type_has_concrete_con(ty: &Type) -> bool { + match ty { + Type::Con(_) => true, + Type::App(f, a) => type_has_concrete_con(f) || type_has_concrete_con(a), + Type::Fun(a, b) => type_has_concrete_con(a) || type_has_concrete_con(b), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| type_has_concrete_con(t)) + || tail.as_ref().map_or(false, |t| type_has_concrete_con(t)) + } + Type::Forall(_, body) => type_has_concrete_con(body), + Type::Var(_) | Type::Unif(_) | Type::TypeString(_) | Type::TypeInt(_) => false, + } +} + /// Check if a type variable is in a valid position for deriving a functor-like class. /// /// `want_covariant` indicates whether we want the variable in covariant (true) or @@ -7180,14 +9771,17 @@ fn check_derive_position( positive: bool, want_covariant: bool, allow_forall: bool, - instances: &HashMap, Vec<(Symbol, Vec)>)>>, + instances: &HashMap, Vec<(QualifiedIdent, Vec)>)>>, tyvar_classes: &HashMap>, - ctor_details: &HashMap, Vec)>, - data_constructors: &HashMap>, + ctor_details: &HashMap, Vec)>, + data_constructors: &HashMap>, + local_concrete_type_names: &HashSet, depth: usize, ) -> bool { - if depth > 50 { return true; } // avoid infinite recursion - // If the variable doesn't appear in this type, it's always fine + if depth > 50 { + return true; + } // avoid infinite recursion + // If the variable doesn't appear in this type, it's always fine if !type_var_occurs_in(var, ty) { return true; } @@ -7201,13 +9795,40 @@ fn check_derive_position( match ty { Type::Var(v) if *v == var => { - if want_covariant { positive } else { !positive } + if want_covariant { + positive + } else { + !positive + } } Type::Var(_) => true, Type::Fun(arg, ret) => { - check_derive_position(arg, var, !positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) - && check_derive_position(ret, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) + check_derive_position( + arg, + var, + !positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) && check_derive_position( + ret, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) } Type::Forall(vars, body) => { @@ -7219,7 +9840,19 @@ fn check_derive_position( // (can't extract values from quantified types) false } else { - check_derive_position(body, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) + check_derive_position( + body, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) } } @@ -7233,25 +9866,45 @@ fn check_derive_position( args.reverse(); if let Type::Con(head_con) = head { - // For known data/newtype types with accessible constructors, - // expand the type structurally rather than requiring instances. - // This matches PureScript's derive mechanism which destructures - // and rebuilds concrete types. - if let Some(expanded_fields) = try_expand_type_constructors(*head_con, &args, ctor_details, depth) { - // Check each expanded field - return expanded_fields.iter().all(|field_ty| { - check_derive_position(field_ty, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) - }); + // Check for class instances first. If the type has a known + // Functor/Contravariant/Bifunctor/Profunctor instance, prefer + // instance-based checking over structural expansion. This avoids + // expanding newtypes like Coyoneda whose internals use abstract + // types (like Exists) that lack instances. + let has_functor = has_class_instance_for(instances, qi(functor_sym), *head_con) + || has_class_instance_for(instances, qi(foldable_sym), *head_con) + || has_class_instance_for(instances, qi(traversable_sym), *head_con); + let has_contravariant = + has_class_instance_for(instances, qi(contravariant_sym), *head_con); + let has_bifunctor = has_class_instance_for(instances, qi(bifunctor_sym), *head_con); + let has_profunctor = has_class_instance_for(instances, qi(profunctor_sym), *head_con); + + let has_any_instance = has_functor || has_contravariant || has_bifunctor || has_profunctor; + + // If no instances are found, try structural expansion for + // single-constructor types (newtypes, single-ctor data types). + if !has_any_instance { + if let Some(expanded_fields) = + try_expand_type_constructors(*head_con, &args, ctor_details, depth) + { + return expanded_fields.iter().all(|field_ty| { + check_derive_position( + field_ty, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) + }); + } } - // Fall back to instance-based checking for abstract types - let has_functor = has_class_instance_for(instances, functor_sym, *head_con) - || has_class_instance_for(instances, foldable_sym, *head_con) - || has_class_instance_for(instances, traversable_sym, *head_con); - let has_contravariant = has_class_instance_for(instances, contravariant_sym, *head_con); - let has_bifunctor = has_class_instance_for(instances, bifunctor_sym, *head_con); - let has_profunctor = has_class_instance_for(instances, profunctor_sym, *head_con); - for (i, arg) in args.iter().enumerate() { if !type_var_occurs_in(var, arg) { continue; @@ -7261,31 +9914,146 @@ fn check_derive_position( if is_last { if has_functor || has_bifunctor || has_profunctor { - if !check_derive_position(arg, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } + if !check_derive_position( + arg, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } } else if has_contravariant { - if !check_derive_position(arg, var, !positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } - } else if data_constructors.get(head_con).map_or(false, |ctors| !ctors.is_empty()) { - // Known concrete data type without imported instances. + if !check_derive_position( + arg, + var, + !positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } + } else if data_constructors + .get(head_con) + .map_or(false, |ctors| !ctors.is_empty()) + || data_constructors.iter().any(|(k, v)| k.name == head_con.name && !v.is_empty()) + || local_concrete_type_names.contains(&head_con.name) + { + // Known concrete data type without imported instances (or locally-defined + // type not yet processed in Pass 1 declaration order). // PureScript's derive can structurally expand any concrete type // regardless of import visibility. Assume covariant (product-like). - if !check_derive_position(arg, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } + // Also check by unqualified name for cross-module types. + if !check_derive_position( + arg, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } } else { return false; } } else if is_second_last { if has_bifunctor { - if !check_derive_position(arg, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } + if !check_derive_position( + arg, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } } else if has_profunctor { - if !check_derive_position(arg, var, !positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } - } else if data_constructors.get(head_con).map_or(false, |ctors| !ctors.is_empty()) { - // Same product type assumption as above - if !check_derive_position(arg, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } + if !check_derive_position( + arg, + var, + !positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } + } else if data_constructors + .get(head_con) + .map_or(false, |ctors| !ctors.is_empty()) + || local_concrete_type_names.contains(&head_con.name) + { + // Same product type assumption as above; also covers locally-defined + // types not yet processed in Pass 1 declaration order. + if !check_derive_position( + arg, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } } else { return false; } - } else if data_constructors.get(head_con).map_or(false, |ctors| !ctors.is_empty()) { - // Variable in earlier positions — assume covariant for known data types - if !check_derive_position(arg, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { return false; } + } else if data_constructors + .get(head_con) + .map_or(false, |ctors| !ctors.is_empty()) + || local_concrete_type_names.contains(&head_con.name) + { + // Variable in earlier positions — assume covariant for known data types, + // or locally-defined types not yet processed in Pass 1 declaration order. + if !check_derive_position( + arg, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { + return false; + } } else { return false; } @@ -7316,7 +10084,9 @@ fn check_derive_position( } for (i, arg) in args.iter().enumerate() { - if !type_var_occurs_in(var, arg) { continue; } + if !type_var_occurs_in(var, arg) { + continue; + } let is_last = i == args.len() - 1; let is_second_last = args.len() >= 2 && i == args.len() - 2; @@ -7329,7 +10099,19 @@ fn check_derive_position( _has_profunctor && !_has_bifunctor }; let pos = if flipped { !positive } else { positive }; - if !check_derive_position(arg, var, pos, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) { + if !check_derive_position( + arg, + var, + pos, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) { return false; } } else { @@ -7344,9 +10126,33 @@ fn check_derive_position( Type::Record(fields, rest) => { fields.iter().all(|(_, ft)| { - check_derive_position(ft, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) + check_derive_position( + ft, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) }) && rest.as_ref().map_or(true, |r| { - check_derive_position(r, var, positive, want_covariant, allow_forall, instances, tyvar_classes, ctor_details, data_constructors, depth + 1) + check_derive_position( + r, + var, + positive, + want_covariant, + allow_forall, + instances, + tyvar_classes, + ctor_details, + data_constructors, + local_concrete_type_names, + depth + 1, + ) }) } @@ -7360,15 +10166,17 @@ fn check_derive_position( /// Returns `None` for types with zero/multiple constructors (must fall back to instances) /// or if the arity doesn't match. fn try_expand_type_constructors( - head_con: Symbol, + head_con: QualifiedIdent, args: &[&Type], - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: usize, ) -> Option> { - if depth > 20 { return None; } // avoid infinite expansion - // Find a constructor whose parent type matches head_con - // For types with exactly one constructor (newtypes, single-ctor data types), - // we can expand structurally + if depth > 20 { + return None; + } // avoid infinite expansion + // Find a constructor whose parent type matches head_con + // For types with exactly one constructor (newtypes, single-ctor data types), + // we can expand structurally let mut matching_ctors = Vec::new(); for (_ctor_name, (parent, type_vars, field_types)) in ctor_details { if *parent == head_con { @@ -7389,12 +10197,15 @@ fn try_expand_type_constructors( } // Build substitution from type vars to actual args - let subst: HashMap = type_vars.iter().copied() + let subst: HashMap = type_vars + .iter() + .copied() .zip(args.iter().map(|a| (*a).clone())) .collect(); // Apply substitution to each field type - let expanded: Vec = field_types.iter() + let expanded: Vec = field_types + .iter() .map(|ft| apply_var_subst(&subst, ft)) .collect(); @@ -7405,18 +10216,28 @@ fn try_expand_type_constructors( /// Looks for instances where the head type of the first/only type argument /// matches the given constructor. fn has_class_instance_for( - instances: &HashMap, Vec<(Symbol, Vec)>)>>, - class: Symbol, - type_con: Symbol, + instances: &HashMap, Vec<(QualifiedIdent, Vec)>)>>, + class: QualifiedIdent, + type_con: QualifiedIdent, ) -> bool { - if let Some(class_instances) = instances.get(&class) { + // Try both the exact class key and unqualified fallback + let class_instances = instances.get(&class).or_else(|| { + if class.module.is_some() { + instances.get(&qi(class.name)) + } else { + None + } + }); + if let Some(class_instances) = class_instances { for (inst_types, _) in class_instances { // Instance like `Functor Array` has inst_types = [Con(Array)] // Instance like `Functor (Tuple a)` has inst_types = [App(Con(Tuple), Var(a))] if let Some(first) = inst_types.first() { - let head = get_type_constructor_head(first); - if head == Some(type_con) { - return true; + if let Some(head) = get_type_constructor_head(first) { + // Match by exact QualifiedIdent or by unqualified name + if head == type_con || head.name == type_con.name { + return true; + } } } } @@ -7425,7 +10246,7 @@ fn has_class_instance_for( } /// Get the head type constructor from a type (unwrapping applications). -fn get_type_constructor_head(ty: &Type) -> Option { +fn get_type_constructor_head(ty: &Type) -> Option { match ty { Type::Con(s) => Some(*s), Type::App(f, _) => get_type_constructor_head(f), @@ -7441,8 +10262,11 @@ fn type_var_occurs_in(var: Symbol, ty: &Type) -> bool { Type::App(f, a) => type_var_occurs_in(var, f) || type_var_occurs_in(var, a), Type::Forall(vars, body) => { // Don't recurse if var is shadowed - if vars.iter().any(|(v, _)| *v == var) { false } - else { type_var_occurs_in(var, body) } + if vars.iter().any(|(v, _)| *v == var) { + false + } else { + type_var_occurs_in(var, body) + } } Type::Record(fields, rest) => { fields.iter().any(|(_, t)| type_var_occurs_in(var, t)) @@ -7464,12 +10288,21 @@ fn try_solve_coercible_with_interactions( wanted_b: &Type, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, ) -> CoercibleResult { // First try direct solve with givens (Rule 0) - let result = solve_coercible(wanted_a, wanted_b, givens, type_roles, newtype_names, type_aliases, ctor_details, 0); + let result = solve_coercible( + wanted_a, + wanted_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + 0, + ); if result == CoercibleResult::Solved || result == CoercibleResult::DepthExceeded { return result; } @@ -7500,8 +10333,19 @@ fn try_solve_coercible_with_interactions( let rewritten_b = apply_var_subst(&canonical_subst, wanted_b); // Only try if rewriting changed something - if !types_structurally_equal(&rewritten_a, wanted_a) || !types_structurally_equal(&rewritten_b, wanted_b) { - let result2 = solve_coercible(&rewritten_a, &rewritten_b, givens, type_roles, newtype_names, type_aliases, ctor_details, 0); + if !types_structurally_equal(&rewritten_a, wanted_a) + || !types_structurally_equal(&rewritten_b, wanted_b) + { + let result2 = solve_coercible( + &rewritten_a, + &rewritten_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + 0, + ); if result2 == CoercibleResult::Solved || result2 == CoercibleResult::DepthExceeded { return result2; } @@ -7509,8 +10353,19 @@ fn try_solve_coercible_with_interactions( // Try second round of substitution (for transitive cases) let rewritten2_a = apply_var_subst(&canonical_subst, &rewritten_a); let rewritten2_b = apply_var_subst(&canonical_subst, &rewritten_b); - if !types_structurally_equal(&rewritten2_a, &rewritten_a) || !types_structurally_equal(&rewritten2_b, &rewritten_b) { - let result3 = solve_coercible(&rewritten2_a, &rewritten2_b, givens, type_roles, newtype_names, type_aliases, ctor_details, 0); + if !types_structurally_equal(&rewritten2_a, &rewritten_a) + || !types_structurally_equal(&rewritten2_b, &rewritten_b) + { + let result3 = solve_coercible( + &rewritten2_a, + &rewritten2_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + 0, + ); if result3 == CoercibleResult::Solved || result3 == CoercibleResult::DepthExceeded { return result3; } @@ -7541,7 +10396,16 @@ fn try_solve_coercible_with_interactions( all_givens.push((da.clone(), db.clone())); decompose_given_pair(da, db, type_roles, &mut all_givens); } - let result4 = solve_coercible(wanted_a, wanted_b, &all_givens, type_roles, newtype_names, type_aliases, ctor_details, 0); + let result4 = solve_coercible( + wanted_a, + wanted_b, + &all_givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + 0, + ); if result4 == CoercibleResult::Solved || result4 == CoercibleResult::DepthExceeded { return result4; } @@ -7578,10 +10442,13 @@ fn decompose_given_pair( let (head_a, args_a) = unapply_type(a); let (head_b, args_b) = unapply_type(b); if let (Type::Con(con_a), Type::Con(con_b)) = (&head_a, &head_b) { - if con_a == con_b && args_a.len() == args_b.len() { - let roles = type_roles.get(con_a); + if con_a.name == con_b.name && args_a.len() == args_b.len() { + let roles = type_roles.get(&con_a.name); for (i, (arg_a, arg_b)) in args_a.iter().zip(args_b.iter()).enumerate() { - let role = roles.and_then(|r| r.get(i)).copied().unwrap_or(Role::Representational); + let role = roles + .and_then(|r| r.get(i)) + .copied() + .unwrap_or(Role::Representational); match role { Role::Representational => { out.push(((*arg_a).clone(), (*arg_b).clone())); @@ -7639,65 +10506,194 @@ fn collect_free_named_vars(ty: &Type, bound: &HashSet, vars: &mut HashSe /// Expand type aliases in a type (standalone version for use outside unification). /// Repeatedly expands until no more aliases apply. +/// Eta-reduce a type alias: for `type F a b = G H a b`, strip trailing params +/// from the body to produce `G H`. Returns None if not eta-reducible (params +/// don't appear as trailing args in order). +fn eta_reduce_alias(params: &[Symbol], body: &Type) -> Option { + if params.is_empty() { + return None; + } + let mut current = body; + let mut remaining_params: Vec = params.to_vec(); + // Strip trailing App(_, Var(param)) from back to front + let mut stripped = Vec::new(); + while let Type::App(f, a) = current { + if let Some(&last_param) = remaining_params.last() { + if let Type::Var(v) = a.as_ref() { + if *v == last_param { + stripped.push(()); + remaining_params.pop(); + current = f.as_ref(); + continue; + } + } + } + break; + } + // Must strip ALL params for full eta-reduction + if remaining_params.is_empty() { + Some(current.clone()) + } else { + None + } +} + fn expand_type_aliases(ty: &Type, type_aliases: &HashMap, Type)>) -> Type { - expand_type_aliases_inner(ty, type_aliases, 0) + let mut expanding = HashSet::new(); + expand_type_aliases_inner(ty, type_aliases, 0, &mut expanding) } -fn expand_type_aliases_inner(ty: &Type, type_aliases: &HashMap, Type)>, depth: u32) -> Type { +fn expand_type_aliases_inner( + ty: &Type, + type_aliases: &HashMap, Type)>, + depth: u32, + expanding: &mut HashSet, +) -> Type { if depth > 100 || type_aliases.is_empty() { return ty.clone(); } - // First expand nested types - let expanded = match ty { - Type::App(f, a) => { - let f2 = expand_type_aliases_inner(f, type_aliases, depth + 1); - let a2 = expand_type_aliases_inner(a, type_aliases, depth + 1); - Type::app(f2, a2) + super::check_deadline(); + + // For App types, collect the full spine first to determine the total arg count. + // This prevents inner App nodes from being independently expanded as aliases + // when they're part of a larger application (e.g., Codec with 5 args where + // Codec also has a 1-param alias — expanding the inner App(Con("Codec"), X) + // would incorrectly treat it as the alias). + if let Type::App(_, _) = ty { + let mut raw_args: Vec<&Type> = Vec::new(); + let mut head = ty; + while let Type::App(f, a) = head { + raw_args.push(a.as_ref()); + head = f.as_ref(); + } + raw_args.reverse(); + + if let Type::Con(name) = head { + if !expanding.contains(name) { + // Use qualified key for qualified types to avoid expanding a data type + // (e.g. HATS.Easing) using an alias from a different module with the same + // unqualified name (e.g. Tick's type Easing = Number -> Number). + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + type_aliases.get(&qualified) + } else { + type_aliases.get(&name.name) + }; + if let Some((params, body)) = alias_entry { + if raw_args.len() == params.len() { + // Exactly saturated: expand args, substitute, recurse + let expanded_args: Vec = raw_args + .iter() + .map(|a| { + expand_type_aliases_inner(a, type_aliases, depth + 1, expanding) + }) + .collect(); + let subst: HashMap = params + .iter() + .copied() + .zip(expanded_args.into_iter()) + .collect(); + expanding.insert(*name); + let result = expand_type_aliases_inner( + &apply_var_subst(&subst, body), + type_aliases, + depth + 1, + expanding, + ); + expanding.remove(name); + return result; + } + } + } } - Type::Fun(a, b) => { - Type::fun( - expand_type_aliases_inner(a, type_aliases, depth + 1), - expand_type_aliases_inner(b, type_aliases, depth + 1), - ) + + // Not an expandable alias — expand each arg independently. + // For the head: if it's a bare Con, don't recurse (not saturated). + let expanded_args: Vec = raw_args + .iter() + .map(|a| expand_type_aliases_inner(a, type_aliases, depth + 1, expanding)) + .collect(); + let expanded_head = match head { + Type::Con(_) => head.clone(), + _ => expand_type_aliases_inner(head, type_aliases, depth + 1, expanding), + }; + let mut result = expanded_head; + for arg in expanded_args { + result = Type::app(result, arg); } + return result; + } + + match ty { + Type::Fun(a, b) => Type::fun( + expand_type_aliases_inner(a, type_aliases, depth + 1, expanding), + expand_type_aliases_inner(b, type_aliases, depth + 1, expanding), + ), Type::Record(fields, tail) => { let fields = fields .iter() - .map(|(l, t)| (*l, expand_type_aliases_inner(t, type_aliases, depth + 1))) + .map(|(l, t)| { + ( + *l, + expand_type_aliases_inner(t, type_aliases, depth + 1, expanding), + ) + }) .collect(); - let tail = tail - .as_ref() - .map(|t| Box::new(expand_type_aliases_inner(t, type_aliases, depth + 1))); + let tail = tail.as_ref().map(|t| { + Box::new(expand_type_aliases_inner( + t, + type_aliases, + depth + 1, + expanding, + )) + }); Type::Record(fields, tail) } - Type::Forall(vars, body) => { - Type::Forall(vars.clone(), Box::new(expand_type_aliases_inner(body, type_aliases, depth + 1))) - } - _ => ty.clone(), - }; - // Now try to expand the head if it's a saturated alias - let mut args = Vec::new(); - let mut head = &expanded; - loop { - match head { - Type::App(f, a) => { - args.push(a.as_ref().clone()); - head = f.as_ref(); - } - _ => break, - } - } - if let Type::Con(name) = head { - if let Some((params, body)) = type_aliases.get(name) { - args.reverse(); - if args.len() == params.len() { - let subst: HashMap = - params.iter().copied().zip(args.into_iter()).collect(); - return expand_type_aliases_inner(&apply_var_subst(&subst, body), type_aliases, depth + 1); + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(expand_type_aliases_inner( + body, + type_aliases, + depth + 1, + expanding, + )), + ), + Type::Con(name) => { + // Zero-arg alias expansion — use qualified key for qualified types + if !expanding.contains(name) { + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + type_aliases.get(&qualified) + } else { + type_aliases.get(&name.name) + }; + if let Some((params, body)) = alias_entry { + if params.is_empty() { + expanding.insert(*name); + let result = + expand_type_aliases_inner(body, type_aliases, depth + 1, expanding); + expanding.remove(name); + return result; + } + // Eta-reduce partially applied aliases: `type Tree a = Cofree Array a` + // as a bare `Tree` (0 args) → try to produce `Cofree Array`. + // This works when the alias body ends with exactly the params in order. + if let Some(reduced) = eta_reduce_alias(params, body) { + expanding.insert(*name); + let result = expand_type_aliases_inner(&reduced, type_aliases, depth + 1, expanding); + expanding.remove(name); + return result; + } + } } + ty.clone() } + _ => ty.clone(), } - expanded } /// Result of instance resolution with depth tracking. @@ -7705,23 +10701,38 @@ enum InstanceResult { Match, NoMatch, DepthExceeded, + UnknownClass(QualifiedIdent), } /// Like `has_matching_instance_depth` but returns a tri-state result to distinguish /// "no instance found" from "possibly infinite instance" (depth exceeded). fn check_instance_depth( - instances: &HashMap, Vec<(Symbol, Vec)>)>>, + instances: &HashMap, Vec<(QualifiedIdent, Vec)>)>>, type_aliases: &HashMap, Type)>, - class_name: &Symbol, + class_name: &QualifiedIdent, concrete_args: &[Type], depth: u32, + known_classes: Option<&HashSet>, + type_con_arities: Option<&HashMap>, ) -> InstanceResult { if depth > 200 { return InstanceResult::DepthExceeded; } - // Built-in solver instances for compiler-magic type classes - let class_str = crate::interner::resolve(*class_name).unwrap_or_default().to_string(); + // Check if the class is in scope (only for sub-constraints at depth > 0) + // Also accept classes that have instances (covers Prim built-in classes like Nub) + if depth > 0 { + if let Some(kc) = known_classes { + if !kc.contains(class_name) && !instances.contains_key(class_name) { + return InstanceResult::UnknownClass(*class_name); + } + } + } + + // Built-in solver instances for compiler-magic type classes. TODO make this module aware + let class_str = crate::interner::resolve(class_name.name) + .unwrap_or_default() + .to_string(); match class_str.as_str() { "IsSymbol" => { if concrete_args.len() == 1 { @@ -7734,16 +10745,17 @@ fn check_instance_depth( if concrete_args.len() == 2 { let matches = match (&concrete_args[0], &concrete_args[1]) { (Type::TypeString(_), Type::Con(s)) => { - crate::interner::resolve(*s).unwrap_or_default() == "String" + crate::interner::resolve(s.name).unwrap_or_default() == "String" } (Type::TypeInt(_), Type::Con(s)) => { - crate::interner::resolve(*s).unwrap_or_default() == "Int" + crate::interner::resolve(s.name).unwrap_or_default() == "Int" } (Type::Con(c), Type::Con(s)) => { - let c_str = crate::interner::resolve(*c).unwrap_or_default().to_string(); - let s_str = crate::interner::resolve(*s).unwrap_or_default().to_string(); + let c_str = crate::interner::resolve(c.name).unwrap_or_default().to_string(); + let s_str = crate::interner::resolve(s.name).unwrap_or_default().to_string(); (c_str == "True" || c_str == "False") && s_str == "Boolean" - || (c_str == "LT" || c_str == "EQ" || c_str == "GT") && s_str == "Ordering" + || (c_str == "LT" || c_str == "EQ" || c_str == "GT") + && s_str == "Ordering" } _ => false, }; @@ -7771,7 +10783,9 @@ fn check_instance_depth( // Lacks label row: the label must NOT appear in the row if concrete_args.len() == 2 { if let Type::TypeString(label_sym) = &concrete_args[0] { - let label_str = crate::interner::resolve(*label_sym).unwrap_or_default().to_string(); + let label_str = crate::interner::resolve(*label_sym) + .unwrap_or_default() + .to_string(); match &concrete_args[1] { Type::Record(fields, tail) => { let has_label = fields.iter().any(|(f, _)| { @@ -7795,19 +10809,27 @@ fn check_instance_depth( } } } - "RowToList" | "Nub" | "Union" | "Cons" - | "Coercible" | "Partial" => { + "RowToList" | "Nub" | "Union" | "Cons" | "Coercible" | "Partial" + | "Warn" | "CompareSymbol" | "Compare" | "Add" | "Mul" + | "ToString" | "Reifiable" => { return InstanceResult::Match; } + "Fail" => { + // Fail always fails — it's a compile-time error mechanism + return InstanceResult::NoMatch; + } _ => {} } let expanded_args: Vec = concrete_args .iter() - .map(|t| expand_type_aliases(t, type_aliases)) + .map(|t| { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + }) .collect(); - let known = match instances.get(class_name) { + let known = match lookup_instances(instances, class_name) { Some(k) => k, None => return InstanceResult::NoMatch, }; @@ -7816,7 +10838,10 @@ fn check_instance_depth( for (inst_types, inst_constraints) in known { let expanded_inst_types: Vec = inst_types .iter() - .map(|t| expand_type_aliases(t, type_aliases)) + .map(|t| { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + }) .collect(); if expanded_inst_types.len() != expanded_args.len() { continue; @@ -7844,12 +10869,18 @@ fn check_instance_depth( fn has_var_not_in_subst(ty: &Type, subst: &HashMap) -> bool { match ty { Type::Var(v) => !subst.contains_key(v), - Type::App(f, a) => has_var_not_in_subst(f, subst) || has_var_not_in_subst(a, subst), - Type::Fun(a, b) => has_var_not_in_subst(a, subst) || has_var_not_in_subst(b, subst), + Type::App(f, a) => { + has_var_not_in_subst(f, subst) || has_var_not_in_subst(a, subst) + } + Type::Fun(a, b) => { + has_var_not_in_subst(a, subst) || has_var_not_in_subst(b, subst) + } Type::Forall(_, body) => has_var_not_in_subst(body, subst), Type::Record(fields, tail) => { fields.iter().any(|(_, t)| has_var_not_in_subst(t, subst)) - || tail.as_ref().map_or(false, |t| has_var_not_in_subst(t, subst)) + || tail + .as_ref() + .map_or(false, |t| has_var_not_in_subst(t, subst)) } _ => false, } @@ -7859,7 +10890,15 @@ fn check_instance_depth( if has_unbound_vars { continue; } - match check_instance_depth(instances, type_aliases, c_class, &substituted_args, depth + 1) { + match check_instance_depth( + instances, + type_aliases, + c_class, + &substituted_args, + depth + 1, + known_classes, + type_con_arities, + ) { InstanceResult::Match => {} InstanceResult::DepthExceeded => { any_depth_exceeded = true; @@ -7870,6 +10909,7 @@ fn check_instance_depth( all_ok = false; break; } + r @ InstanceResult::UnknownClass(_) => return r, } } if all_ok { @@ -7885,11 +10925,12 @@ fn check_instance_depth( } fn has_matching_instance_depth( - instances: &HashMap, Vec<(Symbol, Vec)>)>>, + instances: &HashMap, Vec<(QualifiedIdent, Vec)>)>>, type_aliases: &HashMap, Type)>, - class_name: &Symbol, + class_name: &QualifiedIdent, concrete_args: &[Type], depth: u32, + type_con_arities: Option<&HashMap>, ) -> bool { if depth > 20 { // Avoid infinite recursion on circular constraint chains @@ -7897,7 +10938,9 @@ fn has_matching_instance_depth( } // Built-in solver instances for compiler-magic type classes - let class_str = crate::interner::resolve(*class_name).unwrap_or_default().to_string(); + let class_str = crate::interner::resolve(class_name.name) + .unwrap_or_default() + .to_string(); match class_str.as_str() { // IsSymbol "foo" always holds for any type-level string literal "IsSymbol" => { @@ -7912,16 +10955,17 @@ fn has_matching_instance_depth( if concrete_args.len() == 2 { let matches = match (&concrete_args[0], &concrete_args[1]) { (Type::TypeString(_), Type::Con(s)) => { - crate::interner::resolve(*s).unwrap_or_default() == "String" + crate::interner::resolve(s.name).unwrap_or_default() == "String" } (Type::TypeInt(_), Type::Con(s)) => { - crate::interner::resolve(*s).unwrap_or_default() == "Int" + crate::interner::resolve(s.name).unwrap_or_default() == "Int" } (Type::Con(c), Type::Con(s)) => { - let c_str = crate::interner::resolve(*c).unwrap_or_default().to_string(); - let s_str = crate::interner::resolve(*s).unwrap_or_default().to_string(); + let c_str = crate::interner::resolve(c.name).unwrap_or_default().to_string(); + let s_str = crate::interner::resolve(s.name).unwrap_or_default().to_string(); (c_str == "True" || c_str == "False") && s_str == "Boolean" - || (c_str == "LT" || c_str == "EQ" || c_str == "GT") && s_str == "Ordering" + || (c_str == "LT" || c_str == "EQ" || c_str == "GT") + && s_str == "Ordering" } _ => false, }; @@ -7946,30 +10990,38 @@ fn has_matching_instance_depth( } } } - "RowToList" | "Nub" | "Union" | "Cons" | "Lacks" - | "Coercible" | "Partial" => { + "RowToList" | "Nub" | "Union" | "Cons" | "Lacks" | "Coercible" | "Partial" + | "Warn" | "Fail" | "CompareSymbol" | "Compare" | "Add" | "Mul" + | "ToString" | "Reifiable" => { return true; } _ => {} } - // Expand type aliases in concrete args before matching + // Expand type aliases in concrete args before matching (with over-saturated support) let expanded_args: Vec = concrete_args .iter() - .map(|t| expand_type_aliases(t, type_aliases)) + .map(|t| { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + }) .collect(); - let known = match instances.get(class_name) { + let known = match lookup_instances(instances, class_name) { Some(k) => k, - None => return false, + None => { + return false; + } }; known.iter().any(|(inst_types, inst_constraints)| { - // Also expand aliases in instance types (e.g. `instance Convert Int Words` - // where `Words` is a type synonym for `String`) + // Also expand aliases in instance types let expanded_inst_types: Vec = inst_types .iter() - .map(|t| expand_type_aliases(t, type_aliases)) + .map(|t| { + let mut expanding = HashSet::new(); + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + }) .collect(); if expanded_inst_types.len() != expanded_args.len() { return false; @@ -7994,11 +11046,19 @@ fn has_matching_instance_depth( inst_constraints.iter().all(|(c_class, c_args)| { let substituted_args: Vec = c_args.iter().map(|t| apply_var_subst(&subst, t)).collect(); - let has_vars = substituted_args.iter().any(|t| contains_type_var(t)); + let has_vars = substituted_args.iter().any(|t| contains_type_var_or_unif(t)); if has_vars { return true; } - has_matching_instance_depth(instances, type_aliases, c_class, &substituted_args, depth + 1) + let result = has_matching_instance_depth( + instances, + type_aliases, + c_class, + &substituted_args, + depth + 1, + type_con_arities, + ); + result }) }) } @@ -8011,13 +11071,29 @@ fn has_matching_instance_depth( /// Collect all type constructor names (Type::Con) referenced in a type. fn collect_type_constructors(ty: &Type, out: &mut Vec) { match ty { - Type::Con(name) => out.push(*name), - Type::App(f, arg) => { collect_type_constructors(f, out); collect_type_constructors(arg, out); } - Type::Fun(a, b) => { collect_type_constructors(a, out); collect_type_constructors(b, out); } + Type::Con(name) => { + // Skip qualified type references (e.g. Subject.Checkbox) — they are imported + // and do not require local export validation. + if name.module.is_none() { + out.push(name.name); + } + } + Type::App(f, arg) => { + collect_type_constructors(f, out); + collect_type_constructors(arg, out); + } + Type::Fun(a, b) => { + collect_type_constructors(a, out); + collect_type_constructors(b, out); + } Type::Forall(_, body) => collect_type_constructors(body, out), Type::Record(fields, tail) => { - for (_, ty) in fields { collect_type_constructors(ty, out); } - if let Some(t) = tail { collect_type_constructors(t, out); } + for (_, ty) in fields { + collect_type_constructors(ty, out); + } + if let Some(t) = tail { + collect_type_constructors(t, out); + } } _ => {} } @@ -8029,7 +11105,8 @@ fn type_has_vars(ty: &Type) -> bool { Type::App(f, arg) => type_has_vars(f) || type_has_vars(arg), Type::Fun(a, b) => type_has_vars(a) || type_has_vars(b), Type::Record(fields, tail) => { - fields.iter().any(|(_, t)| type_has_vars(t)) || tail.as_ref().map_or(false, |t| type_has_vars(t)) + fields.iter().any(|(_, t)| type_has_vars(t)) + || tail.as_ref().map_or(false, |t| type_has_vars(t)) } Type::Forall(_, body) => type_has_vars(body), _ => false, @@ -8048,8 +11125,10 @@ fn type_contains_record(ty: &Type) -> bool { fn type_has_local_con(ty: &Type, local_types: &HashSet) -> bool { match ty { - Type::Con(name) => local_types.contains(name), - Type::App(f, arg) => type_has_local_con(f, local_types) || type_has_local_con(arg, local_types), + Type::Con(name) => local_types.contains(&name.name), + Type::App(f, arg) => { + type_has_local_con(f, local_types) || type_has_local_con(arg, local_types) + } _ => false, } } @@ -8059,8 +11138,8 @@ fn type_has_local_con(ty: &Type, local_types: &HashSet) -> bool { /// Without fundeps, at least one type in the instance head must be local. fn check_orphan_with_fundeps( inst_types: &[Type], - class_name: &Symbol, - class_fundeps: &HashMap, Vec<(Vec, Vec)>)>, + class_name: &QualifiedIdent, + class_fundeps: &HashMap, Vec<(Vec, Vec)>)>, local_type_names: &HashSet, ) -> bool { if inst_types.is_empty() { @@ -8068,7 +11147,8 @@ fn check_orphan_with_fundeps( } // Check which parameter positions have local types - let local_positions: Vec = inst_types.iter() + let local_positions: Vec = inst_types + .iter() .map(|ty| type_has_local_con(ty, local_type_names)) .collect(); @@ -8089,7 +11169,9 @@ fn check_orphan_with_fundeps( if covering_set.is_empty() { continue; // Empty covering set: always determined, no local type needed } - let has_local = covering_set.iter().any(|&idx| idx < n && local_positions[idx]); + let has_local = covering_set + .iter() + .any(|&idx| idx < n && local_positions[idx]); if !has_local { return true; // This covering set has no local type → orphan } @@ -8112,7 +11194,10 @@ fn compute_covering_sets(n: usize, fundeps: &[(Vec, Vec)]) -> Vec< for size in 1..=n { for subset in combinations(n, size) { // Skip if any existing covering set is a subset of this one - if covering.iter().any(|cs| cs.iter().all(|x| subset.contains(x))) { + if covering + .iter() + .any(|cs| cs.iter().all(|x| subset.contains(x))) + { continue; } let closure = fundep_closure(&subset, fundeps); @@ -8147,7 +11232,13 @@ fn fundep_closure(initial: &[usize], fundeps: &[(Vec, Vec)]) -> Ha fn combinations(n: usize, size: usize) -> Vec> { let mut result = Vec::new(); let mut current = Vec::new(); - fn helper(start: usize, n: usize, remaining: usize, current: &mut Vec, result: &mut Vec>) { + fn helper( + start: usize, + n: usize, + remaining: usize, + current: &mut Vec, + result: &mut Vec>, + ) { if remaining == 0 { result.push(current.clone()); return; @@ -8162,13 +11253,16 @@ fn combinations(n: usize, size: usize) -> Vec> { result } -fn type_expr_has_kinded(ty: &crate::cst::TypeExpr) -> bool { - use crate::cst::TypeExpr; +fn type_expr_has_kinded(ty: &crate::ast::TypeExpr) -> bool { + use crate::ast::TypeExpr; match ty { TypeExpr::Kinded { .. } => true, - TypeExpr::App { constructor, arg, .. } => type_expr_has_kinded(constructor) || type_expr_has_kinded(arg), - TypeExpr::Parens { ty, .. } => type_expr_has_kinded(ty), - TypeExpr::Function { from, to, .. } => type_expr_has_kinded(from) || type_expr_has_kinded(to), + TypeExpr::App { + constructor, arg, .. + } => type_expr_has_kinded(constructor) || type_expr_has_kinded(arg), + TypeExpr::Function { from, to, .. } => { + type_expr_has_kinded(from) || type_expr_has_kinded(to) + } TypeExpr::Forall { ty, .. } => type_expr_has_kinded(ty), TypeExpr::Constrained { ty, .. } => type_expr_has_kinded(ty), _ => false, @@ -8178,17 +11272,23 @@ fn type_expr_has_kinded(ty: &crate::cst::TypeExpr) -> bool { /// Check if two lists of CST TypeExprs are alpha-equivalent (including kind annotations). /// Used for overlap detection when kind annotations are present, since the internal Type /// representation strips kind info. -fn type_exprs_alpha_eq_list(a: &[crate::cst::TypeExpr], b: &[crate::cst::TypeExpr]) -> bool { +fn type_exprs_alpha_eq_list(a: &[crate::ast::TypeExpr], b: &[crate::ast::TypeExpr]) -> bool { if a.len() != b.len() { return false; } let mut var_map: HashMap = HashMap::new(); - a.iter().zip(b.iter()).all(|(x, y)| type_expr_alpha_eq(x, y, &mut var_map)) + a.iter() + .zip(b.iter()) + .all(|(x, y)| type_expr_alpha_eq(x, y, &mut var_map)) } /// Check if two CST TypeExprs are alpha-equivalent (variables map consistently). -fn type_expr_alpha_eq(a: &crate::cst::TypeExpr, b: &crate::cst::TypeExpr, var_map: &mut HashMap) -> bool { - use crate::cst::TypeExpr; +fn type_expr_alpha_eq( + a: &crate::ast::TypeExpr, + b: &crate::ast::TypeExpr, + var_map: &mut HashMap, +) -> bool { + use crate::ast::TypeExpr; match (a, b) { (TypeExpr::Var { name: na, .. }, TypeExpr::Var { name: nb, .. }) => { if let Some(mapped) = var_map.get(&na.value) { @@ -8201,32 +11301,59 @@ fn type_expr_alpha_eq(a: &crate::cst::TypeExpr, b: &crate::cst::TypeExpr, var_ma (TypeExpr::Constructor { name: na, .. }, TypeExpr::Constructor { name: nb, .. }) => { na.name == nb.name && na.module == nb.module } - (TypeExpr::App { constructor: ca, arg: aa, .. }, TypeExpr::App { constructor: cb, arg: ab, .. }) => { - type_expr_alpha_eq(ca, cb, var_map) && type_expr_alpha_eq(aa, ab, var_map) - } - (TypeExpr::Function { from: fa, to: ta, .. }, TypeExpr::Function { from: fb, to: tb, .. }) => { - type_expr_alpha_eq(fa, fb, var_map) && type_expr_alpha_eq(ta, tb, var_map) - } - (TypeExpr::Parens { ty: ta, .. }, TypeExpr::Parens { ty: tb, .. }) => { - type_expr_alpha_eq(ta, tb, var_map) - } - // Unwrap parens when comparing mixed paren/non-paren - (TypeExpr::Parens { ty, .. }, other) | (other, TypeExpr::Parens { ty, .. }) => { - type_expr_alpha_eq(ty, other, var_map) - } - (TypeExpr::Kinded { ty: ta, kind: ka, .. }, TypeExpr::Kinded { ty: tb, kind: kb, .. }) => { - type_expr_alpha_eq(ta, tb, var_map) && type_expr_alpha_eq(ka, kb, var_map) - } - (TypeExpr::Forall { vars: va, ty: ta, .. }, TypeExpr::Forall { vars: vb, ty: tb, .. }) => { - if va.len() != vb.len() { return false; } + ( + TypeExpr::App { + constructor: ca, + arg: aa, + .. + }, + TypeExpr::App { + constructor: cb, + arg: ab, + .. + }, + ) => type_expr_alpha_eq(ca, cb, var_map) && type_expr_alpha_eq(aa, ab, var_map), + ( + TypeExpr::Function { + from: fa, to: ta, .. + }, + TypeExpr::Function { + from: fb, to: tb, .. + }, + ) => type_expr_alpha_eq(fa, fb, var_map) && type_expr_alpha_eq(ta, tb, var_map), + ( + TypeExpr::Kinded { + ty: ta, kind: ka, .. + }, + TypeExpr::Kinded { + ty: tb, kind: kb, .. + }, + ) => type_expr_alpha_eq(ta, tb, var_map) && type_expr_alpha_eq(ka, kb, var_map), + ( + TypeExpr::Forall { + vars: va, ty: ta, .. + }, + TypeExpr::Forall { + vars: vb, ty: tb, .. + }, + ) => { + if va.len() != vb.len() { + return false; + } for ((a_var, a_vis, _a_kind), (b_var, b_vis, _b_kind)) in va.iter().zip(vb.iter()) { - if a_vis != b_vis { return false; } + if a_vis != b_vis { + return false; + } var_map.insert(a_var.value, b_var.value); } type_expr_alpha_eq(ta, tb, var_map) } - (TypeExpr::StringLiteral { value: va, .. }, TypeExpr::StringLiteral { value: vb, .. }) => va == vb, - (TypeExpr::IntLiteral { value: va, .. }, TypeExpr::IntLiteral { value: vb, .. }) => va == vb, + (TypeExpr::StringLiteral { value: va, .. }, TypeExpr::StringLiteral { value: vb, .. }) => { + va == vb + } + (TypeExpr::IntLiteral { value: va, .. }, TypeExpr::IntLiteral { value: vb, .. }) => { + va == vb + } _ => false, } } @@ -8234,34 +11361,63 @@ fn type_expr_alpha_eq(a: &crate::cst::TypeExpr, b: &crate::cst::TypeExpr, var_ma /// Check if two instance heads are identical (alpha-equivalent after alias expansion). /// This catches cases like `Convert String Bar` vs `Convert String String` (when Bar = String). /// Does NOT match `Foo a` vs `Foo Int` — those are "overlapping" but valid at definition time. +/// `no_expand` names are excluded from alias expansion — used for locally-defined data/newtypes +/// whose names might collide with imported type aliases from other modules. fn instance_heads_overlap( types_a: &[Type], types_b: &[Type], type_aliases: &HashMap, Type)>, + no_expand: &HashSet, ) -> bool { if types_a.len() != types_b.len() { return false; } - let expanded_a: Vec = types_a.iter().map(|t| expand_type_aliases(t, type_aliases)).collect(); - let expanded_b: Vec = types_b.iter().map(|t| expand_type_aliases(t, type_aliases)).collect(); + // Pre-seed the expanding set with locally-defined data/newtype names + // to prevent alias expansion for those names (avoids false overlaps + // e.g. newtype Thread matching Record via imported Thread alias). + let seed: HashSet = no_expand.iter().map(|s| qi(*s)).collect(); + let expanded_a: Vec = types_a + .iter() + .map(|t| { + let mut expanding = seed.clone(); + expand_type_aliases_inner(t, type_aliases, 0, &mut expanding) + }) + .collect(); + let expanded_b: Vec = types_b + .iter() + .map(|t| { + let mut expanding = seed.clone(); + expand_type_aliases_inner(t, type_aliases, 0, &mut expanding) + }) + .collect(); // Check alpha-equivalence: type variables match other type variables (positionally), // but concrete types must be structurally identical. let mut var_map: HashMap = HashMap::new(); - if expanded_a.iter().zip(expanded_b.iter()).all(|(a, b)| instance_types_alpha_eq(a, b, &mut var_map)) { + if expanded_a + .iter() + .zip(expanded_b.iter()) + .all(|(a, b)| instance_types_alpha_eq(a, b, &mut var_map)) + { return true; } // Also check subsumption: if one instance head is strictly more general than the other, // they overlap. E.g. `instance Test a` subsumes `instance Test Int`. // Check both directions: A subsumes B, or B subsumes A. + // Use strict constructor comparison to avoid false positives when types from + // different modules share the same unqualified name (e.g. List vs Lazy.List). let mut subst_ab: HashMap = HashMap::new(); - let a_subsumes_b = expanded_a.iter().zip(expanded_b.iter()) - .all(|(a, b)| match_instance_type(a, b, &mut subst_ab)); + let a_subsumes_b = expanded_a + .iter() + .zip(expanded_b.iter()) + .all(|(a, b)| match_instance_type_strict(a, b, &mut subst_ab)); if a_subsumes_b { return true; } let mut subst_ba: HashMap = HashMap::new(); - expanded_b.iter().zip(expanded_a.iter()) - .all(|(b, a)| match_instance_type(b, a, &mut subst_ba)) + expanded_b + .iter() + .zip(expanded_a.iter()) + .all(|(b, a)| match_instance_type_strict(b, a, &mut subst_ba)) } /// Check if two instance types are alpha-equivalent (identical up to variable renaming). @@ -8281,24 +11437,90 @@ fn instance_types_alpha_eq(a: &Type, b: &Type, var_map: &mut HashMap { instance_types_alpha_eq(f1, f2, var_map) && instance_types_alpha_eq(a1, a2, var_map) } - (Type::Fun(a1, b1), Type::Fun(a2, b2)) => { - instance_types_alpha_eq(a1, a2, var_map) && instance_types_alpha_eq(b1, b2, var_map) + (Type::Fun(a1, b1), Type::Fun(a2, b2)) => { + instance_types_alpha_eq(a1, a2, var_map) && instance_types_alpha_eq(b1, b2, var_map) + } + (Type::Record(f1, t1), Type::Record(f2, t2)) => { + if f1.len() != f2.len() { + return false; + } + for ((l1, ty1), (l2, ty2)) in f1.iter().zip(f2.iter()) { + if l1 != l2 || !instance_types_alpha_eq(ty1, ty2, var_map) { + return false; + } + } + match (t1, t2) { + (None, None) => true, + (Some(a), Some(b)) => instance_types_alpha_eq(a, b, var_map), + _ => false, + } + } + (Type::TypeString(x), Type::TypeString(y)) => x == y, + (Type::TypeInt(x), Type::TypeInt(y)) => x == y, + _ => a == b, + } +} + +/// Check if two type constructor names are equivalent, accounting for +/// the `(->)` / `Function` alias (they're the same type in PureScript). +fn type_con_names_eq(a: Symbol, b: Symbol) -> bool { + a == b || { + let a_str = crate::interner::resolve(a).unwrap_or_default(); + let b_str = crate::interner::resolve(b).unwrap_or_default(); + (a_str == "->" || a_str == "Function") && (b_str == "->" || b_str == "Function") + } +} + +/// Module-aware type constructor comparison. +/// When both types have module qualifiers and they differ, the types are distinct +/// (e.g., `List.List` vs `LazyList.List` are different types even though both are named "List"). +/// When either type has no module qualifier, falls back to name-only comparison. +fn type_con_qi_eq(a: &QualifiedIdent, b: &QualifiedIdent) -> bool { + // When both have module qualifiers and they match, that's a strong positive. + // When they differ, DON'T return false — the difference may be due to + // import aliases vs canonical module names (e.g., "FO" vs "Foreign.Object" + // both referring to Foreign.Object.Object). Always fall back to name comparison. + type_con_names_eq(a.name, b.name) +} + +/// Strict module-aware type constructor comparison for overlap checking. +/// Unlike `type_con_qi_eq`, when one type has a module qualifier and the other +/// doesn't, treats them as distinct (e.g., `List` vs `Lazy.List`). +fn type_con_qi_eq_strict(a: &QualifiedIdent, b: &QualifiedIdent) -> bool { + match (a.module, b.module) { + (Some(ma), Some(mb)) => { + if ma != mb { + return false; + } + } + (Some(_), None) | (None, Some(_)) => { + return false; + } + (None, None) => {} + } + type_con_names_eq(a.name, b.name) +} + +/// Compare two types for equality, using lenient module-qualifier comparison for type constructors. +/// This handles cases where the same type is referenced as `DecodeError` (unqualified) and +/// `Error.DecodeError` (qualified through an import alias). +fn types_eq_lenient(a: &Type, b: &Type) -> bool { + match (a, b) { + (Type::Con(ca), Type::Con(cb)) => type_con_qi_eq(ca, cb), + (Type::App(f1, a1), Type::App(f2, a2)) => types_eq_lenient(f1, f2) && types_eq_lenient(a1, a2), + (Type::Fun(a1, b1), Type::Fun(a2, b2)) => types_eq_lenient(a1, a2) && types_eq_lenient(b1, b2), + (Type::Forall(v1, body1), Type::Forall(v2, body2)) => { + v1.len() == v2.len() && types_eq_lenient(body1, body2) } (Type::Record(f1, t1), Type::Record(f2, t2)) => { - if f1.len() != f2.len() { return false; } - for ((l1, ty1), (l2, ty2)) in f1.iter().zip(f2.iter()) { - if l1 != l2 || !instance_types_alpha_eq(ty1, ty2, var_map) { - return false; + f1.len() == f2.len() + && f1.iter().zip(f2.iter()).all(|((l1, ty1), (l2, ty2))| l1 == l2 && types_eq_lenient(ty1, ty2)) + && match (t1, t2) { + (None, None) => true, + (Some(a), Some(b)) => types_eq_lenient(a, b), + _ => false, } - } - match (t1, t2) { - (None, None) => true, - (Some(a), Some(b)) => instance_types_alpha_eq(a, b, var_map), - _ => false, - } } - (Type::TypeString(x), Type::TypeString(y)) => x == y, - (Type::TypeInt(x), Type::TypeInt(y)) => x == y, _ => a == b, } } @@ -8309,13 +11531,16 @@ fn match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMap { if let Some(existing) = subst.get(v) { - existing == concrete + // Use lenient comparison that ignores module qualifiers on type constructors. + // This handles cases like `DecodeError` vs `Error.DecodeError` referring to + // the same type through different import paths. + types_eq_lenient(existing, concrete) } else { subst.insert(*v, concrete.clone()); true } } - (Type::Con(a), Type::Con(b)) => a == b, + (Type::Con(a), Type::Con(b)) => type_con_qi_eq(a, b), (Type::App(f1, a1), Type::App(f2, a2)) => { match_instance_type(f1, f2, subst) && match_instance_type(a1, a2, subst) } @@ -8347,7 +11572,7 @@ fn match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMap { if let Type::Con(sym) = f.as_ref() { - let name = crate::interner::resolve(*sym).unwrap_or_default(); + let name = crate::interner::resolve(sym.name).unwrap_or_default(); if name == "Record" { return match_instance_type(inst_row, concrete, subst); } @@ -8356,7 +11581,7 @@ fn match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMap { if let Type::Con(sym) = f.as_ref() { - let name = crate::interner::resolve(*sym).unwrap_or_default(); + let name = crate::interner::resolve(sym.name).unwrap_or_default(); if name == "Record" { return match_instance_type(inst_ty, concrete_row, subst); } @@ -8369,6 +11594,30 @@ fn match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMap) -> bool { + match (inst_ty, concrete) { + (Type::Var(v), _) => { + if let Some(existing) = subst.get(v) { + existing == concrete + } else { + subst.insert(*v, concrete.clone()); + true + } + } + (Type::Con(a), Type::Con(b)) => type_con_qi_eq_strict(a, b), + (Type::App(f1, a1), Type::App(f2, a2)) => { + match_instance_type_strict(f1, f2, subst) && match_instance_type_strict(a1, a2, subst) + } + (Type::Fun(a1, b1), Type::Fun(a2, b2)) => { + match_instance_type_strict(a1, a2, subst) && match_instance_type_strict(b1, b2, subst) + } + _ => inst_ty == concrete, + } +} + /// Check if a type variable (Symbol) occurs in a type — used for infinite type detection. fn occurs_var(v: Symbol, ty: &Type) -> bool { match ty { @@ -8398,7 +11647,9 @@ fn could_unify_types(a: &Type, b: &Type) -> bool { could_unify_types(a1, a2) && could_unify_types(b1, b2) } (Type::Record(f1, t1), Type::Record(f2, t2)) => { - if f1.len() != f2.len() { return false; } + if f1.len() != f2.len() { + return false; + } for ((l1, ty1), (l2, ty2)) in f1.iter().zip(f2.iter()) { if l1 != l2 || !could_unify_types(ty1, ty2) { return false; @@ -8419,7 +11670,11 @@ fn could_unify_types(a: &Type, b: &Type) -> bool { /// Liberal version of match_instance_type for chain ambiguity checking. /// Treats Type::Var and Type::Unif in the concrete type as "could be anything". /// Returns true if the instance COULD POSSIBLY match the concrete args. -fn could_match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMap) -> bool { +fn could_match_instance_type( + inst_ty: &Type, + concrete: &Type, + subst: &mut HashMap, +) -> bool { match (inst_ty, concrete) { // Instance type variable matches anything (Type::Var(v), _) => { @@ -8432,7 +11687,7 @@ fn could_match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMa } // Concrete type variable or unif var could be anything (_, Type::Var(_)) | (_, Type::Unif(_)) => true, - (Type::Con(a), Type::Con(b)) => a == b, + (Type::Con(a), Type::Con(b)) => type_con_qi_eq(a, b), (Type::App(f1, a1), Type::App(f2, a2)) => { could_match_instance_type(f1, f2, subst) && could_match_instance_type(a1, a2, subst) } @@ -8440,7 +11695,9 @@ fn could_match_instance_type(inst_ty: &Type, concrete: &Type, subst: &mut HashMa could_match_instance_type(a1, a2, subst) && could_match_instance_type(b1, b2, subst) } (Type::Record(f1, t1), Type::Record(f2, t2)) => { - if f1.len() != f2.len() { return false; } + if f1.len() != f2.len() { + return false; + } for ((l1, ty1), (l2, ty2)) in f1.iter().zip(f2.iter()) { if l1 != l2 || !could_match_instance_type(ty1, ty2, subst) { return false; @@ -8472,7 +11729,7 @@ enum ChainResult { /// Processes instances in order and checks for "Apart" (can't match) vs "could match". /// If an instance could match but doesn't definitely match, the chain is ambiguous. fn check_chain_ambiguity( - instances: &[(Vec, Vec<(Symbol, Vec)>)], + instances: &[(Vec, Vec<(QualifiedIdent, Vec)>)], concrete_args: &[Type], ) -> ChainResult { for (inst_types, _inst_constraints) in instances { @@ -8520,13 +11777,13 @@ fn check_chain_ambiguity( /// /// Based on the PureScript compiler's IntCompare solver. fn solve_compare_graph( - given_relations: &[(Type, Type, &str)], // (lhs, rhs, "LT"|"EQ"|"GT") - extra_concrete_ints: &[Type], // additional TypeInt values for fact generation + given_relations: &[(Type, Type, &str)], // (lhs, rhs, "LT"|"EQ"|"GT") + extra_concrete_ints: &[Type], // additional TypeInt values for fact generation lhs: &Type, rhs: &Type, -) -> Option { +) -> Option { if lhs == rhs { - return Some(crate::interner::intern("EQ")); + return Some(unqualified_ident("Eq")); } // Build adjacency list: directed edges @@ -8623,14 +11880,18 @@ fn solve_compare_graph( // BFS reachability check let reachable = |start: usize, end: usize| -> bool { - if start == end { return true; } + if start == end { + return true; + } let mut visited = vec![false; n]; let mut queue = std::collections::VecDeque::new(); visited[start] = true; queue.push_back(start); while let Some(node) = queue.pop_front() { for &next in &adj[node] { - if next == end { return true; } + if next == end { + return true; + } if !visited[next] { visited[next] = true; queue.push_back(next); @@ -8644,28 +11905,121 @@ fn solve_compare_graph( let rhs_to_lhs = reachable(rhs_idx, lhs_idx); match (lhs_to_rhs, rhs_to_lhs) { - (true, true) => Some(crate::interner::intern("EQ")), - (true, false) => Some(crate::interner::intern("LT")), - (false, true) => Some(crate::interner::intern("GT")), + (true, true) => Some(prim_qi(intern("EQ"))), + (true, false) => Some(prim_qi(intern("LT"))), + (false, true) => Some(prim_qi(intern("GT"))), (false, false) => None, // Can't determine } } /// Apply a variable substitution (Type::Var → Type) to a type. +/// Fully capture-avoiding: when a forall-bound variable would capture a free +/// variable in a substitution value, the forall-bound variable is alpha-renamed. fn apply_var_subst(subst: &HashMap, ty: &Type) -> Type { + apply_var_subst_inner(subst, ty, &mut 0u32) +} + +fn apply_var_subst_inner(subst: &HashMap, ty: &Type, counter: &mut u32) -> Type { + if subst.is_empty() { + return ty.clone(); + } match ty { Type::Var(v) => subst.get(v).cloned().unwrap_or_else(|| ty.clone()), - Type::Fun(a, b) => Type::fun(apply_var_subst(subst, a), apply_var_subst(subst, b)), - Type::App(f, a) => Type::app(apply_var_subst(subst, f), apply_var_subst(subst, a)), + Type::Fun(a, b) => Type::fun( + apply_var_subst_inner(subst, a, counter), + apply_var_subst_inner(subst, b, counter), + ), + Type::App(f, a) => Type::app( + apply_var_subst_inner(subst, f, counter), + apply_var_subst_inner(subst, a, counter), + ), Type::Forall(vars, body) => { - Type::Forall(vars.clone(), Box::new(apply_var_subst(subst, body))) + // Capture-avoiding: remove forall-bound variables from the substitution keys + let mut inner_subst = subst.clone(); + for (v, _) in vars { + inner_subst.remove(v); + } + // Check if any forall-bound variable appears free in the remaining + // substitution values. If so, alpha-rename it to avoid capture. + let mut renames: HashMap = HashMap::new(); + let mut new_vars = vars.clone(); + for (i, (v, _vis)) in vars.iter().enumerate() { + let captured = inner_subst.values().any(|val| type_has_free_var(val, *v)); + if captured { + let fresh = crate::interner::intern(&format!("$r{}", *counter)); + *counter += 1; + renames.insert(*v, fresh); + new_vars[i].0 = fresh; + } + } + let body = if renames.is_empty() { + apply_var_subst_inner(&inner_subst, body, counter) + } else { + // Rename the captured vars in the body first, then apply substitution + let renamed_body = rename_type_vars(body, &renames); + apply_var_subst_inner(&inner_subst, &renamed_body, counter) + }; + Type::Forall(new_vars, Box::new(body)) } Type::Record(fields, tail) => { let fields = fields .iter() - .map(|(l, t)| (*l, apply_var_subst(subst, t))) + .map(|(l, t)| (*l, apply_var_subst_inner(subst, t, counter))) .collect(); - let tail = tail.as_ref().map(|t| Box::new(apply_var_subst(subst, t))); + let tail = tail.as_ref().map(|t| Box::new(apply_var_subst_inner(subst, t, counter))); + Type::Record(fields, tail) + } + Type::Con(_) | Type::Unif(_) | Type::TypeString(_) | Type::TypeInt(_) => ty.clone(), + } +} + +/// Check if a type variable name appears free in a type. +fn type_has_free_var(ty: &Type, name: Symbol) -> bool { + match ty { + Type::Var(v) => *v == name, + Type::Fun(a, b) => type_has_free_var(a, name) || type_has_free_var(b, name), + Type::App(f, a) => type_has_free_var(f, name) || type_has_free_var(a, name), + Type::Forall(vars, body) => { + // name is bound by this forall — not free + if vars.iter().any(|(v, _)| *v == name) { + return false; + } + type_has_free_var(body, name) + } + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| type_has_free_var(t, name)) + || tail.as_ref().map_or(false, |t| type_has_free_var(t, name)) + } + _ => false, + } +} + +/// Rename type variables in a type (capture-avoiding for inner Foralls). +fn rename_type_vars(ty: &Type, renames: &HashMap) -> Type { + if renames.is_empty() { + return ty.clone(); + } + match ty { + Type::Var(v) => { + if let Some(&new) = renames.get(v) { + Type::Var(new) + } else { + ty.clone() + } + } + Type::Fun(a, b) => Type::fun(rename_type_vars(a, renames), rename_type_vars(b, renames)), + Type::App(f, a) => Type::app(rename_type_vars(f, renames), rename_type_vars(a, renames)), + Type::Forall(vars, body) => { + // Don't rename vars that are rebound by this inner forall + let mut inner_renames = renames.clone(); + for (v, _) in vars { + inner_renames.remove(v); + } + Type::Forall(vars.clone(), Box::new(rename_type_vars(body, &inner_renames))) + } + Type::Record(fields, tail) => { + let fields = fields.iter().map(|(l, t)| (*l, rename_type_vars(t, renames))).collect(); + let tail = tail.as_ref().map(|t| Box::new(rename_type_vars(t, renames))); Type::Record(fields, tail) } Type::Con(_) | Type::Unif(_) | Type::TypeString(_) | Type::TypeInt(_) => ty.clone(), @@ -8677,11 +12031,10 @@ fn apply_var_subst(subst: &HashMap, ty: &Type) -> Type { /// if so, because the compiler can't introduce dictionary parameters without a signature. fn check_cannot_generalize_recursive( state: &mut crate::typechecker::unify::UnifyState, - _env: &Env, - deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], - op_deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], + deferred_constraints: &[(crate::span::Span, QualifiedIdent, Vec)], + op_deferred_constraints: &[(crate::span::Span, QualifiedIdent, Vec)], name: Symbol, - span: crate::ast::span::Span, + span: crate::span::Span, zonked_ty: &Type, ) -> Option { use std::collections::HashSet; @@ -8695,11 +12048,18 @@ fn check_cannot_generalize_recursive( // Check if any of those vars appear in deferred constraints (from infer_var) // or op deferred constraints (from infer_op_binary) - for (_, _, constraint_args) in deferred_constraints.iter().chain(op_deferred_constraints.iter()) { + for (_, _, constraint_args) in deferred_constraints + .iter() + .chain(op_deferred_constraints.iter()) + { for arg in constraint_args { let free_in_constraint: HashSet = state.free_unif_vars(arg).into_iter().collect(); - if free_in_ty.intersection(&free_in_constraint).next().is_some() { + if free_in_ty + .intersection(&free_in_constraint) + .next() + .is_some() + { return Some(TypeError::CannotGeneralizeRecursiveFunction { span, name, @@ -8720,9 +12080,9 @@ fn check_cannot_generalize_recursive( /// false positives from partially resolved constraints. fn check_ambiguous_type_variables( state: &mut crate::typechecker::unify::UnifyState, - deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], + deferred_constraints: &[(crate::span::Span, QualifiedIdent, Vec)], constraint_start: usize, - span: crate::ast::span::Span, + span: crate::span::Span, zonked_ty: &Type, ) -> Option { use std::collections::HashSet; @@ -8781,112 +12141,169 @@ fn infer_roles_from_fields( let mut roles = vec![Role::Phantom; type_vars.len()]; for fields in constructor_fields { for field_ty in fields { - update_roles_from_type(field_ty, type_vars, &mut roles, known_roles, Role::Representational); + update_roles_from_type( + field_ty, + type_vars, + &mut roles, + known_roles, + Role::Representational, + ); } } roles } -/// Recursively walk a type and update roles for type variables found within. -/// `position_role` is the role of the current position in the enclosing type constructor. -fn update_roles_from_type( + +/// Extract the head and arguments from a chain of TypeApp nodes. +/// Returns the head type and a vector of arguments (outermost last → reversed then returned in order). +fn unapply_type_args(ty: &Type) -> (&Type, Vec<&Type>) { + let mut args = Vec::new(); + let mut current = ty; + loop { + match current { + Type::App(f, a) => { + args.push(a.as_ref()); + current = f.as_ref(); + } + _ => { + args.reverse(); + return (current, args); + } + } + } +} + +/// Mark ALL type variables in a type as Nominal (used for arguments of TypeVar-head applications). +/// When a type variable is used as a type constructor (e.g., `f a b`), we don't know what `f` +/// will be instantiated to, so all type variables in the arguments must be conservatively +/// treated as Nominal. Respects forall-bound variable shadowing. +fn mark_all_type_vars_nominal( ty: &Type, type_vars: &[Symbol], roles: &mut [Role], - known_roles: &HashMap>, - position_role: Role, + bound: &HashSet, ) { match ty { Type::Var(v) => { - if let Some(idx) = type_vars.iter().position(|tv| tv == v) { - roles[idx] = roles[idx].max(position_role); - } - } - Type::App(_, _) => { - // Unapply to get type constructor and arguments - let (head, args) = unapply_type(ty); - if let Type::Con(con_name) = &head { - let con_roles = known_roles.get(con_name); - for (i, arg) in args.iter().enumerate() { - let arg_role = con_roles - .and_then(|r| r.get(i)) - .copied() - .unwrap_or(Role::Representational); - let effective = position_role.compose(arg_role); - update_roles_from_type(arg, type_vars, roles, known_roles, effective); - } - } else if let Type::Var(v) = &head { - // Type variable in constructor position (e.g. `f a b`) — the type - // variable gets nominal role, and all arguments get nominal too, - // since we don't know the roles of the unknown type constructor. - // We directly mark all type vars in arguments as nominal, bypassing - // compose() which would let Phantom reduce the role. + if !bound.contains(v) { if let Some(idx) = type_vars.iter().position(|tv| tv == v) { roles[idx] = Role::Nominal; } - for arg in &args { - mark_all_type_vars_nominal(arg, type_vars, roles); - } - } else { - // Other unknown head — treat args conservatively as nominal - for arg in &args { - mark_all_type_vars_nominal(arg, type_vars, roles); - } } } + Type::App(f, a) => { + mark_all_type_vars_nominal(f, type_vars, roles, bound); + mark_all_type_vars_nominal(a, type_vars, roles, bound); + } Type::Fun(a, b) => { - update_roles_from_type(a, type_vars, roles, known_roles, position_role); - update_roles_from_type(b, type_vars, roles, known_roles, position_role); + mark_all_type_vars_nominal(a, type_vars, roles, bound); + mark_all_type_vars_nominal(b, type_vars, roles, bound); } Type::Record(fields, tail) => { for (_, field_ty) in fields { - update_roles_from_type(field_ty, type_vars, roles, known_roles, position_role); + mark_all_type_vars_nominal(field_ty, type_vars, roles, bound); } if let Some(t) = tail { - update_roles_from_type(t, type_vars, roles, known_roles, position_role); + mark_all_type_vars_nominal(t, type_vars, roles, bound); } } Type::Forall(vars, body) => { - // Don't track roles for forall-bound variables (they shadow outer vars) - // But we do need to recurse into the body for outer type vars - let bound: HashSet = vars.iter().map(|(v, _)| *v).collect(); - if !type_vars.iter().any(|tv| bound.contains(tv)) { - update_roles_from_type(body, type_vars, roles, known_roles, position_role); + let mut new_bound = bound.clone(); + for (v, _) in vars { + new_bound.insert(*v); } + mark_all_type_vars_nominal(body, type_vars, roles, &new_bound); } Type::Con(_) | Type::Unif(_) | Type::TypeString(_) | Type::TypeInt(_) => {} } } -/// Mark all type variables in a type as nominal. Used when a type appears as an -/// argument to a type variable in constructor position, where all roles must be -/// nominal regardless of inner type constructor roles (e.g. `f (Phantom b)` — b is nominal). -fn mark_all_type_vars_nominal(ty: &Type, type_vars: &[Symbol], roles: &mut [Role]) { +/// Recursively walk a type and update roles for type variables found within. +/// Matches PureScript's `walkType` algorithm from Types/Roles.hs: +/// - For TypeApp chains with a known constructor head: compose position_role with +/// the constructor's declared roles for each argument position. +/// - For TypeApp chains with a TypeVar head: the head var gets position_role, +/// but ALL type vars in the arguments are marked Nominal (conservative — we don't +/// know what the type variable will be instantiated to). +/// - For TypeApp chains with unknown head: generic walk. +/// - TypeVar: report the variable's role. +/// - Fun: both sides get position_role.compose(Representational) (Function is Representational). +fn update_roles_from_type( + ty: &Type, + type_vars: &[Symbol], + roles: &mut [Role], + known_roles: &HashMap>, + position_role: Role, +) { + // For App chains, extract the head and all arguments + if matches!(ty, Type::App(..)) { + let (head, args) = unapply_type_args(ty); + + // Case 1: Known type constructor head — use declared roles + if let Type::Con(name) = head { + if let Some(head_roles) = known_roles.get(&name.name) { + for (i, arg) in args.iter().enumerate() { + let arg_role = if let Some(r) = head_roles.get(i) { + position_role.compose(*r) + } else { + position_role.compose(Role::Representational) + }; + update_roles_from_type(arg, type_vars, roles, known_roles, arg_role); + } + return; + } + } + + // Case 2: Type variable head — head gets position_role, args get Nominal treatment + if let Type::Var(v) = head { + // The head type variable gets the current position role + if let Some(idx) = type_vars.iter().position(|tv| tv == v) { + roles[idx] = roles[idx].max(position_role); + } + // All type variables in the arguments are marked Nominal + let bound = HashSet::new(); + for arg in &args { + mark_all_type_vars_nominal(arg, type_vars, roles, &bound); + } + return; + } + + // Case 3: Unknown head — generic walk + update_roles_from_type(head, type_vars, roles, known_roles, position_role); + for arg in &args { + update_roles_from_type(arg, type_vars, roles, known_roles, Role::Representational); + } + return; + } + + // Non-App types match ty { Type::Var(v) => { if let Some(idx) = type_vars.iter().position(|tv| tv == v) { - roles[idx] = Role::Nominal; + roles[idx] = roles[idx].max(position_role); } } - Type::App(f, a) => { - mark_all_type_vars_nominal(f, type_vars, roles); - mark_all_type_vars_nominal(a, type_vars, roles); - } Type::Fun(a, b) => { - mark_all_type_vars_nominal(a, type_vars, roles); - mark_all_type_vars_nominal(b, type_vars, roles); + // Function is Representational in both positions + let arg_role = position_role.compose(Role::Representational); + update_roles_from_type(a, type_vars, roles, known_roles, arg_role); + update_roles_from_type(b, type_vars, roles, known_roles, arg_role); } Type::Record(fields, tail) => { - for (_, t) in fields { - mark_all_type_vars_nominal(t, type_vars, roles); + for (_, field_ty) in fields { + update_roles_from_type(field_ty, type_vars, roles, known_roles, position_role); } if let Some(t) = tail { - mark_all_type_vars_nominal(t, type_vars, roles); + update_roles_from_type(t, type_vars, roles, known_roles, position_role); } } - Type::Forall(_, body) => { - mark_all_type_vars_nominal(body, type_vars, roles); + Type::Forall(vars, body) => { + let bound: HashSet = vars.iter().map(|(v, _)| *v).collect(); + if !type_vars.iter().any(|tv| bound.contains(tv)) { + update_roles_from_type(body, type_vars, roles, known_roles, position_role); + } } + Type::App(..) => unreachable!(), // handled above Type::Con(_) | Type::Unif(_) | Type::TypeString(_) | Type::TypeInt(_) => {} } } @@ -8895,14 +12312,16 @@ fn mark_all_type_vars_nominal(ty: &Type, type_vars: &[Symbol], roles: &mut [Role /// This handles `data D a = D (C a => a)` — the `a` in `C a` must be nominal because /// constraints cannot be coerced. Respects forall-bound variable shadowing. fn mark_constrained_vars_nominal_cst( - te: &crate::cst::TypeExpr, + te: &crate::ast::TypeExpr, type_vars: &[Symbol], roles: &mut [Role], bound: &HashSet, ) { - use crate::cst::TypeExpr; + use crate::ast::TypeExpr; match te { - TypeExpr::Constrained { constraints, ty, .. } => { + TypeExpr::Constrained { + constraints, ty, .. + } => { // All type vars in constraints are nominal (unless shadowed by forall) for c in constraints { for arg in &c.args { @@ -8911,7 +12330,9 @@ fn mark_constrained_vars_nominal_cst( } mark_constrained_vars_nominal_cst(ty, type_vars, roles, bound); } - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { mark_constrained_vars_nominal_cst(constructor, type_vars, roles, bound); mark_constrained_vars_nominal_cst(arg, type_vars, roles, bound); } @@ -8926,9 +12347,6 @@ fn mark_constrained_vars_nominal_cst( } mark_constrained_vars_nominal_cst(ty, type_vars, roles, &new_bound); } - TypeExpr::Parens { ty, .. } => { - mark_constrained_vars_nominal_cst(ty, type_vars, roles, bound); - } TypeExpr::Record { fields, .. } => { for f in fields { mark_constrained_vars_nominal_cst(&f.ty, type_vars, roles, bound); @@ -8945,22 +12363,18 @@ fn mark_constrained_vars_nominal_cst( TypeExpr::Kinded { ty, .. } => { mark_constrained_vars_nominal_cst(ty, type_vars, roles, bound); } - TypeExpr::TypeOp { left, right, .. } => { - mark_constrained_vars_nominal_cst(left, type_vars, roles, bound); - mark_constrained_vars_nominal_cst(right, type_vars, roles, bound); - } _ => {} // Var, Constructor, StringLiteral, IntLiteral, Wildcard, Hole } } /// Mark all type variables in a CST TypeExpr as nominal (respecting forall shadowing). fn mark_all_cst_vars_nominal( - te: &crate::cst::TypeExpr, + te: &crate::ast::TypeExpr, type_vars: &[Symbol], roles: &mut [Role], bound: &HashSet, ) { - use crate::cst::TypeExpr; + use crate::ast::TypeExpr; match te { TypeExpr::Var { name, .. } => { if !bound.contains(&name.value) { @@ -8969,7 +12383,9 @@ fn mark_all_cst_vars_nominal( } } } - TypeExpr::App { constructor, arg, .. } => { + TypeExpr::App { + constructor, arg, .. + } => { mark_all_cst_vars_nominal(constructor, type_vars, roles, bound); mark_all_cst_vars_nominal(arg, type_vars, roles, bound); } @@ -8984,7 +12400,9 @@ fn mark_all_cst_vars_nominal( } mark_all_cst_vars_nominal(ty, type_vars, roles, &new_bound); } - TypeExpr::Constrained { constraints, ty, .. } => { + TypeExpr::Constrained { + constraints, ty, .. + } => { for c in constraints { for arg in &c.args { mark_all_cst_vars_nominal(arg, type_vars, roles, bound); @@ -8992,9 +12410,6 @@ fn mark_all_cst_vars_nominal( } mark_all_cst_vars_nominal(ty, type_vars, roles, bound); } - TypeExpr::Parens { ty, .. } => { - mark_all_cst_vars_nominal(ty, type_vars, roles, bound); - } TypeExpr::Record { fields, .. } => { for f in fields { mark_all_cst_vars_nominal(&f.ty, type_vars, roles, bound); @@ -9011,10 +12426,6 @@ fn mark_all_cst_vars_nominal( TypeExpr::Kinded { ty, .. } => { mark_all_cst_vars_nominal(ty, type_vars, roles, bound); } - TypeExpr::TypeOp { left, right, .. } => { - mark_all_cst_vars_nominal(left, type_vars, roles, bound); - mark_all_cst_vars_nominal(right, type_vars, roles, bound); - } _ => {} // Constructor, StringLiteral, IntLiteral, Wildcard, Hole } } @@ -9042,7 +12453,7 @@ enum CoercibleResult { TypesDoNotUnify, /// Depth limit exceeded — produce PossiblyInfiniteCoercibleInstance. DepthExceeded, - /// Types have different kinds — produce KindsDoNotUnify. + /// Types have different kinds — produce KindArityMismatch. KindMismatch, } @@ -9054,13 +12465,23 @@ fn solve_coercible( b: &Type, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: u32, ) -> CoercibleResult { let mut visited = HashSet::new(); - solve_coercible_with_visited(a, b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth, &mut visited) + solve_coercible_with_visited( + a, + b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth, + &mut visited, + ) } fn solve_coercible_with_visited( @@ -9068,9 +12489,9 @@ fn solve_coercible_with_visited( b: &Type, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: u32, visited: &mut HashSet<(String, String)>, ) -> CoercibleResult { @@ -9082,7 +12503,17 @@ fn solve_coercible_with_visited( let a_expanded = expand_type_aliases(a, type_aliases); let b_expanded = expand_type_aliases(b, type_aliases); - solve_coercible_inner(&a_expanded, &b_expanded, givens, type_roles, newtype_names, type_aliases, ctor_details, depth, visited) + solve_coercible_inner( + &a_expanded, + &b_expanded, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth, + visited, + ) } fn solve_coercible_inner( @@ -9090,9 +12521,9 @@ fn solve_coercible_inner( b: &Type, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: u32, visited: &mut HashSet<(String, String)>, ) -> CoercibleResult { @@ -9103,7 +12534,17 @@ fn solve_coercible_inner( } visited.insert(goal_key.clone()); - let result = solve_coercible_inner_impl(a, b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth, visited); + let result = solve_coercible_inner_impl( + a, + b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth, + visited, + ); // Remove from visited set — only track goals on current call path visited.remove(&goal_key); @@ -9116,9 +12557,9 @@ fn solve_coercible_inner_impl( b: &Type, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: u32, visited: &mut HashSet<(String, String)>, ) -> CoercibleResult { @@ -9129,6 +12570,19 @@ fn solve_coercible_inner_impl( { return CoercibleResult::Solved; } + // Extended matching: allow type variables in the given to match concrete + // types in the wanted. This handles cases like: + // given: Coercible (f payload) (Element ... payload) + // wanted: Coercible (Element ... payload) (Geometry payload) + // where f=Geometry satisfies the match via symmetry. + let mut subst = HashMap::new(); + if types_match_up_to_vars(ga, a, &mut subst) && types_match_up_to_vars(gb, b, &mut subst) { + return CoercibleResult::Solved; + } + subst.clear(); + if types_match_up_to_vars(ga, b, &mut subst) && types_match_up_to_vars(gb, a, &mut subst) { + return CoercibleResult::Solved; + } } // Rule 1: Reflexivity @@ -9138,7 +12592,19 @@ fn solve_coercible_inner_impl( // Rule 2: Record decomposition if let (Type::Record(fields_a, tail_a), Type::Record(fields_b, tail_b)) = (a, b) { - return solve_coercible_records(fields_a, tail_a, fields_b, tail_b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth, visited); + return solve_coercible_records( + fields_a, + tail_a, + fields_b, + tail_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth, + visited, + ); } let (head_a, args_a) = unapply_type(a); @@ -9147,8 +12613,11 @@ fn solve_coercible_inner_impl( // Rule 3 (newtypes first): Unwrap newtypes before role-based decomposition. // The original PureScript compiler does this because it solves more constraints — // e.g. when a newtype has nominal parameters, unwrapping may still succeed. - let a_is_newtype = matches!(&head_a, Type::Con(c) if newtype_names.contains(c)); - let b_is_newtype = matches!(&head_b, Type::Con(c) if newtype_names.contains(c)); + let is_newtype = |c: &QualifiedIdent| -> bool { + newtype_names.contains(c) || newtype_names.iter().any(|n| n.name == c.name) + }; + let a_is_newtype = matches!(&head_a, Type::Con(c) if is_newtype(c)); + let b_is_newtype = matches!(&head_b, Type::Con(c) if is_newtype(c)); // Track if newtype unwrapping hit DepthExceeded; if Rule 4 also fails, // propagate DepthExceeded (PossiblyInfiniteCoercibleInstance) instead of NotCoercible. let mut newtype_depth_exceeded = false; @@ -9156,16 +12625,29 @@ fn solve_coercible_inner_impl( if a_is_newtype || b_is_newtype { // Try unwrapping both sides first if same constructor if let (Type::Con(con_a), Type::Con(con_b)) = (&head_a, &head_b) { - if con_a == con_b && a_is_newtype { + if con_a.name == con_b.name && a_is_newtype { if let (Some(unwrapped_a), Some(unwrapped_b)) = ( unwrap_newtype(con_a, &args_a, ctor_details), unwrap_newtype(con_b, &args_b, ctor_details), ) { let unwrapped_a = expand_type_aliases(&unwrapped_a, type_aliases); let unwrapped_b = expand_type_aliases(&unwrapped_b, type_aliases); - let result = solve_coercible_inner(&unwrapped_a, &unwrapped_b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); + let result = solve_coercible_inner( + &unwrapped_a, + &unwrapped_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); match result { - CoercibleResult::Solved | CoercibleResult::NotCoercible | CoercibleResult::TypesDoNotUnify | CoercibleResult::KindMismatch => { + CoercibleResult::Solved + | CoercibleResult::NotCoercible + | CoercibleResult::TypesDoNotUnify + | CoercibleResult::KindMismatch => { // For same-constructor newtypes, unwrapping is the definitive path. // Only DepthExceeded falls through to role-based decomposition // (for recursive newtypes solvable via given constraints). @@ -9184,7 +12666,17 @@ fn solve_coercible_inner_impl( if let Type::Con(con_a) = &head_a { if let Some(unwrapped) = unwrap_newtype(con_a, &args_a, ctor_details) { let unwrapped = expand_type_aliases(&unwrapped, type_aliases); - let result = solve_coercible_inner(&unwrapped, b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); + let result = solve_coercible_inner( + &unwrapped, + b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); if result == CoercibleResult::Solved { return result; } @@ -9200,7 +12692,17 @@ fn solve_coercible_inner_impl( if let Type::Con(con_b) = &head_b { if let Some(unwrapped) = unwrap_newtype(con_b, &args_b, ctor_details) { let unwrapped = expand_type_aliases(&unwrapped, type_aliases); - let result = solve_coercible_inner(a, &unwrapped, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); + let result = solve_coercible_inner( + a, + &unwrapped, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); if result == CoercibleResult::Solved { return result; } @@ -9214,10 +12716,13 @@ fn solve_coercible_inner_impl( // Rule 4: Same type constructor — decompose with roles if let (Type::Con(con_a), Type::Con(con_b)) = (&head_a, &head_b) { - if con_a == con_b && args_a.len() == args_b.len() { - let roles = type_roles.get(con_a); + if con_a.name == con_b.name && args_a.len() == args_b.len() { + let roles = type_roles.get(&con_a.name); for (i, (arg_a, arg_b)) in args_a.iter().zip(args_b.iter()).enumerate() { - let role = roles.and_then(|r| r.get(i)).copied().unwrap_or(Role::Representational); + let role = roles + .and_then(|r| r.get(i)) + .copied() + .unwrap_or(Role::Representational); match role { Role::Phantom => { // Phantom args can differ freely @@ -9234,7 +12739,17 @@ fn solve_coercible_inner_impl( } Role::Representational => { // Representational args must be Coercible - match solve_coercible_with_visited(arg_a, arg_b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited) { + match solve_coercible_with_visited( + arg_a, + arg_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ) { CoercibleResult::Solved => continue, other => { if newtype_depth_exceeded { @@ -9260,12 +12775,36 @@ fn solve_coercible_inner_impl( // Rule 6: Function decomposition — both sides are functions if let (Type::Fun(a1, a2), Type::Fun(b1, b2)) = (a, b) { // Check both sub-goals, propagating DepthExceeded even if one fails - let result1 = solve_coercible_with_visited(a1, b1, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); - let result2 = solve_coercible_with_visited(a2, b2, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); + let result1 = solve_coercible_with_visited( + a1, + b1, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); + let result2 = solve_coercible_with_visited( + a2, + b2, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); return match (result1, result2) { (CoercibleResult::Solved, CoercibleResult::Solved) => CoercibleResult::Solved, - (CoercibleResult::DepthExceeded, _) | (_, CoercibleResult::DepthExceeded) => CoercibleResult::DepthExceeded, - (CoercibleResult::TypesDoNotUnify, _) | (_, CoercibleResult::TypesDoNotUnify) => CoercibleResult::TypesDoNotUnify, + (CoercibleResult::DepthExceeded, _) | (_, CoercibleResult::DepthExceeded) => { + CoercibleResult::DepthExceeded + } + (CoercibleResult::TypesDoNotUnify, _) | (_, CoercibleResult::TypesDoNotUnify) => { + CoercibleResult::TypesDoNotUnify + } _ => CoercibleResult::NotCoercible, }; } @@ -9285,7 +12824,17 @@ fn solve_coercible_inner_impl( } else { apply_var_subst(&subst, body_b) }; - return solve_coercible_inner(body_a, &body_b_renamed, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited); + return solve_coercible_inner( + body_a, + &body_b_renamed, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ); } } @@ -9301,9 +12850,13 @@ fn solve_coercible_inner_impl( let (final_head_a, final_args_a) = unapply_type(a); let (final_head_b, final_args_b) = unapply_type(b); if let (Type::Con(a_con), Type::Con(b_con)) = (&final_head_a, &final_head_b) { - if a_con != b_con { - let a_remaining = type_roles.get(a_con).map(|r| r.len().saturating_sub(final_args_a.len())); - let b_remaining = type_roles.get(b_con).map(|r| r.len().saturating_sub(final_args_b.len())); + if a_con.name != b_con.name { + let a_remaining = type_roles + .get(&a_con.name) + .map(|r| r.len().saturating_sub(final_args_a.len())); + let b_remaining = type_roles + .get(&b_con.name) + .map(|r| r.len().saturating_sub(final_args_b.len())); if let (Some(a_n), Some(b_n)) = (a_remaining, b_remaining) { if a_n != b_n { return CoercibleResult::KindMismatch; @@ -9323,9 +12876,9 @@ fn solve_coercible_records( tail_b: &Option>, givens: &[(Type, Type)], type_roles: &HashMap>, - newtype_names: &HashSet, + newtype_names: &HashSet, type_aliases: &HashMap, Type)>, - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, depth: u32, visited: &mut HashSet<(String, String)>, ) -> CoercibleResult { @@ -9337,7 +12890,17 @@ fn solve_coercible_records( for (label, ty_a) in &map_a { match map_b.get(label) { Some(ty_b) => { - match solve_coercible_with_visited(ty_a, ty_b, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited) { + match solve_coercible_with_visited( + ty_a, + ty_b, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ) { CoercibleResult::Solved => continue, other => return other, } @@ -9366,9 +12929,17 @@ fn solve_coercible_records( // Check tails match (tail_a, tail_b) { (None, None) => CoercibleResult::Solved, - (Some(ta), Some(tb)) => { - solve_coercible_with_visited(ta, tb, givens, type_roles, newtype_names, type_aliases, ctor_details, depth + 1, visited) - } + (Some(ta), Some(tb)) => solve_coercible_with_visited( + ta, + tb, + givens, + type_roles, + newtype_names, + type_aliases, + ctor_details, + depth + 1, + visited, + ), // Open vs closed — can't coerce _ => CoercibleResult::NotCoercible, } @@ -9376,13 +12947,15 @@ fn solve_coercible_records( /// Unwrap a newtype: given `N a1 a2 ...`, substitute the type vars in the wrapped type. fn unwrap_newtype( - type_name: &Symbol, + type_name: &QualifiedIdent, args: &[&Type], - ctor_details: &HashMap, Vec)>, + ctor_details: &HashMap, Vec)>, ) -> Option { - // Find a constructor for this newtype + // Find a constructor for this newtype. + // Match by name only (ignoring module qualifier) to handle qualified vs + // unqualified references to the same type (e.g., C.Node vs Node). for (_, (parent, type_vars, field_types)) in ctor_details { - if parent == type_name && field_types.len() == 1 { + if parent.name == type_name.name && field_types.len() == 1 { // Single-field constructor = newtype let wrapped_ty = &field_types[0]; let subst: HashMap = type_vars @@ -9399,7 +12972,7 @@ fn unwrap_newtype( /// Check structural equality of two types (without unification). fn types_structurally_equal(a: &Type, b: &Type) -> bool { match (a, b) { - (Type::Con(a), Type::Con(b)) => a == b, + (Type::Con(a), Type::Con(b)) => a.name == b.name, (Type::Var(a), Type::Var(b)) => a == b, (Type::Unif(a), Type::Unif(b)) => a == b, (Type::App(f1, a1), Type::App(f2, a2)) => { @@ -9414,9 +12987,10 @@ fn types_structurally_equal(a: &Type, b: &Type) -> bool { if fa.len() != fb.len() { return false; } - let all_fields_eq = fa.iter().zip(fb.iter()).all(|((la, ta), (lb, tb))| { - la == lb && types_structurally_equal(ta, tb) - }); + let all_fields_eq = fa + .iter() + .zip(fb.iter()) + .all(|((la, ta), (lb, tb))| la == lb && types_structurally_equal(ta, tb)); if !all_fields_eq { return false; } @@ -9438,28 +13012,80 @@ fn types_structurally_equal(a: &Type, b: &Type) -> bool { } } +/// Match a pattern type against a target type, allowing type variables in the +/// pattern to match any type in the target. Returns true if the match succeeds +/// with a consistent substitution. Used in Coercible Rule 0 to match given +/// constraints (which may contain abstract type variables) against wanted constraints. +fn types_match_up_to_vars(pattern: &Type, target: &Type, subst: &mut HashMap) -> bool { + match (pattern, target) { + // Type variables in the pattern can match anything + (Type::Var(v), _) => { + if let Some(existing) = subst.get(v) { + types_structurally_equal(existing, target) + } else { + subst.insert(*v, target.clone()); + true + } + } + // Concrete types must match structurally + (Type::Con(a), Type::Con(b)) => a.name == b.name, + (Type::Unif(a), Type::Unif(b)) => a == b, + (Type::App(f1, a1), Type::App(f2, a2)) => { + types_match_up_to_vars(f1, f2, subst) && types_match_up_to_vars(a1, a2, subst) + } + (Type::Fun(a1, b1), Type::Fun(a2, b2)) => { + types_match_up_to_vars(a1, a2, subst) && types_match_up_to_vars(b1, b2, subst) + } + (Type::TypeString(a), Type::TypeString(b)) => a == b, + (Type::TypeInt(a), Type::TypeInt(b)) => a == b, + (Type::Record(fa, ta), Type::Record(fb, tb)) => { + if fa.len() != fb.len() { + return false; + } + let all_fields = fa.iter().zip(fb.iter()) + .all(|((la, ta), (lb, tb))| la == lb && types_match_up_to_vars(ta, tb, subst)); + if !all_fields { + return false; + } + match (ta, tb) { + (None, None) => true, + (Some(a), Some(b)) => types_match_up_to_vars(a, b, subst), + _ => false, + } + } + (Type::Forall(va, ba), Type::Forall(vb, bb)) => { + if va.len() != vb.len() { + return false; + } + let vars_eq = va.iter().zip(vb.iter()).all(|((a, _), (b, _))| a == b); + vars_eq && types_match_up_to_vars(ba, bb, subst) + } + _ => false, + } +} + /// Walks through Forall → Constrained patterns, converting constraint args to internal Types. /// Skips Partial and Warn (which are handled separately). pub(crate) fn extract_type_signature_constraints( - ty: &crate::cst::TypeExpr, - type_ops: &HashMap, - known_types: &HashSet, -) -> Vec<(Symbol, Vec)> { - use crate::cst::TypeExpr; + ty: &crate::ast::TypeExpr, + type_ops: &HashMap, +) -> Vec<(QualifiedIdent, Vec)> { + use crate::ast::TypeExpr; match ty { TypeExpr::Forall { ty, .. } => { - extract_type_signature_constraints(ty, type_ops, known_types) + extract_type_signature_constraints(ty, type_ops) } - TypeExpr::Constrained { constraints, ty, .. } => { + TypeExpr::Constrained { + constraints, ty, .. + } => { let mut result = Vec::new(); for c in constraints { let class_str = crate::interner::resolve(c.class.name).unwrap_or_default(); // Skip compiler-magic classes that don't have explicit instances. // These are resolved by special solvers or auto-satisfied. - let is_magic = matches!(class_str.as_str(), - "Partial" | "Warn" - | "Union" | "Cons" | "RowToList" - | "CompareSymbol" + let is_magic = matches!( + class_str.as_str(), + "Partial" | "Warn" | "Union" | "Cons" | "RowToList" | "CompareSymbol" ); if is_magic { continue; @@ -9467,43 +13093,43 @@ pub(crate) fn extract_type_signature_constraints( let mut args = Vec::new(); let mut ok = true; for arg in &c.args { - match convert_type_expr(arg, type_ops, known_types) { + match convert_type_expr(arg, type_ops) { Ok(converted) => args.push(converted), - Err(_) => { ok = false; break; } + Err(_) => { + ok = false; + break; + } } } if ok { - result.push((c.class.name, args)); + result.push((c.class, args)); } else if class_str == "Fail" { // Fail constraints should always be recorded even if args can't // be converted (e.g. type-level Text/Quote from Prim.TypeError). // The args aren't needed for error detection — any use of Fail // means the constraint is deliberately unsatisfiable. - result.push((c.class.name, Vec::new())); + result.push((c.class, Vec::new())); } } // Recurse into the inner type for chained constraints // (e.g. `Compare a b EQ => Compare b c GT => ...` parses as nested Constrained) - result.extend(extract_type_signature_constraints(ty, type_ops, known_types)); + result.extend(extract_type_signature_constraints( + ty, + type_ops, + )); result } - TypeExpr::Parens { ty, .. } => { - extract_type_signature_constraints(ty, type_ops, known_types) - } _ => Vec::new(), } } /// Check if a TypeExpr has a Partial constraint. -fn has_partial_constraint(ty: &crate::cst::TypeExpr) -> bool { +pub fn has_partial_constraint(ty: &crate::ast::TypeExpr) -> bool { match ty { - crate::cst::TypeExpr::Constrained { constraints, .. } => { - constraints.iter().any(|c| { - crate::interner::resolve(c.class.name).unwrap_or_default() == "Partial" - }) - } - crate::cst::TypeExpr::Forall { ty, .. } => has_partial_constraint(ty), - crate::cst::TypeExpr::Parens { ty, .. } => has_partial_constraint(ty), + crate::ast::TypeExpr::Constrained { constraints, .. } => constraints + .iter() + .any(|c| crate::interner::resolve(c.class.name).unwrap_or_default() == "Partial"), + crate::ast::TypeExpr::Forall { ty, .. } => has_partial_constraint(ty), _ => false, } } @@ -9511,11 +13137,10 @@ fn has_partial_constraint(ty: &crate::cst::TypeExpr) -> bool { /// Check if a function type's parameter has a Partial constraint. /// E.g. `(Partial => a) -> a` or `forall a. (Partial => a) -> a` returns true. /// Used to detect functions that discharge the Partial constraint (like unsafePartial). -fn has_partial_in_function_param(ty: &crate::cst::TypeExpr) -> bool { - use crate::cst::TypeExpr; +fn has_partial_in_function_param(ty: &crate::ast::TypeExpr) -> bool { + use crate::ast::TypeExpr; match ty { TypeExpr::Forall { ty, .. } => has_partial_in_function_param(ty), - TypeExpr::Parens { ty, .. } => has_partial_in_function_param(ty), TypeExpr::Constrained { ty, .. } => has_partial_in_function_param(ty), TypeExpr::Function { from, .. } => has_partial_constraint(from), _ => false, @@ -9523,32 +13148,26 @@ fn has_partial_in_function_param(ty: &crate::cst::TypeExpr) -> bool { } /// Check if a type expression contains a wildcard `_` anywhere. -fn find_wildcard_span(ty: &crate::cst::TypeExpr) -> Option { - use crate::cst::TypeExpr; +fn find_wildcard_span(ty: &crate::ast::TypeExpr) -> Option { + use crate::ast::TypeExpr; match ty { TypeExpr::Wildcard { span } => Some(*span), - TypeExpr::App { constructor, arg, .. } => { - find_wildcard_span(constructor).or_else(|| find_wildcard_span(arg)) - } + TypeExpr::App { + constructor, arg, .. + } => find_wildcard_span(constructor).or_else(|| find_wildcard_span(arg)), TypeExpr::Function { from, to, .. } => { find_wildcard_span(from).or_else(|| find_wildcard_span(to)) } TypeExpr::Forall { ty, .. } => find_wildcard_span(ty), TypeExpr::Constrained { ty, .. } => find_wildcard_span(ty), - TypeExpr::Parens { ty, .. } => find_wildcard_span(ty), TypeExpr::Kinded { ty, kind, .. } => { find_wildcard_span(ty).or_else(|| find_wildcard_span(kind)) } - TypeExpr::Record { fields, .. } => { - fields.iter().find_map(|f| find_wildcard_span(&f.ty)) - } - TypeExpr::Row { fields, tail, .. } => { - fields.iter().find_map(|f| find_wildcard_span(&f.ty)) - .or_else(|| tail.as_ref().and_then(|t| find_wildcard_span(t))) - } - TypeExpr::TypeOp { left, right, .. } => { - find_wildcard_span(left).or_else(|| find_wildcard_span(right)) - } + TypeExpr::Record { fields, .. } => fields.iter().find_map(|f| find_wildcard_span(&f.ty)), + TypeExpr::Row { fields, tail, .. } => fields + .iter() + .find_map(|f| find_wildcard_span(&f.ty)) + .or_else(|| tail.as_ref().and_then(|t| find_wildcard_span(t))), _ => None, } } @@ -9558,11 +13177,10 @@ fn find_wildcard_span(ty: &crate::cst::TypeExpr) -> Option y` is NOT. The idea is to only flag the /// simplest cycles like `x = x` or `x = y; y = x`, while allowing `x = f <$> z` /// even if z is in the same SCC (since f creates a thunk/intermediate value). -fn is_direct_var_ref(expr: &crate::cst::Expr, names: &HashSet) -> bool { - use crate::cst::Expr; +fn is_direct_var_ref(expr: &crate::ast::Expr, names: &HashSet) -> bool { + use crate::ast::Expr; match expr { Expr::Var { name, .. } if name.module.is_none() => names.contains(&name.name), - Expr::Parens { expr, .. } => is_direct_var_ref(expr, names), Expr::TypeAnnotation { expr, .. } => is_direct_var_ref(expr, names), _ => false, } @@ -9572,14 +13190,12 @@ fn is_direct_var_ref(expr: &crate::cst::Expr, names: &HashSet) -> bool { /// Returns the unqualified variable name if the head is a simple Var, None otherwise. /// Used for instance method cycle detection: only the head of the application matters, /// not arguments (which may dispatch to different typeclass instances). -fn expr_app_head_name(expr: &crate::cst::Expr) -> Option { - use crate::cst::Expr; +fn expr_app_head_name(expr: &crate::ast::Expr) -> Option { + use crate::ast::Expr; match expr { Expr::Var { name, .. } if name.module.is_none() => Some(name.name), Expr::App { func, .. } => expr_app_head_name(func), - Expr::Parens { expr, .. } | Expr::TypeAnnotation { expr, .. } => { - expr_app_head_name(expr) - } + Expr::TypeAnnotation { expr, .. } => expr_app_head_name(expr), _ => None, } } @@ -9649,11 +13265,12 @@ fn flatten_row(ty: &Type) -> (Vec<(Symbol, Type)>, Option>) { /// /// Returns `Err(KindMismatch)` if the kinds are inconsistent. fn check_class_param_kind_consistency( - span: crate::ast::span::Span, - class_name: Symbol, + span: crate::span::Span, + class_name: QualifiedIdent, constraint_type: &Type, app_args: &[Type], - saved_type_kinds: &HashMap, + saved_type_kinds: &HashMap, + saved_class_kinds: &HashMap, ) -> Result<(), TypeError> { use crate::typechecker::kind::{self, KindState}; use crate::typechecker::unify::UnifyState; @@ -9663,10 +13280,13 @@ fn check_class_param_kind_consistency( return Ok(()); } - // Look up the class kind from saved_type_kinds - let class_kind_raw = match saved_type_kinds.get(&class_name) { + // Look up the class kind — prefer class_kinds (avoids collision with same-named data types) + let class_kind_raw = match saved_class_kinds.get(&class_name) + .or_else(|| saved_type_kinds.get(&class_name)) { Some(k) => k.clone(), - None => return Ok(()), // Unknown class kind — skip + None => { + return Ok(()); + } }; // Check if the class kind has shared type variables (e.g., ix -> ix -> ...). @@ -9679,21 +13299,28 @@ fn check_class_param_kind_consistency( let mut ks = KindState { state: UnifyState::new(), type_kinds: HashMap::new(), + class_kinds: HashMap::new(), binding_group: std::collections::HashSet::new(), deferred_quantification_checks: Vec::new(), + class_param_kind_types: Vec::new(), + qualifier_to_canonical: HashMap::new(), }; // Remap saved type kinds to fresh variables in the new state let mut old_to_new: HashMap = HashMap::new(); - for (&name, kind_val) in saved_type_kinds { + for (name, kind_val) in saved_type_kinds { let remapped = kind::remap_unif_vars(kind_val, &mut old_to_new, &mut ks); - ks.register_type(name, remapped); + ks.register_type(name.name, remapped); + } + for (name, kind_val) in saved_class_kinds { + let remapped = kind::remap_unif_vars(kind_val, &mut old_to_new, &mut ks); + ks.class_kinds.insert(name.name, remapped); } // Look up the class kind and instantiate it (replacing Forall vars with fresh unif vars). // This ensures both occurrences of `ix` in `forall ix. (ix -> ix -> ...) -> Constraint` // map to the SAME unif var, creating the kind equality constraint. - let class_kind = match ks.lookup_type_fresh(class_name) { + let class_kind = match ks.lookup_class_kind_fresh(class_name.name) { Some(k) => kind::instantiate_kind(&mut ks, &k), None => return Ok(()), }; @@ -9714,7 +13341,7 @@ fn check_class_param_kind_consistency( // Unify the constraint type's kind with the class parameter kind. // This establishes kind constraints (e.g., ?k2 = ?k3 = ?ix). if ks.unify_kinds(span, ¶m_kind, &constraint_kind).is_err() { - return Err(TypeError::KindMismatch { + return Err(TypeError::KindsDoNotUnify { span, expected: param_kind, found: constraint_kind, @@ -9723,16 +13350,30 @@ fn check_class_param_kind_consistency( // Now check each application argument's kind against the constrained param kind. // Walk the param kind as a chain of function arrows. + // NOTE: infer_runtime_kind looks up kinds by unqualified name, which can be wrong + // when different imported types share the same name (e.g., Concur.Core.Props.Props + // :: Type -> Type -> Type vs React.DOM.Props.Props :: Type). We detect such cases by + // checking if the inferred arg kind has strictly more function arrows than the + // expected kind position — this indicates a wrong kind lookup for a higher-kinded + // type when a simpler type was expected. let mut remaining_kind = ks.state.zonk(param_kind.clone()); for arg in app_args { let arg_kind = match kind::infer_runtime_kind_pub(arg, &mut ks, span) { Ok(k) => k, - Err(_) => return Ok(()), // Skip if kind inference fails + Err(_) => return Ok(()), }; let result_kind = ks.fresh_kind_var(); - let expected = Type::fun(arg_kind, result_kind.clone()); - if let Err(_) = ks.unify_kinds(span, &expected, &remaining_kind) { - return Err(TypeError::KindMismatch { + let expected = Type::fun(arg_kind.clone(), result_kind.clone()); + if ks.unify_kinds(span, &expected, &remaining_kind).is_err() { + // Check if the failure might be due to wrong kind lookup: if the inferred + // arg_kind is a function type but the expected position is a simple type, + // this suggests a name collision gave us the wrong (higher-kinded) type. + let zonked_arg = ks.state.zonk(arg_kind); + let is_likely_lookup_error = matches!(zonked_arg, Type::Fun(..)); + if is_likely_lookup_error { + return Ok(()); + } + return Err(TypeError::KindsDoNotUnify { span, expected: remaining_kind, found: expected, @@ -9769,12 +13410,15 @@ fn kind_collect_type_vars_shared(ty: &Type, seen: &mut std::collections::HashSet } /// Check if a type expression has any type class constraint (at the top level, under forall/parens). -fn has_any_constraint(ty: &crate::cst::TypeExpr) -> Option { - use crate::cst::TypeExpr; +fn has_any_constraint(ty: &crate::ast::TypeExpr) -> Option { + use crate::ast::TypeExpr; match ty { TypeExpr::Constrained { span, .. } => Some(*span), TypeExpr::Forall { ty, .. } => has_any_constraint(ty), - TypeExpr::Parens { ty, .. } => has_any_constraint(ty), _ => None, } } + +fn is_compare(class_name: &QualifiedIdent) -> bool { + class_name.name == intern("Compare") +} diff --git a/src/typechecker/convert.rs b/src/typechecker/convert.rs index 962426bf..9d37520f 100644 --- a/src/typechecker/convert.rs +++ b/src/typechecker/convert.rs @@ -1,67 +1,63 @@ use std::collections::{HashMap, HashSet}; -use crate::cst::TypeExpr; +use crate::ast::TypeExpr; +use crate::cst::{QualifiedIdent, unqualified_ident}; use crate::interner::Symbol; use crate::typechecker::error::TypeError; use crate::typechecker::types::Type; -/// Convert a CST TypeExpr (parsed surface syntax) into the internal Type representation. +/// Convert an AST TypeExpr into the internal Type representation. /// Used for type annotations like `(expr :: Type)`. /// /// `type_ops` maps type-level operator symbols to their target type constructor names, /// populated from `infixr N type TypeName as op` declarations. /// -/// `known_types` is the set of type constructor names currently in scope. -/// If a `TypeExpr::Constructor` name is not in this set, an `UnknownType` error -/// is returned. -pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap, known_types: &HashSet) -> Result { +/// Type name validation is handled by the AST converter (src/ast.rs) during CST→AST +/// conversion. By the time a TypeExpr reaches this function, all Constructor names +/// have already been verified to be in scope. +pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap) -> Result { super::check_deadline(); match ty { - TypeExpr::Constructor { span, name } => { + TypeExpr::Constructor { name, .. } => { // Check if this is a type operator used as a constructor (e.g. `(/\)`) - if let Some(&target) = type_ops.get(&name.name) { + if let Some(&target) = type_ops.get(&name) { return Ok(Type::Con(target)); } - if !known_types.is_empty() { - // Check both unqualified and qualified name (e.g. RL.Cons) - let found = known_types.contains(&name.name) || name.module.map_or(false, |m| { - let mod_str = crate::interner::resolve(m).unwrap_or_default(); - let name_str = crate::interner::resolve(name.name).unwrap_or_default(); - known_types.contains(&crate::interner::intern(&format!("{}.{}", mod_str, name_str))) - }); - if !found { - return Err(TypeError::UnknownType { - span: *span, - name: name.name, - }); - } - } - Ok(Type::Con(name.name)) + Ok(Type::Con(*name)) } TypeExpr::Var { name, .. } => Ok(Type::Var(name.value)), TypeExpr::Function { from, to, .. } => { - let from_ty = convert_type_expr(from, type_ops, known_types)?; - let to_ty = convert_type_expr(to, type_ops, known_types)?; + let from_ty = convert_type_expr(from, type_ops)?; + let to_ty = convert_type_expr(to, type_ops)?; Ok(Type::fun(from_ty, to_ty)) } TypeExpr::App { constructor, arg, .. } => { - let f = convert_type_expr(constructor, type_ops, known_types)?; - let a = convert_type_expr(arg, type_ops, known_types)?; + let f = convert_type_expr(constructor, type_ops)?; + let a = convert_type_expr(arg, type_ops)?; // Normalize `Record (row)` where the row is a CST Row type (parsed as Record) // to unwrap the redundant `App(Con("Record"), Record(...))`. // This only handles the case where the argument is already a Record type // (i.e., it came from a `TypeExpr::Row`). Type variables like `Record r` are // left as `App(Con("Record"), Var("r"))` to avoid issues with type alias expansion. if let Type::Con(sym) = &f { - if crate::interner::resolve(*sym).unwrap_or_default() == "Record" { + if sym == &unqualified_ident("Record") { if let Type::Record(fields, tail) = a { return Ok(Type::Record(fields, tail)); } } } + // Normalize `(->) a b` to `Fun(a, b)` — the function type constructor + // applied to two arguments should become the function type. + if let Type::App(inner_f, inner_a) = &f { + if let Type::Con(sym) = inner_f.as_ref() { + if crate::interner::resolve(sym.name).unwrap_or_default() == "->" { + return Ok(Type::fun(inner_a.as_ref().clone(), a)); + } + } + } Ok(Type::app(f, a)) } @@ -75,26 +71,24 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap, know // 2. Check forward references within forall (e.g. `forall (a :: k) k.` uses k before declaring it) for (v, _visible, kind) in vars { if let Some(kind_expr) = kind { - convert_type_expr(kind_expr, type_ops, known_types)?; + convert_type_expr(kind_expr, type_ops)?; // Check for forward references: kind vars that are declared later in this forall check_forall_kind_ordering(kind_expr, &bound_in_forall, &all_forall_vars)?; } bound_in_forall.insert(v.value); } - let body = convert_type_expr(ty, type_ops, known_types)?; + let body = convert_type_expr(ty, type_ops)?; Ok(Type::Forall(var_symbols, Box::new(body))) } - TypeExpr::Parens { ty, .. } => convert_type_expr(ty, type_ops, known_types), - // Strip constraints for now (no typeclass solving yet) - TypeExpr::Constrained { ty, .. } => convert_type_expr(ty, type_ops, known_types), + TypeExpr::Constrained { ty, .. } => convert_type_expr(ty, type_ops), TypeExpr::Record { fields, .. } => { let field_types: Vec<_> = fields .iter() .map(|f| { - let ty = convert_type_expr(&f.ty, type_ops, known_types)?; + let ty = convert_type_expr(&f.ty, type_ops)?; Ok((f.label.value, ty)) }) .collect::>()?; @@ -105,13 +99,13 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap, know let field_types: Vec<_> = fields .iter() .map(|f| { - let ty = convert_type_expr(&f.ty, type_ops, known_types)?; + let ty = convert_type_expr(&f.ty, type_ops)?; Ok((f.label.value, ty)) }) .collect::>()?; let tail_ty = tail .as_ref() - .map(|t| convert_type_expr(t, type_ops, known_types)) + .map(|t| convert_type_expr(t, type_ops)) .transpose()? .map(Box::new); Ok(Type::Record(field_types, tail_ty)) @@ -128,7 +122,7 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap, know } // Kind annotations: just strip the kind and convert the inner type - TypeExpr::Kinded { ty, .. } => convert_type_expr(ty, type_ops, known_types), + TypeExpr::Kinded { ty, .. } => convert_type_expr(ty, type_ops), // Type-level string literal TypeExpr::StringLiteral { value, .. } => { @@ -140,16 +134,6 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap, know Ok(Type::TypeInt(*value)) } - // Type-level operators: desugar `left op right` to `App(App(Con(target), left), right)` - // where `target` is resolved from the type operator map if available. - TypeExpr::TypeOp { left, op, right, .. } => { - let left_ty = convert_type_expr(left, type_ops, known_types)?; - let right_ty = convert_type_expr(right, type_ops, known_types)?; - let op_name = op.value.name; - let resolved = type_ops.get(&op_name).copied().unwrap_or(op_name); - let op_ty = Type::Con(resolved); - Ok(Type::app(Type::app(op_ty, left_ty), right_ty)) - } } } @@ -183,9 +167,6 @@ fn check_forall_kind_ordering( TypeExpr::Forall { ty, .. } => { check_forall_kind_ordering(ty, bound, all_forall_vars) } - TypeExpr::Parens { ty, .. } => { - check_forall_kind_ordering(ty, bound, all_forall_vars) - } _ => Ok(()), } } diff --git a/src/typechecker/env.rs b/src/typechecker/env.rs index 42df153c..57ab7e7f 100644 --- a/src/typechecker/env.rs +++ b/src/typechecker/env.rs @@ -175,6 +175,57 @@ impl Env { } vars } + + /// Like free_vars but excluding multiple names' bindings. + fn free_vars_excluding_many(&self, state: &mut UnifyState, exclude: &std::collections::HashSet) -> Vec { + let mut vars = Vec::new(); + for (name, scheme) in &self.bindings { + if exclude.contains(name) { + continue; + } + let scheme_vars = state.free_unif_vars(&scheme.ty); + for v in scheme_vars { + if !vars.contains(&v) { + vars.push(v); + } + } + } + vars + } + + /// Generalize a local let/where binding, excluding all names in a batch. + /// This prevents co-defined bindings' pre-inserted unif vars from polluting + /// the environment free vars, allowing proper polymorphic generalization. + pub fn generalize_local_batch(&self, state: &mut UnifyState, ty: Type, exclude_batch: &std::collections::HashSet) -> Scheme { + let ty_vars = state.free_unif_vars(&ty); + let env_vars = self.free_vars_excluding_many(state, exclude_batch); + + let gen_vars: Vec = ty_vars + .into_iter() + .filter(|v| !env_vars.contains(v)) + .collect(); + + // Track which unif vars were generalized + for &var_id in &gen_vars { + state.generalized_vars.insert(var_id); + } + + // Zonk first to resolve any already-solved unification vars + let ty = state.zonk(ty); + + // Map each generalized TyVarId to a fresh named type variable + let mut subst: HashMap = HashMap::new(); + let mut forall_vars: Vec = Vec::new(); + for &var_id in &gen_vars { + let name = interner::intern(&format!("$t{}", var_id.0)); + subst.insert(var_id, Type::Var(name)); + forall_vars.push(name); + } + + // DON'T convert remaining unif vars — they're still live in the outer context + let ty = apply_tyvarid_subst(&subst, &ty); + Scheme { forall_vars, ty } + } } /// Collect all Type::Unif var ids in a type (without probing — just structural walk). diff --git a/src/typechecker/error.rs b/src/typechecker/error.rs index 87e5c76c..a51c261b 100644 --- a/src/typechecker/error.rs +++ b/src/typechecker/error.rs @@ -1,8 +1,9 @@ use thiserror; -use crate::ast::span::Span; +use crate::cst::QualifiedIdent; use crate::interner; use crate::interner::Symbol; +use crate::span::Span; use crate::typechecker::types::{TyVarId, Type}; /// Type checking errors with source location information. @@ -27,6 +28,10 @@ pub enum TypeError { #[error("Unknown value {} at {span}", interner::resolve(*name).unwrap_or_default())] UndefinedVariable { span: Span, name: Symbol }, + /// Name not found in scope (used during AST name resolution, corresponds to PureScript's UnknownName) + #[error("Unknown name {} at {span}", interner::resolve(*name).unwrap_or_default())] + UnknownName { span: Span, name: Symbol }, + /// Type signature without a corresponding value declaration #[error("The type declaration for {} has no corresponding value declaration at {span}", interner::resolve(*name).unwrap_or_default())] OrphanTypeSignature { span: Span, name: Symbol }, @@ -49,13 +54,12 @@ pub enum TypeError { }, /// No instance found for a type class constraint - #[error("No type class instance was found for {class} {args} at {span}", - class = interner::resolve(*class_name).unwrap_or_default(), + #[error("No type class instance was found for {class_name} {args} at {span}", args = type_args.iter().map(|ty| format!("{}", ty)).collect::>().join(" ") )] NoInstanceFound { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, type_args: Vec, }, @@ -63,7 +67,7 @@ pub enum TypeError { #[error("A case expression could not be determined to cover all inputs. The following patterns are missing: {} at {span}", missing.join(", "))] NonExhaustivePattern { span: Span, - type_name: Symbol, + type_name: QualifiedIdent, missing: Vec, }, @@ -129,12 +133,18 @@ pub enum TypeError { }, /// Invalid newtype derived instance. E.g. derive instance Newtype NotANewtype - #[error("Cannot derive a Newtype instance for {}: it is not a newtype at {span}", interner::resolve(*name).unwrap_or_default())] - InvalidNewtypeInstance { span: Span, name: Symbol }, + #[error( + "Cannot derive a Newtype instance for {}: it is not a newtype at {span}", + name + )] + InvalidNewtypeInstance { span: Span, name: QualifiedIdent }, /// derive newtype instance on a type that isn't an instance of Newtype. E.g. derive newtype instance MyClass NotANewtype - #[error("Cannot use newtype deriving for {} because it does not have a Newtype instance at {span}", interner::resolve(*name).unwrap_or_default())] - InvalidNewtypeDerivation { span: Span, name: Symbol }, + #[error( + "Cannot use newtype deriving for {} because it does not have a Newtype instance at {span}", + name + )] + InvalidNewtypeDerivation { span: Span, name: QualifiedIdent }, /// A constructor argument is not valid for the derived class (e.g. contravariant position for Functor) #[error("Cannot derive instance: invalid constructor argument at {span}")] @@ -220,26 +230,24 @@ pub enum TypeError { others_in_cycle: Vec<(Symbol, Span)>, }, - #[error("The class {} is not defined at {span}", interner::resolve(*name).unwrap_or_default())] - UnknownClass { span: Span, name: Symbol }, + #[error("The class {name} is not defined at {span}")] + UnknownClass { span: Span, name: QualifiedIdent }, - #[error("The class {} is missing the following members at {span}: {}", - interner::resolve(*class_name).unwrap_or_default(), + #[error("The class {class_name} is missing the following members at {span}: {}", members.iter().map(|(n, ty)| format!("{} :: {}", interner::resolve(*n).unwrap_or_default(), ty)).collect::>().join(", ") )] MissingClassMember { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, members: Vec<(Symbol, Type)>, }, - #[error("The class {} has the following extraneous member at {span}: {}", - interner::resolve(*class_name).unwrap_or_default(), + #[error("The class {class_name} has the following extraneous member at {span}: {}", interner::resolve(*member_name).unwrap_or_default() )] ExtraneousClassMember { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, member_name: Symbol, }, /// Declaration conflict: a name is used for two different kinds of declarations @@ -266,47 +274,27 @@ pub enum TypeError { }, #[error("Cannot apply expression of type {type_} to a type argument at {span}")] - CannotApplyExpressionOfTypeOnType { - span: Span, - type_: Type, - }, + CannotApplyExpressionOfTypeOnType { span: Span, type_: Type }, #[error("An anonymous function argument _ appears in an invalid context at {span}")] - IncorrectAnonymousArgument { - span: Span, - }, + IncorrectAnonymousArgument { span: Span }, #[error("Operator {} cannot be used in a pattern as it is an alias for a function, not a data constructor, at {span}", interner::resolve(*op).unwrap_or_default() )] - InvalidOperatorInBinder { - span: Span, - op: Symbol, - }, + InvalidOperatorInBinder { span: Span, op: Symbol }, #[error("Integer value {value} is out of range at {span}. Acceptable values fall within the range -2147483648 to 2147483647 (inclusive).")] - IntOutOfRange { - span: Span, - value: i64, - }, + IntOutOfRange { span: Span, value: i64 }, #[error("The role declaration for {} should follow its definition at {span}", interner::resolve(*name).unwrap_or_default())] - OrphanRoleDeclaration { - span: Span, - name: Symbol, - }, + OrphanRoleDeclaration { span: Span, name: Symbol }, #[error("Duplicate role declaration for {} at {span}", interner::resolve(*name).unwrap_or_default())] - DuplicateRoleDeclaration { - span: Span, - name: Symbol, - }, + DuplicateRoleDeclaration { span: Span, name: Symbol }, #[error("Role declarations are only supported for data types, not for type synonyms nor type classes at {span}")] - UnsupportedRoleDeclaration { - span: Span, - name: Symbol, - }, + UnsupportedRoleDeclaration { span: Span, name: Symbol }, #[error("Role declaration for {} declares {found} roles, but the type has {expected} parameters at {span}", interner::resolve(*name).unwrap_or_default())] RoleDeclarationArityMismatch { @@ -324,157 +312,127 @@ pub enum TypeError { }, #[error("Non-associative operator {} used with another operator of the same precedence at {span}", interner::resolve(*op).unwrap_or_default())] - NonAssociativeError { - span: Span, - op: Symbol, - }, + NonAssociativeError { span: Span, op: Symbol }, #[error("Operators with mixed associativity at the same precedence at {span}")] - MixedAssociativityError { - span: Span, - }, + MixedAssociativityError { span: Span }, #[error("The name {} in a foreign import contains a prime character which is not allowed at {span}", interner::resolve(*name).unwrap_or_default())] - DeprecatedFFIPrime { - span: Span, - name: Symbol, - }, + DeprecatedFFIPrime { span: Span, name: Symbol }, #[error("Type wildcards are not allowed in type definitions at {span}")] - WildcardInTypeDefinition { - span: Span, - }, + WildcardInTypeDefinition { span: Span }, #[error("Constraints are not allowed in foreign import types at {span}")] - ConstraintInForeignImport { - span: Span, - }, + ConstraintInForeignImport { span: Span }, #[error("A forall or wildcard is not allowed in a constraint argument at {span}")] - InvalidConstraintArgument { - span: Span, - }, + InvalidConstraintArgument { span: Span }, + + /// Syntax error in type expression (corresponds to PureScript's ErrorParsingModule) + #[error("Syntax error at {span}")] + SyntaxError { span: Span }, - #[error("Kind mismatch: type synonym {} expects {} argument(s) but was given {} at {span}", - interner::resolve(*name).unwrap_or_default(), expected, found + /// Expected a wildcard type argument in a Newtype derive instance + #[error("Expected a wildcard (_) in the Newtype instance at {span}")] + ExpectedWildcard { span: Span, name: QualifiedIdent }, + + #[error( + "Kind mismatch: type synonym {} expects {} argument(s) but was given {} at {span}", + name, + expected, + found )] - KindsDoNotUnify { + KindArityMismatch { span: Span, - name: Symbol, + name: QualifiedIdent, expected: usize, found: usize, }, - #[error("The type class {} expects {} type argument(s), but the instance provided {} at {span}", - interner::resolve(*class_name).unwrap_or_default(), expected, found - )] + #[error("The type class {class_name} expects {expected} type argument(s), but the instance provided {found} at {span}")] ClassInstanceArityMismatch { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, expected: usize, found: usize, }, #[error("Type variable {} is undefined at {span}", interner::resolve(*name).unwrap_or_default())] - UndefinedTypeVariable { - span: Span, - name: Symbol, - }, + UndefinedTypeVariable { span: Span, name: Symbol }, #[error("Invalid type in instance head at {span}")] - InvalidInstanceHead { - span: Span, - }, + InvalidInstanceHead { span: Span }, - #[error("Type synonym {} is partially applied at {span}", interner::resolve(*name).unwrap_or_default())] - PartiallyAppliedSynonym { - span: Span, - name: Symbol, - }, + #[error("Type synonym {} is partially applied at {span}", name)] + PartiallyAppliedSynonym { span: Span, name: QualifiedIdent }, - #[error("A transitive export error occurred: {} depends on {} which is not exported at {span}", - interner::resolve(*exported).unwrap_or_default(), - interner::resolve(*dependency).unwrap_or_default() + #[error( + "A transitive export error occurred: {} depends on {} which is not exported at {span}", + exported, + dependency )] TransitiveExportError { span: Span, - exported: Symbol, - dependency: Symbol, + exported: QualifiedIdent, + dependency: QualifiedIdent, }, #[error("Scope conflict: the name {} is ambiguous, imported from multiple modules at {span}", interner::resolve(*name).unwrap_or_default() )] - ScopeConflict { - span: Span, - name: Symbol, - }, + ScopeConflict { span: Span, name: Symbol }, #[error("Export conflict: the name {} is exported by multiple re-exported modules at {span}", interner::resolve(*name).unwrap_or_default() )] - ExportConflict { - span: Span, - name: Symbol, - }, + ExportConflict { span: Span, name: Symbol }, - #[error("Overlapping instances found for {class} {args} at {span}", - class = interner::resolve(*class_name).unwrap_or_default(), + #[error("Overlapping instances found for {class_name} {args} at {span}", args = type_args.iter().map(|ty| format!("{}", ty)).collect::>().join(" ") )] OverlappingInstances { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, type_args: Vec, }, - #[error("An orphan instance was found for class {class} at {span}. Instances must be defined in the same module as the class or one of the types in the instance head.", - class = interner::resolve(*class_name).unwrap_or_default() - )] + #[error("An orphan instance was found for class {class_name} at {span}. Instances must be defined in the same module as the class or one of the types in the instance head." )] OrphanInstance { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, }, - #[error("Type class instance for {class} {args} is possibly infinite at {span}", - class = interner::resolve(*class_name).unwrap_or_default(), + #[error("Type class instance for {class_name} {args} is possibly infinite at {span}", args = type_args.iter().map(|ty| format!("{}", ty)).collect::>().join(" ") )] PossiblyInfiniteInstance { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, type_args: Vec, }, #[error("The type variable {name_str} is ambiguous at {span}", name_str = names.iter().map(|n| interner::resolve(*n).unwrap_or_default()).collect::>().join(", ") )] - AmbiguousTypeVariables { - span: Span, - names: Vec, - }, + AmbiguousTypeVariables { span: Span, names: Vec }, #[error("Invalid Coercible instance declaration at {span}")] - InvalidCoercibleInstanceDeclaration { - span: Span, - }, + InvalidCoercibleInstanceDeclaration { span: Span }, #[error("Role mismatch for type {} at {span}", interner::resolve(*name).unwrap_or_default())] - RoleMismatch { - span: Span, - name: Symbol, - }, + RoleMismatch { span: Span, name: Symbol }, #[error("Possibly infinite Coercible instance at {span}")] PossiblyInfiniteCoercibleInstance { span: Span, - class_name: Symbol, + class_name: QualifiedIdent, type_args: Vec, }, /// Kind unification failure: two kinds could not be unified #[error("Could not match kind {expected} with kind {found} at {span}")] - KindMismatch { + KindsDoNotUnify { span: Span, expected: Type, found: Type, @@ -482,42 +440,27 @@ pub enum TypeError { /// Expected a type of kind Type, but found a higher-kinded type #[error("Expected type of kind Type, but found kind {found} at {span}")] - ExpectedType { - span: Span, - found: Type, - }, + ExpectedType { span: Span, found: Type }, /// Constraint used in kind position (e.g., `foreign data Bad :: Ok => Type`) #[error("Unsupported type in kind at {span}")] - UnsupportedTypeInKind { - span: Span, - }, + UnsupportedTypeInKind { span: Span }, /// A rigid type variable (skolem) has escaped its scope #[error("A type variable has escaped its scope at {span}")] - EscapedSkolem { - span: Span, - name: Symbol, - ty: Type, - }, + EscapedSkolem { span: Span, name: Symbol, ty: Type }, /// Implicit kind quantification would be needed inside a user-written forall (type-level) - #[error("Cannot unambiguously generalize kinds at {span}")] - QuantificationCheckFailureInType { - span: Span, - }, + #[error("Cannot unambiguously generalize type kinds for {ty} at {span}")] + QuantificationCheckFailureInType { ty: Type, span: Span }, /// Implicit kind quantification would be needed inside a kind annotation - #[error("Cannot unambiguously generalize kinds at {span}")] - QuantificationCheckFailureInKind { - span: Span, - }, + #[error("Cannot unambiguously generalize kinds for {ty} at {span}")] + QuantificationCheckFailureInKind { ty: Type, span: Span }, /// Visible dependent quantification is not supported #[error("Visible dependent quantification is not supported at {span}")] - VisibleQuantificationCheckFailureInType { - span: Span, - }, + VisibleQuantificationCheckFailureInType { span: Span }, } impl TypeError { @@ -526,6 +469,7 @@ impl TypeError { TypeError::UnificationError { span, .. } | TypeError::InfiniteType { span, .. } | TypeError::UndefinedVariable { span, .. } + | TypeError::UnknownName { span, .. } | TypeError::NotImplemented { span, .. } | TypeError::OrphanTypeSignature { span, .. } | TypeError::DuplicateTypeSignature { span, .. } @@ -569,7 +513,9 @@ impl TypeError { | TypeError::WildcardInTypeDefinition { span, .. } | TypeError::ConstraintInForeignImport { span, .. } | TypeError::InvalidConstraintArgument { span, .. } - | TypeError::KindsDoNotUnify { span, .. } + | TypeError::SyntaxError { span, .. } + | TypeError::ExpectedWildcard { span, .. } + | TypeError::KindArityMismatch { span, .. } | TypeError::ClassInstanceArityMismatch { span, .. } | TypeError::UndefinedTypeVariable { span, .. } | TypeError::InvalidInstanceHead { span, .. } @@ -587,7 +533,7 @@ impl TypeError { | TypeError::InvalidCoercibleInstanceDeclaration { span, .. } | TypeError::RoleMismatch { span, .. } | TypeError::PossiblyInfiniteCoercibleInstance { span, .. } - | TypeError::KindMismatch { span, .. } + | TypeError::KindsDoNotUnify { span, .. } | TypeError::ExpectedType { span, .. } | TypeError::UnsupportedTypeInKind { span, .. } | TypeError::EscapedSkolem { span, .. } @@ -611,6 +557,7 @@ impl TypeError { TypeError::UnificationError { .. } => "UnificationError".into(), TypeError::InfiniteType { .. } => "InfiniteType".into(), TypeError::UndefinedVariable { .. } => "UndefinedVariable".into(), + TypeError::UnknownName { .. } => "UnknownName".into(), TypeError::OrphanTypeSignature { .. } => "OrphanTypeSignature".into(), TypeError::DuplicateTypeSignature { .. } => "DuplicateTypeSignature".into(), TypeError::HoleInferredType { .. } => "HoleInferredType".into(), @@ -628,7 +575,9 @@ impl TypeError { TypeError::DuplicateLabel { .. } => "DuplicateLabel".into(), TypeError::InvalidNewtypeInstance { .. } => "InvalidNewtypeInstance".into(), TypeError::InvalidNewtypeDerivation { .. } => "InvalidNewtypeDerivation".into(), - TypeError::CannotDeriveInvalidConstructorArg { .. } => "CannotDeriveInvalidConstructorArg".into(), + TypeError::CannotDeriveInvalidConstructorArg { .. } => { + "CannotDeriveInvalidConstructorArg".into() + } TypeError::DuplicateTypeClass { .. } => "DuplicateTypeClass".into(), TypeError::DuplicateInstance { .. } => "DuplicateInstance".into(), TypeError::DuplicateTypeArgument { .. } => "DuplicateTypeArgument".into(), @@ -649,7 +598,9 @@ impl TypeError { TypeError::UnknownClass { .. } => "UnknownClass".into(), TypeError::MissingClassMember { .. } => "MissingClassMember".into(), TypeError::ExtraneousClassMember { .. } => "ExtraneousClassMember".into(), - TypeError::CannotGeneralizeRecursiveFunction { .. } => "CannotGeneralizeRecursiveFunction".into(), + TypeError::CannotGeneralizeRecursiveFunction { .. } => { + "CannotGeneralizeRecursiveFunction".into() + } TypeError::IntOutOfRange { .. } => "IntOutOfRange".into(), TypeError::OrphanRoleDeclaration { .. } => "OrphanRoleDeclaration".into(), TypeError::DuplicateRoleDeclaration { .. } => "DuplicateRoleDeclaration".into(), @@ -660,10 +611,12 @@ impl TypeError { TypeError::MixedAssociativityError { .. } => "MixedAssociativityError".into(), TypeError::DeprecatedFFIPrime { .. } => "DeprecatedFFIPrime".into(), TypeError::DeclConflict { .. } => "DeclConflict".into(), - TypeError::WildcardInTypeDefinition { .. } => "SyntaxError".into(), - TypeError::ConstraintInForeignImport { .. } => "SyntaxError".into(), - TypeError::InvalidConstraintArgument { .. } => "SyntaxError".into(), - TypeError::KindsDoNotUnify { .. } => "KindsDoNotUnify".into(), + TypeError::WildcardInTypeDefinition { .. } => "WildcardInTypeDefinition".into(), + TypeError::ConstraintInForeignImport { .. } => "ConstraintInForeignImport".into(), + TypeError::InvalidConstraintArgument { .. } => "InvalidConstraintArgument".into(), + TypeError::SyntaxError { .. } => "SyntaxError".into(), + TypeError::ExpectedWildcard { .. } => "ExpectedWildcard".into(), + TypeError::KindArityMismatch { .. } => "KindArityMismatch".into(), TypeError::ClassInstanceArityMismatch { .. } => "ClassInstanceArityMismatch".into(), TypeError::UndefinedTypeVariable { .. } => "UndefinedTypeVariable".into(), TypeError::InvalidInstanceHead { .. } => "InvalidInstanceHead".into(), @@ -671,23 +624,35 @@ impl TypeError { TypeError::TransitiveExportError { .. } => "TransitiveExportError".into(), TypeError::ScopeConflict { .. } => "ScopeConflict".into(), TypeError::ExportConflict { .. } => "ExportConflict".into(), - TypeError::CannotApplyExpressionOfTypeOnType { .. } => "CannotApplyExpressionOfTypeOnType".into(), + TypeError::CannotApplyExpressionOfTypeOnType { .. } => { + "CannotApplyExpressionOfTypeOnType".into() + } TypeError::IncorrectAnonymousArgument { .. } => "IncorrectAnonymousArgument".into(), TypeError::InvalidOperatorInBinder { .. } => "InvalidOperatorInBinder".into(), TypeError::OverlappingInstances { .. } => "OverlappingInstances".into(), TypeError::OrphanInstance { .. } => "OrphanInstance".into(), TypeError::PossiblyInfiniteInstance { .. } => "PossiblyInfiniteInstance".into(), TypeError::AmbiguousTypeVariables { .. } => "AmbiguousTypeVariables".into(), - TypeError::InvalidCoercibleInstanceDeclaration { .. } => "InvalidCoercibleInstanceDeclaration".into(), + TypeError::InvalidCoercibleInstanceDeclaration { .. } => { + "InvalidCoercibleInstanceDeclaration".into() + } TypeError::RoleMismatch { .. } => "RoleMismatch".into(), - TypeError::PossiblyInfiniteCoercibleInstance { .. } => "PossiblyInfiniteCoercibleInstance".into(), - TypeError::KindMismatch { .. } => "KindsDoNotUnify".into(), + TypeError::PossiblyInfiniteCoercibleInstance { .. } => { + "PossiblyInfiniteCoercibleInstance".into() + } + TypeError::KindsDoNotUnify { .. } => "KindsDoNotUnify".into(), TypeError::ExpectedType { .. } => "ExpectedType".into(), TypeError::UnsupportedTypeInKind { .. } => "UnsupportedTypeInKind".into(), TypeError::EscapedSkolem { .. } => "EscapedSkolem".into(), - TypeError::QuantificationCheckFailureInType { .. } => "QuantificationCheckFailureInType".into(), - TypeError::QuantificationCheckFailureInKind { .. } => "QuantificationCheckFailureInKind".into(), - TypeError::VisibleQuantificationCheckFailureInType { .. } => "VisibleQuantificationCheckFailureInType".into(), + TypeError::QuantificationCheckFailureInType { .. } => { + "QuantificationCheckFailureInType".into() + } + TypeError::QuantificationCheckFailureInKind { .. } => { + "QuantificationCheckFailureInKind".into() + } + TypeError::VisibleQuantificationCheckFailureInType { .. } => { + "VisibleQuantificationCheckFailureInType".into() + } } } } diff --git a/src/typechecker/infer.rs b/src/typechecker/infer.rs index d54127af..6fc415da 100644 --- a/src/typechecker/infer.rs +++ b/src/typechecker/infer.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; -use crate::cst::{Associativity, Binder, Expr, GuardedExpr, GuardPattern, LetBinding, Literal}; +use crate::ast::{Binder, Expr, GuardPattern, GuardedExpr, LetBinding, Literal}; +use crate::cst::{Associativity, QualifiedIdent, unqualified_ident}; use crate::interner::Symbol; use crate::typechecker::convert::convert_type_expr; use crate::typechecker::env::Env; @@ -27,37 +28,39 @@ pub struct InferCtx { pub state: UnifyState, /// Map from method name → (class_name, class_type_vars). /// Set by check_module before typechecking value declarations. - pub class_methods: HashMap)>, + pub class_methods: HashMap)>, /// Deferred type class constraints: (span, class_name, [type_args as unif vars]). /// Checked after inference to verify instances exist. - pub deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub deferred_constraints: Vec<(crate::span::Span, QualifiedIdent, Vec)>, /// Map from type constructor name → list of data constructor names. /// Used for exhaustiveness checking of case expressions. - pub data_constructors: HashMap>, + pub data_constructors: HashMap>, /// Map from type-level operator symbol → target type constructor name. /// Populated from `infixr N type TypeName as op` declarations. - pub type_operators: HashMap, + pub type_operators: HashMap, /// Map from data constructor name → (parent type name, type var symbols, field types). /// Used for nested exhaustiveness checking to know each constructor's field types. - pub ctor_details: HashMap, Vec)>, - /// Set of known type constructor names in scope (Int, String, Maybe, etc.). - /// Used to validate TypeExpr::Constructor references during type conversion. - pub known_types: HashSet, + pub ctor_details: HashMap, Vec)>, /// Number of type parameters for each known type constructor. /// Used to detect over-applied types after type alias expansion. - pub type_con_arities: HashMap, + pub type_con_arities: HashMap, /// Type aliases whose body has kind `Type` (declared with `{ }` record syntax). /// Used to detect invalid row tails like `{ | Foo }` where `Foo = { x :: Number }`. - pub record_type_aliases: HashSet, + pub record_type_aliases: HashSet, /// Type aliases: name → (type_var_names, expanded_body). /// E.g. `type Fn1 a b = a -> b` → ("Fn1", ([a, b], Fun(Var(a), Var(b)))) - pub type_aliases: HashMap, Type)>, + pub type_aliases: HashMap, Type)>, + /// Qualified type alias names (e.g. "CJ.PropCodec") for disambiguation. + /// When convert_type_expr encounters a qualified type constructor that's in this set, + /// it uses the qualified symbol for Type::Con, allowing alias expansion to find the + /// correct module-specific alias body instead of the last-import-wins unqualified one. + pub qualified_type_alias_names: HashSet, /// Value-level operator fixities: operator_symbol → (associativity, precedence). /// Used for re-associating operator chains during inference. pub value_fixities: HashMap, /// Value-level operators that alias functions (NOT constructors). /// Used to detect invalid operator usage in binder patterns. - pub function_op_aliases: HashSet, + pub function_op_aliases: HashSet, /// Class methods whose declared type has extra constraints (e.g. `Applicative m =>`). /// These get implicit dictionary parameters, making them functions even with 0 explicit binders. /// Used to avoid false CycleInDeclaration errors for instance methods. @@ -69,44 +72,44 @@ pub struct InferCtx { pub scope_conflicts: HashSet, /// Map from operator → class method target name (e.g. `<>` → `append`). /// Used for tracking deferred constraints on operator usage. - pub operator_class_targets: HashMap, + pub operator_class_targets: HashMap, /// Deferred constraints from operator usage (e.g. `<>` → Semigroup constraint). /// Only used for CannotGeneralizeRecursiveFunction detection, NOT for instance /// resolution (the instance matcher can't handle complex nested types). - pub op_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub op_deferred_constraints: Vec<(crate::span::Span, QualifiedIdent, Vec)>, /// Map from class name → (type_vars, fundeps as (lhs_indices, rhs_indices)). /// Used for fundep-aware orphan instance checking. - pub class_fundeps: HashMap, Vec<(Vec, Vec)>)>, + pub class_fundeps: HashMap, Vec<(Vec, Vec)>)>, /// Whether the last infer_guarded found non-exhaustive pattern guards. /// Set during inference, consumed by check.rs to emit Partial constraint. pub has_non_exhaustive_pattern_guards: bool, /// Constraints from type signatures: function name → list of (class_name, type_args). /// When a function with signature constraints is called, these are instantiated /// with the forall substitution and added as deferred constraints. - pub signature_constraints: HashMap)>>, + pub signature_constraints: HashMap)>>, /// Deferred constraints from signature propagation (separate from main deferred_constraints). /// These are only checked for zero-instance classes, since our instance resolution /// may not handle complex imported instances correctly. - pub sig_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub sig_deferred_constraints: Vec<(crate::span::Span, QualifiedIdent, Vec)>, /// Classes with instance chains (else keyword). Used to route chained class constraints /// to deferred_constraints for proper chain ambiguity checking. - pub chained_classes: std::collections::HashSet, + pub chained_classes: std::collections::HashSet, /// Roles for each type constructor: type_name → [Role per type parameter]. /// Populated from role declarations and inferred from constructor fields. pub type_roles: HashMap>, /// Set of type names declared as newtypes (for Coercible solving). - pub newtype_names: HashSet, + pub newtype_names: HashSet, /// Type variables in scope from enclosing forall declarations (scoped type variables). /// Used to validate that where/let binding type signatures only reference bound vars. pub scoped_type_vars: HashSet, /// Class names whose constraints are "given" by the current enclosing instance. /// Constraints deferred for these classes within instance method bodies are skipped. - pub given_class_names: HashSet, + pub given_class_names: HashSet, /// Deferred types to kind-check after inference completes for each declaration. /// Collected from lambda inference and type annotations. Each entry is (type, span). /// These are zonked and kind-checked post-inference to catch kind errors like /// type-level literals used as function arguments (e.g., `"foo" -> String`). - pub deferred_kind_checks: Vec<(Type, crate::ast::span::Span)>, + pub deferred_kind_checks: Vec<(Type, crate::span::Span)>, /// Whether a lambda with refutable binders was inferred during the current declaration. /// Set during inference, consumed by check.rs to emit NoInstanceFound for Partial. /// Unlike has_non_exhaustive_pattern_guards, this is independent of the enclosing @@ -115,13 +118,22 @@ pub struct InferCtx { /// Functions whose type signature has Partial in a function parameter position, /// e.g. `unsafePartial :: (Partial => a) -> a`. When applied to a partial expression, /// these functions discharge the Partial constraint. - pub partial_dischargers: HashSet, + pub partial_dischargers: HashSet, /// Map from class parameter unif var ID → application arguments in the method type. /// When a class method like `imap :: (a -> b) -> f x y a -> f x y b` is used, /// the class parameter `f` is applied to arguments `[x, y, a]`. We store the /// fresh unif vars for these args so that at constraint resolution time we can /// check kind consistency between the class kind signature and the concrete types. pub class_param_app_args: HashMap>, + /// Canonical class method type schemes, indexed by method name symbol. + /// Unlike the env, these are NEVER overwritten by value imports. + /// Used when computing instance method expected types so that an explicit + /// `import Data.Array (foldl)` does not shadow the `Foldable.foldl` scheme + /// that the instance checker needs. + pub class_method_schemes: HashMap, + /// Non-exhaustive pattern errors collected during case expression inference. + /// Consumed by check.rs to emit NonExhaustivePattern errors. + pub non_exhaustive_errors: Vec, } impl InferCtx { @@ -133,10 +145,10 @@ impl InferCtx { data_constructors: HashMap::new(), type_operators: HashMap::new(), ctor_details: HashMap::new(), - known_types: HashSet::new(), type_con_arities: HashMap::new(), record_type_aliases: HashSet::new(), type_aliases: HashMap::new(), + qualified_type_alias_names: HashSet::new(), value_fixities: HashMap::new(), function_op_aliases: HashSet::new(), constrained_class_methods: HashSet::new(), @@ -157,9 +169,12 @@ impl InferCtx { has_partial_lambda: false, partial_dischargers: HashSet::new(), class_param_app_args: HashMap::new(), + class_method_schemes: HashMap::new(), + non_exhaustive_errors: Vec::new(), } } + /// Create a qualified symbol by combining a module alias with a name. fn qualified_symbol(module: Symbol, name: Symbol) -> Symbol { let mod_str = crate::interner::resolve(module).unwrap_or_default(); @@ -230,8 +245,8 @@ impl InferCtx { } self.infer_literal(*span, lit) } - Expr::Var { span, name } => self.infer_var(env, *span, name), - Expr::Constructor { span, name } => self.infer_var(env, *span, name), + Expr::Var { span, name, .. } => self.infer_var(env, *span, name), + Expr::Constructor { span, name, .. } => self.infer_var(env, *span, name), Expr::Lambda { span, binders, body } => { self.infer_lambda(env, *span, binders, body) } @@ -250,24 +265,20 @@ impl InferCtx { Expr::TypeAnnotation { span, expr, ty } => { self.infer_annotation(env, *span, expr, ty) } - Expr::Parens { expr, .. } => { - // Underscore sections are only valid inside parentheses: - // `(_ + 1)` or `(f _)` → desugar to lambda - if self.has_direct_underscore_hole(expr) { - return self.infer_underscore_section(env, expr); - } - self.infer(env, expr) - } Expr::Negate { span, expr } => self.infer_negate(env, *span, expr), - Expr::Op { span, left, op, right } => self.infer_op(env, *span, left, op, right), - Expr::OpParens { span, op } => self.infer_op_parens(env, *span, op), Expr::Case { span, exprs, alts } => self.infer_case(env, *span, exprs, alts), Expr::Array { span, elements } => self.infer_array(env, *span, elements), Expr::Hole { span, name } => self.infer_hole(*span, *name), Expr::Record { span, fields } => self.infer_record(env, *span, fields), Expr::RecordAccess { span, expr, field } => self.infer_record_access(env, *span, expr, field), Expr::RecordUpdate { span, expr, updates } => self.infer_record_update(env, *span, expr, updates), - Expr::Do { span, statements, .. } => self.infer_do(env, *span, statements), + Expr::Do { span, module, statements } => { + if let Some(m) = module { + self.infer_qualified_do(env, *span, *m, statements) + } else { + self.infer_do(env, *span, statements) + } + } Expr::Ado { span, statements, result, .. } => self.infer_ado(env, *span, statements, result), Expr::VisibleTypeApp { span, func, ty } => self.infer_visible_type_app(env, *span, func, ty), other => Err(TypeError::NotImplemented { @@ -279,7 +290,7 @@ impl InferCtx { fn infer_literal( &mut self, - _span: crate::ast::span::Span, + _span: crate::span::Span, lit: &Literal, ) -> Result { Ok(match lit { @@ -304,7 +315,7 @@ impl InferCtx { fn infer_var( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, name: &crate::cst::QualifiedIdent, ) -> Result { // Check for scope conflicts (name imported from multiple modules) @@ -320,18 +331,19 @@ impl InferCtx { }); } - // For qualified names (e.g. OM.foo), construct qualified symbol and look up - let lookup_result = if let Some(_module) = name.module { - env.lookup(resolved_name) - } else { - env.lookup(name.name) - }; + let lookup_result = env.lookup(resolved_name); match lookup_result { Some(scheme) => { let ty = self.instantiate(scheme); - // If this is a class method, capture the constraint during instantiation - if let Some((class_name, class_tvs)) = self.class_methods.get(&name.name).cloned() { + // If this is a class method (or an operator aliasing one), capture the constraint. + // Operators like `<>` map to class methods like `append` via operator_class_targets. + let class_method_lookup = self.class_methods.get(name).cloned() + .or_else(|| { + self.operator_class_targets.get(name) + .and_then(|target| self.class_methods.get(target).cloned()) + }); + if let Some((class_name, class_tvs)) = class_method_lookup { if let Type::Forall(vars, body) = &ty { // Verify that the outer Forall vars match the class type vars. // This avoids mishandling when a non-class value with the same name @@ -425,7 +437,7 @@ impl InferCtx { // If the scheme's type is a Forall, also instantiate that // and propagate any signature constraints - let lookup_name = name.name; + let lookup_name = *name; match ty { Type::Forall(vars, body) => { let subst: HashMap = vars @@ -440,11 +452,9 @@ impl InferCtx { .iter() .map(|a| self.apply_symbol_subst(&subst, a)) .collect(); - let class_str = crate::interner::resolve(*class_name).unwrap_or_default(); + let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); let has_solver = matches!(class_str.as_str(), - "Lacks" | "IsSymbol" | "Append" | "Reflectable" - | "ToString" | "Add" | "Mul" | "Compare" - | "Coercible" | "Nub" + "Lacks" | "Append" | "ToString" | "Add" | "Mul" | "Compare" | "Coercible" | "Nub" ); if has_solver { self.deferred_constraints.push((span, *class_name, subst_args)); @@ -545,14 +555,18 @@ impl InferCtx { } // Capture-avoiding: check if any forall-bound var name appears // free in the substitution values. If so, alpha-rename to avoid capture. + // Also conservatively rename when substitution values contain unification + // variables, since those may later be solved to types containing the + // forall-bound var names, causing capture. let mut new_vars = vars.clone(); - let needs_rename = new_vars.iter().any(|(v, _)| { + let any_subst_has_unif = inner_subst.values().any(|val| super::unify::contains_unif_var(val)); + let needs_rename = any_subst_has_unif || new_vars.iter().any(|(v, _)| { inner_subst.values().any(|val| super::unify::type_has_free_var(val, *v)) }); if needs_rename { let mut rename: HashMap = HashMap::new(); for (v, _) in &mut new_vars { - if inner_subst.values().any(|val| super::unify::type_has_free_var(val, *v)) { + if any_subst_has_unif || inner_subst.values().any(|val| super::unify::type_has_free_var(val, *v)) { let fresh = super::unify::fresh_type_var_symbol(*v); rename.insert(*v, Type::Var(fresh)); *v = fresh; @@ -576,7 +590,7 @@ impl InferCtx { fn infer_lambda( &mut self, env: &Env, - _span: crate::ast::span::Span, + _span: crate::span::Span, binders: &[Binder], body: &Expr, ) -> Result { @@ -605,9 +619,12 @@ impl InferCtx { // Record binders with refutable sub-binders (e.g., { sound: Moo }) // are not caught by check_exhaustiveness (which checks top-level // constructors). Detect these by checking if the binder is a record - // with refutable field sub-binders. + // with refutable field sub-binders that are truly refutable (not + // single-constructor types like newtypes). if let Binder::Record { fields, .. } = binder { - if fields.iter().any(|f| f.binder.as_ref().map_or(false, |b| is_refutable(b))) { + if fields.iter().any(|f| f.binder.as_ref().map_or(false, |b| { + is_truly_refutable(b, &self.data_constructors) + })) { self.has_partial_lambda = true; break; } @@ -682,7 +699,7 @@ impl InferCtx { fn infer_app( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, func: &Expr, arg: &Expr, ) -> Result { @@ -692,10 +709,10 @@ impl InferCtx { // `f x { a = 1 }` means `f (x { a = 1 })`, not `(f x) { a = 1 }`. if let Expr::Record { fields, .. } = arg { if !fields.is_empty() && fields.iter().all(|f| f.is_update && f.value.is_some()) { - let updates: Vec = fields + let updates: Vec = fields .iter() .filter_map(|f| { - Some(crate::cst::RecordUpdate { + Some(crate::ast::RecordUpdate { span: f.span, label: f.label.clone(), value: f.value.clone()?, @@ -746,12 +763,17 @@ impl InferCtx { // around the argument inference so partial lambdas in the argument are OK. let discharges_partial = match func { Expr::Var { name, .. } => { - let sym = if let Some(module) = name.module { - Self::qualified_symbol(module, name.name) + self.partial_dischargers.contains(name) + } + // Handle `unsafePartial $ expr` pattern: the `$` operator desugars to + // `App(Var("$"), Var("unsafePartial"))`, so the discharger appears as the + // arg of an inner App (e.g., `apply unsafePartial`). + Expr::App { arg: inner_arg, .. } => { + if let Expr::Var { name, .. } = inner_arg.as_ref() { + self.partial_dischargers.contains(name) } else { - name.name - }; - self.partial_dischargers.contains(&sym) + false + } } _ => false, }; @@ -830,10 +852,36 @@ impl InferCtx { // Post-check 2: verify no forall var leaked into the result type. // Catches escapes like `ST.run (STRef.new 0)` where the result // type ?A gets solved to `STRef ?R Int` containing the forall var ?R. + // + // However, we must avoid false positives when a forall var appears + // in the result only through "constructor vars" — unif vars that + // are the head of an App spine where the forall var is an argument. + // E.g. in `(forall a. ?f a -> ?g a) -> ...`, `?f` and `?g` are + // constructor vars. Their solutions may contain the forall var at + // the monomorphic level, but this doesn't represent a real escape. + let ctor_vars = collect_constructor_vars(&instantiated_param, &forall_var_set); + let mut excused_roots: HashSet = HashSet::new(); + for &ctor_var in &ctor_vars { + let ctor_solution = self.state.zonk(Type::Unif(ctor_var)); + if matches!(&ctor_solution, Type::Unif(_)) { + continue; + } + let ctor_free = self.state.free_unif_vars(&ctor_solution); + for &(_, fv) in &forall_unif_vars { + let fv_root = self.state.find_root(fv); + if ctor_free.iter().any(|v| *v == fv_root) { + excused_roots.insert(fv_root); + } + } + } + let result_zonked = self.state.zonk(result.as_ref().clone()); let result_free = self.state.free_unif_vars(&result_zonked); for &(sym, fv) in &forall_unif_vars { let fv_root = self.state.find_root(fv); + if excused_roots.contains(&fv_root) { + continue; // Forall var appears through a constructor var — not a real escape + } if result_free.iter().any(|v| *v == fv_root) { return Err(TypeError::EscapedSkolem { span, @@ -857,7 +905,7 @@ impl InferCtx { fn infer_if( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, cond: &Expr, then_expr: &Expr, else_expr: &Expr, @@ -882,7 +930,7 @@ impl InferCtx { fn infer_let( &mut self, env: &Env, - _span: crate::ast::span::Span, + _span: crate::span::Span, bindings: &[LetBinding], body: &Expr, ) -> Result { @@ -900,6 +948,7 @@ impl InferCtx { ) -> Result<(), TypeError> { // First pass: collect local type signatures let mut local_sigs: HashMap = HashMap::new(); + let mut local_partial_names: std::collections::HashSet = std::collections::HashSet::new(); for binding in bindings { if let LetBinding::Signature { name, ty, .. } = binding { // Check for undefined type variables (scoped type vars from enclosing forall are OK) @@ -908,12 +957,21 @@ impl InferCtx { if let Some(err) = undef_errors.into_iter().next() { return Err(err); } - let converted = convert_type_expr(ty, &self.type_operators, &self.known_types)?; + let converted = convert_type_expr(ty, &self.type_operators)?; let converted = self.instantiate_wildcards(&converted); + // Expand type aliases so local aliases like `type Builder x y = RB.Builder (Record x) (Record y)` + // are resolved before unification. Without this, self-referential aliases remain + // unexpanded in signatures, causing mismatches when the inferred types use the + // expanded (newtype) form. + let converted = crate::typechecker::check::expand_type_aliases_limited(&converted, &self.state.type_aliases, 0); local_sigs.insert(name.value, converted); - let sig_constraints = crate::typechecker::check::extract_type_signature_constraints(ty, &self.type_operators, &self.known_types); + let sig_constraints = crate::typechecker::check::extract_type_signature_constraints(ty, &self.type_operators); if !sig_constraints.is_empty() { - self.signature_constraints.insert(name.value, sig_constraints); + self.signature_constraints.insert(QualifiedIdent { module: None, name: name.value }, sig_constraints); + } + // Track let bindings with Partial constraint (intentionally non-exhaustive) + if crate::typechecker::check::has_partial_constraint(ty) { + local_partial_names.insert(name.value); } } } @@ -921,28 +979,37 @@ impl InferCtx { // Check for overlapping names in let bindings. // Multi-equation function definitions (same name, lambda exprs) are allowed // only if they are adjacent (not separated by other bindings). - let mut seen_let_names: HashMap> = HashMap::new(); + // (span, is_func, is_guarded_case) per binding + let mut seen_let_names: HashMap> = HashMap::new(); // Track binding order for adjacency check: (name, index) for each value binding let mut binding_order: Vec = Vec::new(); for binding in bindings { if let LetBinding::Value { span, binder, expr } = binding { if let Binder::Var { name, .. } = binder { let is_func = matches!(expr, Expr::Lambda { .. }); - seen_let_names.entry(name.value).or_default().push((*span, is_func)); + // Guarded value bindings are desugared to case on `true` at parse time + let is_guarded = matches!(expr, Expr::Case { exprs, .. } + if exprs.len() == 1 && matches!(&exprs[0], Expr::Literal { lit: Literal::Boolean(true), .. })); + seen_let_names.entry(name.value).or_default().push((*span, is_func, is_guarded)); binding_order.push(name.value); } } } for (name, entries) in &seen_let_names { if entries.len() > 1 { - let all_funcs = entries.iter().all(|(_, is_func)| *is_func); - if !all_funcs { + let all_funcs = entries.iter().all(|(_, is_func, _)| *is_func); + // Multi-equation non-function bindings are only allowed for guarded + // value definitions (e.g., `i' | cond = val; i' = fallback`). + // At least one equation must be a guarded case. + let all_non_funcs = entries.iter().all(|(_, is_func, _)| !*is_func); + let has_guarded = entries.iter().any(|(_, _, is_guarded)| *is_guarded); + if !all_funcs && !(all_non_funcs && has_guarded) { return Err(TypeError::OverlappingNamesInLet { - spans: entries.iter().map(|(s, _)| *s).collect(), + spans: entries.iter().map(|(s, _, _)| *s).collect(), name: *name, }); } - // All are functions — check they're adjacent in binding order + // All are functions (or guarded values) — check they're adjacent in binding order let indices: Vec = binding_order.iter().enumerate() .filter(|(_, n)| **n == *name) .map(|(i, _)| i) @@ -950,7 +1017,7 @@ impl InferCtx { let is_adjacent = indices.windows(2).all(|w| w[1] == w[0] + 1); if !is_adjacent { return Err(TypeError::OverlappingNamesInLet { - spans: entries.iter().map(|(s, _)| *s).collect(), + spans: entries.iter().map(|(s, _, _)| *s).collect(), name: *name, }); } @@ -1010,12 +1077,48 @@ impl InferCtx { } } + // Phase 2.5: Eagerly process trivially independent bindings — those whose RHS + // is a single variable reference that's not another let binding. This covers the + // common pattern `goTsName = identity` where polymorphic generalization is needed + // before other bindings use the name at different types. + let all_binding_names: std::collections::HashSet = seen_let_names.keys().cloned().collect(); + let mut eagerly_processed: std::collections::HashSet = std::collections::HashSet::new(); + for binding in bindings { + if let LetBinding::Value { span, binder: Binder::Var { name, .. }, expr } = binding { + if eagerly_processed.contains(&name.value) { continue; } + // Only for bindings without local sigs (sigs are already proper schemes) + if local_sigs.contains_key(&name.value) { continue; } + // Skip multi-equation bindings + if seen_let_names.get(&name.value).map_or(false, |e| e.len() > 1) { continue; } + // Only handle trivial cases: RHS is a single variable not in this let block + let is_trivial_independent = match expr { + Expr::Var { name: var_name, .. } => { + var_name.module.is_some() || !all_binding_names.contains(&var_name.name) + } + _ => false, + }; + if !is_trivial_independent { continue; } + // Trivially independent binding — infer, generalize, insert scheme + let binding_ty = self.infer(env, expr)?; + if let Some(self_ty) = pre_inserted.get(&name.value) { + self.state.unify(*span, self_ty, &binding_ty)?; + } + let scheme = env.generalize_local_batch(&mut self.state, binding_ty, &all_binding_names); + env.insert_scheme(name.value, scheme); + eagerly_processed.insert(name.value); + } + } + // Third pass: infer value bindings (all bindings stay monomorphic) let mut pending_generalizations: Vec<(Symbol, Type)> = Vec::new(); for binding in bindings { match binding { LetBinding::Value { span, binder, expr } => match binder { Binder::Var { name, .. } => { + // Skip bindings already processed in Phase 2.5 + if eagerly_processed.contains(&name.value) { + continue; + } // For multi-equation functions, subsequent equations // still need to be type-checked (to detect type errors) // but shouldn't re-register the scheme. @@ -1026,7 +1129,9 @@ impl InferCtx { } // Save partial lambda flag: multi-equation functions have // individually partial patterns but are collectively exhaustive. - let saved_partial_lambda = if is_multi_eq { + // Also suppress for bindings with Partial constraint in their signature. + let has_partial_sig = local_partial_names.contains(&name.value); + let saved_partial_lambda = if is_multi_eq || has_partial_sig { let saved = self.has_partial_lambda; self.has_partial_lambda = false; Some(saved) @@ -1104,10 +1209,14 @@ impl InferCtx { } // Fourth pass: generalize all inferred bindings after all are checked. - // This ensures bindings in the same where/let block share monomorphic types, - // preventing over-generalization of polykinded types. + // Collect all names being generalized so we exclude the entire batch + // from environment free vars — otherwise co-defined bindings' pre-inserted + // unif vars prevent proper polymorphic generalization (e.g., `goTsName = identity` + // used at both String and TsName in DTS.Types). + let batch_names: std::collections::HashSet = pending_generalizations.iter() + .map(|(name, _)| *name).collect(); for (name, binding_ty) in pending_generalizations { - let scheme = env.generalize_local(&mut self.state, binding_ty, name); + let scheme = env.generalize_local_batch(&mut self.state, binding_ty, &batch_names); env.insert_scheme(name, scheme); } Ok(()) @@ -1116,12 +1225,12 @@ impl InferCtx { fn infer_annotation( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, expr: &Expr, - ty_expr: &crate::cst::TypeExpr, + ty_expr: &crate::ast::TypeExpr, ) -> Result { let inferred = self.infer(env, expr)?; - let annotated = convert_type_expr(ty_expr, &self.type_operators, &self.known_types)?; + let annotated = convert_type_expr(ty_expr, &self.type_operators)?; // Replace wildcard type variables (_) with fresh unification variables let annotated = self.instantiate_wildcards(&annotated); // Extract annotation constraints for deferred checking (e.g., Fail (Text "...")) @@ -1133,10 +1242,10 @@ impl InferCtx { /// Extract constraints from an inline type annotation and add to deferred constraints. fn extract_inline_annotation_constraints( &mut self, - ty: &crate::cst::TypeExpr, - span: crate::ast::span::Span, + ty: &crate::ast::TypeExpr, + span: crate::span::Span, ) { - use crate::cst::TypeExpr; + use crate::ast::TypeExpr; match ty { TypeExpr::Constrained { constraints, ty, .. } => { for constraint in constraints { @@ -1147,18 +1256,18 @@ impl InferCtx { let mut args = Vec::new(); let mut ok = true; for arg in &constraint.args { - match convert_type_expr(arg, &self.type_operators, &self.known_types) { + match convert_type_expr(arg, &self.type_operators) { Ok(converted) => args.push(converted), Err(_) => { ok = false; break; } } } if ok { - self.deferred_constraints.push((constraint.span, constraint.class.name, args)); + self.deferred_constraints.push((constraint.span, constraint.class, args)); } } self.extract_inline_annotation_constraints(ty, span); } - TypeExpr::Forall { ty, .. } | TypeExpr::Parens { ty, .. } => { + TypeExpr::Forall { ty, .. } => { self.extract_inline_annotation_constraints(ty, span); } _ => {} @@ -1185,12 +1294,12 @@ impl InferCtx { fn infer_visible_type_app( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, func: &Expr, - ty_expr: &crate::cst::TypeExpr, + ty_expr: &crate::ast::TypeExpr, ) -> Result { // Flatten chained VTAs: f @A @B @C → base=f, args=[A, B, C] - let mut vta_args: Vec<&crate::cst::TypeExpr> = vec![ty_expr]; + let mut vta_args: Vec<&crate::ast::TypeExpr> = vec![ty_expr]; let mut base: &Expr = func; while let Expr::VisibleTypeApp { func: inner, ty: inner_ty, .. } = base { vta_args.push(inner_ty); @@ -1202,7 +1311,7 @@ impl InferCtx { // Check if the base expression is a class method let class_info = if let Expr::Var { name, .. } = base { - self.class_methods.get(&name.name).cloned() + self.class_methods.get(name).cloned() } else { None }; @@ -1211,7 +1320,7 @@ impl InferCtx { // Process all VTA args sequentially let mut ty = func_ty; for (arg_idx, arg_ty_expr) in vta_args.iter().enumerate() { - let applied_ty = convert_type_expr(arg_ty_expr, &self.type_operators, &self.known_types)?; + let applied_ty = convert_type_expr(arg_ty_expr, &self.type_operators)?; let applied_ty = self.instantiate_wildcards(&applied_ty); let is_last = arg_idx == vta_args.len() - 1; @@ -1264,16 +1373,25 @@ impl InferCtx { } } - // After all VTA args processed, defer class constraint if applicable - if let Some((class_name, ref class_tvs)) = class_info { - // Instantiate any remaining forall vars - if let Type::Forall(ref vars, _) = ty { - for &(v, _) in vars.iter() { - if !var_subst.contains_key(&v) { - var_subst.insert(v, Type::Unif(self.state.fresh_var())); - } + // After all VTA args processed, instantiate any remaining invisible forall vars. + // This applies to both class methods and regular functions — without it, + // remaining invisible foralls leak into the result type (e.g., `forall ty. String`). + if let Type::Forall(ref vars, ref body) = ty.clone() { + let mut extra_subst: HashMap = HashMap::new(); + for &(v, _) in vars.iter() { + if !var_subst.contains_key(&v) { + let fresh = Type::Unif(self.state.fresh_var()); + var_subst.insert(v, fresh.clone()); + extra_subst.insert(v, fresh); } } + if !extra_subst.is_empty() { + ty = self.apply_symbol_subst(&extra_subst, body); + } + } + + // Defer class constraint if applicable + if let Some((class_name, ref class_tvs)) = class_info { let constraint_types: Vec = class_tvs.iter() .map(|tv| var_subst.get(tv).cloned() .unwrap_or_else(|| Type::Unif(self.state.fresh_var()))) @@ -1328,7 +1446,9 @@ impl InferCtx { } } - self.deferred_constraints.push((span, class_name, constraint_types)); + if !self.given_class_names.contains(&class_name) { + self.deferred_constraints.push((span, class_name, constraint_types)); + } } Ok(ty) @@ -1338,8 +1458,13 @@ impl InferCtx { /// Unlike `infer`, this does NOT instantiate Forall types — VTA peels them explicitly. fn infer_preserving_forall(&mut self, env: &Env, expr: &Expr) -> Result { match expr { - Expr::Var { span, name } | Expr::Constructor { span, name } => { - match env.lookup(name.name) { + Expr::Var { span, name, .. } | Expr::Constructor { span, name, .. } => { + let resolved_name = if let Some(module) = name.module { + Self::qualified_symbol(module, name.name) + } else { + name.name + }; + match env.lookup(resolved_name) { Some(scheme) => Ok(self.scheme_to_forall(scheme)), None => Err(TypeError::UndefinedVariable { span: *span, name: name.name }), } @@ -1347,12 +1472,6 @@ impl InferCtx { Expr::VisibleTypeApp { span, func, ty } => { self.infer_visible_type_app(env, *span, func, ty) } - Expr::Parens { expr, .. } => { - if self.has_direct_underscore_hole(expr) { - return self.infer_underscore_section(env, expr); - } - self.infer_preserving_forall(env, expr) - } other => self.infer(env, other), } } @@ -1372,7 +1491,7 @@ impl InferCtx { fn infer_negate( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, expr: &Expr, ) -> Result { // Check that `negate` is in scope (module mode only) @@ -1395,214 +1514,45 @@ impl InferCtx { return Ok(Type::int()); } // PureScript negate uses Ring class: negate :: Ring a => a -> a - // Only Int and Number have Ring instances. let ty = self.infer(env, expr)?; let zonked = self.state.zonk(ty.clone()); - // If the type is a concrete type constructor, check it's Int or Number + // If the type is a concrete type constructor, check immediately for types known + // to NOT have Ring instances. Otherwise, defer the constraint to Pass 3 so that + // imported Ring instances (e.g., Ring BigInt) are checked properly. if let Type::Con(name) = &zonked { - let name_str = crate::interner::resolve(*name).unwrap_or_default(); - if name_str != "Int" && name_str != "Number" { - return Err(TypeError::NoInstanceFound { - span, - class_name: crate::interner::intern("Ring"), - type_args: vec![zonked], - }); - } - } - Ok(ty) - } - - fn infer_op( - &mut self, - env: &Env, - span: crate::ast::span::Span, - left: &Expr, - op: &crate::cst::Spanned, - right: &Expr, - ) -> Result { - // Flatten the right-associative operator chain into a list of operands and operators. - // The grammar parses `a + b * c` as `Op(a, +, Op(b, *, c))` (right-associative). - // We flatten to [a, b, c] with operators [+, *] then re-associate using fixity. - let mut operands: Vec<&Expr> = vec![left]; - let mut operators: Vec<&crate::cst::Spanned> = vec![op]; - let mut current = right; - while let Expr::Op { left: rl, op: rop, right: rr, .. } = current { - operands.push(rl.as_ref()); - operators.push(rop); - current = rr.as_ref(); - } - // Handle trailing type annotation: `a <<< b :: T` is parsed as - // `Op(a, <<<, TypeAnnotation(b, T))` but should be `(a <<< b) :: T`. - // Extract the annotation and wrap the final result after reassociation. - let trailing_annotation = if let Expr::TypeAnnotation { expr, ty, .. } = current { - current = expr.as_ref(); - Some(ty) - } else { - None - }; - operands.push(current); - - // Reject `_` (anonymous argument) in operator expressions that aren't - // inside parentheses. Operator sections like `(_ + 1)` are valid only - // inside Parens and are handled by the Parens case of infer. - let is_underscore = |e: &Expr| matches!(e, Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_"); - for operand in &operands { - if is_underscore(operand) { - return Err(TypeError::IncorrectAnonymousArgument { span: operand.span() }); - } - } - - // If only one operator, do simple binary inference (common case, fast path) - if operators.len() == 1 { - return self.infer_op_binary(env, span, operands[0], operators[0], operands[1]); - } - - // Detect underscore holes for operator sections in chains - let first_is_hole = Self::is_underscore_hole(operands[0]); - let last_is_hole = Self::is_underscore_hole(operands[operands.len() - 1]); - - // Infer all operand types (holes get fresh type vars) - let operand_types: Vec = operands - .iter() - .map(|e| { - if Self::is_underscore_hole(e) { - Ok(Type::Unif(self.state.fresh_var())) - } else { - self.infer(env, e) - } - }) - .collect::>()?; - - // Save hole types for wrapping later - let first_hole_ty = if first_is_hole { Some(operand_types[0].clone()) } else { None }; - let last_hole_ty = if last_is_hole { Some(operand_types[operand_types.len() - 1].clone()) } else { None }; - - // Look up and instantiate all operator types - let mut op_types: Vec = Vec::new(); - for op in &operators { - let op_lookup = if let Some(module) = op.value.module { - let qual_sym = Self::qualified_symbol(module, op.value.name); - env.lookup(qual_sym) - } else { - env.lookup(op.value.name) - }; - let op_ty = match op_lookup { - Some(scheme) => { - let ty = self.instantiate(scheme); - self.instantiate_forall_type(ty)? - } - None => { - return Err(TypeError::UndefinedVariable { - span: op.span, - name: op.value.name, + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + match name_str.as_ref() { + "Int" | "Number" => { + // Known Ring types — no constraint needed + } + "Boolean" | "String" | "Char" | "Array" => { + // Known non-Ring types — error immediately + return Err(TypeError::NoInstanceFound { + span, + class_name: unqualified_ident("Ring"), + type_args: vec![zonked], }); } - }; - op_types.push(op_ty); - } - - // Shunting-yard algorithm: re-associate based on operator precedence - let mut output: Vec = Vec::new(); - let mut op_stack: Vec = Vec::new(); // indices into operators/op_types - - output.push(operand_types[0].clone()); - - for i in 0..operators.len() { - let (assoc_i, prec_i) = self.get_fixity(operators[i].value.name); - - while let Some(&top_idx) = op_stack.last() { - let (assoc_top, prec_top) = self.get_fixity(operators[top_idx].value.name); - if prec_top == prec_i { - // Same precedence: check for mixed associativity before non-associative - if assoc_i != assoc_top { - return Err(TypeError::MixedAssociativityError { - span: operators[i].span, - }); - } - if assoc_i == Associativity::None { - return Err(TypeError::NonAssociativeError { - span: operators[i].span, - op: operators[i].value.name, - }); - } - } - let should_pop = prec_top > prec_i - || (prec_top == prec_i && assoc_i == Associativity::Left); - if should_pop { - op_stack.pop(); - let right_ty = output.pop().unwrap(); - let left_ty = output.pop().unwrap(); - let result = self.apply_binop(span, &op_types[top_idx], left_ty, right_ty)?; - output.push(result); - } else { - break; + _ => { + // Unknown type constructor — defer to Pass 3 + self.deferred_constraints.push(( + span, + unqualified_ident("Ring"), + vec![ty.clone()], + )); } } - - op_stack.push(i); - output.push(operand_types[i + 1].clone()); - } - - // Pop remaining operators - while let Some(top_idx) = op_stack.pop() { - let right_ty = output.pop().unwrap(); - let left_ty = output.pop().unwrap(); - let result = self.apply_binop(span, &op_types[top_idx], left_ty, right_ty)?; - output.push(result); - } - - let mut result = output.pop().unwrap(); - - // Wrap in function types for operator sections - if let Some(hole_ty) = last_hole_ty { - result = Type::fun(hole_ty, result); - } - if let Some(hole_ty) = first_hole_ty { - result = Type::fun(hole_ty, result); - } - - // Apply trailing type annotation: `a <<< b :: T` → check result against T - if let Some(ty_expr) = trailing_annotation { - let annotated = convert_type_expr(ty_expr, &self.type_operators, &self.known_types)?; - let annotated = self.instantiate_wildcards(&annotated); - self.extract_inline_annotation_constraints(ty_expr, span); - self.state.unify(span, &result, &annotated)?; - result = annotated; + } else { + // Non-Con types (Unif, Var, App, etc.) — defer + self.deferred_constraints.push(( + span, + unqualified_ident("Ring"), + vec![ty.clone()], + )); } - - Ok(result) - } - - /// Get the fixity (associativity, precedence) for an operator. - /// Defaults to infixl 9 for operators without declared fixity. - fn get_fixity(&self, op_name: Symbol) -> (Associativity, u8) { - self.value_fixities - .get(&op_name) - .copied() - .unwrap_or((Associativity::Left, 9)) - } - - /// Apply a binary operator type to two operand types. - /// op_ty should be `a -> b -> c`; unifies a with left, b with right, returns c. - fn apply_binop( - &mut self, - span: crate::ast::span::Span, - op_ty: &Type, - left_ty: Type, - right_ty: Type, - ) -> Result { - let intermediate_ty = Type::Unif(self.state.fresh_var()); - let result_ty = Type::Unif(self.state.fresh_var()); - - self.state - .unify(span, op_ty, &Type::fun(left_ty, intermediate_ty.clone()))?; - self.state - .unify(span, &intermediate_ty, &Type::fun(right_ty, result_ty.clone()))?; - - Ok(result_ty) + Ok(ty) } - /// Infer the type of a single binary operator expression (no chain flattening). /// Check if a type contains any Forall anywhere in its structure. fn type_contains_forall(ty: &Type) -> bool { match ty { @@ -1619,140 +1569,19 @@ impl InferCtx { /// Check if an expression is a lambda, possibly wrapped in Parens. fn expr_is_lambda(e: &Expr) -> bool { - match e { - Expr::Lambda { .. } => true, - Expr::Parens { expr, .. } => Self::expr_is_lambda(expr), - _ => false, - } + matches!(e, Expr::Lambda { .. }) } fn is_underscore_hole(e: &Expr) -> bool { matches!(e, Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_") } - fn infer_op_binary( - &mut self, - env: &Env, - span: crate::ast::span::Span, - left: &Expr, - op: &crate::cst::Spanned, - right: &Expr, - ) -> Result { - let op_lookup = if let Some(module) = op.value.module { - let qual_sym = Self::qualified_symbol(module, op.value.name); - env.lookup(qual_sym) - } else { - env.lookup(op.value.name) - }; - let op_name = op.value.name; - let op_ty = match op_lookup { - Some(scheme) => { - let ty = self.instantiate(scheme); - // Check if this operator targets a class method; if so, push op deferred constraint - // (used only for CannotGeneralizeRecursiveFunction, not instance resolution) - let class_info = self.class_methods.get(&op_name).cloned() - .or_else(|| { - self.operator_class_targets.get(&op_name) - .and_then(|target| self.class_methods.get(target).cloned()) - }); - if let Some((class_name, class_tvs)) = class_info { - if let Type::Forall(vars, body) = &ty { - let var_names: Vec = vars.iter().map(|&(v, _)| v).collect(); - let is_class_forall = !class_tvs.is_empty() - && var_names.len() >= class_tvs.len() - && var_names[..class_tvs.len()] == class_tvs[..]; - if is_class_forall { - let subst: HashMap = vars - .iter() - .map(|&(v, _)| (v, Type::Unif(self.state.fresh_var()))) - .collect(); - let result = self.apply_symbol_subst(&subst, body); - let result = self.instantiate_forall_type(result)?; - let constraint_types: Vec = class_tvs - .iter() - .filter_map(|tv| subst.get(tv).cloned()) - .collect(); - self.op_deferred_constraints.push((span, class_name, constraint_types)); - result - } else { - self.instantiate_forall_type(ty)? - } - } else { - self.instantiate_forall_type(ty)? - } - } else { - self.instantiate_forall_type(ty)? - } - } - None => { - return Err(TypeError::UndefinedVariable { - span: op.span, - name: op.value.name, - }); - } - }; - - // Operator sections: (_ op expr) or (expr op _) creates a function - let left_is_hole = Self::is_underscore_hole(left); - let right_is_hole = Self::is_underscore_hole(right); - - if left_is_hole && right_is_hole { - // Both holes: (_ op _) → equivalent to (op) - return Ok(op_ty); - } - - if left_is_hole { - // (_ op expr) → \x -> x op expr - let param_ty = Type::Unif(self.state.fresh_var()); - let right_ty = self.infer(env, right)?; - let result_ty = self.apply_binop(span, &op_ty, param_ty.clone(), right_ty)?; - return Ok(Type::fun(param_ty, result_ty)); - } - - if right_is_hole { - // (expr op _) → \x -> expr op x - let left_ty = self.infer(env, left)?; - let param_ty = Type::Unif(self.state.fresh_var()); - let result_ty = self.apply_binop(span, &op_ty, left_ty, param_ty.clone())?; - return Ok(Type::fun(param_ty, result_ty)); - } - - let left_ty = self.infer(env, left)?; - let right_ty = self.infer(env, right)?; - - self.apply_binop(span, &op_ty, left_ty, right_ty) - } - - fn infer_op_parens( - &mut self, - env: &Env, - span: crate::ast::span::Span, - op: &crate::cst::Spanned, - ) -> Result { - let lookup_result = if let Some(module) = op.value.module { - let qual_sym = Self::qualified_symbol(module, op.value.name); - env.lookup(qual_sym) - } else { - env.lookup(op.value.name) - }; - match lookup_result { - Some(scheme) => { - let ty = self.instantiate(scheme); - self.instantiate_forall_type(ty) - } - None => Err(TypeError::UndefinedVariable { - span, - name: op.value.name, - }), - } - } - fn infer_case( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, exprs: &[Expr], - alts: &[crate::cst::CaseAlternative], + alts: &[crate::ast::CaseAlternative], ) -> Result { // Detect underscore scrutinees: `case _, _ of` creates a lambda function let is_underscore: Vec = exprs @@ -1815,11 +1644,17 @@ impl InferCtx { &self.data_constructors, &self.ctor_details, ) { - return Err(TypeError::NonExhaustivePattern { - span, - type_name, - missing, - }); + // Emit NonExhaustivePattern error for the missing constructors + self.non_exhaustive_errors.push( + crate::typechecker::error::TypeError::NonExhaustivePattern { + span, + type_name, + missing, + }, + ); + // Also set the partial flag so check.rs can emit Partial + // constraint if needed (for unsafePartial support). + self.has_partial_lambda = true; } } } @@ -1844,7 +1679,7 @@ impl InferCtx { fn infer_array( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, elements: &[Expr], ) -> Result { let elem_ty = Type::Unif(self.state.fresh_var()); @@ -1859,7 +1694,7 @@ impl InferCtx { fn infer_hole( &mut self, - span: crate::ast::span::Span, + span: crate::span::Span, name: Symbol, ) -> Result { let ty = Type::Unif(self.state.fresh_var()); @@ -1873,165 +1708,14 @@ impl InferCtx { } } - /// Check if an Op/App expression has a DIRECT `_` hole child (not nested). - /// This prevents infinite recursion — we only desugar at the level where `_` appears. - fn has_direct_underscore_hole(&self, expr: &Expr) -> bool { - let is_hole = |e: &Expr| matches!(e, Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_"); - match expr { - Expr::Op { left, right, .. } => { - is_hole(left) || is_hole(right) - || self.has_direct_underscore_hole(left) - || self.has_direct_underscore_hole(right) - } - Expr::App { func, arg, .. } => is_hole(func) || is_hole(arg), - _ => false, - } - } - - /// Replace `_` holes in an operator chain with a variable reference. - fn replace_underscore_in_op_chain(&self, expr: &Expr, replacement_name: Symbol) -> Expr { - let is_hole = |e: &Expr| matches!(e, Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_"); - if is_hole(expr) { - return Expr::Var { - span: expr.span(), - name: crate::cst::QualifiedIdent { module: None, name: replacement_name }, - }; - } - match expr { - Expr::Op { span, left, op, right } => Expr::Op { - span: *span, - left: Box::new(self.replace_underscore_in_op_chain(left, replacement_name)), - op: op.clone(), - right: Box::new(self.replace_underscore_in_op_chain(right, replacement_name)), - }, - other => other.clone(), - } - } - - /// Desugar underscore sections into lambdas and infer. - /// `(_ * 1000.0)` → `\x -> x * 1000.0` - fn infer_underscore_section( - &mut self, - env: &Env, - expr: &Expr, - ) -> Result { - let param_ty = Type::Unif(self.state.fresh_var()); - let param_name = crate::interner::intern("$_arg"); - - let mut local_env = env.clone(); - local_env.insert_scheme(param_name, Scheme::mono(param_ty.clone())); - - let is_hole = |e: &Expr| matches!(e, Expr::Hole { name, .. } if crate::interner::resolve(*name).unwrap_or_default() == "_"); - let hole_ref = &crate::cst::QualifiedIdent { module: None, name: param_name }; - - // Infer the body with direct `_` replaced by the parameter - let body_ty = match expr { - Expr::Op { .. } => { - // Validate that _ appears at the top level after operator precedence. - // Flatten the chain and check: _ at the left is valid iff the first - // operator has the lowest (or equal-lowest) precedence; _ at the right - // is valid iff the last operator has the lowest precedence. - // e.g., `_ + 2 * 3` → valid (+ is lowest, _ is left operand of +) - // e.g., `_ * 4 + 1` → invalid (* is NOT lowest, _ is nested inside _ * 4) - { - let mut operands: Vec<&Expr> = Vec::new(); - let mut operators: Vec<&crate::cst::Spanned> = Vec::new(); - let mut current: &Expr = expr; - while let Expr::Op { left, op, right, .. } = current { - operands.push(left.as_ref()); - operators.push(op); - current = right.as_ref(); - } - operands.push(current); - - if operators.len() > 1 { - // Find which operands are holes - let hole_positions: Vec = operands.iter().enumerate() - .filter(|(_, e)| is_hole(e)) - .map(|(i, _)| i) - .collect(); - - // Find the minimum precedence among all operators - let min_prec = operators.iter() - .map(|op| self.get_fixity(op.value.name).1) - .min() - .unwrap_or(0); - - for &pos in &hole_positions { - let valid = if pos == 0 { - // Left edge: valid iff first operator has the lowest precedence - self.get_fixity(operators[0].value.name).1 <= min_prec - } else if pos == operands.len() - 1 { - // Right edge: valid iff last operator has the lowest precedence - self.get_fixity(operators[operators.len() - 1].value.name).1 <= min_prec - } else { - // Middle: never valid in a multi-operator chain - false - }; - if !valid { - return Err(TypeError::IncorrectAnonymousArgument { - span: operands[pos].span(), - }); - } - } - } - } - - // Replace all `_` holes in the Op chain with variable references, - // then infer normally (handles nested Op chains like `_ + 2 * 3`) - let replaced = self.replace_underscore_in_op_chain(expr, param_name); - self.infer(&local_env, &replaced)? - } - Expr::App { span, func, arg } => { - // Check if this is a record update underscore section: _ {field = value} - if is_hole(func) { - if let Expr::Record { fields, .. } = arg.as_ref() { - if !fields.is_empty() && fields.iter().all(|f| f.is_update && f.value.is_some()) { - let updates: Vec = fields.iter().filter_map(|f| { - Some(crate::cst::RecordUpdate { span: f.span, label: f.label.clone(), value: f.value.clone()? }) - }).collect(); - if !updates.is_empty() { - // Create a var expression referencing the lambda parameter - let param_var = Expr::Var { - span: func.span(), - name: crate::cst::QualifiedIdent { module: None, name: param_name }, - }; - return Ok(Type::fun( - param_ty, - self.infer_record_update(&local_env, *span, ¶m_var, &updates)?, - )); - } - } - } - } - let func_ty = if is_hole(func) { - self.infer_var(&local_env, func.span(), hole_ref)? - } else { - self.infer(&local_env, func)? - }; - let arg_ty = if is_hole(arg) { - self.infer_var(&local_env, arg.span(), hole_ref)? - } else { - self.infer(&local_env, arg)? - }; - let result_ty = Type::Unif(self.state.fresh_var()); - self.state.unify(*span, &func_ty, &Type::fun(arg_ty, result_ty.clone()))?; - result_ty - } - _ => unreachable!("has_direct_underscore_hole only matches Op/App"), - }; - - Ok(Type::fun(param_ty, body_ty)) - } - fn infer_record( &mut self, env: &Env, - span: crate::ast::span::Span, - fields: &[crate::cst::RecordField], + span: crate::span::Span, + fields: &[crate::ast::RecordField], ) -> Result { // Check for duplicate labels - let mut label_spans: HashMap> = HashMap::new(); + let mut label_spans: HashMap> = HashMap::new(); for field in fields { label_spans.entry(field.label.value).or_default().push(field.span); } @@ -2093,7 +1777,7 @@ impl InferCtx { fn infer_record_access( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, expr: &Expr, field: &crate::cst::Spanned, ) -> Result { @@ -2143,9 +1827,9 @@ impl InferCtx { fn infer_record_update( &mut self, env: &Env, - span: crate::ast::span::Span, + span: crate::span::Span, expr: &Expr, - updates: &[crate::cst::RecordUpdate], + updates: &[crate::ast::RecordUpdate], ) -> Result { // Check if the record expression is an underscore hole (record update section) let record_is_section = Self::is_underscore_hole(expr); @@ -2204,8 +1888,8 @@ impl InferCtx { fn collect_record_update_fields( &mut self, env: &Env, - span: crate::ast::span::Span, - updates: &[crate::cst::RecordUpdate], + span: crate::span::Span, + updates: &[crate::ast::RecordUpdate], update_fields: &mut Vec<(crate::interner::Symbol, Type)>, section_params: &mut Vec, nested_input_fields: &mut Vec<(crate::interner::Symbol, Type)>, @@ -2221,8 +1905,8 @@ impl InferCtx { if !fields.is_empty() && fields.iter().all(|f| f.is_update && f.value.is_some()) { // Nested update: the bar field of the original record is accessed, // and the inner fields are applied as updates to it. - let inner_updates: Vec = fields.iter().filter_map(|f| { - Some(crate::cst::RecordUpdate { + let inner_updates: Vec = fields.iter().filter_map(|f| { + Some(crate::ast::RecordUpdate { span: f.span, label: f.label.clone(), value: f.value.clone()?, @@ -2272,8 +1956,8 @@ impl InferCtx { fn infer_do( &mut self, env: &Env, - span: crate::ast::span::Span, - statements: &[crate::cst::DoStatement], + span: crate::span::Span, + statements: &[crate::ast::DoStatement], ) -> Result { if statements.is_empty() { return Err(TypeError::NotImplemented { @@ -2282,13 +1966,10 @@ impl InferCtx { }); } - let monad_ty = Type::Unif(self.state.fresh_var()); - let mut current_env = env.child(); - // Pure do-blocks (no `<-` binds) don't require monadic wrapping let has_binds = statements .iter() - .any(|s| matches!(s, crate::cst::DoStatement::Bind { .. })); + .any(|s| matches!(s, crate::ast::DoStatement::Bind { .. })); // Check that `bind` is in scope when do-notation uses bind (module mode only) if self.module_mode && has_binds { @@ -2302,7 +1983,7 @@ impl InferCtx { let has_non_last_discards = statements.len() > 1 && statements[..statements.len() - 1] .iter() - .any(|s| matches!(s, crate::cst::DoStatement::Discard { .. })); + .any(|s| matches!(s, crate::ast::DoStatement::Discard { .. })); if self.module_mode && has_non_last_discards { let discard_sym = crate::interner::intern("discard"); if env.lookup(discard_sym).is_none() { @@ -2310,68 +1991,246 @@ impl InferCtx { } } - for (i, stmt) in statements.iter().enumerate() { - let is_last = i == statements.len() - 1; - match stmt { - crate::cst::DoStatement::Discard { expr, .. } => { - let expr_ty = self.infer(¤t_env, expr)?; - if is_last { - if has_binds { - // Last statement in monadic do: m a - let result_inner = Type::Unif(self.state.fresh_var()); - let expected = Type::app(monad_ty.clone(), result_inner.clone()); - self.state.unify(span, &expr_ty, &expected)?; + if has_binds { + // Desugar do-notation as applications of bind/discard from the environment. + // This supports both standard monads (bind :: m a -> (a -> m b) -> m b) + // and indexed monads via rebindable do-notation (where bind = ibind). + self.infer_do_bind_stmts(env, span, statements, 0) + } else { + // Pure do-block (no binds): just infer each expression + let mut current_env = env.child(); + for (i, stmt) in statements.iter().enumerate() { + let is_last = i == statements.len() - 1; + match stmt { + crate::ast::DoStatement::Discard { expr, .. } => { + let expr_ty = self.infer(¤t_env, expr)?; + if is_last { + return Ok(expr_ty); } - return Ok(expr_ty); - } else if has_binds { - // Non-last discard in monadic do: m _ - let discard_inner = Type::Unif(self.state.fresh_var()); - let expected = Type::app(monad_ty.clone(), discard_inner); - self.state.unify(span, &expr_ty, &expected)?; } - } - crate::cst::DoStatement::Bind { binder, expr, .. } => { - // Check for reserved do-notation names - check_do_reserved_names(binder)?; - let expr_ty = self.infer(¤t_env, expr)?; - // expr : m a, bind binder to a - let inner_ty = Type::Unif(self.state.fresh_var()); - let expected = Type::app(monad_ty.clone(), inner_ty.clone()); - self.state.unify(span, &expr_ty, &expected)?; - self.infer_binder(&mut current_env, binder, &inner_ty)?; - } - crate::cst::DoStatement::Let { bindings, .. } => { - // Check for reserved do-notation names in let bindings - for binding in bindings { - if let LetBinding::Value { binder, .. } = binding { - check_do_reserved_names(binder)?; + crate::ast::DoStatement::Let { bindings, .. } => { + for binding in bindings { + if let LetBinding::Value { binder, .. } = binding { + check_do_reserved_names(binder)?; + } } + self.process_let_bindings(&mut current_env, bindings)?; + } + crate::ast::DoStatement::Bind { span: bind_span, .. } => { + // Shouldn't happen since has_binds is false + return Err(TypeError::InvalidDoBind { span: *bind_span }); } - self.process_let_bindings(&mut current_env, bindings)?; } } + // If we get here, the last statement was a Let + match statements.last() { + Some(crate::ast::DoStatement::Let { span: let_span, .. }) => { + Err(TypeError::InvalidDoLet { span: *let_span }) + } + _ => Err(TypeError::NotImplemented { + span, + feature: "do block must end with an expression".to_string(), + }) + } + } + } + + /// Recursively infer do-notation statements by desugaring binds as + /// function applications of the `bind` function from the environment. + /// This handles both standard monads and rebindable do-notation + /// (e.g., indexed monads with `where bind = ibind`). + fn infer_do_bind_stmts( + &mut self, + env: &Env, + span: crate::span::Span, + statements: &[crate::ast::DoStatement], + idx: usize, + ) -> Result { + if idx >= statements.len() { + return Err(TypeError::NotImplemented { + span, + feature: "empty do block".to_string(), + }); } - // If we get here, the last statement was a Bind or Let - match statements.last() { - Some(crate::cst::DoStatement::Bind { span: bind_span, .. }) => { - Err(TypeError::InvalidDoBind { span: *bind_span }) + let is_last = idx == statements.len() - 1; + let stmt = &statements[idx]; + + match stmt { + crate::ast::DoStatement::Discard { expr, .. } if is_last => { + // Last statement: just infer its type + self.infer(env, expr) + } + crate::ast::DoStatement::Discard { expr, .. } => { + // Non-last discard: discard expr (\_ -> rest), fallback to bind + let discard_sym = crate::interner::intern("discard"); + let func_ty = if let Some(scheme) = env.lookup(discard_sym) { + self.instantiate(&scheme) + } else { + let bind_sym = crate::interner::intern("bind"); + let scheme = env.lookup(bind_sym) + .ok_or_else(|| TypeError::UndefinedVariable { span, name: bind_sym })?; + self.instantiate(&scheme) + }; + + let expr_ty = self.infer(env, expr)?; + let rest_ty = self.infer_do_bind_stmts(env, span, statements, idx + 1)?; + + // Apply: func expr (\_ -> rest) + let after_first = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?; + let discard_arg = Type::Unif(self.state.fresh_var()); + let cont_ty = Type::fun(discard_arg, rest_ty); + let result = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &after_first, &Type::fun(cont_ty, result.clone()))?; + Ok(result) } - Some(crate::cst::DoStatement::Let { span: let_span, .. }) => { - Err(TypeError::InvalidDoLet { span: *let_span }) + crate::ast::DoStatement::Bind { span: bind_span, binder, expr } => { + if is_last { + return Err(TypeError::InvalidDoBind { span: *bind_span }); + } + + check_do_reserved_names(binder)?; + + let bind_sym = crate::interner::intern("bind"); + let scheme = env.lookup(bind_sym) + .ok_or_else(|| TypeError::UndefinedVariable { span, name: bind_sym })?; + let func_ty = self.instantiate(&scheme); + + let expr_ty = self.infer(env, expr)?; + + // Create continuation environment with binder + let mut cont_env = env.child(); + let binder_ty = Type::Unif(self.state.fresh_var()); + self.infer_binder(&mut cont_env, binder, &binder_ty)?; + + let rest_ty = self.infer_do_bind_stmts(&cont_env, span, statements, idx + 1)?; + + // Apply: bind expr (\binder -> rest) + let after_first = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?; + let cont_ty = Type::fun(binder_ty, rest_ty); + let result = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &after_first, &Type::fun(cont_ty, result.clone()))?; + Ok(result) + } + crate::ast::DoStatement::Let { span: let_span, bindings, .. } => { + for binding in bindings { + if let LetBinding::Value { binder, .. } = binding { + check_do_reserved_names(binder)?; + } + } + let mut let_env = env.child(); + self.process_let_bindings(&mut let_env, bindings)?; + if is_last { + return Err(TypeError::InvalidDoLet { span: *let_span }); + } + self.infer_do_bind_stmts(&let_env, span, statements, idx + 1) } - _ => Err(TypeError::NotImplemented { + } + } + + /// Infer the type of a qualified do block (e.g. `Module.do`). + /// Uses the `bind` and `discard` from the specified module instead of + /// assuming monadic semantics. + fn infer_qualified_do( + &mut self, + env: &Env, + span: crate::span::Span, + module: Symbol, + statements: &[crate::ast::DoStatement], + ) -> Result { + self.infer_qualified_do_stmts(env, span, module, statements, 0) + } + + fn infer_qualified_do_stmts( + &mut self, + env: &Env, + span: crate::span::Span, + module: Symbol, + statements: &[crate::ast::DoStatement], + idx: usize, + ) -> Result { + if idx >= statements.len() { + return Err(TypeError::NotImplemented { span, - feature: "do block must end with an expression".to_string(), - }) + feature: "empty qualified do block".to_string(), + }); + } + + let is_last = idx == statements.len() - 1; + let stmt = &statements[idx]; + + match stmt { + crate::ast::DoStatement::Discard { expr, .. } if is_last => { + // Last statement: just infer its type + self.infer(env, expr) + } + crate::ast::DoStatement::Discard { expr, .. } => { + // Non-last discard: Module.discard expr (\_ -> rest) + let bind_sym = Self::qualified_symbol(module, crate::interner::intern("discard")); + let func_ty = if let Some(scheme) = env.lookup(bind_sym) { + self.instantiate(&scheme) + } else { + // Fallback to Module.bind if discard not found + let bind_sym2 = Self::qualified_symbol(module, crate::interner::intern("bind")); + let scheme = env.lookup(bind_sym2) + .ok_or_else(|| TypeError::UndefinedVariable { span, name: bind_sym2 })?; + self.instantiate(&scheme) + }; + + let expr_ty = self.infer(env, expr)?; + let rest_ty = self.infer_qualified_do_stmts(env, span, module, statements, idx + 1)?; + + // Apply: func expr (\_ -> rest) + let after_first = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?; + let discard_arg_ty = Type::Unif(self.state.fresh_var()); + let cont_ty = Type::fun(discard_arg_ty, rest_ty); + let result = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &after_first, &Type::fun(cont_ty, result.clone()))?; + Ok(result) + } + crate::ast::DoStatement::Bind { span: bind_span, binder, expr } => { + if is_last { + return Err(TypeError::InvalidDoBind { span: *bind_span }); + } + // Module.bind expr (\x -> rest) + let bind_sym = Self::qualified_symbol(module, crate::interner::intern("bind")); + let scheme = env.lookup(bind_sym) + .ok_or_else(|| TypeError::UndefinedVariable { span, name: bind_sym })?; + let func_ty = self.instantiate(&scheme); + + let expr_ty = self.infer(env, expr)?; + + // Create continuation environment with binder + let mut cont_env = env.child(); + let binder_ty = Type::Unif(self.state.fresh_var()); + self.infer_binder(&mut cont_env, binder, &binder_ty)?; + + let rest_ty = self.infer_qualified_do_stmts(&cont_env, span, module, statements, idx + 1)?; + + // Apply: func expr (\binder -> rest) + let after_first = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?; + let cont_ty = Type::fun(binder_ty, rest_ty); + let result = Type::Unif(self.state.fresh_var()); + self.state.unify(span, &after_first, &Type::fun(cont_ty, result.clone()))?; + Ok(result) + } + crate::ast::DoStatement::Let { bindings, .. } => { + let mut let_env = env.child(); + self.process_let_bindings(&mut let_env, bindings)?; + self.infer_qualified_do_stmts(&let_env, span, module, statements, idx + 1) + } } } fn infer_ado( &mut self, env: &Env, - span: crate::ast::span::Span, - statements: &[crate::cst::DoStatement], + span: crate::span::Span, + statements: &[crate::ast::DoStatement], result: &Expr, ) -> Result { let functor_ty = Type::Unif(self.state.fresh_var()); @@ -2379,17 +2238,17 @@ impl InferCtx { for stmt in statements { match stmt { - crate::cst::DoStatement::Bind { binder, expr, .. } => { + crate::ast::DoStatement::Bind { binder, expr, .. } => { let expr_ty = self.infer(¤t_env, expr)?; let inner_ty = Type::Unif(self.state.fresh_var()); let expected = Type::app(functor_ty.clone(), inner_ty.clone()); self.state.unify(span, &expr_ty, &expected)?; self.infer_binder(&mut current_env, binder, &inner_ty)?; } - crate::cst::DoStatement::Let { bindings, .. } => { + crate::ast::DoStatement::Let { bindings, .. } => { self.process_let_bindings(&mut current_env, bindings)?; } - crate::cst::DoStatement::Discard { expr, .. } => { + crate::ast::DoStatement::Discard { expr, .. } => { let expr_ty = self.infer(¤t_env, expr)?; let discard_inner = Type::Unif(self.state.fresh_var()); let expected = Type::app(functor_ty.clone(), discard_inner); @@ -2437,14 +2296,15 @@ impl InferCtx { self.state.unify(*span, expected, &lit_ty)?; Ok(()) } - Binder::Constructor { span, name, args } => { + Binder::Constructor { span, name, args, .. } => { // Check constructor arity against ctor_details if available let lookup_name = if let Some(module) = name.module { Self::qualified_symbol(module, name.name) } else { name.name }; - if let Some((_, _, field_types)) = self.ctor_details.get(&lookup_name) { + let lookup_qid = QualifiedIdent { module: None, name: lookup_name }; + if let Some((_, _, field_types)) = self.ctor_details.get(&lookup_qid) { let expected_arity = field_types.len(); if args.len() != expected_arity { return Err(TypeError::IncorrectConstructorArity { @@ -2481,6 +2341,20 @@ impl InferCtx { } } + // If the constructor pattern was qualified (e.g. HATS.Linear), + // qualify the return type's head constructor ONLY when the + // unqualified name conflicts with a type alias. Without this, + // unqualified Con(Easing) from the constructor may be incorrectly + // expanded as a type alias (e.g. Tick.Easing = Number -> Number) + // instead of matching the data type Con(HATS.Easing). + if let Some(module) = name.module { + if let Some(head_name) = extract_type_con(&ctor_ty) { + if self.state.type_aliases.contains_key(&head_name.name) { + ctor_ty = qualify_type_head(ctor_ty, module); + } + } + } + // The remaining type should unify with expected self.state.unify(*span, expected, &ctor_ty)?; Ok(()) @@ -2491,13 +2365,12 @@ impl InferCtx { }), } } - Binder::Parens { binder, .. } => self.infer_binder(env, binder, expected), Binder::As { name, binder, .. } => { env.insert_mono(name.value, expected.clone()); self.infer_binder(env, binder, expected) } Binder::Typed { span, binder, ty } => { - let annotated = convert_type_expr(ty, &self.type_operators, &self.known_types)?; + let annotated = convert_type_expr(ty, &self.type_operators)?; let annotated = self.instantiate_wildcards(&annotated); self.state.unify(*span, expected, &annotated)?; self.infer_binder(env, binder, expected) @@ -2510,64 +2383,9 @@ impl InferCtx { } Ok(()) } - Binder::Op { span, left, op, right } => { - let op_name = op.value.name; - let resolved_op = if let Some(module) = op.value.module { - Self::qualified_symbol(module, op_name) - } else { - op_name - }; - // Check if the operator aliases a function (not a constructor). - // Only data constructor operators are valid in binder patterns. - // Also check ctor_details as a secondary source: if the operator - // is known as a constructor (e.g. from a different import), allow it. - if self.function_op_aliases.contains(&op_name) - && !self.ctor_details.contains_key(&op_name) - { - return Err(TypeError::InvalidOperatorInBinder { - span: op.span, - op: op_name, - }); - } - // Treat as constructor pattern: op left right - match env.lookup(resolved_op) { - Some(scheme) => { - let mut ctor_ty = self.instantiate(scheme); - ctor_ty = self.instantiate_forall_type(ctor_ty)?; - - // Peel off two argument types (left, right) - match ctor_ty { - Type::Fun(left_ty, rest) => { - self.infer_binder(env, left, &left_ty)?; - match *rest { - Type::Fun(right_ty, result_ty) => { - self.infer_binder(env, right, &right_ty)?; - self.state.unify(*span, expected, &result_ty)?; - Ok(()) - } - result_ty => { - // Unary operator constructor — right becomes result - self.infer_binder(env, right, &result_ty)?; - // This is unusual but handle gracefully - Ok(()) - } - } - } - _ => Err(TypeError::UndefinedVariable { - span: *span, - name: resolved_op, - }), - } - } - None => Err(TypeError::UndefinedVariable { - span: *span, - name: resolved_op, - }), - } - } Binder::Record { span, fields } => { // Check for duplicate labels - let mut label_spans: HashMap> = HashMap::new(); + let mut label_spans: HashMap> = HashMap::new(); for field in fields { label_spans.entry(field.label.value).or_default().push(field.span); } @@ -2672,7 +2490,7 @@ impl InferCtx { /// Check for overlapping variable names within a set of binders (e.g. a case alternative). /// Returns an error if the same variable appears more than once. pub fn check_overlapping_pattern_vars(binders: &[&Binder]) -> Option { - let mut seen: HashMap> = HashMap::new(); + let mut seen: HashMap> = HashMap::new(); for binder in binders { collect_pattern_vars(binder, &mut seen); } @@ -2687,7 +2505,7 @@ pub fn check_overlapping_pattern_vars(binders: &[&Binder]) -> Option None } -fn collect_pattern_vars(binder: &Binder, seen: &mut HashMap>) { +fn collect_pattern_vars(binder: &Binder, seen: &mut HashMap>) { match binder { Binder::Var { name, .. } => { seen.entry(name.value).or_default().push(name.span); @@ -2697,7 +2515,6 @@ fn collect_pattern_vars(binder: &Binder, seen: &mut HashMap collect_pattern_vars(binder, seen), Binder::As { name, binder, .. } => { seen.entry(name.value).or_default().push(name.span); collect_pattern_vars(binder, seen); @@ -2708,10 +2525,6 @@ fn collect_pattern_vars(binder: &Binder, seen: &mut HashMap { - collect_pattern_vars(left, seen); - collect_pattern_vars(right, seen); - } Binder::Record { fields, .. } => { for field in fields { if let Some(binder) = &field.binder { @@ -2726,7 +2539,7 @@ fn collect_pattern_vars(binder: &Binder, seen: &mut HashMap Option { +pub fn extract_type_con(ty: &Type) -> Option { match ty { Type::Con(name) => Some(*name), Type::App(f, _) => extract_type_con(f), @@ -2737,7 +2550,7 @@ pub fn extract_type_con(ty: &Type) -> Option { /// Classify a binder for exhaustiveness checking. /// Sets `has_catchall` if the binder matches anything (wildcard, variable, literal). /// Adds constructor names to `covered` for constructor patterns. -/// Recurses through Parens, As, and Typed wrappers. +/// Recurses through As and Typed wrappers. pub fn classify_binder(binder: &Binder, has_catchall: &mut bool, covered: &mut Vec) { match binder { Binder::Wildcard { .. } | Binder::Var { .. } => { @@ -2755,19 +2568,9 @@ pub fn classify_binder(binder: &Binder, has_catchall: &mut bool, covered: &mut V Binder::As { binder: inner, .. } => { classify_binder(inner, has_catchall, covered); } - Binder::Parens { binder: inner, .. } => { - classify_binder(inner, has_catchall, covered); - } Binder::Typed { binder: inner, .. } => { classify_binder(inner, has_catchall, covered); } - Binder::Op { op, .. } => { - // Operator patterns (e.g. `a : as` for Cons, `a :| bs` for NonEmpty) - // contribute to constructor exhaustiveness. - if !covered.contains(&op.value.name) { - covered.push(op.value.name); - } - } Binder::Array { .. } | Binder::Record { .. } => { // These don't contribute to constructor exhaustiveness } @@ -2783,21 +2586,49 @@ pub fn is_refutable(binder: &Binder) -> bool { Binder::Array { .. } => true, Binder::Literal { .. } => true, Binder::Constructor { .. } => true, - Binder::Op { .. } => true, Binder::Record { fields, .. } => { fields.iter().any(|f| { f.binder.as_ref().map_or(false, |b| is_refutable(b)) }) } Binder::As { binder: inner, .. } => is_refutable(inner), - Binder::Parens { binder: inner, .. } => is_refutable(inner), Binder::Typed { binder: inner, .. } => is_refutable(inner), } } +/// Like `is_refutable`, but treats single-constructor types (newtypes) as irrefutable. +/// For constructor binders, looks up the parent type in `data_constructors` to check +/// if the type has more than one constructor. +pub fn is_truly_refutable(binder: &Binder, data_constructors: &HashMap>) -> bool { + match binder { + Binder::Wildcard { .. } | Binder::Var { .. } => false, + Binder::Array { .. } => true, + Binder::Literal { .. } => true, + Binder::Constructor { name, args, .. } => { + // Check if this constructor belongs to a single-constructor type + let is_single_ctor = data_constructors.values().any(|ctors| { + ctors.len() == 1 && ctors.iter().any(|c| c.name == name.name) + }); + if is_single_ctor { + // Single-constructor type (like newtype) — only refutable if args are + args.iter().any(|a| is_truly_refutable(a, data_constructors)) + } else { + true + } + } + Binder::Record { fields, .. } => { + fields.iter().any(|f| { + f.binder.as_ref().map_or(false, |b| is_truly_refutable(b, data_constructors)) + }) + } + Binder::As { binder: inner, .. } => is_truly_refutable(inner, data_constructors), + Binder::Typed { binder: inner, .. } => is_truly_refutable(inner, data_constructors), + } +} + /// Extract the outermost type constructor name AND its type arguments from a type. /// E.g. `Maybe Int` → `Some((Maybe, [Int]))`, `Either String Int` → `Some((Either, [String, Int]))`. -pub fn extract_type_con_and_args(ty: &Type) -> Option<(Symbol, Vec)> { +pub fn extract_type_con_and_args(ty: &Type) -> Option<(QualifiedIdent, Vec)> { match ty { Type::Con(name) => Some((*name, Vec::new())), Type::App(f, a) => { @@ -2847,11 +2678,10 @@ fn substitute_type_vars(ty: &Type, subst: &HashMap) -> Type { } } -/// Unwrap a binder through Parens, As, and Typed wrappers to get the inner binder. +/// Unwrap a binder through As and Typed wrappers to get the inner binder. pub fn unwrap_binder(binder: &Binder) -> &Binder { match binder { - Binder::Parens { binder: inner, .. } - | Binder::As { binder: inner, .. } + Binder::As { binder: inner, .. } | Binder::Typed { binder: inner, .. } => unwrap_binder(inner), _ => binder, } @@ -2897,11 +2727,22 @@ pub fn is_unconditional_for_exhaustiveness(guarded: &GuardedExpr) -> bool { pub fn check_exhaustiveness( binders: &[&Binder], scrutinee_ty: &Type, - data_constructors: &HashMap>, - ctor_details: &HashMap, Vec)>, + data_constructors: &HashMap>, + ctor_details: &HashMap, Vec)>, ) -> Option> { let type_name = extract_type_con(scrutinee_ty)?; - let all_ctors = data_constructors.get(&type_name)?; + let all_ctors = data_constructors.get(&type_name).or_else(|| { + // Fallback: if qualified lookup fails, try matching by unqualified name + if type_name.module.is_some() { + let unq = QualifiedIdent { module: None, name: type_name.name }; + data_constructors.get(&unq) + } else { + // Unqualified lookup failed — search for any qualified variant + data_constructors.iter() + .find(|(k, _)| k.name == type_name.name) + .map(|(_, v)| v) + } + })?; // Classify all binders let mut has_catchall = false; @@ -2914,21 +2755,44 @@ pub fn check_exhaustiveness( return None; // Exhaustive via wildcard/variable } + // When multiple types share the same unqualified name (e.g. Data.List.List and + // Data.List.Lazy.List both map to unqualified "List"), the data_constructors entry + // may have been overwritten by the wrong type. Detect this by checking if any + // covered constructor appears in all_ctors; if not, use ctor_details to find the + // correct parent type and its constructor set. + let any_covered_matches = covered.iter().any(|c| all_ctors.iter().any(|ac| ac.name == *c)); + let all_ctors = if !any_covered_matches && !covered.is_empty() { + // Look up the parent type of the first covered constructor via ctor_details + let first_ctor_qi = QualifiedIdent { module: None, name: covered[0] }; + if let Some((parent_type, _, _)) = ctor_details.get(&first_ctor_qi) { + if let Some(correct_ctors) = data_constructors.get(parent_type) { + correct_ctors + } else { + all_ctors + } + } else { + all_ctors + } + } else { + all_ctors + }; + // Resolve operator aliases in covered set: if a covered symbol (e.g. operator `:`) // is NOT one of the declared constructors but has identical ctor_details as one, // then they are aliases (e.g. `:` is an alias for `Cons`). let mut resolved_covered = covered.clone(); for &op_sym in &covered { // Only resolve aliases for symbols that aren't already declared constructors - if all_ctors.contains(&op_sym) { + if all_ctors.iter().any(|c| c.name == op_sym) { continue; } - if let Some(op_details) = ctor_details.get(&op_sym) { - for &ctor in all_ctors { - if !resolved_covered.contains(&ctor) { - if let Some(ctor_det) = ctor_details.get(&ctor) { + let op_qid = QualifiedIdent { module: None, name: op_sym }; + if let Some(op_details) = ctor_details.get(&op_qid) { + for ctor in all_ctors { + if !resolved_covered.contains(&ctor.name) { + if let Some(ctor_det) = ctor_details.get(ctor) { if op_details == ctor_det { - resolved_covered.push(ctor); + resolved_covered.push(ctor.name); } } } @@ -2937,9 +2801,9 @@ pub fn check_exhaustiveness( } // Find missing constructors at this level - let missing_at_this_level: Vec = all_ctors + let missing_at_this_level: Vec = all_ctors .iter() - .filter(|c| !resolved_covered.contains(c)) + .filter(|c| !resolved_covered.contains(&c.name)) .copied() .collect(); @@ -2947,7 +2811,7 @@ pub fn check_exhaustiveness( // Missing constructors — report them let missing_strs: Vec = missing_at_this_level .iter() - .map(|c| crate::interner::resolve(*c).unwrap_or_default()) + .map(|c| crate::interner::resolve(c.name).unwrap_or_default()) .collect(); return Some(missing_strs); } @@ -2991,7 +2855,7 @@ pub fn check_exhaustiveness( for binder in binders { let inner = unwrap_binder(binder); match inner { - Binder::Constructor { name, args, .. } if name.name == *ctor_name => { + Binder::Constructor { name, args, .. } if name.name == ctor_name.name => { if args.len() == 1 { sub_binders.push(&args[0]); } @@ -3015,7 +2879,7 @@ pub fn check_exhaustiveness( data_constructors, ctor_details, ) { - let ctor_str = crate::interner::resolve(*ctor_name).unwrap_or_default(); + let ctor_str = crate::interner::resolve(ctor_name.name).unwrap_or_default(); let missing_strs = nested_missing .into_iter() .map(|m| format!("{} ({})", ctor_str, m)) @@ -3034,8 +2898,170 @@ fn expr_references_name(expr: &Expr, target: Symbol, _let_names: &HashSet name.name == target, - Expr::Parens { expr, .. } => expr_references_name(expr, target, _let_names), Expr::TypeAnnotation { expr, .. } => expr_references_name(expr, target, _let_names), _ => false, } } + +/// Check if an expression references any name from a given set anywhere in its tree. +/// Used for dependency analysis of let/where bindings. +fn expr_references_any(expr: &Expr, names: &HashSet) -> bool { + use crate::ast::*; + match expr { + Expr::Var { name, .. } if name.module.is_none() => names.contains(&name.name), + Expr::Var { .. } | Expr::Constructor { .. } | Expr::Hole { .. } => false, + Expr::Literal { lit, .. } => match lit { + Literal::Array(es) => es.iter().any(|e| expr_references_any(e, names)), + _ => false, + }, + Expr::Array { elements, .. } => elements.iter().any(|e| expr_references_any(e, names)), + Expr::Record { fields, .. } => fields.iter().any(|f| f.value.as_ref().map_or(false, |v| expr_references_any(v, names))), + Expr::App { func, arg, .. } => { + expr_references_any(func, names) || expr_references_any(arg, names) + } + Expr::VisibleTypeApp { func, .. } => expr_references_any(func, names), + Expr::Lambda { body, .. } => expr_references_any(body, names), + Expr::If { cond, then_expr, else_expr, .. } => { + expr_references_any(cond, names) || expr_references_any(then_expr, names) || expr_references_any(else_expr, names) + } + Expr::Case { exprs, alts, .. } => { + exprs.iter().any(|e| expr_references_any(e, names)) || + alts.iter().any(|alt| match &alt.result { + GuardedExpr::Unconditional(e) => expr_references_any(e, names), + GuardedExpr::Guarded(guards) => guards.iter().any(|g| { + g.patterns.iter().any(|p| match p { + GuardPattern::Boolean(e) => expr_references_any(e, names), + GuardPattern::Pattern(_, e) => expr_references_any(e, names), + }) || expr_references_any(&g.expr, names) + }), + }) + } + Expr::Let { bindings, body, .. } => { + bindings.iter().any(|b| match b { + LetBinding::Value { expr, .. } => expr_references_any(expr, names), + _ => false, + }) || expr_references_any(body, names) + } + Expr::Do { statements, .. } => { + statements.iter().any(|s| match s { + DoStatement::Bind { expr, .. } | DoStatement::Discard { expr, .. } => expr_references_any(expr, names), + DoStatement::Let { bindings, .. } => bindings.iter().any(|b| match b { + LetBinding::Value { expr, .. } => expr_references_any(expr, names), + _ => false, + }), + }) + } + Expr::Ado { statements, result, .. } => { + statements.iter().any(|s| match s { + DoStatement::Bind { expr, .. } | DoStatement::Discard { expr, .. } => expr_references_any(expr, names), + DoStatement::Let { bindings, .. } => bindings.iter().any(|b| match b { + LetBinding::Value { expr, .. } => expr_references_any(expr, names), + _ => false, + }), + }) || expr_references_any(result, names) + } + Expr::TypeAnnotation { expr, .. } | Expr::Negate { expr, .. } + | Expr::RecordAccess { expr, .. } => expr_references_any(expr, names), + Expr::RecordUpdate { expr, updates, .. } => { + expr_references_any(expr, names) || updates.iter().any(|u| expr_references_any(&u.value, names)) + } + Expr::AsPattern { name: n, pattern, .. } => { + expr_references_any(n, names) || expr_references_any(pattern, names) + } + } +} + +/// Apply a module qualifier to the head type constructor of a type. +/// Walks through nested `App` to reach the head `Con`, then adds the qualifier. +/// Used when a qualified constructor pattern (e.g. `HATS.Linear`) produces a return +/// type with an unqualified head (e.g. `Con(Easing)`) that should be qualified to +/// match the scrutinee type (e.g. `Con(HATS.Easing)`). +fn qualify_type_head(ty: Type, module: Symbol) -> Type { + match ty { + Type::Con(qi) if qi.module.is_none() => { + Type::Con(QualifiedIdent { module: Some(module), name: qi.name }) + } + Type::App(f, a) => Type::App(Box::new(qualify_type_head(*f, module)), a), + _ => ty, + } +} + +/// Collect "constructor vars" from an instantiated param type: unification variables +/// that appear as the head of an application spine where a forall variable is an argument. +/// E.g. in `App(?g, ?b_fresh)`, `?g` is a constructor var when `?b_fresh` is a forall var. +/// These vars represent type constructors whose solutions may contain the forall var at +/// the monomorphic level — this is an artifact of App decomposition, not a real escape. +fn collect_constructor_vars( + ty: &Type, + forall_vars: &HashSet, +) -> Vec { + let mut result = Vec::new(); + collect_ctor_vars_inner(ty, forall_vars, &mut result); + result +} + +fn collect_ctor_vars_inner( + ty: &Type, + forall_vars: &HashSet, + result: &mut Vec, +) { + match ty { + Type::App(_, _) => { + // Decompose the full application spine + let mut spine_args: Vec<&Type> = Vec::new(); + let mut head = ty; + while let Type::App(f, a) = head { + spine_args.push(a.as_ref()); + head = f.as_ref(); + } + // Check if any spine arg is (or contains) a forall var + let has_forall_arg = spine_args.iter().any(|arg| type_has_forall_unif(arg, forall_vars)); + if has_forall_arg { + // If the head is a unif var, it's a constructor var + if let Type::Unif(v) = head { + if !result.contains(v) { + result.push(*v); + } + } + } + // Recurse into args (head already decomposed) + for arg in &spine_args { + collect_ctor_vars_inner(arg, forall_vars, result); + } + } + Type::Fun(from, to) => { + collect_ctor_vars_inner(from, forall_vars, result); + collect_ctor_vars_inner(to, forall_vars, result); + } + Type::Forall(_, body) => { + collect_ctor_vars_inner(body, forall_vars, result); + } + Type::Record(fields, tail) => { + for (_, t) in fields { + collect_ctor_vars_inner(t, forall_vars, result); + } + if let Some(t) = tail { + collect_ctor_vars_inner(t, forall_vars, result); + } + } + _ => {} + } +} + +/// Check if a type contains any of the given forall unif vars. +fn type_has_forall_unif( + ty: &Type, + forall_vars: &HashSet, +) -> bool { + match ty { + Type::Unif(v) => forall_vars.contains(v), + Type::App(f, a) => type_has_forall_unif(f, forall_vars) || type_has_forall_unif(a, forall_vars), + Type::Fun(f, t) => type_has_forall_unif(f, forall_vars) || type_has_forall_unif(t, forall_vars), + Type::Forall(_, body) => type_has_forall_unif(body, forall_vars), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| type_has_forall_unif(t, forall_vars)) + || tail.as_ref().map_or(false, |t| type_has_forall_unif(t, forall_vars)) + } + _ => false, + } +} diff --git a/src/typechecker/kind.rs b/src/typechecker/kind.rs index ea09ab18..16449e43 100644 --- a/src/typechecker/kind.rs +++ b/src/typechecker/kind.rs @@ -1,7 +1,8 @@ use std::collections::{HashMap, HashSet}; -use crate::ast::span::Span; -use crate::cst::TypeExpr; +use crate::span::Span; +use crate::ast::TypeExpr; +use crate::cst::QualifiedIdent; use crate::interner::{self, Symbol}; use crate::typechecker::error::TypeError; use crate::typechecker::types::Type; @@ -13,12 +14,24 @@ pub struct KindState { pub state: UnifyState, /// Maps type constructor names (e.g. "Array", "Maybe") to their kinds. pub type_kinds: HashMap, + /// Separate kind storage for type classes, used for instance head checking. + /// When a class name collides with a data type name, instance heads need + /// the class kind (e.g. Type -> Constraint) not the data type kind. + pub class_kinds: HashMap, /// Types in the current binding group (SCC). Lookups for these types /// skip freshening to enable monomorphic kind inference within the group. pub binding_group: HashSet, /// Deferred quantification checks: (span, TyVarIds of unannotated forall var kinds). /// Checked after top-level kind inference completes, when all unifications are done. pub deferred_quantification_checks: Vec<(Span, Vec)>, + /// Kind types from class type parameters. At end-of-pass, these are zonked and + /// their unsolved var IDs excluded from quantification checks, since class type + /// param kinds are legitimately determined by the outer class context. + pub class_param_kind_types: Vec, + /// Maps import qualifier aliases to canonical (full) module names. + /// Used to canonicalize kind constructor qualifiers so that the same type + /// imported via different aliases produces identical kind representations. + pub qualifier_to_canonical: HashMap, } impl KindState { @@ -69,13 +82,12 @@ impl KindState { Type::fun(k_type.clone(), k_type.clone()), ); - // Well-known classes: (Type -> Type) -> Constraint - // These are safe to register as builtins because they're standard and their - // kind is well-established. Modules that define them locally will override - // in Pass A. Modules that import them will use these kinds for instance head checking. + // Well-known classes registered in class_kinds (separate from type_kinds + // to avoid collisions when a class and data type share the same name). let k_constraint = Type::kind_constraint(); let k_type_to_type = Type::fun(k_type.clone(), k_type.clone()); let k_type1_to_constraint = Type::fun(k_type_to_type.clone(), k_constraint.clone()); + let mut class_kinds = HashMap::new(); for name in &[ "Functor", "Foldable", "Traversable", "Apply", "Applicative", "Bind", "Monad", @@ -83,7 +95,7 @@ impl KindState { "Extend", "Comonad", "Unfoldable", "Unfoldable1", ] { - type_kinds.insert(interner::intern(name), k_type1_to_constraint.clone()); + class_kinds.insert(interner::intern(name), k_type1_to_constraint.clone()); } // Well-known classes: Type -> Constraint @@ -93,17 +105,37 @@ impl KindState { "Semiring", "Ring", "CommutativeRing", "EuclideanRing", "Field", "Semigroup", "Monoid", ] { - type_kinds.insert(interner::intern(name), k_type_to_constraint.clone()); + class_kinds.insert(interner::intern(name), k_type_to_constraint.clone()); } + // Prim.Symbol: IsSymbol :: Symbol -> Constraint + let k_symbol = Type::kind_symbol(); + class_kinds.insert( + interner::intern("IsSymbol"), + Type::fun(k_symbol.clone(), k_constraint.clone()), + ); + + // Note: We do NOT register Row.Cons, Union, Nub, Lacks here because their + // unqualified names collide with RowList.Cons and potentially other types. + // Instead, these are handled via qualified name lookup in the constraint + // processing code (infer_constraint_kinds). + KindState { state: UnifyState::new(), type_kinds, + class_kinds, binding_group: HashSet::new(), deferred_quantification_checks: Vec::new(), + class_param_kind_types: Vec::new(), + qualifier_to_canonical: HashMap::new(), } } + /// Convert a kind expression (delegates to the free function). + pub fn convert_kind_expr_canonical(&self, kind_expr: &TypeExpr) -> Type { + convert_kind_expr(kind_expr) + } + /// Create a fresh kind unification variable. pub fn fresh_kind_var(&mut self) -> Type { Type::Unif(self.state.fresh_var()) @@ -113,7 +145,7 @@ impl KindState { pub fn unify_kinds(&mut self, span: Span, expected: &Type, found: &Type) -> Result<(), TypeError> { self.state.unify(span, expected, found).map_err(|e| match e { TypeError::UnificationError { span, expected, found } => { - TypeError::KindMismatch { span, expected, found } + TypeError::KindsDoNotUnify { span, expected, found } } TypeError::InfiniteType { span, var, ty } => { TypeError::InfiniteKind { span, var, ty } @@ -130,18 +162,18 @@ impl KindState { /// Run deferred quantification checks. Call this after top-level kind /// inference is complete, when all kind unification variables are maximally /// constrained. Returns the first error found, if any. + /// + /// Automatically excludes unsolved vars from class type parameter kinds, + /// since those are legitimately determined by the outer class context. pub fn check_deferred_quantification(&mut self) -> Option { - self.check_deferred_quantification_excluding(&HashSet::new()) - } + // Compute exclude set from class type parameter kinds. + // Zonk now (at end of pass) so we get the maximally-constrained form. + let mut exclude: HashSet = HashSet::new(); + for kind_ty in std::mem::take(&mut self.class_param_kind_types) { + let zonked = self.zonk_kind(kind_ty); + collect_unif_var_ids(&zonked, &mut exclude); + } - /// Run deferred quantification checks, but exclude the given unif var IDs - /// from being considered "unsolved". This is used for class methods where - /// the class type parameter kind vars are legitimately unsolved but are - /// determined by the outer class context. - pub fn check_deferred_quantification_excluding( - &mut self, - exclude: &HashSet, - ) -> Option { for (span, var_ids) in std::mem::take(&mut self.deferred_quantification_checks) { for &var_id in &var_ids { if self.state.is_untouched(var_id) { @@ -151,8 +183,9 @@ impl KindState { } // Variable was used — check if its kind is fully determined let zonked = self.zonk_kind(Type::Unif(var_id)); - if kind_contains_unif_var_excluding(&zonked, exclude) { + if kind_contains_unif_var_excluding(&zonked, &exclude) { return Some(TypeError::QuantificationCheckFailureInType { + ty: zonked, span, }); } @@ -166,6 +199,20 @@ impl KindState { self.type_kinds.insert(name, kind); } + pub fn register_class_kind(&mut self, name: Symbol, kind: Type) { + self.class_kinds.insert(name, kind); + } + + /// Look up the kind of a class, freshening unsolved unification variables. + /// Falls back to type_kinds if not in class_kinds. + pub fn lookup_class_kind_fresh(&mut self, name: Symbol) -> Option { + let kind = self.class_kinds.get(&name) + .or_else(|| self.type_kinds.get(&name))? + .clone(); + let zonked = self.zonk_kind(kind); + Some(self.freshen_unif_vars(&zonked)) + } + /// Look up the kind of a type constructor, freshening unsolved unification /// variables so that each usage gets its own copy (prevents cross-contamination /// between declarations that share kind variables). @@ -232,7 +279,6 @@ pub fn check_kind_expr_supported(kind_expr: &TypeExpr) -> Result<(), TypeError> check_kind_expr_supported(arg) } TypeExpr::Forall { ty, .. } => check_kind_expr_supported(ty), - TypeExpr::Parens { ty, .. } => check_kind_expr_supported(ty), _ => Ok(()), } } @@ -243,7 +289,16 @@ pub fn check_kind_expr_supported(kind_expr: &TypeExpr) -> Result<(), TypeError> pub fn check_type_expr_partial_synonym( te: &TypeExpr, type_aliases: &HashMap, crate::typechecker::types::Type)>, - type_ops: &HashMap, + type_ops: &HashMap, +) -> Result<(), TypeError> { + check_type_expr_partial_synonym_inner(te, type_aliases, type_ops, false) +} + +fn check_type_expr_partial_synonym_inner( + te: &TypeExpr, + type_aliases: &HashMap, crate::typechecker::types::Type)>, + type_ops: &HashMap, + is_arg: bool, ) -> Result<(), TypeError> { // Count applied arguments and find the constructor at the head fn count_args(te: &TypeExpr) -> (&TypeExpr, usize) { @@ -266,20 +321,21 @@ pub fn check_type_expr_partial_synonym( Some(name.name) } else { // Operator-as-constructor like (~>) resolves to a type alias - type_ops.get(&name.name).copied() + type_ops.get(name).map(|qi| qi.name) } } TypeExpr::Var { name, .. } => { - type_ops.get(&name.value).copied() + let qi = QualifiedIdent { module: None, name: name.value }; + type_ops.get(&qi).map(|qi| qi.name) } _ => None, }; - if let Some(name) = alias_name { - if let Some((params, _)) = type_aliases.get(&name) { + if let Some(alias_sym) = alias_name { + if let Some((params, _)) = type_aliases.get(&alias_sym) { if arg_count < params.len() { return Err(TypeError::PartiallyAppliedSynonym { span: te.span(), - name, + name: QualifiedIdent { module: None, name: alias_sym }, }); } } @@ -295,74 +351,62 @@ pub fn check_type_expr_partial_synonym( } let mut args = Vec::new(); collect_app_args(te, &mut args); + // Args may be higher-kinded type arguments — pass is_arg=true for arg in args { - check_type_expr_partial_synonym(arg, type_aliases, type_ops)?; + check_type_expr_partial_synonym_inner(arg, type_aliases, type_ops, true)?; } // Check head if it's not a simple Constructor/Var (those were checked above) match head { TypeExpr::Constructor { .. } | TypeExpr::Var { .. } => {} - other => check_type_expr_partial_synonym(other, type_aliases, type_ops)?, + other => check_type_expr_partial_synonym_inner(other, type_aliases, type_ops, false)?, } Ok(()) } TypeExpr::Constructor { name, .. } => { + // When this constructor appears as an argument to a type application, + // it may be a higher-kinded type argument (e.g. `Id` in `ReactAttributesF Id r`). + // Don't flag these as partially applied. + if is_arg { + return Ok(()); + } // Unapplied constructor — check if it's a synonym with params // Also resolve through type_ops for operator-as-constructor like (~>) let resolved = if type_aliases.contains_key(&name.name) { Some(name.name) } else { - type_ops.get(&name.name).copied() + type_ops.get(name).map(|qi| qi.name) }; if let Some(alias_name) = resolved { if let Some((params, _)) = type_aliases.get(&alias_name) { if !params.is_empty() { return Err(TypeError::PartiallyAppliedSynonym { span: te.span(), - name: alias_name, + name: QualifiedIdent { module: None, name: alias_name }, }); } } } Ok(()) } - TypeExpr::TypeOp { span, op, left, right, .. } => { - // Resolve the operator to its target type name - let op_name = op.value.name; - let resolved = type_ops.get(&op_name).copied().unwrap_or(op_name); - if let Some((params, _)) = type_aliases.get(&resolved) { - // TypeOp always has 2 args (left and right) - if 2 < params.len() { - return Err(TypeError::PartiallyAppliedSynonym { - span: *span, - name: resolved, - }); - } - } - check_type_expr_partial_synonym(left, type_aliases, type_ops)?; - check_type_expr_partial_synonym(right, type_aliases, type_ops) - } TypeExpr::Function { from, to, .. } => { - check_type_expr_partial_synonym(from, type_aliases, type_ops)?; - check_type_expr_partial_synonym(to, type_aliases, type_ops) + check_type_expr_partial_synonym_inner(from, type_aliases, type_ops, false)?; + check_type_expr_partial_synonym_inner(to, type_aliases, type_ops, false) } TypeExpr::Forall { ty, vars, .. } => { // Check kind annotations on forall vars for (_, _, kind_ann) in vars { if let Some(k) = kind_ann { - check_type_expr_partial_synonym(k, type_aliases, type_ops)?; + check_type_expr_partial_synonym_inner(k, type_aliases, type_ops, false)?; } } - check_type_expr_partial_synonym(ty, type_aliases, type_ops) - } - TypeExpr::Parens { ty, .. } => { - check_type_expr_partial_synonym(ty, type_aliases, type_ops) + check_type_expr_partial_synonym_inner(ty, type_aliases, type_ops, false) } TypeExpr::Constrained { ty, .. } => { - check_type_expr_partial_synonym(ty, type_aliases, type_ops) + check_type_expr_partial_synonym_inner(ty, type_aliases, type_ops, false) } TypeExpr::Kinded { ty, kind, .. } => { - check_type_expr_partial_synonym(ty, type_aliases, type_ops)?; - check_type_expr_partial_synonym(kind, type_aliases, type_ops) + check_type_expr_partial_synonym_inner(ty, type_aliases, type_ops, false)?; + check_type_expr_partial_synonym_inner(kind, type_aliases, type_ops, false) } _ => Ok(()), } @@ -374,7 +418,7 @@ pub fn check_type_expr_partial_synonym( pub fn check_kind_annotations_for_partial_synonym( te: &TypeExpr, type_aliases: &HashMap, crate::typechecker::types::Type)>, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result<(), TypeError> { match te { TypeExpr::Kinded { kind, ty, .. } => { @@ -403,9 +447,6 @@ pub fn check_kind_annotations_for_partial_synonym( TypeExpr::Constrained { ty, .. } => { check_kind_annotations_for_partial_synonym(ty, type_aliases, type_ops) } - TypeExpr::Parens { ty, .. } => { - check_kind_annotations_for_partial_synonym(ty, type_aliases, type_ops) - } TypeExpr::Record { fields, .. } | TypeExpr::Row { fields, .. } => { for f in fields { check_kind_annotations_for_partial_synonym(&f.ty, type_aliases, type_ops)?; @@ -421,13 +462,7 @@ pub fn check_kind_annotations_for_partial_synonym( pub fn convert_kind_expr(kind_expr: &TypeExpr) -> Type { match kind_expr { TypeExpr::Constructor { name, .. } => { - if let Some(m) = name.module { - let mod_str = interner::resolve(m).unwrap_or_default(); - let name_str = interner::resolve(name.name).unwrap_or_default(); - Type::Con(interner::intern(&format!("{}.{}", mod_str, name_str))) - } else { - Type::Con(name.name) - } + Type::Con(QualifiedIdent { module: name.module, name: name.name }) } TypeExpr::Var { name, .. } => { Type::Var(name.value) @@ -442,7 +477,6 @@ pub fn convert_kind_expr(kind_expr: &TypeExpr) -> Type { let var_symbols: Vec<_> = vars.iter().map(|(v, vis, _kind)| (v.value, *vis)).collect(); Type::Forall(var_symbols, Box::new(convert_kind_expr(ty))) } - TypeExpr::Parens { ty, .. } => convert_kind_expr(ty), TypeExpr::Kinded { ty, .. } => convert_kind_expr(ty), TypeExpr::Record { fields, .. } => { // Record type in kind annotation: { label :: Kind, ... } @@ -476,25 +510,42 @@ pub fn infer_kind( ks: &mut KindState, te: &TypeExpr, type_var_kinds: &HashMap, - type_ops: &HashMap, + type_ops: &HashMap, self_type: Option, ) -> Result { super::check_deadline(); match te { TypeExpr::Constructor { name, .. } => { // Check if this is a type operator used as a constructor - if let Some(&target) = type_ops.get(&name.name) { + if let Some(target) = type_ops.get(name) { + let target_name = target.name; // Don't freshen for self-referencing or binding group members - let lookup = if self_type == Some(target) || ks.binding_group.contains(&target) { - ks.lookup_type(target).cloned() + let lookup = if self_type == Some(target_name) || ks.binding_group.contains(&target_name) { + ks.lookup_type(target_name).cloned() } else { - ks.lookup_type_fresh(target) + ks.lookup_type_fresh(target_name) }; if let Some(kind) = lookup { return Ok(kind); } } - // Don't freshen for self-referencing types or binding group members + // When a module qualifier is present, try the qualified name first. + // This ensures `Codec.Codec` resolves to the data type kind rather than + // a local alias kind when both share the unqualified name. + if let Some(m) = name.module { + let mod_str = interner::resolve(m).unwrap_or_default(); + let name_str = interner::resolve(name.name).unwrap_or_default(); + let qualified = interner::intern(&format!("{}.{}", mod_str, name_str)); + let in_group = ks.binding_group.contains(&qualified); + if let Some(kind) = if in_group { + ks.lookup_type(qualified).cloned() + } else { + ks.lookup_type_fresh(qualified) + } { + return Ok(kind); + } + } + // Fall back to unqualified lookup let in_group = self_type == Some(name.name) || ks.binding_group.contains(&name.name); let lookup = if in_group { ks.lookup_type(name.name).cloned() @@ -504,15 +555,6 @@ pub fn infer_kind( match lookup { Some(kind) => Ok(kind), None => { - // Check for qualified name - if let Some(m) = name.module { - let mod_str = interner::resolve(m).unwrap_or_default(); - let name_str = interner::resolve(name.name).unwrap_or_default(); - let qualified = interner::intern(&format!("{}.{}", mod_str, name_str)); - if let Some(kind) = ks.lookup_type_fresh(qualified) { - return Ok(kind); - } - } // Unknown type constructor — use a fresh kind variable so its kind // is inferred from usage. UnknownType errors are handled separately // during the type conversion pass. @@ -552,17 +594,23 @@ pub fn infer_kind( // f_kind should be k1 -> k2; unify k1 with a_kind, return k2 let result_kind = ks.fresh_kind_var(); - let expected_f_kind = Type::fun(a_kind, result_kind.clone()); - ks.unify_kinds(*span, &expected_f_kind, &f_kind)?; + let expected_f_kind = Type::fun(a_kind.clone(), result_kind.clone()); + if let Err(e) = ks.unify_kinds(*span, &expected_f_kind, &f_kind) { + return Err(e); + } Ok(result_kind) } TypeExpr::Function { span, from, to } => { let k_type = Type::kind_type(); let from_kind = infer_kind(ks, from, type_var_kinds, type_ops, self_type)?; - ks.unify_kinds(*span, &k_type, &from_kind)?; + if let Err(e) = ks.unify_kinds(*span, &k_type, &from_kind) { + return Err(e); + } let to_kind = infer_kind(ks, to, type_var_kinds, type_ops, self_type)?; - ks.unify_kinds(*span, &k_type, &to_kind)?; + if let Err(e) = ks.unify_kinds(*span, &k_type, &to_kind) { + return Err(e); + } Ok(k_type) } @@ -571,7 +619,7 @@ pub fn infer_kind( let mut unannotated_kind_var_ids: Vec = Vec::new(); for (v, _visible, kind_ann) in vars { let var_kind = match kind_ann { - Some(k) => convert_kind_expr(k), + Some(k) => ks.convert_kind_expr_canonical(k), None => { let id = ks.state.fresh_var(); unannotated_kind_var_ids.push(id); @@ -595,15 +643,21 @@ pub fn infer_kind( TypeExpr::Constrained { constraints, ty, .. } => { // Check constraint argument kinds against the class's expected parameter kinds for constraint in constraints { - let class_kind = ks.lookup_type_fresh(constraint.class.name); + let class_kind = ks.lookup_class_kind_fresh(constraint.class.name) + .or_else(|| lookup_prim_constraint_kind(ks, &constraint.class)); if let Some(class_kind) = class_kind { let class_kind = instantiate_kind(ks, &class_kind); let mut remaining = class_kind; for arg in &constraint.args { let arg_kind = infer_kind(ks, arg, type_var_kinds, type_ops, self_type)?; + // Instantiate to strip top-level forall from kind-polymorphic types + // (e.g. `type Fail :: forall k. k -> Type` used bare in constraints) + let arg_kind = instantiate_kind(ks, &arg_kind); let result = ks.fresh_kind_var(); let expected = Type::fun(arg_kind, result.clone()); - ks.unify_kinds(constraint.span, &expected, &remaining)?; + if let Err(e) = ks.unify_kinds(constraint.span, &expected, &remaining) { + return Err(e); + } remaining = result; } } else { @@ -617,10 +671,10 @@ pub fn infer_kind( } TypeExpr::Record { fields, .. } => { - // Record fields are typically kind Type, but we just infer each field's - // kind without constraining — the unification will catch mismatches. + // Record fields must have kind Type. for field in fields { - let _field_kind = infer_kind(ks, &field.ty, type_var_kinds, type_ops, self_type)?; + let field_kind = infer_kind(ks, &field.ty, type_var_kinds, type_ops, self_type)?; + ks.unify_kinds(field.span, &field_kind, &Type::kind_type())?; } Ok(Type::kind_type()) } @@ -655,33 +709,15 @@ pub fn infer_kind( } } - TypeExpr::Parens { ty, .. } => infer_kind(ks, ty, type_var_kinds, type_ops, self_type), - TypeExpr::Kinded { span, ty, kind } => { let inferred_kind = infer_kind(ks, ty, type_var_kinds, type_ops, self_type)?; - let annotated_kind = convert_kind_expr(kind); - ks.unify_kinds(*span, &annotated_kind, &inferred_kind)?; + let annotated_kind = ks.convert_kind_expr_canonical(kind); + if let Err(e) = ks.unify_kinds(*span, &annotated_kind, &inferred_kind) { + return Err(e); + } Ok(annotated_kind) } - TypeExpr::TypeOp { span, left, op, right } => { - let op_name = op.value.name; - let resolved = type_ops.get(&op_name).copied().unwrap_or(op_name); - let op_kind = match ks.lookup_type(resolved) { - Some(k) => k.clone(), - None => ks.fresh_kind_var(), - }; - let left_kind = infer_kind(ks, left, type_var_kinds, type_ops, self_type)?; - let right_kind = infer_kind(ks, right, type_var_kinds, type_ops, self_type)?; - - // op :: k1 -> k2 -> k3 - let result_kind = ks.fresh_kind_var(); - let k2_to_result = Type::fun(right_kind, result_kind.clone()); - let expected_op_kind = Type::fun(left_kind, k2_to_result); - ks.unify_kinds(*span, &expected_op_kind, &op_kind)?; - Ok(result_kind) - } - TypeExpr::StringLiteral { .. } => Ok(Type::kind_symbol()), TypeExpr::IntLiteral { .. } => Ok(Type::kind_int()), @@ -697,9 +733,9 @@ pub fn infer_data_kind( name: Symbol, type_vars: &[crate::cst::Spanned], type_var_kind_anns: &[Option>], - constructors: &[crate::cst::DataConstructor], + constructors: &[crate::ast::DataConstructor], span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result { let k_type = Type::kind_type(); let mut var_kinds = HashMap::new(); @@ -707,7 +743,7 @@ pub fn infer_data_kind( // Assign kind to each type variable (from annotation or fresh) for (i, tv) in type_vars.iter().enumerate() { let var_kind = if let Some(Some(kind_ann)) = type_var_kind_anns.get(i) { - convert_kind_expr(kind_ann) + ks.convert_kind_expr_canonical(kind_ann) } else { ks.fresh_kind_var() }; @@ -723,9 +759,16 @@ pub fn infer_data_kind( } } - // Check deferred quantification (forall vars with unsolved kinds) - if let Some(err) = ks.check_deferred_quantification() { - return Err(err); + // Note: deferred quantification checks are NOT run here — they are run at the + // end of the kind pass when all kind vars are maximally constrained. Running + // them per-declaration causes false positives when forall vars reference types + // whose kinds aren't yet fully inferred (e.g., type aliases defined later). + + // Save data type parameter kind types for end-of-pass exclusion, same as + // class parameters. Foralls in constructor fields may reference data type + // params whose kinds are intentionally polymorphic (e.g. `∀ k. (k → Type) → Type`). + for tv in type_vars { + ks.class_param_kind_types.push(var_kinds[&tv.value].clone()); } // Build the overall kind: k1 -> k2 -> ... -> Type @@ -739,6 +782,7 @@ pub fn infer_data_kind( } /// Infer the kind of a newtype declaration. +#[allow(clippy::too_many_arguments)] pub fn infer_newtype_kind( ks: &mut KindState, name: Symbol, @@ -746,14 +790,14 @@ pub fn infer_newtype_kind( type_var_kind_anns: &[Option>], field_ty: &TypeExpr, span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result { let k_type = Type::kind_type(); let mut var_kinds = HashMap::new(); for (i, tv) in type_vars.iter().enumerate() { let var_kind = if let Some(Some(kind_ann)) = type_var_kind_anns.get(i) { - convert_kind_expr(kind_ann) + ks.convert_kind_expr_canonical(kind_ann) } else { ks.fresh_kind_var() }; @@ -764,9 +808,12 @@ pub fn infer_newtype_kind( let field_kind = infer_kind(ks, field_ty, &var_kinds, type_ops, Some(name))?; ks.unify_kinds(span, &k_type, &field_kind)?; - // Check deferred quantification (forall vars with unsolved kinds) - if let Some(err) = ks.check_deferred_quantification() { - return Err(err); + // Note: deferred quantification checks are NOT run here — they are run at the + // end of the kind pass when all kind vars are maximally constrained. + + // Save newtype parameter kind types for end-of-pass exclusion. + for tv in type_vars { + ks.class_param_kind_types.push(var_kinds[&tv.value].clone()); } // Build kind: k1 -> k2 -> ... -> Type @@ -787,13 +834,13 @@ pub fn infer_type_alias_kind( type_var_kind_anns: &[Option>], body: &TypeExpr, _span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result { let mut var_kinds = HashMap::new(); for (i, tv) in type_vars.iter().enumerate() { let var_kind = if let Some(Some(kind_ann)) = type_var_kind_anns.get(i) { - convert_kind_expr(kind_ann) + ks.convert_kind_expr_canonical(kind_ann) } else { ks.fresh_kind_var() }; @@ -818,9 +865,9 @@ pub fn infer_class_kind( ks: &mut KindState, name: Symbol, type_vars: &[crate::cst::Spanned], - members: &[crate::cst::ClassMember], + members: &[crate::ast::ClassMember], _span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result { let mut var_kinds = HashMap::new(); @@ -829,34 +876,16 @@ pub fn infer_class_kind( var_kinds.insert(tv.value, var_kind); } - // Save the count of deferred quantification checks before member inference, - // so we only drain checks added by this class's members (not earlier type aliases). - let deferred_before = ks.deferred_quantification_checks.len(); - // Check member type signatures for member in members { let _member_kind = infer_kind(ks, &member.ty, &var_kinds, type_ops, Some(name))?; } - // Drain only the deferred quantification checks added during this class's member inference. - // Exclude all unif vars that appear in the (zonked) class type parameter kinds, - // since those are legitimately determined by the outer class context. - let class_deferred: Vec<_> = ks.deferred_quantification_checks.drain(deferred_before..).collect(); - let mut exclude_ids: HashSet = HashSet::new(); + // Save class type parameter kind types for end-of-pass exclusion. + // Foralls in class members may reference class type params whose kinds + // are intentionally flexible — these should not trigger quantification errors. for tv in type_vars { - let zonked = ks.zonk_kind(var_kinds[&tv.value].clone()); - collect_unif_var_ids(&zonked, &mut exclude_ids); - } - for (span, var_ids) in class_deferred { - for &var_id in &var_ids { - if ks.state.is_untouched(var_id) { - continue; - } - let zonked = ks.zonk_kind(Type::Unif(var_id)); - if kind_contains_unif_var_excluding(&zonked, &exclude_ids) { - return Err(TypeError::QuantificationCheckFailureInType { span }); - } - } + ks.class_param_kind_types.push(var_kinds[&tv.value].clone()); } // Build kind: k1 -> k2 -> ... -> Constraint @@ -895,8 +924,11 @@ pub fn create_temp_kind_state(ks: &mut KindState) -> KindState { let mut tmp = KindState { state: UnifyState::new(), type_kinds: HashMap::new(), + class_kinds: HashMap::new(), binding_group: HashSet::new(), deferred_quantification_checks: Vec::new(), + class_param_kind_types: Vec::new(), + qualifier_to_canonical: ks.qualifier_to_canonical.clone(), }; let mut mapping: HashMap = HashMap::new(); @@ -905,8 +937,15 @@ pub fn create_temp_kind_state(ks: &mut KindState) -> KindState { let remapped = remap_unif_vars(&zonked, &mut mapping, &mut tmp); tmp.type_kinds.insert(name, remapped); } + for (&name, kind) in &ks.class_kinds { + let zonked = ks.state.zonk(kind.clone()); + let remapped = remap_unif_vars(&zonked, &mut mapping, &mut tmp); + tmp.class_kinds.insert(name, remapped); + } // Copy type aliases so the temp state can expand them tmp.state.type_aliases = ks.state.type_aliases.clone(); + // Copy qualifier mapping so kind unification can detect module conflicts + tmp.state.qualifier_to_canonical = ks.state.qualifier_to_canonical.clone(); tmp } @@ -951,7 +990,7 @@ pub fn check_body_against_standalone_kind( body_fields: &[&TypeExpr], name: Symbol, span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Option { // Only applies to forall-quantified standalone kinds if !matches!(standalone, Type::Forall(..)) { @@ -980,7 +1019,7 @@ pub fn check_body_against_standalone_kind( match infer_kind(&mut tmp, field, &var_kinds, type_ops, Some(name)) { Ok(field_kind) => { if let Err(_) = tmp.unify_kinds(span, &k_type, &field_kind) { - return Some(TypeError::KindMismatch { + return Some(TypeError::KindsDoNotUnify { span, expected: k_type, found: tmp.zonk_kind(field_kind), @@ -1000,7 +1039,7 @@ pub fn check_body_against_standalone_kind( pub fn check_standalone_kind_quantification( ks: &mut KindState, kind_ty: &TypeExpr, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Option { // Only check forall kind sigs if let TypeExpr::Forall { vars, ty, span, .. } = kind_ty { @@ -1010,7 +1049,7 @@ pub fn check_standalone_kind_quantification( for (v, _visible, kind_ann) in vars { let var_kind = match kind_ann { - Some(k) => convert_kind_expr(k), + Some(k) => tmp.convert_kind_expr_canonical(k), None => { let id = tmp.state.fresh_var(); unannotated_ids.push(id); @@ -1038,7 +1077,7 @@ pub fn check_standalone_kind_quantification( continue; } if kind_contains_unif_var(&zonked) { - return Some(TypeError::QuantificationCheckFailureInKind { span: *span }); + return Some(TypeError::QuantificationCheckFailureInKind { ty: zonked, span: *span }); } } @@ -1054,7 +1093,7 @@ pub fn check_standalone_kind_quantification( continue; } if kind_contains_unif_var(&zonked) { - return Some(TypeError::QuantificationCheckFailureInKind { span: sub_span }); + return Some(TypeError::QuantificationCheckFailureInKind { ty: zonked, span: sub_span }); } } } @@ -1109,7 +1148,7 @@ fn type_expr_references_any(te: &TypeExpr, names: &HashSet) -> bool { } type_expr_references_any(ty, &inner_names) } - TypeExpr::Parens { ty, .. } | TypeExpr::Kinded { ty, .. } => { + TypeExpr::Kinded { ty, .. } => { type_expr_references_any(ty, names) } _ => false, @@ -1122,7 +1161,7 @@ fn type_expr_references_any(te: &TypeExpr, names: &HashSet) -> bool { pub fn check_type_expr_kind( ks: &mut KindState, te: &TypeExpr, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result { let mut tmp = create_temp_kind_state(ks); let empty_var_kinds = HashMap::new(); @@ -1146,12 +1185,12 @@ pub fn check_instance_head_kinds( class_name: Symbol, types: &[TypeExpr], span: Span, - type_ops: &HashMap, + type_ops: &HashMap, ) -> Result<(), TypeError> { let mut tmp = create_temp_kind_state(ks); // Look up the class kind and instantiate it - let class_kind = match tmp.lookup_type_fresh(class_name) { + let class_kind = match tmp.lookup_class_kind_fresh(class_name) { Some(k) => instantiate_kind(&mut tmp, &k), None => return Ok(()), // Unknown class — skip kind checking }; @@ -1176,10 +1215,10 @@ pub fn check_instance_head_kinds( /// like `Pair :: Pair Int "foo"` in expressions. pub fn check_value_decl_kinds( ks: &mut KindState, - binders: &[crate::cst::Binder], - guarded: &crate::cst::GuardedExpr, - where_clause: &[crate::cst::LetBinding], - type_ops: &HashMap, + binders: &[crate::ast::Binder], + guarded: &crate::ast::GuardedExpr, + where_clause: &[crate::ast::LetBinding], + type_ops: &HashMap, ) -> Vec { let mut type_exprs = Vec::new(); for b in binders { @@ -1189,18 +1228,35 @@ pub fn check_value_decl_kinds( for lb in where_clause { collect_type_exprs_from_let_binding(lb, &mut type_exprs); } + if type_exprs.is_empty() { + return Vec::new(); + } + // Create ONE temp kind state and reuse for all type expressions. + // This is safe because lookup_type_fresh freshens vars on each lookup. + let mut tmp = create_temp_kind_state(ks); + let empty_var_kinds = HashMap::new(); + let k_type = Type::kind_type(); let mut errors = Vec::new(); for te in type_exprs { - if let Err(e) = check_type_expr_kind(ks, te, type_ops) { - errors.push(e); + match infer_kind(&mut tmp, te, &empty_var_kinds, type_ops, None) { + Ok(kind) => { + let zonked = tmp.zonk_kind(kind); + if zonked != k_type && !matches!(zonked, Type::Unif(_)) { + errors.push(TypeError::ExpectedType { + span: te.span(), + found: zonked, + }); + } + } + Err(e) => errors.push(e), } } errors } -fn collect_type_exprs_from_expr<'a>(expr: &'a crate::cst::Expr, out: &mut Vec<&'a TypeExpr>) { - use crate::cst::Expr; +fn collect_type_exprs_from_expr<'a>(expr: &'a crate::ast::Expr, out: &mut Vec<&'a TypeExpr>) { + use crate::ast::Expr; match expr { Expr::TypeAnnotation { ty, expr, .. } => { out.push(ty); @@ -1220,10 +1276,6 @@ fn collect_type_exprs_from_expr<'a>(expr: &'a crate::cst::Expr, out: &mut Vec<&' } collect_type_exprs_from_expr(body, out); } - Expr::Op { left, right, .. } => { - collect_type_exprs_from_expr(left, out); - collect_type_exprs_from_expr(right, out); - } Expr::If { cond, then_expr, else_expr, .. } => { collect_type_exprs_from_expr(cond, out); collect_type_exprs_from_expr(then_expr, out); @@ -1270,9 +1322,6 @@ fn collect_type_exprs_from_expr<'a>(expr: &'a crate::cst::Expr, out: &mut Vec<&' collect_type_exprs_from_expr(&u.value, out); } } - Expr::Parens { expr, .. } => { - collect_type_exprs_from_expr(expr, out); - } Expr::Negate { expr, .. } => { collect_type_exprs_from_expr(expr, out); } @@ -1287,12 +1336,12 @@ fn collect_type_exprs_from_expr<'a>(expr: &'a crate::cst::Expr, out: &mut Vec<&' } // Terminal nodes with no sub-expressions or type annotations Expr::Var { .. } | Expr::Constructor { .. } | Expr::Literal { .. } - | Expr::OpParens { .. } | Expr::Hole { .. } => {} + | Expr::Hole { .. } => {} } } -fn collect_type_exprs_from_binder<'a>(binder: &'a crate::cst::Binder, out: &mut Vec<&'a TypeExpr>) { - use crate::cst::Binder; +pub fn collect_type_exprs_from_binder<'a>(binder: &'a crate::ast::Binder, out: &mut Vec<&'a TypeExpr>) { + use crate::ast::Binder; match binder { Binder::Typed { ty, binder, .. } => { out.push(ty); @@ -1310,7 +1359,7 @@ fn collect_type_exprs_from_binder<'a>(binder: &'a crate::cst::Binder, out: &mut } } } - Binder::As { binder, .. } | Binder::Parens { binder, .. } => { + Binder::As { binder, .. } => { collect_type_exprs_from_binder(binder, out); } Binder::Array { elements, .. } => { @@ -1318,16 +1367,12 @@ fn collect_type_exprs_from_binder<'a>(binder: &'a crate::cst::Binder, out: &mut collect_type_exprs_from_binder(e, out); } } - Binder::Op { left, right, .. } => { - collect_type_exprs_from_binder(left, out); - collect_type_exprs_from_binder(right, out); - } Binder::Wildcard { .. } | Binder::Var { .. } | Binder::Literal { .. } => {} } } -fn collect_type_exprs_from_guarded<'a>(g: &'a crate::cst::GuardedExpr, out: &mut Vec<&'a TypeExpr>) { - use crate::cst::GuardedExpr; +pub fn collect_type_exprs_from_guarded<'a>(g: &'a crate::ast::GuardedExpr, out: &mut Vec<&'a TypeExpr>) { + use crate::ast::GuardedExpr; match g { GuardedExpr::Unconditional(expr) => { collect_type_exprs_from_expr(expr, out); @@ -1336,8 +1381,8 @@ fn collect_type_exprs_from_guarded<'a>(g: &'a crate::cst::GuardedExpr, out: &mut for guard in guards { for p in &guard.patterns { match p { - crate::cst::GuardPattern::Boolean(e) => collect_type_exprs_from_expr(e, out), - crate::cst::GuardPattern::Pattern(b, e) => { + crate::ast::GuardPattern::Boolean(e) => collect_type_exprs_from_expr(e, out), + crate::ast::GuardPattern::Pattern(b, e) => { collect_type_exprs_from_binder(b, out); collect_type_exprs_from_expr(e, out); } @@ -1349,8 +1394,8 @@ fn collect_type_exprs_from_guarded<'a>(g: &'a crate::cst::GuardedExpr, out: &mut } } -fn collect_type_exprs_from_let_binding<'a>(lb: &'a crate::cst::LetBinding, out: &mut Vec<&'a TypeExpr>) { - use crate::cst::LetBinding; +pub fn collect_type_exprs_from_let_binding<'a>(lb: &'a crate::ast::LetBinding, out: &mut Vec<&'a TypeExpr>) { + use crate::ast::LetBinding; match lb { LetBinding::Value { binder, expr, .. } => { collect_type_exprs_from_binder(binder, out); @@ -1362,8 +1407,8 @@ fn collect_type_exprs_from_let_binding<'a>(lb: &'a crate::cst::LetBinding, out: } } -fn collect_type_exprs_from_do_statement<'a>(s: &'a crate::cst::DoStatement, out: &mut Vec<&'a TypeExpr>) { - use crate::cst::DoStatement; +fn collect_type_exprs_from_do_statement<'a>(s: &'a crate::ast::DoStatement, out: &mut Vec<&'a TypeExpr>) { + use crate::ast::DoStatement; match s { DoStatement::Bind { binder, expr, .. } => { collect_type_exprs_from_binder(binder, out); @@ -1460,7 +1505,7 @@ pub fn skolemize_kind(kind: &Type) -> Type { let mut subst = HashMap::new(); for (var, _visible) in vars { let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let skolem = Type::Con(interner::intern(&format!("$kind_skolem_{}", n))); + let skolem = Type::Con(QualifiedIdent { module: None, name: interner::intern(&format!("$kind_skolem_{}", n)) }); subst.insert(*var, skolem); } substitute_kind_vars(&subst, body) @@ -1471,6 +1516,59 @@ pub fn skolemize_kind(kind: &Type) -> Type { /// Instantiate a polymorphic kind signature by replacing forall-bound variables /// with fresh kind unification variables. Returns the instantiated kind. +/// Look up the kind of a known Prim constraint class by its qualified name. +/// This handles classes like Row.Cons, Row.Union, Row.Nub, Row.Lacks which cannot +/// be registered in the global type_kinds map because their unqualified names +/// collide with other types (e.g. RowList.Cons). +fn lookup_prim_constraint_kind( + ks: &mut KindState, + class: &QualifiedIdent, +) -> Option { + let name_str = crate::interner::resolve(class.name).unwrap_or_default(); + let module_str = class.module.and_then(|m| crate::interner::resolve(m)); + let module_str = module_str.as_deref().unwrap_or(""); + + let k_type = Type::kind_type(); + let k_constraint = Type::kind_constraint(); + let k_symbol = Type::kind_symbol(); + let _k_row_type = Type::kind_row_of(k_type.clone()); + + match (module_str, name_str.as_str()) { + // Row.Cons :: Symbol -> k -> Row k -> Row k -> Constraint + // Use fresh kind var for k to support polykinded rows + ("Row" | "RowCons" | "Prim.Row", "Cons") => { + let k = ks.fresh_kind_var(); + let k_row_k = Type::kind_row_of(k.clone()); + Some(Type::fun( + k_symbol, + Type::fun(k.clone(), Type::fun(k_row_k.clone(), Type::fun(k_row_k, k_constraint))), + )) + } + // Row.Union :: forall k. Row k -> Row k -> Row k -> Constraint + ("Row" | "Prim.Row", "Union") | ("", "Union") => { + let k = ks.fresh_kind_var(); + let k_row_k = Type::kind_row_of(k); + Some(Type::fun( + k_row_k.clone(), + Type::fun(k_row_k.clone(), Type::fun(k_row_k, k_constraint)), + )) + } + // Row.Nub :: forall k. Row k -> Row k -> Constraint + ("Row" | "Prim.Row", "Nub") | ("", "Nub") => { + let k = ks.fresh_kind_var(); + let k_row_k = Type::kind_row_of(k); + Some(Type::fun(k_row_k.clone(), Type::fun(k_row_k, k_constraint))) + } + // Row.Lacks :: forall k. Symbol -> Row k -> Constraint + ("Row" | "Prim.Row", "Lacks") | ("", "Lacks") => { + let k = ks.fresh_kind_var(); + let k_row_k = Type::kind_row_of(k); + Some(Type::fun(k_symbol, Type::fun(k_row_k, k_constraint))) + } + _ => None, + } +} + pub fn instantiate_kind(ks: &mut KindState, kind: &Type) -> Type { match kind { Type::Forall(vars, body) => { @@ -1501,7 +1599,6 @@ fn collect_type_refs(te: &TypeExpr, out: &mut HashSet) { for c in constraints { for a in &c.args { collect_type_refs(a, out); } } collect_type_refs(ty, out); } - TypeExpr::Parens { ty, .. } => collect_type_refs(ty, out), TypeExpr::Kinded { ty, kind, .. } => { collect_type_refs(ty, out); collect_type_refs(kind, out); @@ -1509,18 +1606,14 @@ fn collect_type_refs(te: &TypeExpr, out: &mut HashSet) { TypeExpr::Record { fields, .. } | TypeExpr::Row { fields, .. } => { for f in fields { collect_type_refs(&f.ty, out); } } - TypeExpr::TypeOp { left, right, .. } => { - collect_type_refs(left, out); - collect_type_refs(right, out); - } _ => {} } } /// Compute SCCs of type declarations for kind inference binding groups. /// Returns groups in topological order (dependencies first). -pub fn compute_type_sccs(decls: &[crate::cst::Decl]) -> Vec> { - use crate::cst::Decl; +pub fn compute_type_sccs(decls: &[crate::ast::Decl]) -> Vec> { + use crate::ast::Decl; // Collect all local type names and their dependencies let mut local_types: HashSet = HashSet::new(); @@ -1618,11 +1711,14 @@ pub fn compute_type_sccs(decls: &[crate::cst::Decl]) -> Vec> { } } - // Group by component and return in topological order + // Group by component and return in topological order (dependencies first). + // Kosaraju's assigns component IDs in reverse topological order of the + // condensation graph, so we reverse to get dependencies-first order. let mut groups: Vec> = vec![vec![]; num_comp]; for (i, &c) in comp.iter().enumerate() { groups[c].push(names[i]); } + groups.reverse(); groups } @@ -1667,8 +1763,11 @@ pub fn check_inferred_type_kind( let mut ks = KindState { state: UnifyState::new(), type_kinds: HashMap::new(), + class_kinds: HashMap::new(), binding_group: HashSet::new(), deferred_quantification_checks: Vec::new(), + class_param_kind_types: Vec::new(), + qualifier_to_canonical: HashMap::new(), }; // Re-map old Unif vars from the kind pass to fresh Unif vars in the new state. // Old Unif IDs reference the kind pass's UnifyState; we need them to reference @@ -1699,7 +1798,16 @@ fn infer_runtime_kind( ) -> Result { match ty { Type::Con(name) => { - match ks.lookup_type_fresh(*name) { + // Try qualified lookup first (e.g., "P.Props" for a qualified import) + if let Some(m) = name.module { + let mod_str = crate::interner::resolve(m).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + if let Some(kind) = ks.lookup_type_fresh(qualified) { + return Ok(instantiate_kind(ks, &kind)); + } + } + match ks.lookup_type_fresh(name.name) { Some(kind) => Ok(instantiate_kind(ks, &kind)), None => Ok(ks.fresh_kind_var()), } @@ -1710,8 +1818,10 @@ fn infer_runtime_kind( let f_kind = infer_runtime_kind(f, ks, span)?; let a_kind = infer_runtime_kind(a, ks, span)?; let result_kind = ks.fresh_kind_var(); - let expected = Type::fun(a_kind, result_kind.clone()); - ks.unify_kinds(span, &expected, &f_kind)?; + let expected = Type::fun(a_kind.clone(), result_kind.clone()); + if let Err(e) = ks.unify_kinds(span, &expected, &f_kind) { + return Err(e); + } Ok(result_kind) } Type::Fun(from, to) => { diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index ad3b94e0..293f175d 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -6,60 +6,102 @@ pub mod infer; pub mod convert; pub mod check; pub mod kind; +pub mod registry; +pub mod resolve; + +use std::collections::HashMap; -use crate::cst::{Expr, Module}; -use crate::typechecker::env::Env; use crate::typechecker::error::TypeError; -use crate::typechecker::infer::InferCtx; use crate::typechecker::types::Type; -pub use check::{CheckResult, ModuleExports, ModuleRegistry}; +pub use check::CheckResult; +pub use registry::{ModuleExports, ModuleRegistry}; +pub use resolve::{ResolvedResult, ResolvedName, Namespace, DefinitionSite, ResolutionExports}; // ===== Deadline mechanism for aborting long-running typechecks ===== use std::time::Instant; thread_local! { - static DEADLINE: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static DEADLINE: std::cell::Cell> = const { std::cell::Cell::new(None) }; + static DEADLINE_PATH: std::cell::RefCell = const { std::cell::RefCell::new(String::new()) }; + static DEADLINE_COUNTER: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// Set a per-thread deadline. If `check_deadline()` is called after this /// instant, it will panic (caught by `catch_unwind` in the build pipeline). -pub fn set_deadline(deadline: Option) { - DEADLINE.with(|d| d.set(deadline)); +pub fn set_deadline(deadline: Option, module_name: crate::interner::Symbol, path: &str) { + DEADLINE.with(|d| d.set(deadline.map(|dl| (dl, module_name)))); + DEADLINE_PATH.with(|p| *p.borrow_mut() = path.to_string()); + DEADLINE_COUNTER.with(|c| c.set(0)); } /// Check the thread-local deadline; panic if exceeded. /// Called from hot paths in the typechecker (infer, check, unify). +/// Only actually checks the clock every 4096 calls to minimize overhead. #[inline] +#[track_caller] pub fn check_deadline() { - DEADLINE.with(|d| { - if let Some(deadline) = d.get() { - if Instant::now() > deadline { - panic!("typechecking deadline exceeded"); - } + DEADLINE_COUNTER.with(|c| { + let n = c.get().wrapping_add(1); + c.set(n); + if n & 0xFFF != 0 { + return; } + let caller = std::panic::Location::caller(); + DEADLINE.with(|d| { + if let Some((deadline, module)) = d.get() { + if Instant::now() > deadline { + let name = crate::interner::resolve(module).unwrap_or_default(); + let path = DEADLINE_PATH.with(|p| p.borrow().clone()); + panic!( + "typechecking deadline exceeded for module '{}' at '{}' (called from {}:{}:{})", + name, path, caller.file(), caller.line(), caller.column() + ); + } + } + }); }); } -/// Infer the type of an expression in an empty environment. -pub fn infer_expr(expr: &Expr) -> Result { - let mut ctx = InferCtx::new(); - let env = Env::new(); - let ty = ctx.infer(&env, expr)?; +/// Infer the type of a CST expression in an empty environment. +/// Note: standalone expression inference still uses CST types since +/// `ast::convert` operates on whole modules, not standalone expressions. +pub fn infer_expr(expr: &crate::cst::Expr) -> Result { + // Convert the CST expression to AST by wrapping in a minimal module + let ast_expr = crate::ast::convert_expr(expr.clone()); + let mut ctx = infer::InferCtx::new(); + let env = env::Env::new(); + let ty = ctx.infer(&env, &ast_expr)?; Ok(ctx.state.zonk(ty)) } -/// Infer the type of an expression with a pre-populated environment. -pub fn infer_expr_with_env(env: &Env, expr: &Expr) -> Result { - let mut ctx = InferCtx::new(); - let ty = ctx.infer(env, expr)?; +/// Infer the type of a CST expression with a pre-populated environment. +pub fn infer_expr_with_env(env: &env::Env, expr: &crate::cst::Expr) -> Result { + let ast_expr = crate::ast::convert_expr(expr.clone()); + let mut ctx = infer::InferCtx::new(); + let ty = ctx.infer(env, &ast_expr)?; Ok(ctx.state.zonk(ty)) } -/// Typecheck a full module, returning partial results and accumulated errors. -pub fn check_module(module: &Module) -> CheckResult { - check::check_module(module, &ModuleRegistry::default()) +/// Typecheck a full CST module, returning partial results and accumulated errors. +/// Performs CST→AST conversion internally; returns conversion errors if any. +pub fn check_module(module: &crate::cst::Module) -> CheckResult { + check_module_with_registry(module, &ModuleRegistry::default()) +} + +/// Typecheck a full CST module with a registry, returning partial results and accumulated errors. +/// Performs CST→AST conversion internally; returns conversion errors if any. +pub fn check_module_with_registry(module: &crate::cst::Module, registry: &ModuleRegistry) -> CheckResult { + let (ast_module, convert_errors) = crate::ast::convert(module.clone(), registry); + if !convert_errors.is_empty() { + return CheckResult { + types: HashMap::new(), + errors: convert_errors, + exports: ModuleExports::default(), + }; + } + check::check_module(&ast_module, registry) } #[cfg(test)] @@ -261,7 +303,7 @@ mod tests { let ty = check_expr("[]").unwrap(); match ty { Type::App(f, _) => { - assert_eq!(*f, Type::Con(interner::intern("Array"))); + assert_eq!(*f, Type::prim_con("Array")); } other => panic!("Expected Array type, got: {}", other), } @@ -370,7 +412,7 @@ mod tests { let x_sym = interner::intern("x"); assert_eq!( *types.get(&x_sym).unwrap(), - Type::Con(interner::intern("MyBool")), + Type::con_local( "MyBool"), ); } @@ -382,7 +424,7 @@ mod tests { let x_sym = interner::intern("x"); assert_eq!( *types.get(&x_sym).unwrap(), - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -399,7 +441,7 @@ f x = case x of let f_ty = types.get(&f_sym).unwrap(); match f_ty { Type::Fun(from, to) => { - assert_eq!(**from, Type::Con(interner::intern("MyBool"))); + assert_eq!(**from, Type::con("T", "MyBool")); assert_eq!(**to, Type::int()); } other => panic!("Expected function type, got: {}", other), diff --git a/src/typechecker/registry.rs b/src/typechecker/registry.rs new file mode 100644 index 00000000..0fb44011 --- /dev/null +++ b/src/typechecker/registry.rs @@ -0,0 +1,113 @@ +use std::collections::{HashMap, HashSet}; + +use crate::cst::{Associativity, QualifiedIdent}; +use crate::interner::Symbol; +use crate::typechecker::types::{Role, Scheme, Type}; + +/// Exported information from a type-checked module, available for import by other modules. +#[derive(Debug, Clone, Default)] +pub struct ModuleExports { + /// Value/constructor/method schemes + pub values: HashMap, + /// Class method info: method_name → (class_name, class_type_vars) + pub class_methods: HashMap)>, + /// Data type → constructor names + pub data_constructors: HashMap>, + /// Constructor details: ctor_name → (parent_type, type_vars, field_types) + pub ctor_details: HashMap, Vec)>, + /// Class instances: class_name → [(types, constraints)] + pub instances: HashMap, Vec<(QualifiedIdent, Vec)>)>>, + /// Type-level operators: op → target type name + pub type_operators: HashMap, + /// Value-level operator fixities: operator → (associativity, precedence) + pub value_fixities: HashMap, + /// Type-level operator fixities: operator → (associativity, precedence) + pub type_fixities: HashMap, + /// Value-level operators that alias functions (not constructors) + pub function_op_aliases: HashSet, + /// Value-level operator targets: operator → target name (e.g. + → add, : → Cons) + pub value_operator_targets: HashMap, + /// Class methods whose declared type has extra constraints (e.g. `Applicative m =>`). + /// Used for CycleInDeclaration detection across module boundaries. + pub constrained_class_methods: HashSet, + /// Type aliases: alias_name → (params, body_type) + pub type_aliases: HashMap, Type)>, + /// Class definitions: class_name → param_count (for arity checking and orphan detection) + pub class_param_counts: HashMap, + /// Origin tracking: name → original defining module symbol. + /// Used to distinguish re-exports of the same definition from + /// independently defined names that happen to have the same type. + pub value_origins: HashMap, + pub type_origins: HashMap, + pub class_origins: HashMap, + /// Operator → class method target (e.g. `<>` → `append`). + /// Used for tracking deferred constraints on operator usage. + pub operator_class_targets: HashMap, + /// Class functional dependencies: class_name → (type_vars, fundeps as index pairs). + /// Used for fundep-aware orphan instance checking. + pub class_fundeps: HashMap, Vec<(Vec, Vec)>)>, + /// Type constructor arities: type_name → number of type parameters. + /// Used to detect over-applied types after type alias expansion. + pub type_con_arities: HashMap, + /// Roles for each type constructor: type_name → [Role per type parameter]. + pub type_roles: HashMap>, + /// Set of type names declared as newtypes (for Coercible solving). + pub newtype_names: HashSet, + /// Signature constraints for exported functions: name → [(class_name, type_args)]. + pub signature_constraints: HashMap)>>, + /// Type constructor kinds: type_name → kind Type. + /// Used for cross-module kind checking (e.g., detecting kind mismatches + /// between types with the same unqualified name from different modules). + pub type_kinds: HashMap, + /// Class kinds: class_name → kind (e.g., Type -> Constraint). + /// Separate from type_kinds so class kinds don't interfere with type checking + /// when a class and data type share the same name. + pub class_type_kinds: HashMap, + /// Functions whose type has Partial in a function parameter position, + /// e.g. `unsafePartial :: (Partial => a) -> a`. These discharge Partial + /// when applied to a partial expression. + pub partial_dischargers: HashSet, + /// Pre-computed self-referential type aliases from this module. + /// Imported at import time to avoid recomputing from scratch. + pub self_referential_aliases: HashSet, +} + +/// Registry of compiled modules, used to resolve imports. +/// +/// Supports layering: a child registry can be created with `with_base()`, +/// which shares an immutable base via `Arc` (zero-copy) and stores new +/// modules in a local overlay. Lookups check the overlay first, then the base. +#[derive(Debug, Clone, Default)] +pub struct ModuleRegistry { + modules: HashMap, ModuleExports>, + base: Option>, +} + +impl ModuleRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Create a child registry layered on top of a shared base. + /// New modules are stored locally; lookups fall through to the base. + pub fn with_base(base: std::sync::Arc) -> Self { + Self { + modules: HashMap::new(), + base: Some(base), + } + } + + pub fn register(&mut self, name: &[Symbol], exports: ModuleExports) { + self.modules.insert(name.to_vec(), exports); + } + + pub fn lookup(&self, name: &[Symbol]) -> Option<&ModuleExports> { + self.modules + .get(name) + .or_else(|| self.base.as_ref().and_then(|b| b.lookup(name))) + } + + pub fn contains(&self, name: &[Symbol]) -> bool { + self.modules.contains_key(name) || self.base.as_ref().map_or(false, |b| b.contains(name)) + } +} diff --git a/src/typechecker/resolve.rs b/src/typechecker/resolve.rs new file mode 100644 index 00000000..d32fb326 --- /dev/null +++ b/src/typechecker/resolve.rs @@ -0,0 +1,2722 @@ + +use std::collections::{HashMap, HashSet}; + +use crate::span::Span; +use crate::cst::{ + Binder, CaseAlternative, Decl, DoStatement, Export, Expr, GuardPattern, GuardedExpr, + ImportList, LetBinding, Literal, Module, QualifiedIdent, TypeExpr, +}; +use crate::interner::{self, Symbol}; +use crate::typechecker::error::TypeError; + +// ===== Public types ===== + +/// Names exported by a module, organized by namespace. +/// Used to resolve open/hiding imports without needing full type information. +#[derive(Clone)] +struct ModuleResolvedNames { + values: HashSet, + types: HashSet, + classes: HashSet, + type_operators: HashSet, + /// Constructors per data type (for `Type(..)` import resolution) + data_constructors: HashMap>, +} + +impl ModuleResolvedNames { + fn new() -> Self { + ModuleResolvedNames { + values: HashSet::new(), + types: HashSet::new(), + classes: HashSet::new(), + type_operators: HashSet::new(), + data_constructors: HashMap::new(), + } + } + + fn merge_from(&mut self, other: &ModuleResolvedNames) { + self.values.extend(&other.values); + self.types.extend(&other.types); + self.classes.extend(&other.classes); + self.type_operators.extend(&other.type_operators); + for (k, v) in &other.data_constructors { + self.data_constructors.entry(*k).or_default().extend(v); + } + } +} + +pub struct ResolutionExports { + modules: HashMap, +} + +impl ResolutionExports { + pub fn new(modules: &[Module]) -> Self { + // Pass 1: collect all declared names per module (unfiltered) + let mut all_names_map: HashMap = HashMap::new(); + for module in modules { + let mod_sym = module_name_to_symbol(&module.name.value); + let all_names = collect_module_all_names(module); + all_names_map.insert(mod_sym, all_names); + } + + // Add Prim submodule names so `module Prim.Row` re-exports work + for sub in &[ + "Boolean", "Coerce", "Int", "Ordering", "Row", "RowList", "Symbol", "TypeError", + ] { + let prim_mod_name = crate::cst::ModuleName { + parts: vec![interner::intern("Prim"), interner::intern(sub)], + }; + let prim_sym = interner::intern(&format!("Prim.{}", sub)); + let prim_exports = super::check::prim_submodule_exports(&prim_mod_name); + all_names_map.insert(prim_sym, module_exports_to_resolved_names(&prim_exports)); + } + + // Pass 2: filter by export lists. Run multiple iterations so transitive + // re-exports (A re-exports B which re-exports C) converge regardless of + // processing order. + let mut result: HashMap = HashMap::new(); + for _iteration in 0..3 { + for module in modules { + let mod_sym = module_name_to_symbol(&module.name.value); + let all_names = all_names_map.get(&mod_sym).unwrap(); + let exported = match &module.exports { + Some(export_list) => { + let mut qualifier_to_modules: HashMap> = + HashMap::new(); + for imp in &module.imports { + if let Some(q) = &imp.qualified { + let q_sym = module_name_to_symbol(q); + let imp_mod = module_name_to_symbol(&imp.module); + qualifier_to_modules + .entry(q_sym) + .or_default() + .push(imp_mod); + } + } + filter_by_exports( + all_names, + &export_list.value.exports, + mod_sym, + &qualifier_to_modules, + &result, + &all_names_map, + ) + } + None => all_names.clone(), + }; + result.insert(mod_sym, exported); + } + } + ResolutionExports { modules: result } + } + + pub fn empty() -> Self { + ResolutionExports { + modules: HashMap::new(), + } + } + + fn get(&self, module: Symbol) -> Option<&ModuleResolvedNames> { + self.modules.get(&module) + } +} + +/// Result of name resolution for a module. +pub struct ResolvedResult { + /// Name resolution errors (unresolved names, scope conflicts, etc.) + pub errors: Vec, + /// All resolutions, sorted by `src_span.start` for binary search. + resolutions: Vec, + /// Resolutions keyed by source span for direct lookup (index into `resolutions`). + resolution_map: HashMap, +} + +impl ResolvedResult { + /// Create an empty result with no resolutions or errors. + pub fn empty() -> Self { + ResolvedResult { + errors: Vec::new(), + resolutions: Vec::new(), + resolution_map: HashMap::new(), + } + } + + /// Look up the resolution for a given byte offset (e.g. cursor position). + /// Returns the resolution whose span contains the offset, if any. + pub fn lookup_at(&self, offset: usize) -> Option<&ResolvedName> { + // Binary search for the last span whose start <= offset + let idx = self + .resolutions + .partition_point(|r| r.src_span.start <= offset); + if idx == 0 { + return None; + } + let r = &self.resolutions[idx - 1]; + if offset < r.src_span.end { + Some(r) + } else { + None + } + } + + /// Look up the resolution for an exact source span. + pub fn lookup_at_span(&self, span: Span) -> Option<&ResolvedName> { + self.resolution_map + .get(&span) + .map(|&idx| &self.resolutions[idx]) + } +} + +/// A single name resolution: maps a usage site to its definition. +#[derive(Clone)] +pub struct ResolvedName { + /// Span of the name at the usage site + pub src_span: Span, + /// The symbol as written at the usage site + pub src_symbol: Symbol, + /// The namespace of the name + pub namespace: Namespace, + /// Where the definition lives + pub definition: DefinitionSite, +} + +/// What kind of name this is. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Namespace { + Value, + Type, + Class, + TypeOperator, +} + +/// Where a name is defined. +#[derive(Debug, Clone)] +pub enum DefinitionSite { + /// Defined in the current module at this span + Local(Span), + /// Imported from another module + Imported(Symbol), + /// Built-in from Prim + Prim, + /// Local variable (lambda param, let binding, case binder, etc.) + LocalVar(Span), +} + +// ===== Internal types ===== + +/// Origin of a name in the scope. +#[derive(Clone)] +enum NameOrigin { + /// Defined locally in this module at this declaration span + Local(Span), + /// Imported from module (module name as symbol e.g. "Data.Array") + Imported(Symbol), + /// Built-in from Prim + Prim, +} + +impl NameOrigin { + fn to_definition_site(&self) -> DefinitionSite { + match self { + NameOrigin::Local(span) => DefinitionSite::Local(*span), + NameOrigin::Imported(module) => DefinitionSite::Imported(*module), + NameOrigin::Prim => DefinitionSite::Prim, + } + } +} + +/// Names available at module scope, partitioned by namespace. +/// Each name maps to its origin (where it was defined). +struct NameScope { + values: HashMap, + types: HashMap, + classes: HashMap, + type_operators: HashMap, +} + +impl NameScope { + fn new() -> Self { + NameScope { + values: HashMap::new(), + types: HashMap::new(), + classes: HashMap::new(), + type_operators: HashMap::new(), + } + } +} + +/// Local variable scope: name → span where introduced. +type LocalScope = HashMap; + +/// Context for the resolution walk. +struct Resolver<'a> { + scope: &'a NameScope, + resolutions: Vec, + errors: Vec, +} + +// ===== Helpers ===== + +fn qualified_symbol(module: Symbol, name: Symbol) -> Symbol { + let mod_str = interner::resolve(module).unwrap_or_default(); + let name_str = interner::resolve(name).unwrap_or_default(); + interner::intern(&format!("{}.{}", mod_str, name_str)) +} + +fn is_prim_module(module: &crate::cst::ModuleName) -> bool { + module.parts.len() == 1 && interner::resolve(module.parts[0]).unwrap_or_default() == "Prim" +} + +fn is_prim_submodule(module: &crate::cst::ModuleName) -> bool { + module.parts.len() > 1 && interner::resolve(module.parts[0]).unwrap_or_default() == "Prim" +} + +fn module_name_to_symbol(module: &crate::cst::ModuleName) -> Symbol { + let parts: Vec = module + .parts + .iter() + .map(|s| interner::resolve(*s).unwrap_or_default().to_string()) + .collect(); + interner::intern(&parts.join(".")) +} + +fn maybe_qualify(name: Symbol, qualifier: Option) -> Symbol { + match qualifier { + Some(q) => qualified_symbol(q, name), + None => name, + } +} + +// ===== Module export collection ===== + +/// Convert a `ModuleExports` (from the typechecker, used for Prim) into a `ModuleResolvedNames`. +fn module_exports_to_resolved_names(exports: &super::registry::ModuleExports) -> ModuleResolvedNames { + let mut names = ModuleResolvedNames::new(); + for name in exports.values.keys() { + names.values.insert(name.name); + } + for (ty_name, ctors) in &exports.data_constructors { + names.types.insert(ty_name.name); + for ctor in ctors { + names.values.insert(ctor.name); + } + names.data_constructors.insert(ty_name.name, ctors.iter().map(|c| c.name).collect()); + } + for name in exports.instances.keys() { + names.classes.insert(name.name); + } + for (op, _) in &exports.type_operators { + names.type_operators.insert(op.name); + } + for op in exports.value_fixities.keys() { + names.values.insert(op.name); + } + for name in exports.class_methods.keys() { + names.values.insert(name.name); + } + names +} + +/// Collect all declared names from a module's declarations. +fn collect_module_all_names(module: &Module) -> ModuleResolvedNames { + let mut names = ModuleResolvedNames::new(); + for decl in &module.decls { + match decl { + Decl::Value { name, .. } => { + names.values.insert(name.value); + } + Decl::Data { + name, + constructors, + kind_sig, + is_role_decl, + .. + } => { + names.types.insert(name.value); + if *kind_sig == crate::cst::KindSigSource::None && !*is_role_decl { + let ctors: Vec = + constructors.iter().map(|c| c.name.value).collect(); + for &ctor in &ctors { + names.values.insert(ctor); + } + names.data_constructors.insert(name.value, ctors); + } + } + Decl::Newtype { + name, constructor, .. + } => { + names.types.insert(name.value); + names.values.insert(constructor.value); + names + .data_constructors + .insert(name.value, vec![constructor.value]); + } + Decl::TypeAlias { name, .. } => { + names.types.insert(name.value); + } + Decl::ForeignData { name, .. } => { + names.types.insert(name.value); + } + Decl::Foreign { name, .. } => { + names.values.insert(name.value); + } + Decl::Class { name, members, .. } => { + names.classes.insert(name.value); + for member in members { + names.values.insert(member.name.value); + } + } + Decl::Fixity { + is_type, operator, .. + } => { + if *is_type { + names.type_operators.insert(operator.value); + } else { + names.values.insert(operator.value); + } + } + Decl::Instance { .. } | Decl::Derive { .. } | Decl::TypeSignature { .. } => {} + } + } + names +} + +/// Filter a module's names by its explicit export list. +fn filter_by_exports( + all_names: &ModuleResolvedNames, + exports: &[Export], + current_module: Symbol, + qualifier_to_modules: &HashMap>, + resolved_modules: &HashMap, + all_modules_names: &HashMap, +) -> ModuleResolvedNames { + let mut result = ModuleResolvedNames::new(); + for export in exports { + match export { + Export::Value(name) => { + if all_names.values.contains(name) { + result.values.insert(*name); + } + } + Export::Type(name, members) => { + if all_names.types.contains(name) { + result.types.insert(*name); + } + match members { + Some(crate::cst::DataMembers::All) => { + if let Some(ctors) = all_names.data_constructors.get(name) { + for ctor in ctors { + result.values.insert(*ctor); + } + result.data_constructors.insert(*name, ctors.clone()); + } + } + Some(crate::cst::DataMembers::Explicit(names)) => { + let mut exported_ctors = Vec::new(); + for n in names { + result.values.insert(*n); + exported_ctors.push(*n); + } + if !exported_ctors.is_empty() { + result.data_constructors.insert(*name, exported_ctors); + } + } + None => {} + } + } + Export::TypeOp(name) => { + if all_names.type_operators.contains(name) { + result.type_operators.insert(*name); + } + } + Export::Class(name) => { + if all_names.classes.contains(name) { + result.classes.insert(*name); + } + } + Export::Module(mod_name) => { + let mod_sym = module_name_to_symbol(mod_name); + if mod_sym == current_module { + // Self re-export: include all local names + result.merge_from(all_names); + } else if let Some(real_mods) = qualifier_to_modules.get(&mod_sym) { + // Qualifier alias: merge exports from all modules imported under this qualifier + for real_mod in real_mods { + if let Some(reexported) = resolved_modules + .get(real_mod) + .or_else(|| all_modules_names.get(real_mod)) + { + result.merge_from(reexported); + } + } + } else { + // Direct module name (e.g. `module Prim.Row`) + if let Some(reexported) = resolved_modules + .get(&mod_sym) + .or_else(|| all_modules_names.get(&mod_sym)) + { + result.merge_from(reexported); + } + } + } + } + } + result +} + +// ===== Scope building ===== + +/// Import all exports from a known module into scope with an optional qualifier. +fn import_known_exports_to_scope( + exports: &super::registry::ModuleExports, + scope: &mut NameScope, + qualifier: Option, + origin: NameOrigin, +) { + for name in exports.values.keys() { + scope + .values + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } + for name in exports.data_constructors.keys() { + scope + .types + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } + for (op, _) in &exports.type_operators { + scope.type_operators.insert(op.name, origin.clone()); + } + for op in exports.value_fixities.keys() { + scope + .values + .insert(maybe_qualify(op.name, qualifier), origin.clone()); + } + for name in exports.class_methods.keys() { + scope + .classes + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } + for name in exports.class_param_counts.keys() { + scope + .classes + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } + for name in exports.type_aliases.keys() { + scope + .types + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } + for ctors in exports.data_constructors.values() { + for ctor in ctors { + scope + .values + .insert(maybe_qualify(ctor.name, qualifier), origin.clone()); + } + } + for name in exports.type_con_arities.keys() { + scope + .types + .insert(maybe_qualify(name.name, qualifier), origin.clone()); + } +} + +/// Import Prim exports into scope (unqualified). Prim types are built-in. +fn import_prim_to_scope(scope: &mut NameScope) { + let prim = super::check::prim_exports(); + import_known_exports_to_scope(prim, scope, None, NameOrigin::Prim); +} + +/// Import a Prim module or submodule's exports into scope. +fn import_prim_module_to_scope( + module: &crate::cst::ModuleName, + scope: &mut NameScope, + qualifier: Option, + imports: &Option, +) { + let owned_exports; + let exports: &super::registry::ModuleExports = if is_prim_module(module) { + super::check::prim_exports() + } else { + owned_exports = super::check::prim_submodule_exports(module); + &owned_exports + }; + + let origin = NameOrigin::Prim; + match imports { + None | Some(ImportList::Hiding(_)) => { + import_known_exports_to_scope(exports, scope, qualifier, origin); + } + Some(ImportList::Explicit(items)) => { + // For explicit Prim imports, we can enumerate constructors/methods + // from the known exports + for item in items { + match item { + crate::cst::Import::Value(name) => { + scope + .values + .insert(maybe_qualify(*name, qualifier), origin.clone()); + } + crate::cst::Import::Type(name, members) => { + scope + .types + .insert(maybe_qualify(*name, qualifier), origin.clone()); + let name_qi = QualifiedIdent { module: None, name: *name }; + if let Some(ctors) = exports.data_constructors.get(&name_qi) { + match members { + Some(crate::cst::DataMembers::All) => { + for ctor in ctors { + scope.values.insert( + maybe_qualify(ctor.name, qualifier), + origin.clone(), + ); + } + } + Some(crate::cst::DataMembers::Explicit(names)) => { + for n in names { + scope + .values + .insert(maybe_qualify(*n, qualifier), origin.clone()); + } + } + None => {} + } + } + } + crate::cst::Import::TypeOp(name) => { + scope.type_operators.insert(*name, origin.clone()); + } + crate::cst::Import::Class(name) => { + scope + .classes + .insert(maybe_qualify(*name, qualifier), origin.clone()); + // Also import class methods + for (method, (class, _)) in &exports.class_methods { + if class.name == *name { + scope + .values + .insert(maybe_qualify(method.name, qualifier), origin.clone()); + } + } + } + } + } + } + } +} + +/// Import all names from a `ModuleResolvedNames` into scope. +fn import_resolved_names_to_scope( + names: &ModuleResolvedNames, + scope: &mut NameScope, + qualifier: Option, + origin: NameOrigin, +) { + for &name in &names.values { + scope + .values + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + for &name in &names.types { + scope + .types + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + for &name in &names.classes { + scope + .classes + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + for &name in &names.type_operators { + scope.type_operators.insert(name, origin.clone()); + } +} + +/// Import all names from a `ModuleResolvedNames` except those in the hiding list. +fn import_resolved_names_hiding( + names: &ModuleResolvedNames, + hidden_items: &[crate::cst::Import], + scope: &mut NameScope, + qualifier: Option, + origin: NameOrigin, +) { + let mut hidden_values: HashSet = HashSet::new(); + let mut hidden_types: HashSet = HashSet::new(); + let mut hidden_classes: HashSet = HashSet::new(); + let mut hidden_type_ops: HashSet = HashSet::new(); + + for item in hidden_items { + match item { + crate::cst::Import::Value(name) => { + hidden_values.insert(*name); + } + crate::cst::Import::Type(name, members) => { + hidden_types.insert(*name); + match members { + Some(crate::cst::DataMembers::All) => { + if let Some(ctors) = names.data_constructors.get(name) { + for ctor in ctors { + hidden_values.insert(*ctor); + } + } + } + Some(crate::cst::DataMembers::Explicit(ctors)) => { + for ctor in ctors { + hidden_values.insert(*ctor); + } + } + None => {} + } + } + crate::cst::Import::TypeOp(name) => { + hidden_type_ops.insert(*name); + } + crate::cst::Import::Class(name) => { + hidden_classes.insert(*name); + } + } + } + + for &name in &names.values { + if !hidden_values.contains(&name) { + scope + .values + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + } + for &name in &names.types { + if !hidden_types.contains(&name) { + scope + .types + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + } + for &name in &names.classes { + if !hidden_classes.contains(&name) { + scope + .classes + .insert(maybe_qualify(name, qualifier), origin.clone()); + } + } + for &name in &names.type_operators { + if !hidden_type_ops.contains(&name) { + scope.type_operators.insert(name, origin.clone()); + } + } +} + +/// Import an explicitly named item from the CST import declaration. +/// Does NOT consult any registry — derives scope entries purely from the import syntax. +fn import_explicit_item( + item: &crate::cst::Import, + scope: &mut NameScope, + qualifier: Option, + origin: NameOrigin, +) { + match item { + crate::cst::Import::Value(name) => { + scope.values.insert(maybe_qualify(*name, qualifier), origin); + } + crate::cst::Import::Type(name, members) => { + scope + .types + .insert(maybe_qualify(*name, qualifier), origin.clone()); + match members { + Some(crate::cst::DataMembers::All) => { + // We can't enumerate constructors without the registry. + // As a fallback, add the type name as a value since many + // data types have a constructor with the same name. + scope + .values + .insert(maybe_qualify(*name, qualifier), origin.clone()); + } + Some(crate::cst::DataMembers::Explicit(names)) => { + for n in names { + scope + .values + .insert(maybe_qualify(*n, qualifier), origin.clone()); + } + } + None => {} + } + } + crate::cst::Import::TypeOp(name) => { + scope.type_operators.insert(*name, origin); + } + crate::cst::Import::Class(name) => { + scope.classes.insert(*name, origin); + } + } +} + +/// Import an explicitly named item, using `ModuleResolvedNames` to enumerate +/// constructors for `Type(..)` and methods for `Class` imports. +fn import_explicit_item_with_resolution( + item: &crate::cst::Import, + module_names: &ModuleResolvedNames, + scope: &mut NameScope, + qualifier: Option, + origin: NameOrigin, +) { + match item { + crate::cst::Import::Value(name) => { + scope.values.insert(maybe_qualify(*name, qualifier), origin); + } + crate::cst::Import::Type(name, members) => { + scope + .types + .insert(maybe_qualify(*name, qualifier), origin.clone()); + match members { + Some(crate::cst::DataMembers::All) => { + if let Some(ctors) = module_names.data_constructors.get(name) { + for ctor in ctors { + scope + .values + .insert(maybe_qualify(*ctor, qualifier), origin.clone()); + } + } + } + Some(crate::cst::DataMembers::Explicit(names)) => { + for n in names { + scope + .values + .insert(maybe_qualify(*n, qualifier), origin.clone()); + } + } + None => {} + } + } + crate::cst::Import::TypeOp(name) => { + scope.type_operators.insert(*name, origin); + } + crate::cst::Import::Class(name) => { + scope + .classes + .insert(maybe_qualify(*name, qualifier), origin); + } + } +} + +fn build_module_scope(module: &Module, resolution_exports: &ResolutionExports) -> NameScope { + let mut scope = NameScope::new(); + + // Import Prim (unless module has explicit Prim import) + let has_explicit_prim = module + .imports + .iter() + .any(|imp| is_prim_module(&imp.module) && imp.imports.is_some() && imp.qualified.is_none()); + if !has_explicit_prim { + import_prim_to_scope(&mut scope); + } + // Prim is always available as a qualifier (for Prim.Int, Prim.Boolean, etc.) + let prim = super::check::prim_exports(); + let prim_sym = interner::intern("Prim"); + import_known_exports_to_scope(prim, &mut scope, Some(prim_sym), NameOrigin::Prim); + + // Process imports + for import_decl in &module.imports { + let qualifier = import_decl.qualified.as_ref().map(module_name_to_symbol); + let mod_sym = module_name_to_symbol(&import_decl.module); + + // Handle Prim and Prim submodules specially (we have their exports built-in) + if is_prim_module(&import_decl.module) || is_prim_submodule(&import_decl.module) { + import_prim_module_to_scope( + &import_decl.module, + &mut scope, + qualifier, + &import_decl.imports, + ); + continue; + } + + // Non-Prim imports + let origin = NameOrigin::Imported(mod_sym); + + match &import_decl.imports { + Some(ImportList::Explicit(items)) => { + // Explicit imports: if we have the module's exports, use them + // for Type(..) and Class constructor/method resolution. + // Otherwise fall back to syntax-only import. + if let Some(module_names) = resolution_exports.get(mod_sym) { + for item in items { + import_explicit_item_with_resolution( + item, + module_names, + &mut scope, + qualifier, + origin.clone(), + ); + } + } else { + for item in items { + import_explicit_item(item, &mut scope, qualifier, origin.clone()); + } + } + } + None => { + // Open import: import all exported names from the module. + if let Some(module_names) = resolution_exports.get(mod_sym) { + import_resolved_names_to_scope( + module_names, + &mut scope, + qualifier, + origin, + ); + } + } + Some(ImportList::Hiding(hidden_items)) => { + // Hiding import: import all exported names except hidden ones. + if let Some(module_names) = resolution_exports.get(mod_sym) { + import_resolved_names_hiding( + module_names, + hidden_items, + &mut scope, + qualifier, + origin, + ); + } + } + } + } + + // Register local declarations + for decl in &module.decls { + match decl { + Decl::Value { name, span, .. } => { + scope.values.insert(name.value, NameOrigin::Local(*span)); + } + Decl::Data { + name, + span, + constructors, + kind_sig, + is_role_decl, + .. + } => { + scope.types.insert(name.value, NameOrigin::Local(*span)); + if *kind_sig == crate::cst::KindSigSource::None && !*is_role_decl { + for ctor in constructors { + scope + .values + .insert(ctor.name.value, NameOrigin::Local(ctor.span)); + } + } + } + Decl::Newtype { + name, + span, + constructor, + .. + } => { + scope.types.insert(name.value, NameOrigin::Local(*span)); + scope + .values + .insert(constructor.value, NameOrigin::Local(*span)); + } + Decl::TypeAlias { name, span, .. } => { + scope.types.insert(name.value, NameOrigin::Local(*span)); + } + Decl::ForeignData { name, span, .. } => { + scope.types.insert(name.value, NameOrigin::Local(*span)); + } + Decl::Foreign { name, span, .. } => { + scope.values.insert(name.value, NameOrigin::Local(*span)); + } + Decl::Class { + name, + span, + members, + .. + } => { + scope.classes.insert(name.value, NameOrigin::Local(*span)); + for member in members { + scope + .values + .insert(member.name.value, NameOrigin::Local(member.span)); + } + } + Decl::Fixity { + is_type, + operator, + span, + .. + } => { + if *is_type { + scope + .type_operators + .insert(operator.value, NameOrigin::Local(*span)); + } else { + scope + .values + .insert(operator.value, NameOrigin::Local(*span)); + } + } + Decl::Instance { .. } | Decl::Derive { .. } | Decl::TypeSignature { .. } => {} + } + } + + scope +} + +// ===== Resolution methods ===== + +impl<'a> Resolver<'a> { + fn new(scope: &'a NameScope) -> Self { + Resolver { + scope, + resolutions: Vec::new(), + errors: Vec::new(), + } + } + + /// Resolve a value name (variable, constructor, operator). + fn resolve_value(&mut self, name: &QualifiedIdent, locals: &LocalScope, span: Span) { + let resolved = match name.module { + Some(module) => qualified_symbol(module, name.name), + None => name.name, + }; + + // Check locals first (unqualified only) + if name.module.is_none() { + if let Some(&local_span) = locals.get(&name.name) { + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: name.name, + namespace: Namespace::Value, + definition: DefinitionSite::LocalVar(local_span), + }); + return; + } + } + + // Check module scope + if let Some(origin) = self.scope.values.get(&resolved) { + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: resolved, + namespace: Namespace::Value, + definition: origin.to_definition_site(), + }); + } else { + self.errors.push(TypeError::UndefinedVariable { + span, + name: resolved, + }); + } + } + + /// Resolve a type name. + fn resolve_type(&mut self, name: &QualifiedIdent, span: Span) { + let resolved = match name.module { + Some(module) => qualified_symbol(module, name.name), + None => name.name, + }; + + if let Some(origin) = self.scope.types.get(&resolved) { + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: resolved, + namespace: Namespace::Type, + definition: origin.to_definition_site(), + }); + } else if let Some(origin) = self.scope.type_operators.get(&resolved) { + // Operator used as type constructor via section syntax: (/\), (~>) + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: resolved, + namespace: Namespace::TypeOperator, + definition: origin.to_definition_site(), + }); + } else { + self.errors.push(TypeError::UnknownType { + span, + name: resolved, + }); + } + } + + /// Resolve a class name. + fn resolve_class(&mut self, name: &QualifiedIdent, span: Span) { + let class_sym = match name.module { + Some(module) => qualified_symbol(module, name.name), + None => name.name, + }; + if let Some(origin) = self.scope.classes.get(&class_sym) { + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: class_sym, + namespace: Namespace::Class, + definition: origin.to_definition_site(), + }); + } else { + self.errors.push(TypeError::UnknownClass { + span, + name: *name, + }); + } + } + + /// Resolve a type operator. + fn resolve_type_op(&mut self, name: &QualifiedIdent, span: Span) { + if let Some(origin) = self.scope.type_operators.get(&name.name) { + self.resolutions.push(ResolvedName { + src_span: span, + src_symbol: name.name, + namespace: Namespace::TypeOperator, + definition: origin.to_definition_site(), + }); + } else { + self.errors.push(TypeError::UnknownType { + span, + name: name.name, + }); + } + } +} + +// ===== CST walking ===== + +fn walk_expr(r: &mut Resolver, expr: &Expr, locals: &LocalScope, type_vars: &HashSet) { + match expr { + Expr::Var { span, name } => { + r.resolve_value(name, locals, *span); + } + Expr::Constructor { span, name } => { + r.resolve_value(name, locals, *span); + } + Expr::Literal { lit, .. } => { + walk_literal(r, lit, locals, type_vars); + } + Expr::App { func, arg, .. } => { + walk_expr(r, func, locals, type_vars); + walk_expr(r, arg, locals, type_vars); + } + Expr::VisibleTypeApp { func, ty, .. } => { + walk_expr(r, func, locals, type_vars); + walk_type_expr(r, ty, type_vars); + } + Expr::Lambda { binders, body, .. } => { + let mut inner = locals.clone(); + for binder in binders { + collect_binder_names(binder, &mut inner); + walk_binder(r, binder, locals, type_vars); + } + walk_expr(r, body, &inner, type_vars); + } + Expr::Op { + left, op, right, .. + } => { + walk_expr(r, left, locals, type_vars); + r.resolve_value(&op.value, locals, op.span); + walk_expr(r, right, locals, type_vars); + } + Expr::OpParens { op, .. } => { + r.resolve_value(&op.value, locals, op.span); + } + Expr::If { + cond, + then_expr, + else_expr, + .. + } => { + walk_expr(r, cond, locals, type_vars); + walk_expr(r, then_expr, locals, type_vars); + walk_expr(r, else_expr, locals, type_vars); + } + Expr::Case { exprs, alts, .. } => { + for e in exprs { + walk_expr(r, e, locals, type_vars); + } + for alt in alts { + walk_case_alt(r, alt, locals, type_vars); + } + } + Expr::Let { bindings, body, .. } => { + let mut inner = locals.clone(); + for binding in bindings { + collect_let_binding_names(binding, &mut inner); + } + for binding in bindings { + walk_let_binding(r, binding, &inner, type_vars); + } + walk_expr(r, body, &inner, type_vars); + } + Expr::Do { statements, .. } => { + walk_do_statements(r, statements, locals, type_vars); + } + Expr::Ado { + statements, result, .. + } => { + let mut inner = locals.clone(); + for stmt in statements { + collect_do_statement_names(stmt, &mut inner); + } + walk_do_statements(r, statements, locals, type_vars); + walk_expr(r, result, &inner, type_vars); + } + Expr::Record { fields, .. } => { + for field in fields { + match &field.value { + None => { + // Punned field: { x } uses x as a value + let qi = QualifiedIdent { + module: None, + name: field.label.value, + }; + r.resolve_value(&qi, locals, field.label.span); + } + Some(value_expr) => { + walk_expr(r, value_expr, locals, type_vars); + } + } + } + } + Expr::RecordAccess { expr, .. } => { + walk_expr(r, expr, locals, type_vars); + } + Expr::RecordUpdate { expr, updates, .. } => { + walk_expr(r, expr, locals, type_vars); + for update in updates { + walk_expr(r, &update.value, locals, type_vars); + } + } + Expr::Parens { expr, .. } => { + walk_expr(r, expr, locals, type_vars); + } + Expr::TypeAnnotation { expr, ty, .. } => { + walk_expr(r, expr, locals, type_vars); + walk_type_expr(r, ty, type_vars); + } + Expr::Hole { .. } => {} + Expr::Array { elements, .. } => { + for e in elements { + walk_expr(r, e, locals, type_vars); + } + } + Expr::Negate { expr, .. } => { + walk_expr(r, expr, locals, type_vars); + } + Expr::AsPattern { name, pattern, .. } => { + walk_expr(r, name, locals, type_vars); + walk_expr(r, pattern, locals, type_vars); + } + Expr::Wildcard { .. } => {} + Expr::BacktickApp { + func, left, right, .. + } => { + walk_expr(r, func, locals, type_vars); + walk_expr(r, left, locals, type_vars); + walk_expr(r, right, locals, type_vars); + } + } +} + +fn walk_literal(r: &mut Resolver, lit: &Literal, locals: &LocalScope, type_vars: &HashSet) { + if let Literal::Array(exprs) = lit { + for e in exprs { + walk_expr(r, e, locals, type_vars); + } + } +} + +fn walk_type_expr(r: &mut Resolver, ty: &TypeExpr, type_vars: &HashSet) { + match ty { + TypeExpr::Var { .. } => { + // Type variables — implicitly bound in PureScript, no resolution needed + } + TypeExpr::Constructor { span, name } => { + r.resolve_type(name, *span); + } + TypeExpr::App { + constructor, arg, .. + } => { + walk_type_expr(r, constructor, type_vars); + walk_type_expr(r, arg, type_vars); + } + TypeExpr::Function { from, to, .. } => { + walk_type_expr(r, from, type_vars); + walk_type_expr(r, to, type_vars); + } + TypeExpr::Forall { vars, ty, .. } => { + let mut inner_tvs = type_vars.clone(); + for (var, _, kind) in vars { + inner_tvs.insert(var.value); + if let Some(kind_expr) = kind { + walk_type_expr(r, kind_expr, &inner_tvs); + } + } + walk_type_expr(r, ty, &inner_tvs); + } + TypeExpr::Constrained { + constraints, ty, .. + } => { + for constraint in constraints { + r.resolve_class(&constraint.class, constraint.span); + for arg in &constraint.args { + walk_type_expr(r, arg, type_vars); + } + } + walk_type_expr(r, ty, type_vars); + } + TypeExpr::Record { fields, .. } => { + for field in fields { + walk_type_expr(r, &field.ty, type_vars); + } + } + TypeExpr::Row { fields, tail, .. } => { + for field in fields { + walk_type_expr(r, &field.ty, type_vars); + } + if let Some(tail) = tail { + walk_type_expr(r, tail, type_vars); + } + } + TypeExpr::Parens { ty, .. } => { + walk_type_expr(r, ty, type_vars); + } + TypeExpr::Hole { .. } | TypeExpr::Wildcard { .. } => {} + TypeExpr::TypeOp { + left, op, right, .. + } => { + walk_type_expr(r, left, type_vars); + r.resolve_type_op(&op.value, op.span); + walk_type_expr(r, right, type_vars); + } + TypeExpr::Kinded { ty, kind, .. } => { + walk_type_expr(r, ty, type_vars); + walk_type_expr(r, kind, type_vars); + } + TypeExpr::StringLiteral { .. } | TypeExpr::IntLiteral { .. } => {} + } +} + +// ===== Binder helpers ===== + +/// Collect names introduced by a binder into the local scope. +fn collect_binder_names(binder: &Binder, locals: &mut LocalScope) { + match binder { + Binder::Var { name, .. } => { + locals.insert(name.value, name.span); + } + Binder::Constructor { args, .. } => { + for arg in args { + collect_binder_names(arg, locals); + } + } + Binder::As { name, binder, .. } => { + locals.insert(name.value, name.span); + collect_binder_names(binder, locals); + } + Binder::Parens { binder, .. } => { + collect_binder_names(binder, locals); + } + Binder::Array { elements, .. } => { + for e in elements { + collect_binder_names(e, locals); + } + } + Binder::Record { fields, .. } => { + for field in fields { + match &field.binder { + None => { + locals.insert(field.label.value, field.label.span); + } + Some(binder) => { + collect_binder_names(binder, locals); + } + } + } + } + Binder::Op { left, right, .. } => { + collect_binder_names(left, locals); + collect_binder_names(right, locals); + } + Binder::Typed { binder, .. } => { + collect_binder_names(binder, locals); + } + Binder::Wildcard { .. } | Binder::Literal { .. } => {} + } +} + +fn walk_binder( + r: &mut Resolver, + binder: &Binder, + locals: &LocalScope, + type_vars: &HashSet, +) { + match binder { + Binder::Constructor { span, name, args } => { + r.resolve_value(name, locals, *span); + for arg in args { + walk_binder(r, arg, locals, type_vars); + } + } + Binder::Op { + left, op, right, .. + } => { + walk_binder(r, left, locals, type_vars); + r.resolve_value(&op.value, locals, op.span); + walk_binder(r, right, locals, type_vars); + } + Binder::Typed { binder, ty, .. } => { + walk_binder(r, binder, locals, type_vars); + walk_type_expr(r, ty, type_vars); + } + Binder::As { binder, .. } | Binder::Parens { binder, .. } => { + walk_binder(r, binder, locals, type_vars); + } + Binder::Array { elements, .. } => { + for e in elements { + walk_binder(r, e, locals, type_vars); + } + } + Binder::Record { fields, .. } => { + for field in fields { + if let Some(binder) = &field.binder { + walk_binder(r, binder, locals, type_vars); + } + } + } + Binder::Literal { lit, .. } => { + if let Literal::Array(exprs) = lit { + for e in exprs { + walk_expr(r, e, locals, type_vars); + } + } + } + Binder::Var { .. } | Binder::Wildcard { .. } => {} + } +} + +// ===== Case / guard ===== + +fn walk_case_alt( + r: &mut Resolver, + alt: &CaseAlternative, + locals: &LocalScope, + type_vars: &HashSet, +) { + let mut inner = locals.clone(); + for binder in &alt.binders { + collect_binder_names(binder, &mut inner); + walk_binder(r, binder, locals, type_vars); + } + walk_guarded(r, &alt.result, &inner, type_vars); +} + +fn walk_guarded( + r: &mut Resolver, + guarded: &GuardedExpr, + locals: &LocalScope, + type_vars: &HashSet, +) { + match guarded { + GuardedExpr::Unconditional(expr) => { + walk_expr(r, expr, locals, type_vars); + } + GuardedExpr::Guarded(guards) => { + for guard in guards { + let mut guard_locals = locals.clone(); + for pattern in &guard.patterns { + match pattern { + GuardPattern::Boolean(e) => { + walk_expr(r, e, &guard_locals, type_vars); + } + GuardPattern::Pattern(binder, e) => { + walk_expr(r, e, &guard_locals, type_vars); + collect_binder_names(binder, &mut guard_locals); + walk_binder(r, binder, locals, type_vars); + } + } + } + walk_expr(r, &guard.expr, &guard_locals, type_vars); + } + } + } +} + +// ===== Let / do ===== + +fn collect_let_binding_names(binding: &LetBinding, locals: &mut LocalScope) { + if let LetBinding::Value { binder, .. } = binding { + collect_binder_names(binder, locals); + } +} + +fn walk_let_binding( + r: &mut Resolver, + binding: &LetBinding, + locals: &LocalScope, + type_vars: &HashSet, +) { + match binding { + LetBinding::Value { binder, expr, .. } => { + walk_binder(r, binder, locals, type_vars); + walk_expr(r, expr, locals, type_vars); + } + LetBinding::Signature { ty, .. } => { + walk_type_expr(r, ty, type_vars); + } + } +} + +fn collect_do_statement_names(stmt: &DoStatement, locals: &mut LocalScope) { + match stmt { + DoStatement::Bind { binder, .. } => { + collect_binder_names(binder, locals); + } + DoStatement::Let { bindings, .. } => { + for binding in bindings { + collect_let_binding_names(binding, locals); + } + } + DoStatement::Discard { .. } => {} + } +} + +fn walk_do_statements( + r: &mut Resolver, + statements: &[DoStatement], + locals: &LocalScope, + type_vars: &HashSet, +) { + let mut current = locals.clone(); + for stmt in statements { + match stmt { + DoStatement::Bind { binder, expr, .. } => { + walk_expr(r, expr, ¤t, type_vars); + walk_binder(r, binder, ¤t, type_vars); + collect_binder_names(binder, &mut current); + } + DoStatement::Let { bindings, .. } => { + for binding in bindings { + collect_let_binding_names(binding, &mut current); + } + for binding in bindings { + walk_let_binding(r, binding, ¤t, type_vars); + } + } + DoStatement::Discard { expr, .. } => { + walk_expr(r, expr, ¤t, type_vars); + } + } + } +} + +// ===== Declaration walking ===== + +fn walk_decl(r: &mut Resolver, decl: &Decl) { + let type_vars = HashSet::new(); + let locals = LocalScope::new(); + + match decl { + Decl::Value { + binders, + guarded, + where_clause, + .. + } => { + let mut body_locals = locals.clone(); + for binder in binders { + collect_binder_names(binder, &mut body_locals); + walk_binder(r, binder, &locals, &type_vars); + } + for binding in where_clause { + collect_let_binding_names(binding, &mut body_locals); + } + for binding in where_clause { + walk_let_binding(r, binding, &body_locals, &type_vars); + } + walk_guarded(r, guarded, &body_locals, &type_vars); + } + Decl::TypeSignature { ty, .. } => { + walk_type_expr(r, ty, &type_vars); + } + Decl::Data { + constructors, + type_vars: tvs, + kind_type, + type_var_kind_anns, + .. + } => { + let bound_tvs: HashSet = tvs.iter().map(|tv| tv.value).collect(); + for ann in type_var_kind_anns { + if let Some(kind_expr) = ann { + walk_type_expr(r, kind_expr, &bound_tvs); + } + } + if let Some(kind_ty) = kind_type { + walk_type_expr(r, kind_ty, &bound_tvs); + } + for ctor in constructors { + for field in &ctor.fields { + walk_type_expr(r, field, &bound_tvs); + } + } + } + Decl::Newtype { + type_vars: tvs, + ty, + type_var_kind_anns, + .. + } => { + let bound_tvs: HashSet = tvs.iter().map(|tv| tv.value).collect(); + for ann in type_var_kind_anns { + if let Some(kind_expr) = ann { + walk_type_expr(r, kind_expr, &bound_tvs); + } + } + walk_type_expr(r, ty, &bound_tvs); + } + Decl::TypeAlias { + type_vars: tvs, + ty, + type_var_kind_anns, + .. + } => { + let bound_tvs: HashSet = tvs.iter().map(|tv| tv.value).collect(); + for ann in type_var_kind_anns { + if let Some(kind_expr) = ann { + walk_type_expr(r, kind_expr, &bound_tvs); + } + } + walk_type_expr(r, ty, &bound_tvs); + } + Decl::Class { + constraints, + type_vars: tvs, + members, + type_var_kind_anns, + .. + } => { + let bound_tvs: HashSet = tvs.iter().map(|tv| tv.value).collect(); + for ann in type_var_kind_anns { + if let Some(kind_expr) = ann { + walk_type_expr(r, kind_expr, &bound_tvs); + } + } + for constraint in constraints { + r.resolve_class(&constraint.class, constraint.span); + for arg in &constraint.args { + walk_type_expr(r, arg, &bound_tvs); + } + } + for member in members { + walk_type_expr(r, &member.ty, &bound_tvs); + } + } + Decl::Instance { + constraints, + class_name, + types, + members, + span, + .. + } => { + r.resolve_class(class_name, *span); + for constraint in constraints { + r.resolve_class(&constraint.class, constraint.span); + for arg in &constraint.args { + walk_type_expr(r, arg, &type_vars); + } + } + for ty in types { + walk_type_expr(r, ty, &type_vars); + } + for member in members { + walk_decl(r, member); + } + } + Decl::Derive { + constraints, + class_name, + types, + span, + .. + } => { + r.resolve_class(class_name, *span); + for constraint in constraints { + r.resolve_class(&constraint.class, constraint.span); + for arg in &constraint.args { + walk_type_expr(r, arg, &type_vars); + } + } + for ty in types { + walk_type_expr(r, ty, &type_vars); + } + } + Decl::Fixity { target, span, .. } => { + let resolved = match target.module { + Some(module) => qualified_symbol(module, target.name), + None => target.name, + }; + if !r.scope.values.contains_key(&resolved) + && !r.scope.types.contains_key(&resolved) + { + r.errors.push(TypeError::UndefinedVariable { + span: *span, + name: resolved, + }); + } + } + Decl::Foreign { ty, .. } => { + walk_type_expr(r, ty, &type_vars); + } + Decl::ForeignData { kind, .. } => { + walk_type_expr(r, kind, &type_vars); + } + } +} + +// ===== Public API ===== + +/// Resolve all names in a module. +/// +/// Returns a `ResolvedResult` containing: +/// - All name resolutions (usage span → definition site), sorted by span start +/// - Any name resolution errors +pub fn resolve_names(module: &Module, exports: &ResolutionExports) -> ResolvedResult { + let scope = build_module_scope(module, exports); + let mut resolver = Resolver::new(&scope); + + for decl in &module.decls { + walk_decl(&mut resolver, decl); + } + + // Sort resolutions by span start for binary search + resolver.resolutions.sort_by_key(|r| r.src_span.start); + + let resolution_map: HashMap = resolver + .resolutions + .iter() + .enumerate() + .map(|(i, r)| (r.src_span, i)) + .collect(); + + ResolvedResult { + errors: resolver.errors, + resolutions: resolver.resolutions, + resolution_map, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::interner; + use crate::parser; + + /// Parse a module and resolve names. + fn resolve(source: &str) -> ResolvedResult { + let module = parser::parse(source).expect("parsing failed"); + resolve_names(&module, &ResolutionExports::empty()) + } + + /// Parse a module and resolve names, with dependency modules for imports. + fn resolve_with_deps(source: &str, dep_sources: &[&str]) -> ResolvedResult { + let module = parser::parse(source).expect("parsing failed"); + let deps: Vec<_> = dep_sources + .iter() + .map(|s| parser::parse(s).expect("dep parse failed")) + .collect(); + let exports = ResolutionExports::new(&deps); + resolve_names(&module, &exports) + } + + /// Find resolutions matching a given symbol name. + fn find_resolutions<'a>(result: &'a ResolvedResult, name: &str) -> Vec<&'a ResolvedName> { + let sym = interner::intern(name); + result + .resolutions + .iter() + .filter(|r| r.src_symbol == sym) + .collect() + } + + /// Check if any error is an UndefinedVariable for the given name. + fn has_undefined_variable(result: &ResolvedResult, name: &str) -> bool { + let sym = interner::intern(name); + result + .errors + .iter() + .any(|e| matches!(e, TypeError::UndefinedVariable { name: n, .. } if *n == sym)) + } + + /// Check if any error is an UnknownType for the given name. + fn has_unknown_type(result: &ResolvedResult, name: &str) -> bool { + let sym = interner::intern(name); + result + .errors + .iter() + .any(|e| matches!(e, TypeError::UnknownType { name: n, .. } if *n == sym)) + } + + /// Check if any error is an UnknownClass for the given name. + fn has_unknown_class(result: &ResolvedResult, name: &str) -> bool { + let sym = interner::intern(name); + result + .errors + .iter() + .any(|e| matches!(e, TypeError::UnknownClass { name: n, .. } if n.name == sym)) + } + + // ===== Error cases ===== + + #[test] + fn test_error_undefined_variable() { + let result = resolve("module T where\nx = unknownVar"); + assert!( + has_undefined_variable(&result, "unknownVar"), + "expected UndefinedVariable for unknownVar, got errors: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_error_unknown_type_in_signature() { + let result = resolve("module T where\nx :: UnknownType\nx = 42"); + assert!( + has_unknown_type(&result, "UnknownType"), + "expected UnknownType for UnknownType, got errors: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_error_unknown_class_in_constraint() { + let result = resolve("module T where\nx :: UnknownClass a => a -> a\nx y = y"); + assert!( + has_unknown_class(&result, "UnknownClass"), + "expected UnknownClass, got errors: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_error_unknown_constructor_in_case() { + let result = resolve("module T where\nf x = case x of\n UnknownCtor -> 1"); + assert!( + has_undefined_variable(&result, "UnknownCtor"), + "expected UndefinedVariable for UnknownCtor, got errors: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_error_undefined_in_lambda() { + let result = resolve("module T where\nf = \\x -> unknownFn x"); + assert!(has_undefined_variable(&result, "unknownFn")); + } + + #[test] + fn test_error_undefined_in_let() { + let result = resolve("module T where\nx = let\n y = unknownFn\nin y"); + assert!(has_undefined_variable(&result, "unknownFn")); + } + + // ===== No-error cases ===== + + #[test] + fn test_no_errors_simple_value() { + let result = resolve("module T where\nx = 42"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_data_and_constructor_use() { + let result = resolve("module T where\ndata MyBool = MyTrue | MyFalse\nx = MyTrue"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_prim_types() { + let result = resolve("module T where\nx :: Int\nx = 42\ny :: String\ny = \"hello\""); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_function_with_binders() { + let result = resolve("module T where\nf x y = x"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_let_and_where() { + let result = resolve("module T where\nf = let\n x = 1\nin x"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_case_with_data() { + let result = + resolve("module T where\ndata AB = A | B\nf x = case x of\n A -> 1\n B -> 2"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_type_alias() { + let result = resolve("module T where\ntype MyInt = Int\nx :: MyInt\nx = 42"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_class_and_instance() { + let result = resolve("module T where\nclass MyClass a where\n myMethod :: a -> a\ndata Foo = Foo\ninstance MyClass Foo where\n myMethod x = x"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_newtype() { + let result = resolve("module T where\nnewtype Wrapper = Wrapper Int\nx = Wrapper 42"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_no_errors_lambda() { + let result = resolve("module T where\nf = \\x -> x"); + assert!(result.errors.is_empty()); + } + + // ===== Top-level declaration references ===== + + #[test] + fn test_top_level_value_reference() { + let result = resolve("module T where\nf x = x\ng = f 42"); + assert!(result.errors.is_empty()); + let f_refs = find_resolutions(&result, "f"); + assert!(!f_refs.is_empty(), "expected resolution for 'f'"); + // The reference to f in g should resolve to a local definition + let f_ref = f_refs + .iter() + .find(|r| r.namespace == Namespace::Value) + .unwrap(); + assert!( + matches!(f_ref.definition, DefinitionSite::Local(_)), + "expected local definition for f, got {:?}", + f_ref.definition + ); + } + + #[test] + fn test_top_level_constructor_reference() { + let result = resolve("module T where\ndata Color = Red | Green | Blue\nx = Red"); + assert!(result.errors.is_empty()); + let red_refs = find_resolutions(&result, "Red"); + assert!(!red_refs.is_empty(), "expected resolution for 'Red'"); + let red_ref = red_refs + .iter() + .find(|r| r.namespace == Namespace::Value) + .unwrap(); + assert!(matches!(red_ref.definition, DefinitionSite::Local(_))); + } + + #[test] + fn test_top_level_type_reference_in_sig() { + let result = resolve("module T where\ndata Foo = Foo\nx :: Foo\nx = Foo"); + assert!(result.errors.is_empty()); + let foo_type_refs: Vec<_> = find_resolutions(&result, "Foo") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!( + !foo_type_refs.is_empty(), + "expected type resolution for 'Foo'" + ); + assert!(matches!( + foo_type_refs[0].definition, + DefinitionSite::Local(_) + )); + } + + #[test] + fn test_top_level_class_reference() { + let result = resolve("module T where\nclass MyShow a where\n myShow :: a -> Int\ndata Bar = Bar\ninstance MyShow Bar where\n myShow _ = 1"); + assert!(result.errors.is_empty()); + let class_refs: Vec<_> = find_resolutions(&result, "MyShow") + .into_iter() + .filter(|r| r.namespace == Namespace::Class) + .collect(); + // Class is referenced in the instance declaration + assert!( + !class_refs.is_empty(), + "expected class resolution for 'MyShow'" + ); + assert!(matches!(class_refs[0].definition, DefinitionSite::Local(_))); + } + + #[test] + fn test_top_level_mutual_reference() { + let result = resolve("module T where\nf x = g x\ng x = f x"); + assert!(result.errors.is_empty()); + // f references g + let g_refs = find_resolutions(&result, "g"); + assert!(!g_refs.is_empty(), "expected resolution for 'g'"); + assert!(matches!(g_refs[0].definition, DefinitionSite::Local(_))); + // g references f + let f_refs = find_resolutions(&result, "f"); + assert!(!f_refs.is_empty(), "expected resolution for 'f'"); + } + + // ===== Local declaration references ===== + + #[test] + fn test_local_let_binding_reference() { + let result = resolve("module T where\nf = let\n x = 1\nin x"); + assert!(result.errors.is_empty()); + let x_refs = find_resolutions(&result, "x"); + // The reference to x in the let body should resolve to LocalVar + let body_ref = x_refs + .iter() + .find(|r| matches!(r.definition, DefinitionSite::LocalVar(_))); + assert!( + body_ref.is_some(), + "expected LocalVar reference for let-bound 'x'" + ); + } + + #[test] + fn test_local_lambda_param_reference() { + let result = resolve("module T where\nf = \\x -> x"); + assert!(result.errors.is_empty()); + let x_refs = find_resolutions(&result, "x"); + let param_ref = x_refs + .iter() + .find(|r| matches!(r.definition, DefinitionSite::LocalVar(_))); + assert!( + param_ref.is_some(), + "expected LocalVar reference for lambda param 'x'" + ); + } + + #[test] + fn test_local_case_binder_reference() { + let result = resolve("module T where\ndata Maybe a = Just a | Nothing\nf mx = case mx of\n Just x -> x\n Nothing -> 0"); + assert!(result.errors.is_empty()); + let x_refs = find_resolutions(&result, "x"); + let binder_ref = x_refs + .iter() + .find(|r| matches!(r.definition, DefinitionSite::LocalVar(_))); + assert!( + binder_ref.is_some(), + "expected LocalVar reference for case-bound 'x'" + ); + } + + #[test] + fn test_local_function_binder_reference() { + let result = resolve("module T where\nf x = x"); + assert!(result.errors.is_empty()); + let x_refs = find_resolutions(&result, "x"); + let binder_ref = x_refs + .iter() + .find(|r| matches!(r.definition, DefinitionSite::LocalVar(_))); + assert!( + binder_ref.is_some(), + "expected LocalVar reference for function binder 'x'" + ); + } + + #[test] + fn test_local_shadows_top_level() { + // Lambda param 'x' should shadow the top-level 'x' + let result = resolve("module T where\nx = 1\nf = \\x -> x"); + assert!(result.errors.is_empty()); + // The 'x' in the lambda body should resolve to LocalVar, not Local + let x_refs = find_resolutions(&result, "x"); + let local_var_ref = x_refs + .iter() + .find(|r| matches!(r.definition, DefinitionSite::LocalVar(_))); + assert!( + local_var_ref.is_some(), + "expected lambda param to shadow top-level 'x'" + ); + } + + // ===== Imported value declaration references ===== + + #[test] + fn test_imported_value_reference() { + // Explicit import: bar added to scope from CST + let result = resolve("module T where\nimport Data.Foo (bar)\nx = bar"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let bar_refs = find_resolutions(&result, "bar"); + assert!( + !bar_refs.is_empty(), + "expected resolution for imported 'bar'" + ); + assert!(matches!( + bar_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + #[test] + fn test_imported_open_import() { + let result = resolve_with_deps( + "module T where\nimport Data.Foo\nx = bar\ny = baz", + &["module Data.Foo where\nbar = 1\nbaz = 2"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_imported_hiding_is_open() { + let result = resolve_with_deps( + "module T where\nimport Data.Foo hiding (baz)\nx = bar", + &["module Data.Foo where\nbar = 1\nbaz = 2"], + ); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_imported_constructor_reference() { + let result = resolve_with_deps( + "module T where\nimport Data.Maybe (Maybe(..))\nx = Just 42", + &["module Data.Maybe where\ndata Maybe a = Just a | Nothing"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let just_refs = find_resolutions(&result, "Just"); + assert!( + !just_refs.is_empty(), + "expected resolution for imported 'Just'" + ); + assert!(matches!( + just_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + #[test] + fn test_imported_explicit_constructors() { + // Type(Ctor1, Ctor2) import: constructors added explicitly from CST + let result = + resolve("module T where\nimport Data.Maybe (Maybe(Just, Nothing))\nx = Just 42"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let just_refs = find_resolutions(&result, "Just"); + assert!( + !just_refs.is_empty(), + "expected resolution for imported 'Just'" + ); + assert!(matches!( + just_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + // ===== Imported value operator references ===== + + #[test] + fn test_imported_value_operator() { + let result = resolve("module T where\nimport Data.Semigroup (append)\nx = append"); + assert!( + result.errors.is_empty(), + "expected no errors for imported operator, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + // ===== Qualified imported value references ===== + + #[test] + fn test_qualified_imported_value() { + let result = resolve_with_deps( + "module T where\nimport Data.Foo as Foo\nx = Foo.bar", + &["module Data.Foo where\nbar = 1"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let qbar_refs = find_resolutions(&result, "Foo.bar"); + assert!( + !qbar_refs.is_empty(), + "expected resolution for qualified 'Foo.bar'" + ); + assert!(matches!( + qbar_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + // ===== Qualified imported type references ===== + + #[test] + fn test_qualified_imported_type() { + let result = resolve_with_deps( + "module T where\nimport Data.Foo as Foo\nx :: Foo.Bar\nx = 42", + &["module Data.Foo where\ndata Bar = MkBar"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let qbar_refs: Vec<_> = find_resolutions(&result, "Foo.Bar") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!( + !qbar_refs.is_empty(), + "expected type resolution for 'Foo.Bar'" + ); + assert!(matches!( + qbar_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + // ===== Qualified imported constructor references ===== + + #[test] + fn test_qualified_imported_constructor() { + let result = resolve_with_deps( + "module T where\nimport Data.Maybe as M\nx = M.Just 42", + &["module Data.Maybe where\ndata Maybe a = Just a | Nothing"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + let qjust_refs = find_resolutions(&result, "M.Just"); + assert!(!qjust_refs.is_empty(), "expected resolution for 'M.Just'"); + assert!(matches!( + qjust_refs[0].definition, + DefinitionSite::Imported(_) + )); + } + + // ===== Qualified imported operator references ===== + + #[test] + fn test_local_type_operator_reference() { + // Define a type operator locally and use it in a type signature + let result = resolve( + "module T where\ndata TypeApply f a = TypeApply (f a)\ninfixr 0 type TypeApply as $\nx :: Int $ String\nx = TypeApply 42", + ); + // The type operator $ should resolve + let op_refs: Vec<_> = find_resolutions(&result, "$") + .into_iter() + .filter(|r| r.namespace == Namespace::TypeOperator) + .collect(); + assert!( + !op_refs.is_empty(), + "expected type operator resolution for '$'" + ); + assert!(matches!(op_refs[0].definition, DefinitionSite::Local(_))); + } + + // ===== Local type declaration overriding imported type ===== + + #[test] + fn test_local_type_alias_overrides_imported_type() { + // Explicit import adds Codec to types scope, then local alias overrides it + let result = resolve( + "module T where\nimport Data.Codec (Codec)\ntype Codec a = Int\nx :: Codec Int\nx = 42", + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + + // The Codec in the type signature should resolve to Local (the local alias), not Imported + let codec_type_refs: Vec<_> = find_resolutions(&result, "Codec") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!( + !codec_type_refs.is_empty(), + "expected type resolution for 'Codec'" + ); + // The local type alias should override the import + let has_local = codec_type_refs + .iter() + .any(|r| matches!(r.definition, DefinitionSite::Local(_))); + assert!( + has_local, + "expected local Codec alias to override imported Codec. Definitions: {:?}", + codec_type_refs + .iter() + .map(|r| &r.definition) + .collect::>() + ); + } + + #[test] + fn test_local_data_overrides_imported_type() { + let result = + resolve("module T where\nimport Data.Foo (Bar)\ndata Bar = MkBar\nx :: Bar\nx = MkBar"); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + + // Bar should resolve to Local (local data decl overrides import) + let bar_type_refs: Vec<_> = find_resolutions(&result, "Bar") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!(!bar_type_refs.is_empty()); + assert!( + bar_type_refs + .iter() + .all(|r| matches!(r.definition, DefinitionSite::Local(_))), + "expected local data Bar to override imported Bar" + ); + } + + // ===== lookup_at (IDE support) ===== + + #[test] + fn test_lookup_at_returns_resolution() { + let result = resolve("module T where\nx = 42"); + // There may or may not be resolutions for literals, but at least verify no panic + // and that lookup_at works for valid offsets + let _ = result.lookup_at(0); + let _ = result.lookup_at(100); + } + + #[test] + fn test_lookup_at_finds_correct_resolution() { + let result = resolve("module T where\ndata Foo = Foo\nx = Foo"); + // Find the span of a resolution and verify lookup_at returns it + if let Some(r) = result.resolutions.first() { + let found = result.lookup_at(r.src_span.start); + assert!( + found.is_some(), + "expected lookup_at to find resolution at span start" + ); + assert_eq!(found.unwrap().src_span.start, r.src_span.start); + } + } + + #[test] + fn test_lookup_at_out_of_range() { + let result = resolve("module T where\nx = 42"); + assert!(result.lookup_at(99999).is_none()); + } + + // ===== Resolutions are sorted ===== + + #[test] + fn test_resolutions_sorted_by_span() { + let result = + resolve("module T where\ndata Foo = A | B\nf x = case x of\n A -> 1\n B -> 2"); + for window in result.resolutions.windows(2) { + assert!( + window[0].src_span.start <= window[1].src_span.start, + "resolutions not sorted: {} > {}", + window[0].src_span.start, + window[1].src_span.start + ); + } + } + + // ===== Prim references ===== + + #[test] + fn test_prim_type_resolves_to_prim() { + let result = resolve("module T where\nx :: Int\nx = 42"); + assert!(result.errors.is_empty()); + let int_refs: Vec<_> = find_resolutions(&result, "Int") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!(!int_refs.is_empty(), "expected type resolution for 'Int'"); + assert!( + matches!(int_refs[0].definition, DefinitionSite::Prim), + "expected Prim definition for Int, got {:?}", + int_refs[0].definition + ); + } + + #[test] + fn test_prim_boolean_type() { + let result = resolve("module T where\nx :: Boolean\nx = true"); + assert!(result.errors.is_empty()); + let bool_refs: Vec<_> = find_resolutions(&result, "Boolean") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!( + !bool_refs.is_empty(), + "expected type resolution for 'Boolean'" + ); + assert!(matches!(bool_refs[0].definition, DefinitionSite::Prim)); + } + + // ===== Complex scenarios ===== + + #[test] + fn test_nested_let_scoping() { + let result = + resolve("module T where\nf = let\n x = 1\n y = let\n z = x\n in z\nin y"); + assert!( + result.errors.is_empty(), + "expected no errors in nested let, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_where_clause_reference() { + let result = resolve("module T where\nf x = g x\n where\n g y = y"); + assert!( + result.errors.is_empty(), + "expected no errors with where clause, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_record_pun_resolves() { + let result = resolve("module T where\nf x = { x }"); + assert!( + result.errors.is_empty(), + "expected no errors for record pun, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + // x in { x } should resolve as a value + let x_refs = find_resolutions(&result, "x"); + assert!(!x_refs.is_empty(), "expected resolution for record pun 'x'"); + } + + #[test] + fn test_imported_class_without_methods() { + // Importing `class Show` does NOT bring class methods into scope + let result = resolve_with_deps( + "module T where\nimport Data.Show (class Show)\nx = show", + &["module Data.Show where\nclass Show a where\n show :: a -> String"], + ); + assert!( + has_undefined_variable(&result, "show"), + "expected UndefinedVariable for 'show' (class methods not auto-imported)" + ); + } + + #[test] + fn test_imported_class_method_explicitly() { + // Importing `show` explicitly brings it into scope + let result = resolve_with_deps( + "module T where\nimport Data.Show (class Show, show)\nx = show", + &["module Data.Show where\nclass Show a where\n show :: a -> String"], + ); + assert!( + result.errors.is_empty(), + "expected no errors, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_imported_type_operator() { + // Explicit type operator import: added to scope from CST + let result = resolve( + "module T where\nimport Data.NaturalTransformation (type (~>))\nx :: forall f g. (f ~> g) -> Int\nx _ = 42", + ); + assert!( + result.errors.is_empty(), + "expected no errors for imported type operator, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + + let op_refs: Vec<_> = find_resolutions(&result, "~>") + .into_iter() + .filter(|r| r.namespace == Namespace::TypeOperator) + .collect(); + assert!( + !op_refs.is_empty(), + "expected type operator resolution for '~>'" + ); + assert!( + matches!(op_refs[0].definition, DefinitionSite::Imported(_)), + "expected imported definition for '~>', got {:?}", + op_refs[0].definition + ); + } + + #[test] + fn test_forall_type_in_signature() { + let result = resolve("module T where\nid :: forall a. a -> a\nid x = x"); + assert!( + result.errors.is_empty(), + "expected no errors for forall type, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + #[test] + fn test_type_annotation_in_expr() { + let result = resolve("module T where\nx = (42 :: Int)"); + assert!( + result.errors.is_empty(), + "expected no errors for type annotation, got: {:?}", + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + // Int should be resolved + let int_refs: Vec<_> = find_resolutions(&result, "Int") + .into_iter() + .filter(|r| r.namespace == Namespace::Type) + .collect(); + assert!( + !int_refs.is_empty(), + "expected type resolution for Int in annotation" + ); + } + + #[test] + fn test_if_then_else_resolves_all_branches() { + let result = resolve("module T where\ndata B = T | F\nf b = if true then T else F"); + assert!(result.errors.is_empty()); + assert!(!find_resolutions(&result, "T").is_empty()); + assert!(!find_resolutions(&result, "F").is_empty()); + } + + #[test] + fn test_array_elements_resolved() { + let result = resolve("module T where\ndata X = A | B | C\nxs = [A, B, C]"); + assert!(result.errors.is_empty()); + assert!(!find_resolutions(&result, "A").is_empty()); + assert!(!find_resolutions(&result, "B").is_empty()); + assert!(!find_resolutions(&result, "C").is_empty()); + } + + // ===== Scoping: let/where bindings out of scope ===== + + #[test] + fn test_let_binding_not_visible_outside() { + // 'y' is bound in the let of 'f', but referenced in a separate decl 'g' + let result = resolve("module T where\nf = let\n y = 1\nin y\ng = y"); + assert!( + has_undefined_variable(&result, "y"), + "expected UndefinedVariable for 'y' used outside let scope" + ); + } + + #[test] + fn test_let_binding_not_visible_in_sibling_decl() { + // Two top-level decls; 'x' from let in 'f' should not leak to 'g' + let result = resolve("module T where\nf = let\n x = 42\nin x\ng = x"); + assert!( + has_undefined_variable(&result, "x"), + "expected UndefinedVariable for 'x' used outside its let" + ); + } + + #[test] + fn test_where_binding_not_visible_outside() { + // 'helper' is in the where clause of 'f', referenced in 'g' + let result = resolve("module T where\nf x = helper x\n where\n helper y = y\ng = helper 1"); + assert!( + has_undefined_variable(&result, "helper"), + "expected UndefinedVariable for 'helper' used outside where scope" + ); + } + + #[test] + fn test_where_binding_not_visible_in_other_decl() { + // 'w' defined in where clause of 'a', used in 'b' + let result = resolve("module T where\na = w\n where\n w = 99\nb = w"); + assert!( + has_undefined_variable(&result, "w"), + "expected UndefinedVariable for 'w' used outside its where clause" + ); + } + + #[test] + fn test_nested_let_inner_not_visible_in_outer() { + // Inner let binding 'z' should not be visible in the outer let body + let result = resolve( + "module T where\nf = let\n x = let\n z = 1\n in z\n y = z\nin y", + ); + assert!( + has_undefined_variable(&result, "z"), + "expected UndefinedVariable for 'z' from inner let used in outer let" + ); + } + + #[test] + fn test_lambda_param_not_visible_outside() { + // Lambda param 'p' should not leak to sibling decl + let result = resolve("module T where\nf = \\p -> p\ng = p"); + assert!( + has_undefined_variable(&result, "p"), + "expected UndefinedVariable for lambda param 'p' used outside" + ); + } + +} diff --git a/src/typechecker/types.rs b/src/typechecker/types.rs index da38311c..6eae8a16 100644 --- a/src/typechecker/types.rs +++ b/src/typechecker/types.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::interner::{self, Symbol}; +use crate::{cst::{QualifiedIdent, unqualified_ident}, interner::{self, Symbol}}; /// Type parameter role for Coercible solving. /// Ordered: Phantom < Representational < Nominal (most restrictive). @@ -40,7 +40,7 @@ pub enum Type { Var(Symbol), /// Type constructor: Int, String, Boolean, Array, Maybe, etc. - Con(Symbol), + Con(QualifiedIdent), /// Type application: Array Int, Maybe a App(Box, Box), @@ -64,24 +64,40 @@ pub enum Type { } impl Type { + pub fn prim_con(name: &str) -> Type { + // Use unqualified names for now — module qualification will be added + // when convert_type_expr gains proper module resolution. + Type::Con(unqualified_ident(name)) + } + + pub fn con(_module: &str, name: &str) -> Type { + // Module qualifier ignored for now — will be used when convert_type_expr + // gains proper module resolution. + Type::Con(unqualified_ident(name)) + } + + pub fn con_local(name: &str) -> Type { + Type::Con(unqualified_ident(name)) + } + pub fn int() -> Type { - Type::Con(interner::intern("Int")) + Type::prim_con("Int") } pub fn float() -> Type { - Type::Con(interner::intern("Number")) + Type::prim_con("Number") } pub fn string() -> Type { - Type::Con(interner::intern("String")) + Type::prim_con("String") } pub fn char() -> Type { - Type::Con(interner::intern("Char")) + Type::prim_con("Char") } pub fn boolean() -> Type { - Type::Con(interner::intern("Boolean")) + Type::prim_con("Boolean") } pub fn fun(from: Type, to: Type) -> Type { @@ -93,35 +109,35 @@ impl Type { } pub fn array(elem: Type) -> Type { - Type::app(Type::Con(interner::intern("Array")), elem) + Type::app(Type::prim_con("Array"), elem) } // Kind constructors — kinds are represented as Types /// The kind of ordinary types: `Type` pub fn kind_type() -> Type { - Type::Con(interner::intern("Type")) + Type::prim_con("Type") } /// The kind of type class constraints: `Constraint` pub fn kind_constraint() -> Type { - Type::Con(interner::intern("Constraint")) + Type::prim_con("Constraint") } /// The kind of type-level strings: `Symbol` pub fn kind_symbol() -> Type { - Type::Con(interner::intern("Symbol")) + Type::prim_con("Symbol") } /// The kind of type-level integers (PureScript uses "Int" at kind level too, but /// we use a distinct interned name to avoid collision with the value-level Int type) pub fn kind_int() -> Type { - Type::Con(interner::intern("Int")) + Type::prim_con("Int") } /// The `Row` kind constructor (takes a kind argument: `Row Type`) pub fn kind_row() -> Type { - Type::Con(interner::intern("Row")) + Type::prim_con("Row") } /// `Row k` — the kind of row types parameterized by element kind `k` @@ -135,7 +151,7 @@ impl fmt::Display for Type { match self { Type::Unif(id) => write!(f, "?{}", id.0), Type::Var(sym) => write!(f, "{}", interner::resolve(*sym).unwrap_or_default()), - Type::Con(sym) => write!(f, "{}", interner::resolve(*sym).unwrap_or_default()), + Type::Con(sym) => write!(f, "{}", sym), Type::App(func, arg) => write!(f, "({} {})", func, arg), Type::Fun(from, to) => write!(f, "({} -> {})", from, to), Type::Forall(vars, ty) => { diff --git a/src/typechecker/unify.rs b/src/typechecker/unify.rs index e2cb84cc..e6513021 100644 --- a/src/typechecker/unify.rs +++ b/src/typechecker/unify.rs @@ -1,7 +1,96 @@ -use crate::ast::span::Span; +use crate::span::Span; +use crate::cst::{QualifiedIdent, unqualified_ident}; use crate::typechecker::error::TypeError; use crate::interner::Symbol; use crate::typechecker::types::{TyVarId, Type}; +use std::collections::HashSet; + +/// Check if a type body contains `Con(name)` applied to exactly `expected_args` +/// arguments anywhere in the type tree. Used to detect truly self-referential +/// aliases where expanding the alias produces a usage that would trigger +/// re-expansion with the same arity. +/// +/// For example, if alias `Codec` has 1 param and its expanded body contains +/// `Con("Codec")` with 5 args (the data type), that's NOT self-referential +/// because 5 != 1. But if it contains `Con("Codec")` with 1 arg, it IS +/// self-referential because it would be re-expanded as the same alias. +fn contains_self_referential_usage(ty: &Type, name: Symbol, expected_args: usize) -> bool { + match ty { + Type::Con(n) => n.name == name && expected_args == 0, + Type::App(_, _) => { + // Collect the full App spine + let mut head = ty; + let mut args: Vec<&Type> = Vec::new(); + while let Type::App(f, a) = head { + args.push(a.as_ref()); + head = f.as_ref(); + } + // Check if this App chain is headed by Con(name) with exactly expected_args + if let Type::Con(n) = head { + if n.name == name && args.len() == expected_args { + return true; + } + } + // Recurse into head and all args + contains_self_referential_usage(head, name, expected_args) + || args.iter().any(|a| contains_self_referential_usage(a, name, expected_args)) + } + Type::Fun(from, to) => { + contains_self_referential_usage(from, name, expected_args) + || contains_self_referential_usage(to, name, expected_args) + } + Type::Forall(_, body) => contains_self_referential_usage(body, name, expected_args), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| contains_self_referential_usage(t, name, expected_args)) + || tail.as_ref().map_or(false, |t| contains_self_referential_usage(t, name, expected_args)) + } + _ => false, + } +} + +/// Quick check: does a type body reference any Con name from the given set? +/// Used as a pre-filter to skip aliases that can't possibly be self-referential. +fn type_references_any_name(ty: &Type, names: &HashSet) -> bool { + match ty { + Type::Con(n) => names.contains(&n.name), + Type::App(f, a) => type_references_any_name(f, names) || type_references_any_name(a, names), + Type::Fun(from, to) => type_references_any_name(from, names) || type_references_any_name(to, names), + Type::Forall(_, body) => type_references_any_name(body, names), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| type_references_any_name(t, names)) + || tail.as_ref().map_or(false, |t| type_references_any_name(t, names)) + } + _ => false, + } +} + +/// Collect the head and arguments of an App chain. +/// E.g. `App(App(Con(X), a), b)` → `(Con(X), [a, b])`. +fn collect_app_spine(ty: &Type) -> (&Type, Vec<&Type>) { + let mut head = ty; + let mut args: Vec<&Type> = Vec::new(); + while let Type::App(f, a) = head { + args.push(a.as_ref()); + head = f.as_ref(); + } + args.reverse(); + (head, args) +} + +/// Cached well-known symbols to avoid repeated interner lookups on hot paths. +struct WellKnownSyms { + arrow: QualifiedIdent, + function: QualifiedIdent, + record: QualifiedIdent, +} + +static WELL_KNOWN: std::sync::LazyLock = std::sync::LazyLock::new(|| { + WellKnownSyms { + arrow: unqualified_ident("->"), + function: unqualified_ident("Function"), + record: unqualified_ident("Record"), + } +}); /// Entry in the union-find table for a unification variable. #[derive(Debug, Clone)] @@ -21,9 +110,23 @@ pub struct UnifyState { pub type_aliases: std::collections::HashMap, Type)>, /// Guard against infinite alias expansion cycles (e.g. `type A = A`). expanding_aliases: Vec, + /// Recursion depth for unify to prevent stack overflow. + unify_depth: u32, /// Unification variables that were generalized (part of a polymorphic type scheme). /// Used to distinguish polymorphic constraints (skip) from ambiguous ones (error). pub generalized_vars: std::collections::HashSet, + /// Aliases whose fully-expanded body still contains Con(alias_name). + /// These must not be eagerly re-expanded during unification to prevent infinite loops. + pub self_referential_aliases: std::collections::HashSet, + /// Maps import qualifier aliases to canonical (full) module names. + /// Used during kind unification to detect when two `Type::Con` with the same + /// unqualified name but different module qualifiers actually refer to different types + /// (e.g., `LibA.DemoKind` vs `LibB.DemoKind`). Only populated for kind-level UnifyState. + pub qualifier_to_canonical: std::collections::HashMap, + /// Type constructor arities: used to disambiguate alias vs data type when they share + /// the same name (e.g., `type Codec a = ...` with 1 param vs `data Codec m i o a b` with 5). + /// Maps QualifiedIdent → param count. Populated from check.rs. + pub type_con_arities: std::collections::HashMap, } impl UnifyState { @@ -32,7 +135,11 @@ impl UnifyState { entries: Vec::new(), type_aliases: std::collections::HashMap::new(), expanding_aliases: Vec::new(), + unify_depth: 0, generalized_vars: std::collections::HashSet::new(), + self_referential_aliases: std::collections::HashSet::new(), + qualifier_to_canonical: std::collections::HashMap::new(), + type_con_arities: std::collections::HashMap::new(), } } @@ -82,6 +189,27 @@ impl UnifyState { matches!(&self.entries[root.0 as usize], UfEntry::Root(0)) } + /// Check if two type constructors with the same unqualified name actually conflict + /// because they come from different modules (different canonical origins). + /// Returns true if they conflict (should NOT unify). + /// Only applies when both sides have explicit module qualifiers and the + /// qualifier_to_canonical mapping is populated (kind-level unification). + fn con_modules_conflict(&self, a: &crate::cst::QualifiedIdent, b: &crate::cst::QualifiedIdent) -> bool { + if self.qualifier_to_canonical.is_empty() { + return false; + } + if let (Some(mod_a), Some(mod_b)) = (a.module, b.module) { + if mod_a == mod_b { + return false; // Same qualifier → same module + } + let canon_a = self.qualifier_to_canonical.get(&mod_a).copied().unwrap_or(mod_a); + let canon_b = self.qualifier_to_canonical.get(&mod_b).copied().unwrap_or(mod_b); + canon_a != canon_b + } else { + false + } + } + /// Get the solved type for a variable, if any. pub fn probe(&mut self, var: TyVarId) -> Option { let root = self.find(var); @@ -119,30 +247,67 @@ impl UnifyState { to_z.unwrap_or_else(|| (**to).clone()), )) } - Type::App(f, a) => { - let f_z = self.zonk_ref(f); - let a_z = self.zonk_ref(a); - let f_resolved = f_z.as_ref().unwrap_or(f); - let a_resolved = a_z.as_ref().unwrap_or(a); - // Normalize App(App(Con("->"), from), to) and App(App(Con("Function"), from), to) → Fun(from, to) - if let Type::App(ff, from) = f_resolved { - if let Type::Con(sym) = ff.as_ref() { - let name = crate::interner::resolve(*sym).unwrap_or_default(); - if name == "->" || name == "Function" { - return Some(Type::fun(from.as_ref().clone(), a_resolved.clone())); + Type::App(_, _) => { + // Collect the full application spine to avoid alias-expanding + // partial applications. Without this, App(Con("Codec"), x) inside + // a 5-arg application would be incorrectly expanded as a 1-param + // alias, causing exponential type growth. + let mut spine_args: Vec<&Type> = Vec::new(); + let mut head = ty; + while let Type::App(f, a) = head { + spine_args.push(a.as_ref()); + head = f.as_ref(); + } + spine_args.reverse(); + + // Zonk the head + let head_z = self.zonk_ref(head); + let head_resolved = head_z.as_ref().unwrap_or(&head); + + // Zonk each argument + let mut any_changed = head_z.is_some(); + let mut args_z: Vec> = Vec::with_capacity(spine_args.len()); + for arg in &spine_args { + let z = self.zonk_ref(arg); + if z.is_some() { any_changed = true; } + args_z.push(z); + } + + // Normalize arrow: App(App(Con("->"), from), to) → Fun(from, to) + if spine_args.len() >= 2 { + if let Type::Con(sym) = head_resolved { + let wk = &*WELL_KNOWN; + if *sym == wk.arrow || *sym == wk.function { + if spine_args.len() == 2 { + let from = args_z[0].clone().unwrap_or_else(|| (*spine_args[0]).clone()); + let to = args_z[1].clone().unwrap_or_else(|| (*spine_args[1]).clone()); + return Some(Type::fun(from, to)); + } } } } - if f_z.is_none() && a_z.is_none() { - // No unif var changes, but still try alias expansion - let expanded = self.try_expand_alias(ty.clone()); - if expanded == *ty { None } else { Some(expanded) } + + if !any_changed { + // No subterm changes — try alias expansion on the full spine + if self.is_alias_app_non_self_referential(ty) { + let expanded = self.try_expand_alias(ty.clone()); + if expanded == *ty { None } else { Some(expanded) } + } else { + None + } } else { - let result = Type::app( - f_z.unwrap_or_else(|| (**f).clone()), - a_z.unwrap_or_else(|| (**a).clone()), - ); - Some(self.try_expand_alias(result)) + // Rebuild from zonked parts + let mut result = head_z.unwrap_or_else(|| head.clone()); + for (i, arg) in spine_args.iter().enumerate() { + let a = args_z[i].clone().unwrap_or_else(|| (*arg).clone()); + result = Type::app(result, a); + } + // Try alias expansion on the full rebuilt type + if self.is_alias_app_non_self_referential(&result) { + Some(self.try_expand_alias(result)) + } else { + Some(result) + } } } Type::Forall(vars, body) => { @@ -190,16 +355,28 @@ impl UnifyState { } } Type::Con(sym) => { - let name = crate::interner::resolve(*sym).unwrap_or_default(); - if name == "Function" { - return Some(Type::Con(crate::interner::intern("->"))); + let wk = &*WELL_KNOWN; + if *sym == wk.function { + return Some(Type::Con(wk.arrow)); } - if self.type_aliases.is_empty() { - return None; + // Try to expand zero-arg type aliases (e.g. `Size` → `Int`, `NegOne` → -1). + // Skip self-referential aliases to avoid infinite expansion. + // Use qualified key when module qualifier is present (e.g. Tick.Easing). + let alias_key = if let Some(module) = sym.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(sym.name).unwrap_or_default(); + crate::interner::intern(&format!("{}.{}", mod_str, name_str)) + } else { + sym.name + }; + let is_zero_arg = self.type_aliases.get(&alias_key).map_or(false, |(params, _)| params.is_empty()); + if !self.self_referential_aliases.contains(&sym.name) && is_zero_arg + { + let expanded = self.try_expand_alias(ty.clone()); + if expanded == *ty { None } else { Some(expanded) } + } else { + None } - // Try to expand zero-arg type aliases (e.g. `Size` → `Int`) - let expanded = self.try_expand_alias(ty.clone()); - if expanded == *ty { None } else { Some(expanded) } } Type::Var(_) | Type::TypeString(_) | Type::TypeInt(_) => None, } @@ -232,10 +409,24 @@ impl UnifyState { /// Unify two types. Returns Ok(()) on success, Err(TypeError) on failure. pub fn unify(&mut self, span: Span, t1: &Type, t2: &Type) -> Result<(), TypeError> { + self.unify_depth += 1; + let result = self.unify_inner(span, t1, t2); + self.unify_depth -= 1; + result + } + + fn unify_inner(&mut self, span: Span, t1: &Type, t2: &Type) -> Result<(), TypeError> { + if self.unify_depth > 500 { + return Err(TypeError::UnificationError { + span, + expected: t1.clone(), + found: t2.clone(), + }); + } super::check_deadline(); // Fast path for leaf types: avoid clone+zonk when both sides are simple match (t1, t2) { - (Type::Con(a), Type::Con(b)) if a == b => { + (Type::Con(a), Type::Con(b)) if a.name == b.name && !self.con_modules_conflict(a, b) => { return Ok(()); } // Don't fast-fail Con mismatches — one side may be a type alias @@ -278,6 +469,22 @@ impl UnifyState { let t1 = self.zonk(t1.clone()); let t2 = self.zonk(t2.clone()); + // Eagerly expand type aliases after zonk so structural matching + // sees the expanded forms (e.g., `Except e a` → `ExceptT e Identity a`). + // Skip transitively self-referential aliases to prevent infinite loops + // where App-App recursion re-discovers partially-applied fragments. + let t1 = if self.is_alias_app_non_self_referential(&t1) { + let t1_exp = self.try_expand_alias(t1.clone()); + t1_exp + } else { + t1 + }; + let t2 = if self.is_alias_app_non_self_referential(&t2) { + self.try_expand_alias(t2.clone()) + } else { + t2 + }; + match (&t1, &t2) { // Both are the same unification variable (Type::Unif(a), Type::Unif(b)) => { @@ -322,7 +529,7 @@ impl UnifyState { // Same type constructor (already handled in fast path, but zonk may have reduced to Con) (Type::Con(a), Type::Con(b)) => { - if a == b { + if a.name == b.name && !self.con_modules_conflict(a, b) { Ok(()) } else { let t1_exp = self.try_expand_alias(t1.clone()); @@ -330,6 +537,20 @@ impl UnifyState { if t1_exp != t1 || t2_exp != t2 { return self.unify(span, &t1_exp, &t2_exp); } + // Eta-expand partially applied aliases + let missing1 = self.partially_applied_alias_missing(&t1); + let missing2 = self.partially_applied_alias_missing(&t2); + let n = missing1.max(missing2); + if n > 0 { + let fresh_vars: Vec = (0..n).map(|_| Type::Unif(self.fresh_var())).collect(); + let mut t1_eta = t1.clone(); + let mut t2_eta = t2.clone(); + for v in &fresh_vars { + t1_eta = Type::app(t1_eta, v.clone()); + t2_eta = Type::app(t2_eta, v.clone()); + } + return self.unify(span, &t1_eta, &t2_eta); + } Err(TypeError::UnificationError { span, expected: t1, @@ -363,20 +584,58 @@ impl UnifyState { // because zonk normalizes App(App(Con(->),a),b) back to Fun(a,b), // which would cause infinite recursion. (Type::Fun(a, b), Type::App(f, arg)) => { - let partial_arrow = Type::app(Type::Con(crate::interner::intern("->")), (**a).clone()); + let partial_arrow = Type::app(Type::Con(WELL_KNOWN.arrow), (**a).clone()); self.unify(span, &partial_arrow, f)?; self.unify(span, b, arg) } (Type::App(f, arg), Type::Fun(a, b)) => { - let partial_arrow = Type::app(Type::Con(crate::interner::intern("->")), (**a).clone()); + let partial_arrow = Type::app(Type::Con(WELL_KNOWN.arrow), (**a).clone()); self.unify(span, f, &partial_arrow)?; self.unify(span, arg, b) } // Type application - (Type::App(f1, a1), Type::App(f2, a2)) => { - self.unify(span, f1, f2)?; - self.unify(span, a1, a2) + (Type::App(_, _), Type::App(_, _)) => { + // Collect full App spines to compare at the correct granularity. + let (head1, args1) = collect_app_spine(&t1); + let (head2, args2) = collect_app_spine(&t2); + + // When both heads are the same Con and arg counts match, unify args + // directly without creating intermediate App sub-expressions. This + // prevents spurious alias expansion when a name is both a type alias + // (N params) and a data type (M params, M > N): structural decomposition + // of the M-arg chain would create an N-arg sub-expression that looks + // like the alias and triggers infinite re-expansion. + if let (Type::Con(a), Type::Con(b)) = (head1, head2) { + if a.name == b.name && args1.len() == args2.len() { + for (a1, a2) in args1.iter().zip(args2.iter()) { + self.unify(span, a1, a2)?; + } + return Ok(()); + } + } + + // When spine lengths differ, one side may be an alias that needs + // expanding before structural decomposition can match. + if args1.len() != args2.len() { + let t1_is_alias = self.is_alias_app_non_self_referential(&t1); + let t2_is_alias = self.is_alias_app_non_self_referential(&t2); + if t1_is_alias || t2_is_alias { + let t1_exp = if t1_is_alias { self.try_expand_alias(t1.clone()) } else { t1.clone() }; + let t2_exp = if t2_is_alias { self.try_expand_alias(t2.clone()) } else { t2.clone() }; + if t1_exp != t1 || t2_exp != t2 { + return self.unify(span, &t1_exp, &t2_exp); + } + } + } + + // Default: pairwise structural decomposition + if let (Type::App(f1, a1), Type::App(f2, a2)) = (&t1, &t2) { + self.unify(span, f1, f2)?; + self.unify(span, a1, a2) + } else { + unreachable!() + } } // Type-level string literals @@ -414,13 +673,13 @@ impl UnifyState { // `Record r` is equivalent to `{ | r }`, i.e. Record([], Some(r)). // Convert to Record form and unify as records. (Type::App(f, row), Type::Record(fields2, tail2)) if { - matches!(f.as_ref(), Type::Con(sym) if crate::interner::resolve(*sym).unwrap_or_default() == "Record") + matches!(f.as_ref(), Type::Con(sym) if *sym == WELL_KNOWN.record) } => { let empty: Vec<(Symbol, Type)> = vec![]; self.unify_records(span, &empty, &Some(Box::new((**row).clone())), fields2, tail2, &t1, &t2) } (Type::Record(fields1, tail1), Type::App(f, row)) if { - matches!(f.as_ref(), Type::Con(sym) if crate::interner::resolve(*sym).unwrap_or_default() == "Record") + matches!(f.as_ref(), Type::Con(sym) if *sym == WELL_KNOWN.record) } => { let empty: Vec<(Symbol, Type)> = vec![]; self.unify_records(span, fields1, tail1, &empty, &Some(Box::new((**row).clone())), &t1, &t2) @@ -443,6 +702,23 @@ impl UnifyState { if t1_exp != t1 || t2_exp != t2 { return self.unify(span, &t1_exp, &t2_exp); } + // Try eta-expanding partially applied type aliases. + // e.g., `Tree` (alias `type Tree a = Cofree Array a`) vs `Cofree Array`: + // apply a fresh var to both → `Tree ?v` vs `(Cofree Array) ?v`, + // then expand Tree → `(Cofree Array) ?v` — now they match. + let missing1 = self.partially_applied_alias_missing(&t1); + let missing2 = self.partially_applied_alias_missing(&t2); + let n = missing1.max(missing2); + if n > 0 { + let fresh_vars: Vec = (0..n).map(|_| Type::Unif(self.fresh_var())).collect(); + let mut t1_eta = t1.clone(); + let mut t2_eta = t2.clone(); + for v in &fresh_vars { + t1_eta = Type::app(t1_eta, v.clone()); + t2_eta = Type::app(t2_eta, v.clone()); + } + return self.unify(span, &t1_eta, &t2_eta); + } Err(TypeError::UnificationError { span, expected: t1, @@ -560,8 +836,121 @@ impl UnifyState { } } - /// Try to expand a type alias application. - /// Collects args from nested App(App(Con(alias), a1), a2) and substitutes into the alias body. + /// Pre-compute which aliases are transitively self-referential. + /// An alias is self-referential if fully expanding its body (including inner aliases) + /// produces a type that still contains `Con(alias_name)`. + /// For example: `type Codec a = Codec' (Except DecodeError) JSON a` where + /// `type Codec' m a b = Codec m a a b b` — expanding Codec produces a type + /// containing the data type `Codec`. + /// Must be called after all type aliases are registered. + pub fn compute_self_referential_aliases(&mut self) { + let alias_names: Vec = self.type_aliases.keys().cloned().collect(); + let alias_name_set: HashSet = alias_names.iter().cloned().collect(); + for name in alias_names { + // Skip aliases already known to be self-referential (imported from other modules). + if self.self_referential_aliases.contains(&name) { + continue; + } + let (params, body) = self.type_aliases[&name].clone(); + // Quick pre-filter: if the alias body doesn't reference any alias name, + // it cannot be self-referential (even transitively). + if !type_references_any_name(&body, &alias_name_set) { + continue; + } + let param_count = params.len(); + // Build a fully-applied type: App(...App(Con(name), Var(p1)), ..., Var(pN)) + let mut ty = Type::Con(QualifiedIdent { module: None, name }); + for p in ¶ms { + ty = Type::app(ty, Type::Var(*p)); + } + // Expand it (try_expand_alias handles cycles via expanding_aliases) + let expanded = self.try_expand_alias(ty); + // Check if the expanded form contains Con(name) applied to exactly + // param_count args (i.e., a usage that would trigger re-expansion). + // This is arity-aware: if an alias `Codec` (1 param) expands to a body + // containing `Con("Codec")` with 5 args (the data type), that's NOT + // self-referential because 5 != 1. + if contains_self_referential_usage(&expanded, name, param_count) { + self.self_referential_aliases.insert(name); + } + } + } + + /// Like `is_alias_app`, but also rejects "self-referential" aliases whose + /// fully-expanded body contains the same Con name. These cause infinite + /// re-expansion loops when the App-App unification case recursively + /// discovers partially-applied fragments of the expanded type. + fn is_alias_app_non_self_referential(&self, ty: &Type) -> bool { + if self.type_aliases.is_empty() { + return false; + } + let mut head = ty; + let mut arg_count = 0usize; + loop { + match head { + Type::App(f, _) => { + arg_count += 1; + head = f.as_ref(); + } + Type::Con(name) => { + if self.self_referential_aliases.contains(&name.name) { + return false; + } + // When the name has a module qualifier, prefer the qualified alias key. + // This handles cases where two imports provide different aliases with the + // same unqualified name but different param counts (e.g. Common.Replicate + // with 3 params vs CommonM.Replicate with 4 params). + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + self.type_aliases.get(&qualified) + } else { + self.type_aliases.get(&name.name) + }; + return alias_entry + .map_or(false, |(params, _)| params.len() == arg_count); + } + _ => return false, + } + } + } + + /// How many extra args does a partially-applied alias need to become saturated? + /// Returns 0 if the type isn't headed by a known alias, or if already saturated. + fn partially_applied_alias_missing(&self, ty: &Type) -> usize { + let mut head = ty; + let mut applied = 0usize; + loop { + match head { + Type::App(f, _) => { + applied += 1; + head = f.as_ref(); + } + _ => break, + } + } + if let Type::Con(name) = head { + if self.self_referential_aliases.contains(&name.name) { + return 0; + } + let alias_entry = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); + self.type_aliases.get(&qualified) + } else { + self.type_aliases.get(&name.name) + }; + if let Some((params, _)) = alias_entry { + if params.len() > applied { + return params.len() - applied; + } + } + } + 0 + } + fn try_expand_alias(&mut self, ty: Type) -> Type { if self.type_aliases.is_empty() { return ty; @@ -579,25 +968,64 @@ impl UnifyState { } } if let Type::Con(name) = head { + // Compute the actual alias lookup key — use qualified form when module + // qualifier is present, to distinguish e.g. local `Tree` from imported `Y.Tree`. + let alias_key = if let Some(module) = name.module { + let mod_str = crate::interner::resolve(module).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + crate::interner::intern(&format!("{}.{}", mod_str, name_str)) + } else { + name.name + }; // Guard against infinite alias expansion (e.g. `type Number = P.Number` // where P.Number resolves back to Con("Number")) - if self.expanding_aliases.contains(name) { + if self.expanding_aliases.contains(&alias_key) { return ty; } - if let Some((params, body)) = self.type_aliases.get(name).cloned() { + // When the name has a module qualifier, only look up the qualified alias key. + // Do NOT fall back to the unqualified name: a qualified reference like + // `Thread.Thread` (data type from another module) must not be expanded using + // the local alias `type Thread = { ... }` which happens to share the short name. + let alias_entry = self.type_aliases.get(&alias_key).cloned(); + if let Some((params, body)) = alias_entry { // Args collected in reverse order (outermost last) args.reverse(); - if args.len() == params.len() { - // Fully saturated alias — expand + if args.len() >= params.len() { + // Over-saturated expansion: block when the name also exists as a + // data type and arg count fits the data type arity (alias/data-type + // name collision, e.g. `type Codec a = ...` vs `data Codec m i o a b`). + if args.len() > params.len() { + if self.self_referential_aliases.contains(&alias_key) { + return ty; + } + // Check type_con_arities: if the arg count fits a data type with + // that name, this is a data type usage, not an alias application. + if !self.type_con_arities.is_empty() { + let data_arity = self.type_con_arities.iter() + .filter(|(k, _)| k.name == name.name) + .map(|(_, &v)| v) + .max(); + if let Some(arity) = data_arity { + if args.len() <= arity { + return ty; + } + } + } + } + // Expand alias body with the first params.len() args, then apply remaining let subst: std::collections::HashMap = params .iter() .zip(args.iter()) .map(|(&p, &a)| (p, a.clone())) .collect(); - let expanded = self.apply_symbol_subst(&subst, &body); - // Zonk the result in case expansion introduces more structure - self.expanding_aliases.push(*name); - let result = self.zonk(expanded); + let mut expanded = self.apply_symbol_subst(&subst, &body); + // Apply remaining over-saturated args + for extra_arg in &args[params.len()..] { + expanded = Type::App(Box::new(expanded), Box::new((*extra_arg).clone())); + } + self.expanding_aliases.push(alias_key); + // Recursively expand nested aliases in the result + let result = self.try_expand_alias(expanded); self.expanding_aliases.pop(); return result; } @@ -640,15 +1068,19 @@ impl UnifyState { } // Capture-avoiding: check if any forall-bound var name appears // free in the substitution values. If so, alpha-rename to avoid capture. + // Also conservatively rename when substitution values contain unification + // variables, since those may later be solved to types containing the + // forall-bound var names, causing capture. let mut new_vars = vars.clone(); - let needs_rename = new_vars.iter().any(|(v, _)| { + let any_subst_has_unif = inner_subst.values().any(|val| contains_unif_var(val)); + let needs_rename = any_subst_has_unif || new_vars.iter().any(|(v, _)| { inner_subst.values().any(|val| type_has_free_var(val, *v)) }); if needs_rename { use std::collections::HashMap; let mut rename: HashMap = HashMap::new(); for (v, _) in &mut new_vars { - if inner_subst.values().any(|val| type_has_free_var(val, *v)) { + if any_subst_has_unif || inner_subst.values().any(|val| type_has_free_var(val, *v)) { let fresh = fresh_type_var_symbol(*v); rename.insert(*v, Type::Var(fresh)); *v = fresh; @@ -739,6 +1171,21 @@ pub fn type_has_free_var(ty: &Type, name: Symbol) -> bool { } } +/// Check if a type contains any unification variables (unsolved or solved). +pub fn contains_unif_var(ty: &Type) -> bool { + match ty { + Type::Unif(_) => true, + Type::Fun(a, b) => contains_unif_var(a) || contains_unif_var(b), + Type::App(f, a) => contains_unif_var(f) || contains_unif_var(a), + Type::Forall(_, body) => contains_unif_var(body), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| contains_unif_var(t)) + || tail.as_ref().map_or(false, |t| contains_unif_var(t)) + } + Type::Var(_) | Type::Con(_) | Type::TypeString(_) | Type::TypeInt(_) => false, + } +} + /// Generate a fresh unique symbol for alpha-renaming forall-bound variables. pub fn fresh_type_var_symbol(base: Symbol) -> Symbol { use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/tests/ast.rs b/tests/ast.rs new file mode 100644 index 00000000..f7af3f6b --- /dev/null +++ b/tests/ast.rs @@ -0,0 +1,1029 @@ +use purescript_fast_compiler::ast::{self, Binder, Decl, Expr, Literal, TypeExpr, DefinitionSite}; +use purescript_fast_compiler::cst::unqualified_ident; +use purescript_fast_compiler::interner::intern; +use purescript_fast_compiler::parser; +use purescript_fast_compiler::typechecker::error::TypeError; +use purescript_fast_compiler::typechecker::registry::{ModuleExports, ModuleRegistry}; +use purescript_fast_compiler::typechecker::types::{Scheme, Type}; + +fn convert_module(source: &str) -> (ast::Module, Vec) { + let module = parser::parse(source).expect("parse failed"); + ast::convert(module, &ModuleRegistry::new()) +} + +fn convert_module_with_registry(source: &str, registry: &ModuleRegistry) -> (ast::Module, Vec) { + let module = parser::parse(source).expect("parse failed"); + ast::convert(module, registry) +} + +/// Build a registry containing a module "Data.Foo" that exports: +/// - values: foo, bar +/// - data type: Baz with constructors MkBaz, NoBaz +/// - class: MyClass (with method myMethod) +fn make_test_registry() -> ModuleRegistry { + let mut exports = ModuleExports::default(); + + // Values + exports.values.insert( + unqualified_ident("foo"), + Scheme::mono(Type::prim_con("Int")), + ); + exports.values.insert( + unqualified_ident("bar"), + Scheme::mono(Type::prim_con("String")), + ); + + // Data constructors for type Baz + let mk_baz = unqualified_ident("MkBaz"); + let no_baz = unqualified_ident("NoBaz"); + exports.data_constructors.insert( + unqualified_ident("Baz"), + vec![mk_baz, no_baz], + ); + // Constructors are also values + exports.values.insert(mk_baz, Scheme::mono(Type::prim_con("Baz"))); + exports.values.insert(no_baz, Scheme::mono(Type::prim_con("Baz"))); + + // Class + exports.class_param_counts.insert(unqualified_ident("MyClass"), 1); + // Class method + exports.class_methods.insert( + unqualified_ident("myMethod"), + (unqualified_ident("MyClass"), vec![unqualified_ident("a")]), + ); + exports.values.insert( + unqualified_ident("myMethod"), + Scheme::mono(Type::prim_con("Int")), + ); + + let mut registry = ModuleRegistry::new(); + registry.register(&[intern("Data"), intern("Foo")], exports); + registry +} + +fn get_value_decl_expr<'a>(module: &'a ast::Module, name: &str) -> &'a Expr { + let sym = intern(name); + for decl in &module.decls { + if let Decl::Value { + name: n, guarded, .. + } = decl + { + if n.value == sym { + if let ast::GuardedExpr::Unconditional(expr) = guarded { + return expr; + } + } + } + } + panic!("value declaration '{}' not found", name); +} + +// ===== Paren stripping ===== + +#[test] +fn test_paren_stripping() { + let (module, errors) = convert_module("module T where\nf = (42)"); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + assert!( + matches!(expr, Expr::Literal { lit: Literal::Int(42), .. }), + "expected Literal(42), got {:?}", + expr + ); +} + +#[test] +fn test_nested_paren_stripping() { + let (module, errors) = convert_module("module T where\nf = ((((42))))"); + assert!(errors.is_empty()); + let expr = get_value_decl_expr(&module, "f"); + assert!(matches!(expr, Expr::Literal { lit: Literal::Int(42), .. })); +} + +// ===== Operator desugaring ===== + +#[test] +fn test_value_operator_desugaring() { + let source = "module T where\ninfixl 6 add as +\nadd a b = a\nx = 1 + 2"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + // Should be App(App(Var(+), 1), 2) — operator name is preserved + match expr { + Expr::App { func, arg, .. } => { + // arg = 2 + assert!( + matches!(arg.as_ref(), Expr::Literal { lit: Literal::Int(2), .. }), + "expected 2 as right arg" + ); + // func = App(Var(+), 1) + match func.as_ref() { + Expr::App { func: inner_func, arg: inner_arg, .. } => { + assert!( + matches!(inner_arg.as_ref(), Expr::Literal { lit: Literal::Int(1), .. }), + "expected 1 as left arg" + ); + // inner_func should be Var(+) — the operator name (env lookup uses operator names) + match inner_func.as_ref() { + Expr::Var { name, .. } => { + let name_str = purescript_fast_compiler::interner::resolve(name.name).unwrap_or_default(); + assert_eq!(name_str, "+", "operator should use operator name, not target"); + } + other => panic!("expected Var for operator, got {:?}", other), + } + } + other => panic!("expected inner App, got {:?}", other), + } + } + other => panic!("expected App(App(...)), got {:?}", other), + } +} + +#[test] +fn test_op_parens_desugaring() { + let source = "module T where\ninfixl 6 add as +\nadd a b = a\nf = (+)"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + // (+) should become Var { name: + } — the operator name (env lookup uses operator names) + match expr { + Expr::Var { name, .. } => { + let sym = intern("+"); + assert_eq!(name.name, sym, "expected +"); + } + other => panic!("expected Var for (+), got {:?}", other), + } +} + +// ===== Operator precedence rebalancing ===== + +#[test] +fn test_operator_precedence_rebalancing() { + // `1 + 2 * 3` with + at prec 6 and * at prec 7 + // Parser gives right-assoc: Op(1, +, Op(2, *, 3)) + // After rebalancing: App(App(+, 1), App(App(*, 2), 3)) + // i.e. (1 + (2 * 3)) + let source = "module T where\ninfixl 6 add as +\ninfixl 7 mul as *\nadd a b = a\nmul a b = a\nx = 1 + 2 * 3"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + // Top-level: App(App(+, 1), App(App(*, 2), 3)) + match expr { + Expr::App { func, arg: right, .. } => { + // right = App(App(*, 2), 3) + match right.as_ref() { + Expr::App { func: mul_app, arg: three, .. } => { + assert!(matches!(three.as_ref(), Expr::Literal { lit: Literal::Int(3), .. })); + match mul_app.as_ref() { + Expr::App { arg: two, .. } => { + assert!(matches!(two.as_ref(), Expr::Literal { lit: Literal::Int(2), .. })); + } + other => panic!("expected App(*, 2), got {:?}", other), + } + } + other => panic!("expected App(App(*, 2), 3), got {:?}", other), + } + // func = App(+, 1) + match func.as_ref() { + Expr::App { arg: one, .. } => { + assert!(matches!(one.as_ref(), Expr::Literal { lit: Literal::Int(1), .. })); + } + other => panic!("expected App(+, 1), got {:?}", other), + } + } + other => panic!("expected outer App, got {:?}", other), + } +} + +#[test] +fn test_operator_precedence_reverse() { + // `1 * 2 + 3` with * at prec 7 and + at prec 6 + // Parser gives: Op(1, *, Op(2, +, 3)) + // After rebalancing with shunting-yard: App(App(+, App(App(*, 1), 2)), 3) + // i.e. ((1 * 2) + 3) + let source = "module T where\ninfixl 6 add as +\ninfixl 7 mul as *\nadd a b = a\nmul a b = a\nx = 1 * 2 + 3"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + // Top-level: App(App(+, App(App(*, 1), 2)), 3) + match expr { + Expr::App { func, arg: three, .. } => { + assert!( + matches!(three.as_ref(), Expr::Literal { lit: Literal::Int(3), .. }), + "expected 3 as right arg of +" + ); + match func.as_ref() { + Expr::App { func: plus_var, arg: mul_expr, .. } => { + // plus_var should use operator name (+), not target name (add) + match plus_var.as_ref() { + Expr::Var { name, .. } => { + let s = purescript_fast_compiler::interner::resolve(name.name).unwrap_or_default(); + assert_eq!(s, "+"); + } + other => panic!("expected Var(+), got {:?}", other), + } + // mul_expr = App(App(*, 1), 2) + match mul_expr.as_ref() { + Expr::App { func: mul_app, arg: two, .. } => { + assert!(matches!(two.as_ref(), Expr::Literal { lit: Literal::Int(2), .. })); + match mul_app.as_ref() { + Expr::App { arg: one, .. } => { + assert!(matches!(one.as_ref(), Expr::Literal { lit: Literal::Int(1), .. })); + } + other => panic!("expected App(*, 1), got {:?}", other), + } + } + other => panic!("expected App(App(*, 1), 2), got {:?}", other), + } + } + other => panic!("expected App(+, ...), got {:?}", other), + } + } + other => panic!("expected outer App, got {:?}", other), + } +} + +// ===== Type operator desugaring ===== + +#[test] +fn test_type_operator_desugaring() { + let source = "module T where\ninfixr 0 type RowApply as +\ndata RowApply\nf :: forall r. Record (x :: Int + r) -> Int\nf x = 42"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + // Find the type signature + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature for f not found"); + // The type should contain no TypeOp — it should be desugared to App(App(Constructor(RowApply), ...)) + fn has_no_type_op(ty: &TypeExpr) -> bool { + match ty { + TypeExpr::App { constructor, arg, .. } => has_no_type_op(constructor) && has_no_type_op(arg), + TypeExpr::Function { from, to, .. } => has_no_type_op(from) && has_no_type_op(to), + TypeExpr::Forall { vars, ty, .. } => { + vars.iter().all(|(_, _, k)| k.as_ref().map_or(true, |k| has_no_type_op(k))) && has_no_type_op(ty) + } + TypeExpr::Constrained { constraints, ty, .. } => { + constraints.iter().all(|c| c.args.iter().all(has_no_type_op)) && has_no_type_op(ty) + } + TypeExpr::Record { fields, .. } => fields.iter().all(|f| has_no_type_op(&f.ty)), + TypeExpr::Row { fields, tail, .. } => { + fields.iter().all(|f| has_no_type_op(&f.ty)) && tail.as_ref().map_or(true, |t| has_no_type_op(t)) + } + TypeExpr::Kinded { ty, kind, .. } => has_no_type_op(ty) && has_no_type_op(kind), + _ => true, + } + } + assert!(has_no_type_op(sig), "type expression should not contain TypeOp after conversion"); +} + +// ===== Binder operator desugaring ===== + +#[test] +fn test_binder_op_becomes_constructor() { + let source = "module T where\ndata List a = Nil | Cons a (List a)\ninfixr 6 Cons as :\nf x = case x of\n a : b -> a"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + // Should be Case with a Binder::Constructor (not Binder::Op) + match expr { + Expr::Case { alts, .. } => { + assert!(!alts.is_empty()); + let binder = &alts[0].binders[0]; + match binder { + Binder::Constructor { name, args, .. } => { + let name_str = purescript_fast_compiler::interner::resolve(name.name).unwrap_or_default(); + assert_eq!(name_str, "Cons", "binder op should desugar to target 'Cons'"); + assert_eq!(args.len(), 2); + } + other => panic!("expected Binder::Constructor, got {:?}", other), + } + } + other => panic!("expected Case expression, got {:?}", other), + } +} + +// ===== Definition sites ===== + +#[test] +fn test_local_definition_site() { + let source = "module T where\nf = 1\ng = f"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "g"); + match expr { + Expr::Var { definition_site, .. } => { + assert!( + matches!(definition_site, DefinitionSite::Local(_)), + "expected Local definition site for f" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_lambda_scope_definition_site() { + let source = "module T where\nf = \\x -> x"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + match expr { + Expr::Lambda { body, .. } => match body.as_ref() { + Expr::Var { definition_site, .. } => { + assert!( + matches!(definition_site, DefinitionSite::Local(_)), + "lambda-bound var should have Local definition site" + ); + } + other => panic!("expected Var in lambda body, got {:?}", other), + }, + other => panic!("expected Lambda, got {:?}", other), + } +} + +#[test] +fn test_let_scope_definition_site() { + let source = "module T where\nf = let\n y = 1\n in y"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + match expr { + Expr::Let { body, .. } => match body.as_ref() { + Expr::Var { definition_site, .. } => { + assert!(matches!(definition_site, DefinitionSite::Local(_))); + } + other => panic!("expected Var in let body, got {:?}", other), + }, + other => panic!("expected Let, got {:?}", other), + } +} + +#[test] +fn test_constructor_definition_site() { + let source = "module T where\ndata Maybe a = Just a | Nothing\nx = Just 1"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::App { func, .. } => match func.as_ref() { + Expr::Constructor { + definition_site, .. + } => { + assert!(matches!(definition_site, DefinitionSite::Local(_))); + } + other => panic!("expected Constructor, got {:?}", other), + }, + other => panic!("expected App(Constructor, ...), got {:?}", other), + } +} + +// ===== Type expression paren stripping ===== + +#[test] +fn test_type_paren_stripping() { + let source = "module T where\nf :: (Int) -> Int\nf x = x"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature not found"); + // The `from` of the function type should be Constructor(Int), not Parens + match sig { + TypeExpr::Function { from, .. } => { + assert!( + matches!(from.as_ref(), TypeExpr::Constructor { .. }), + "expected Constructor after paren stripping, got {:?}", + from + ); + } + other => panic!("expected Function type, got {:?}", other), + } +} + +// ===== Instance/Derive class definition site ===== + +#[test] +fn test_instance_class_definition_site() { + let source = "module T where\nclass MyClass a where\n foo :: a -> Int\ninstance MyClass Int where\n foo x = 1"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let instance = module.decls.iter().find_map(|d| { + if let Decl::Instance { class_definition_site, .. } = d { + Some(class_definition_site) + } else { + None + } + }).expect("instance not found"); + assert!( + matches!(instance, DefinitionSite::Local(_)), + "class definition site should be Local" + ); +} + +// ===== Fixity target definition site ===== + +#[test] +fn test_fixity_target_definition_site() { + let source = "module T where\nadd a b = a\ninfixl 6 add as +"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let fixity_site = module.decls.iter().find_map(|d| { + if let Decl::Fixity { target_definition_site, .. } = d { + Some(target_definition_site) + } else { + None + } + }).expect("fixity decl not found"); + assert!(matches!(fixity_site, DefinitionSite::Local(_))); +} + +// ===== No panics on various module shapes ===== + +#[test] +fn test_empty_module() { + let (module, errors) = convert_module("module T where"); + assert!(errors.is_empty()); + assert!(module.decls.is_empty()); +} + +#[test] +fn test_do_notation() { + let source = "module T where\npure x = x\nf = do\n x <- pure 1\n pure x"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + assert!(matches!(expr, Expr::Do { .. })); +} + +#[test] +fn test_record_literal() { + let source = "module T where\nx = { a: 1, b: 2 }"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + assert!(matches!(expr, Expr::Record { .. })); +} + +#[test] +fn test_case_expression() { + let source = "module T where\ndata Bool2 = T2 | F2\nf x = case x of\n T2 -> 1\n F2 -> 0"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "f"); + assert!(matches!(expr, Expr::Case { .. })); +} + +// ===== Error reporting for unresolved names ===== + +#[test] +fn test_error_undefined_variable() { + let source = "module T where\nf = unknownVar"; + let (_module, errors) = convert_module(source); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UndefinedVariable { .. })), + "expected UndefinedVariable error, got: {:?}", errors + ); +} + +#[test] +fn test_error_undefined_constructor() { + let source = "module T where\nf = UnknownCtor"; + let (_module, errors) = convert_module(source); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UndefinedVariable { .. })), + "expected UndefinedVariable error for unknown constructor, got: {:?}", errors + ); +} + +#[test] +fn test_error_unknown_type_in_signature() { + let source = "module T where\nf :: UnknownType\nf = 1"; + let (_module, errors) = convert_module(source); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UnknownName { .. })), + "expected UnknownName error, got: {:?}", errors + ); +} + +#[test] +fn test_error_unknown_class_in_constraint() { + let source = "module T where\nf :: UnknownClass a => a -> a\nf x = x"; + let (_module, errors) = convert_module(source); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UnknownName { .. })), + "expected UnknownName error for unknown class, got: {:?}", errors + ); +} + +#[test] +fn test_error_unknown_type_operator() { + let source = "module T where\nf :: Int ~> String\nf = 1"; + let (_module, errors) = convert_module(source); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UndefinedVariable { .. })), + "expected UndefinedVariable error for unknown type operator, got: {:?}", errors + ); +} + +#[test] +fn test_no_error_for_known_names() { + let source = "module T where\ndata Maybe a = Just a | Nothing\nf = Just 1"; + let (_module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); +} + +// ===== Qualified import definition sites ===== + +#[test] +fn test_qualified_import_value_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nx = F.foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified value should have Imported definition site" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_qualified_import_constructor_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nx = F.MkBaz"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Constructor { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified constructor should have Imported definition site" + ); + } + other => panic!("expected Constructor, got {:?}", other), + } +} + +#[test] +fn test_qualified_import_type_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nf :: F.Baz -> Int\nf x = 42"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature for f not found"); + // The `from` of the Function type should be Constructor with Imported site + match sig { + TypeExpr::Function { from, .. } => match from.as_ref() { + TypeExpr::Constructor { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified type should have Imported definition site" + ); + } + other => panic!("expected Constructor type, got {:?}", other), + }, + other => panic!("expected Function type, got {:?}", other), + } +} + +#[test] +fn test_qualified_import_class_in_constraint_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nf :: F.MyClass a => a -> a\nf x = x"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature for f not found"); + // Should be Constrained with Imported definition site on the constraint + match sig { + TypeExpr::Constrained { constraints, .. } => { + assert!(!constraints.is_empty(), "expected at least one constraint"); + assert_eq!( + constraints[0].definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified class constraint should have Imported definition site" + ); + } + other => panic!("expected Constrained type, got {:?}", other), + } +} + +#[test] +fn test_unqualified_import_value_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo\nx = foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "unqualified imported value should have Imported definition site" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_unqualified_import_constructor_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo\nx = MkBaz"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Constructor { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "unqualified imported constructor should have Imported definition site" + ); + } + other => panic!("expected Constructor, got {:?}", other), + } +} + +#[test] +fn test_qualified_import_undefined_value_error() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nx = F.nonexistent"; + let (_module, errors) = convert_module_with_registry(source, ®istry); + assert!( + errors.iter().any(|e| matches!(e, TypeError::UndefinedVariable { .. })), + "expected UndefinedVariable error for F.nonexistent, got: {:?}", errors + ); +} + +#[test] +fn test_qualified_import_class_method_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo as F\nx = F.myMethod"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified class method should have Imported definition site" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_explicit_import_value_definition_site() { + let registry = make_test_registry(); + let source = "module T where\nimport Data.Foo (foo)\nx = foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "explicitly imported value should have Imported definition site" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_hiding_import_definition_site() { + let registry = make_test_registry(); + // Import everything except bar; foo should still be available + let source = "module T where\nimport Data.Foo hiding (bar)\nx = foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_local_shadows_import_definition_site() { + let registry = make_test_registry(); + // Local definition of `foo` should shadow the import + let source = "module T where\nimport Data.Foo\nfoo = 42\nx = foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert!( + matches!(definition_site, DefinitionSite::Local(_)), + "local definition should shadow import, got {:?}", + definition_site + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_qualified_bypasses_local_shadow() { + let registry = make_test_registry(); + // Local `foo` shadows unqualified import, but F.foo should still resolve to import + let source = "module T where\nimport Data.Foo as F\nfoo = 42\nx = F.foo"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Data.Foo") }, + "qualified reference should bypass local shadow" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_module_not_found_error() { + let registry = ModuleRegistry::new(); + let source = "module T where\nimport Data.Unknown\nx = 1"; + let (_module, errors) = convert_module_with_registry(source, ®istry); + assert!( + errors.iter().any(|e| matches!(e, TypeError::ModuleNotFound { .. })), + "expected ModuleNotFound error, got: {:?}", errors + ); +} + +// ===== Operator definition site points to target value, not fixity decl ===== + +#[test] +fn test_operator_definition_site_points_to_target_value() { + // Fixity declared BEFORE value — operator's definition site should still + // point to the value's span, not the fixity declaration's span. + let source = "module T where\ninfixl 6 add as +\nadd a b = a\nx = 1 + 2"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + + // Get the span of the `add` value declaration + let add_sym = intern("add"); + let add_span = module.decls.iter().find_map(|d| { + if let Decl::Value { name, span, .. } = d { + if name.value == add_sym { Some(*span) } else { None } + } else { + None + } + }).expect("add value decl not found"); + + // The desugared operator in `x = 1 + 2` should have Var(add) whose + // definition site is Local(add_span), not Local(fixity_span) + let expr = get_value_decl_expr(&module, "x"); + // Dig into App(App(Var(add), 1), 2) → get the Var(add) node + match expr { + Expr::App { func, .. } => match func.as_ref() { + Expr::App { func: inner_func, .. } => match inner_func.as_ref() { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Local(add_span), + "operator's definition site should point to the target value's span" + ); + } + other => panic!("expected Var(add), got {:?}", other), + }, + other => panic!("expected inner App, got {:?}", other), + }, + other => panic!("expected App, got {:?}", other), + } +} + +#[test] +fn test_op_parens_definition_site_points_to_target_value() { + // `(+)` should resolve to Var(add) with add's span + let source = "module T where\ninfixl 6 add as +\nadd a b = a\nf = (+)"; + let (module, errors) = convert_module(source); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + + let add_sym = intern("add"); + let add_span = module.decls.iter().find_map(|d| { + if let Decl::Value { name, span, .. } = d { + if name.value == add_sym { Some(*span) } else { None } + } else { + None + } + }).expect("add value decl not found"); + + let expr = get_value_decl_expr(&module, "f"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Local(add_span), + "op-parens definition site should point to target value's span" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +// ===== Re-exported definition sites use original defining module ===== + +/// Build a registry where module "Reexport.Mod" re-exports values from "Original.Mod". +/// The origin maps point back to "Original.Mod". +fn make_reexport_registry() -> ModuleRegistry { + let mut exports = ModuleExports::default(); + + // Values originally from Original.Mod, re-exported by Reexport.Mod + exports.values.insert( + unqualified_ident("thing"), + Scheme::mono(Type::prim_con("Int")), + ); + exports.values.insert( + unqualified_ident("MkThing"), + Scheme::mono(Type::prim_con("Thing")), + ); + + // Data constructors + exports.data_constructors.insert( + unqualified_ident("Thing"), + vec![unqualified_ident("MkThing")], + ); + + // Class + exports.class_param_counts.insert(unqualified_ident("ThingClass"), 1); + exports.values.insert( + unqualified_ident("thingMethod"), + Scheme::mono(Type::prim_con("Int")), + ); + exports.class_methods.insert( + unqualified_ident("thingMethod"), + (unqualified_ident("ThingClass"), vec![unqualified_ident("a")]), + ); + + // Origin maps: everything originally comes from "Original.Mod" + let original_mod = intern("Original.Mod"); + exports.value_origins.insert(intern("thing"), original_mod); + exports.value_origins.insert(intern("MkThing"), original_mod); + exports.value_origins.insert(intern("thingMethod"), original_mod); + exports.type_origins.insert(intern("Thing"), original_mod); + exports.class_origins.insert(intern("ThingClass"), original_mod); + + let mut registry = ModuleRegistry::new(); + registry.register(&[intern("Reexport"), intern("Mod")], exports); + registry +} + +#[test] +fn test_reexported_value_definition_site_uses_original_module() { + let registry = make_reexport_registry(); + let source = "module T where\nimport Reexport.Mod\nx = thing"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Original.Mod") }, + "re-exported value should point to original module, not re-exporter" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} + +#[test] +fn test_reexported_constructor_definition_site_uses_original_module() { + let registry = make_reexport_registry(); + let source = "module T where\nimport Reexport.Mod\nx = MkThing"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Constructor { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Original.Mod") }, + "re-exported constructor should point to original module" + ); + } + other => panic!("expected Constructor, got {:?}", other), + } +} + +#[test] +fn test_reexported_type_definition_site_uses_original_module() { + let registry = make_reexport_registry(); + let source = "module T where\nimport Reexport.Mod\nf :: Thing -> Int\nf x = 42"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature for f not found"); + match sig { + TypeExpr::Function { from, .. } => match from.as_ref() { + TypeExpr::Constructor { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Original.Mod") }, + "re-exported type should point to original module" + ); + } + other => panic!("expected Constructor type, got {:?}", other), + }, + other => panic!("expected Function type, got {:?}", other), + } +} + +#[test] +fn test_reexported_class_definition_site_uses_original_module() { + let registry = make_reexport_registry(); + let source = "module T where\nimport Reexport.Mod\nf :: ThingClass a => a -> a\nf x = x"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let sym = intern("f"); + let sig = module.decls.iter().find_map(|d| { + if let Decl::TypeSignature { name, ty, .. } = d { + if name.value == sym { Some(ty) } else { None } + } else { + None + } + }).expect("type signature for f not found"); + match sig { + TypeExpr::Constrained { constraints, .. } => { + assert!(!constraints.is_empty()); + assert_eq!( + constraints[0].definition_site, + DefinitionSite::Imported { module: intern("Original.Mod") }, + "re-exported class constraint should point to original module" + ); + } + other => panic!("expected Constrained type, got {:?}", other), + } +} + +#[test] +fn test_qualified_reexported_value_uses_original_module() { + let registry = make_reexport_registry(); + let source = "module T where\nimport Reexport.Mod as R\nx = R.thing"; + let (module, errors) = convert_module_with_registry(source, ®istry); + assert!(errors.is_empty(), "unexpected errors: {:?}", errors); + let expr = get_value_decl_expr(&module, "x"); + match expr { + Expr::Var { definition_site, .. } => { + assert_eq!( + *definition_site, + DefinitionSite::Imported { module: intern("Original.Mod") }, + "qualified re-exported value should point to original module" + ); + } + other => panic!("expected Var, got {:?}", other), + } +} diff --git a/tests/build.rs b/tests/build.rs index d6965218..eee3f2e7 100644 --- a/tests/build.rs +++ b/tests/build.rs @@ -5,13 +5,42 @@ use ntest_timeout::timeout; use purescript_fast_compiler::build::{ - build_from_sources, build_from_sources_with_js, build_from_sources_with_options, - build_from_sources_with_registry, BuildError, BuildOptions, + build_from_sources_with_js, build_from_sources_with_options, build_from_sources_with_registry, + BuildError, BuildOptions, BuildResult, }; use purescript_fast_compiler::typechecker::error::TypeError; +use purescript_fast_compiler::typechecker::ModuleRegistry; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; + +/// Shared support package build result. Built lazily on first access so that +/// all three tests (build_support_packages, _passing, _failing) share a single +/// build of the ~290 support modules instead of each rebuilding independently. +/// This eliminates CPU contention when tests run in parallel. +struct SupportBuild { + sources: Vec<(String, String)>, + result: BuildResult, + registry: Arc, +} + +static SUPPORT_BUILD: OnceLock = OnceLock::new(); + +fn get_support_build() -> &'static SupportBuild { + SUPPORT_BUILD.get_or_init(|| { + let sources = collect_support_sources(); + let source_refs: Vec<(&str, &str)> = sources + .iter() + .map(|(p, s)| (p.as_str(), s.as_str())) + .collect(); + let (result, registry) = build_from_sources_with_registry(&source_refs, None); + SupportBuild { + sources, + result, + registry: Arc::new(registry), + } + }) +} /// Support packages from tests/fixtures/packages used by the original compiler tests. const SUPPORT_PACKAGES: &[&str] = &[ @@ -74,22 +103,16 @@ const SUPPORT_PACKAGES: &[&str] = &[ ]; #[test] +#[timeout(6000)] // 6 second timeout to prevent infinite loops in failing fixtures. 6 seconds is far more than this test should ever need. fn build_support_packages() { - // Collect all .purs source files from support package src/ directories - let source_refs_string = collect_support_sources(); + let support = get_support_build(); + let result = &support.result; eprintln!( "Building support packages ({} modules)...", - source_refs_string.len() + support.sources.len() ); - let source_refs: Vec<(&str, &str)> = source_refs_string - .iter() - .map(|(p, s)| (p.as_str(), s.as_str())) - .collect(); - - let result = build_from_sources(&source_refs); - assert!( result.build_errors.is_empty(), "Expected no build errors in support packages, but got:\n{}", @@ -173,7 +196,9 @@ fn collect_js_companions(sources: &[(String, String)]) -> HashMap Vec<(String, Vec<(String, String)>, HashMap)> { +fn collect_build_units( + fixtures_dir: &Path, +) -> Vec<(String, Vec<(String, String)>, HashMap)> { // First, collect all directory names and file stems let mut dir_names: HashSet = HashSet::new(); let mut file_stems: HashSet = HashSet::new(); @@ -290,6 +315,7 @@ fn extract_module_name(source: &str) -> Option { } #[test] +#[timeout(6000)] // 6 second timeout to prevent infinite loops in failing fixtures. 6 seconds is far more than this test should ever need. fn build_fixture_original_compiler_passing() { let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/original-compiler/passing"); @@ -300,14 +326,8 @@ fn build_fixture_original_compiler_passing() { let units = collect_build_units(&fixtures_dir); assert!(!units.is_empty(), "Expected passing fixture build units"); - // Build support packages once to get a shared registry - let support_sources_string = collect_support_sources(); - let support_sources: Vec<(&str, &str)> = support_sources_string - .iter() - .map(|(p, s)| (p.as_str(), s.as_str())) - .collect(); - let (_, registry) = build_from_sources_with_registry(&support_sources, None); - let registry = Arc::new(registry); + // Use shared support build (built lazily on first access, shared across tests) + let registry = Arc::clone(&get_support_build().registry); let mut total = 0; let mut clean = 0; @@ -364,7 +384,11 @@ fn build_fixture_original_compiler_passing() { } for m in &result.modules { if fixture_module_names.contains(&m.module_name) && !m.type_errors.is_empty() { - lines.push(format!(" [{}]", m.module_name)); + lines.push(format!( + " [{}, {}]", + m.module_name, + m.path.to_string_lossy() + )); for e in &m.type_errors { lines.push(format!(" {}", e)); } @@ -384,252 +408,26 @@ fn build_fixture_original_compiler_passing() { failures.len(), ); - if !failures.is_empty() { - let summary: Vec = failures - .iter() - .map(|(name, errors)| format!("{}:\n{}", name, errors)) - .collect(); - panic!( - "{}/{} build units failed:\n\n{}", - failures.len(), - total, - summary.join("\n\n") - ); - } -} + let summary: Vec = failures + .iter() + .map(|(name, errors)| format!("{}:\n{}", name, errors)) + .collect(); -/// Failing fixtures skipped: compile cleanly in our compiler due to missing checks. -const SKIP_FAILING_FIXTURES: &[&str] = &[ - // "3765", -- fixed: infinite row type detection (same tail with conflicting fields) - // Kind checking not implemented - // "1570", -- fixed: ExpectedType check for partially-applied type in binder annotation - // "2601", -- fixed: type alias kind annotation now preserved + Pass C catches mismatch - // "3077", -- fixed: post-inference kind checking catches Symbol/Type kind mismatch - // "3765-kinds", -- fixed: row kinds in convert_kind_expr enables kind-level row unification - // "DiffKindsSameName", // fixed: cross-module kind propagation with qualified names - // "InfiniteKind", -- fixed: kind checking detects infinite kinds - // "InfiniteKind2", -- fixed: kind checking detects self-referencing infinite kinds - // "MonoKindDataBindingGroup", - // "PolykindInstantiatedInstance", -- fixed: deferred lambda kind check catches Symbol-as-Type domain - // "PolykindInstantiation", -- fixed: expression-level type annotation kind checking - // "RowsInKinds", -- fixed: row kinds in convert_kind_expr enables kind-level row unification - // "StandaloneKindSignatures1", -- fixed: expression-level type annotation kind checking - // "StandaloneKindSignatures2", -- fixed: skolemized standalone kind checking - // "StandaloneKindSignatures3", -- fixed: kind checking catches standalone kind sig violations - // "StandaloneKindSignatures4", -- fixed: class standalone kind sig storage + instance head checking - // "SkolemEscapeKinds", -- fixed: impredicative kind detection (higher-rank kind as type arg) - // "UnsupportedTypeInKind", -- fixed: constraint in kind position detection - // "QuantificationCheckFailure", -- fixed: standalone kind sig quantification check - // "QuantificationCheckFailure2", -- fixed: deferred quantification check detects unsolved kind vars in forall - // "QuantificationCheckFailure3", -- fixed: visible dependent quantification detection - // "QuantifiedKind", -- fixed: forall kind annotation forward reference check - // "ScopedKindVariableSynonym", -- fixed: check free type vars in type alias bodies - // Orphan instance / overlapping instance checks not implemented - // "OrphanInstance", -- fixed: orphan instance detection - // "OrphanInstanceFunDepCycle", -- fixed: fundep-aware orphan instance detection - // "OrphanInstanceNullary", -- fixed: orphan instance detection - // "OrphanInstanceWithDetermined", -- fixed: fundep-aware orphan instance detection - // "OrphanUnnamedInstance", -- fixed: orphan instance detection - // "OverlapAcrossModules", -- fixed: cross-module overlap detection - // "OverlapAcrossModulesUnnamedInstance", -- fixed: cross-module overlap detection - // "OverlappingInstances", -- fixed: use-time overlap detection - // "OverlappingUnnamedInstances", -- fixed: use-time overlap detection - // "PolykindInstanceOverlapping", -- fixed: CST-level alpha-eq for kind-annotated instances - // "PolykindUnnamedInstanceOverlapping", -- fixed: CST-level alpha-eq for kind-annotated instances - // Role system not implemented - // "CoercibleRepresentational6", - // "CoercibleRepresentational7", - // "CoercibleRoleMismatch1", - // "CoercibleRoleMismatch2", - // "CoercibleRoleMismatch3", - // "CoercibleRoleMismatch4", - // "CoercibleRoleMismatch5", - // Export/import conflict and transitive export checks not implemented - // "ConflictingExports", -- fixed: ExportConflict with origin tracking - // "ConflictingImports", -- fixed: scope conflict detection - // "ConflictingImports2", -- fixed: scope conflict detection - // "ConflictingQualifiedImports", -- fixed: scope conflict detection - // "ConflictingQualifiedImports2", -- fixed: ExportConflict detection - // "ExportConflictClass", -- fixed: class names in data_constructors for export conflict - // "ExportConflictClassAndType", -- fixed: class names in data_constructors for export conflict - // "ExportConflictCtor", -- fixed: ExportConflict with origin tracking - // "ExportConflictType", -- fixed: ExportConflict with origin tracking - // "ExportConflictTypeOp", -- fixed: ExportConflict with origin tracking - // "ExportConflictValue", -- fixed: ExportConflict with origin tracking - // "ExportConflictValueOp", -- fixed: ExportConflict with origin tracking - // "RequiredHiddenType", -- fixed: transitive export check for value types - // "TransitiveDctorExport", -- fixed: constructor field type transitive export check - // "TransitiveDctorExportError", -- fixed: partial constructor export check - // "DctorOperatorAliasExport", -- fixed: constructor operator export check - // "TransitiveSynonymExport", -- fixed: type synonym transitive export check - // "TransitiveKindExport", - // "2197-shouldFail", -- fixed: ScopeConflict for type alias re-defining explicitly imported type - // FFI checks — fixed: js_ffi module parses JS and validates exports - // "DeprecatedFFICommonJSModule", - // "MissingFFIImplementations", - // "UnsupportedFFICommonJSExports1", - // "UnsupportedFFICommonJSExports2", - // "UnsupportedFFICommonJSImports1", - // "UnsupportedFFICommonJSImports2", - // Instance signature checks not implemented - // "InstanceSigsBodyIncorrect", -- fixed: instance sig body check - // "InstanceSigsDifferentTypes", -- fixed: instance sig type check - // "InstanceSigsIncorrectType", -- fixed: instance sig type check - // "InstanceSigsOrphanTypeDeclaration", -- fixed: OrphanTypeDeclaration detection - // Type-level integer comparison — fixed: graph-based Compare solver - // "CompareInt1", -- fixed: graph-based Compare constraint solver - // "CompareInt2", -- fixed: graph-based Compare constraint solver - // "CompareInt3", -- fixed: graph-based Compare constraint solver - // "CompareInt4", -- fixed: graph-based Compare constraint solver - // "CompareInt5", -- fixed: graph-based Compare constraint solver - // "CompareInt6", -- fixed: graph-based Compare constraint solver - // "CompareInt7", -- fixed: graph-based Compare constraint solver - // "CompareInt8", -- fixed: graph-based Compare constraint solver - // "CompareInt9", -- fixed: graph-based Compare constraint solver - // "CompareInt10", -- fixed: graph-based Compare constraint solver - // "CompareInt11", -- fixed: graph-based Compare constraint solver - // "CompareInt12", -- fixed: graph-based Compare constraint solver - // VTA class head checks not implemented - // "ClassHeadNoVTA3", -- fixed: VTA reachability check in infer_visible_type_app - // Specific instance / constraint checks not implemented - // "2567", -- fixed: annotation constraint extraction catches Fail constraint - // "2806", -- fixed: non-exhaustive pattern guard requires Partial - // "3531", -- fixed: instance chain ambiguity detection - // "3531-2", -- fixed: structured-type chain ambiguity - // "3531-3", -- fixed: structured-type chain ambiguity (rows) - // "3531-4", -- fixed: instance chain ambiguity detection - // "3531-5", -- fixed: instance chain ambiguity detection - // "3531-6", -- fixed: instance chain ambiguity detection - // "4024", -- fixed: zero-instance class constraint from signature - // "4024-2", -- fixed: zero-instance class constraint from signature - // "LacksWithSubGoal", -- fixed: per-function Lacks solver with sub-goal decomposition - // "NonExhaustivePatGuard", -- fixed: non-exhaustive pattern guard requires Partial - // Scope / class member / misc checks not implemented - // "2378", -- fixed: OrphanInstance detection - // "2534", -- fixed: multi-equation where-clause type checking - // "2542", -- fixed: UndefinedTypeVariable for free type vars in where/let sigs - // "2874-forall", -- fixed: InvalidConstraintArgument for forall in constraint args - // "2874-forall2", -- fixed: InvalidConstraintArgument - // "2874-wildcard", -- fixed: InvalidConstraintArgument for wildcard in constraint args - // "3701", // fixed: Row.Nub solver detects duplicate labels → TypesDoNotUnify - // "4382", -- fixed: skip orphan check for unknown classes → UnknownClass - // "AnonArgument1", -- fixed: bare `_` rejected in infer_hole - // "InvalidOperatorInBinder", -- fixed: check operator aliases function vs constructor - // "PolykindGeneralizationLet", -- fixed: delayed let-binding generalization catches polykind reuse - // "VisibleTypeApplications1", -- fixed: VTA visibility check for @-marked forall vars - "Whitespace1", // intentionally accept tabs for compatibility with real-world packages - // FalsePass: compile cleanly but should fail — need typechecker improvements - // NoInstanceFound (25 fixtures) - // "2616", -- fixed: derive instance for open record rows rejects Eq/Ord without constraints - // "3329", -- fixed: sig_deferred chain ambiguity check with structured args - // "4028", -- fixed: constraint propagation from type signatures catches this - // "ClassHeadNoVTA2", -- fixed: ambiguous class var detection in infer_var - // "ClassHeadNoVTA7", -- fixed: ambiguous class var detection in infer_var - // "CoercibleConstrained1", - // "CoercibleHigherKindedData", - // "CoercibleHigherKindedNewtypes", -- fixed: type var in constructor position → nominal role - // "CoercibleNonCanonical1", -- fixed: given/wanted interaction solver - // "CoercibleNonCanonical2", -- fixed: given/wanted interaction solver - // "CoercibleOpenRowsDoNotUnify", - // "CoercibleRepresentational", - // "CoercibleRepresentational2", - // "CoercibleRepresentational3", - // "CoercibleRepresentational4", - // "CoercibleRepresentational5", - // "CoercibleRepresentational8", -- fixed: given/wanted interaction solver - // "CoercibleUnknownRowTail1", -- fixed: Coercible solver in has_unsolved block - // "CoercibleUnknownRowTail2", -- fixed: open row tail → NotCoercible - // "InstanceChainBothUnknownAndMatch", -- fixed: chain ambiguity with structured types - // "InstanceChainSkolemUnknownMatch", -- fixed: chain ambiguity with type vars - // "PossiblyInfiniteCoercibleInstance", - // "Superclasses1", -- fixed: superclass validation catches missing Su Number - // "Superclasses5", -- fixed: array binder non-exhaustiveness → NoInstanceFound for Partial - // TypesDoNotUnify (14 fixtures) - // "CoercibleClosedRowsDoNotUnify", - // "CoercibleConstrained2", - // "CoercibleConstrained3", // fixed: constrained-type vars are nominal - // "CoercibleForeign", - // "CoercibleForeign2", - // "CoercibleForeign3", - // "CoercibleNominal", - // "CoercibleNominalTypeApp", // fixed: higher-kinded role tracking - // "CoercibleNominalWrapped", - // KindsDoNotUnify - // "3549", -- fixed: Pass C type signature kind checking catches Functor kind mismatch - // "4019-1", -- fixed: class param kind consistency check at constraint resolution - // "4019-2", -- fixed: class param kind consistency check at constraint resolution - // "CoercibleKindMismatch", - // "FoldableInstance1", -- fixed: imported class kind registration (Foldable) - // "FoldableInstance2", -- fixed: imported class kind registration (Foldable) - // "FoldableInstance3", -- fixed: imported class kind registration (Foldable) - // "KindError", -- fixed: kind checking detects kind mismatches in data constructors - // "NewtypeInstance6", -- fixed: imported class kind registration (Functor) - // "TypeSynonyms10", -- fixed: KindsDoNotUnify maps to PartiallyAppliedSynonym - // PartiallyAppliedSynonym in kind annotations (need kind checking) - // "PASTrumpsKDNU2", - // "PASTrumpsKDNU4", - // "PASTrumpsKDNU6", - // "PASTrumpsKDNU7", - // ErrorParsingModule (5 fixtures) - // "2947", -- fixed: empty layout block + Sep1 in class/instance body - // CannotDeriveInvalidConstructorArg (9 fixtures) -- fixed: derive variance checking - // "BifunctorInstance1", - // "ContravariantInstance1", - // "FoldableInstance10", - // "FoldableInstance4", - // "FoldableInstance6", - // "FoldableInstance8", - // "FoldableInstance9", - // "FunctorInstance1", - // InvalidInstanceHead (6 fixtures — record/row types need fundep support) - "3510", // regression: now produces OrphanInstance instead of InvalidInstanceHead - // "InvalidDerivedInstance2", -- fixed: bare record type in instance head - // "RowInInstanceNotDetermined0", -- fixed: fundep-aware row-in-instance check - // "RowInInstanceNotDetermined1", -- fixed: fundep-aware row-in-instance check - // "RowInInstanceNotDetermined2", -- fixed: fundep-aware row-in-instance check - // "TypeSynonyms7", -- fixed: synonym-to-record instance head check - // "365", -- fixed: CycleInDeclaration for instance methods - // "Foldable", -- fixed: CycleInDeclaration for instance methods - // TransitiveExportError — remaining - // "3132", -- fixed: superclass transitive export - // UnknownName (2 fixtures) - // "3549-a", -- fixed: validate kind annotations in forall type vars - // "PrimRow", -- fixed: Prim submodule class_param_counts propagation - // IncorrectAnonymousArgument — fixed: _ rejected in non-parenthesized operator expressions - // "AnonArgument2", - // "AnonArgument3", - // "OperatorSections2", -- fixed: precedence-aware anonymous arg validation - // OverlappingInstances (2 fixtures) — fixed: definition-time overlap detection - // "TypeSynonymsOverlappingInstance", - // "TypeSynonymsOverlappingUnnamedInstance", - // InvalidNewtypeInstance (2 fixtures) - // "NewtypeInstance3", -- fixed: InvalidNewtypeInstance detection - // "NewtypeInstance5", -- fixed: bare type variable check for derive newtype instance - // EscapedSkolem (2 fixtures) -- fixed: ambient-var escape detection in infer_app - // "SkolemEscape", - // "SkolemEscape2", - // CannotGeneralizeRecursiveFunction (2 fixtures) -- fixed: op_deferred_constraints tracking - // "Generalization1", - // "Generalization2", - // Misc single fixtures - // "3405", -- testing: OrphanInstance for synonym-to-primitive derive - // "438", -- fixed: PossiblyInfiniteInstance via depth-exceeded instance resolution - // "ConstraintInference", -- fixed: AmbiguousTypeVariables detection for polymorphic bindings - // "FFIDefaultCJSExport", -- fixed: js_ffi detects CJS-only modules - // "Rank2Types", -- fixed: higher-rank type checking via post-unification polymorphism check - // "RowLacks", -- fixed: Lacks constraint propagation from type signatures - // "TypedBinders2", -- fixed: typed binder in do-notation - // "ProgrammablePolykindedTypeErrorsTypeString", -- fixed: Fail constraint in type signature - // WrongError: produce different error type than expected - // "4466", -- fixed: partial lambda binder detection (refutable pattern in lambda) - // "LetPatterns1", -- fixed: reject pattern binders with extra args in let bindings -]; + assert!( + !failures.is_empty(), + "{}/{} build units failed:\n\n{}", + failures.len(), + total, + summary.join("\n\n") + ); +} /// Extract the `-- @shouldFailWith ErrorName` annotation from the first source file. /// Searches the first few comment lines (not just the first line). fn extract_expected_error(sources: &[(String, String)]) -> Option { sources.first().and_then(|(_, source)| { - source.lines() + source + .lines() .take_while(|line| line.trim().starts_with("--")) .find_map(|line| { line.trim() @@ -660,19 +458,19 @@ fn matches_expected_error( "TypesDoNotUnify" => has("UnificationError"), "NoInstanceFound" => has("NoInstanceFound"), "ErrorParsingModule" => has("LexError") || has("SyntaxError"), - "UnknownName" => has("UndefinedVariable") || has("UnknownType") || has("UnknownClass"), + "UnknownName" => has("UnknownName") || has("UndefinedVariable"), "HoleInferredType" => has("HoleInferredType") || has("UnificationError"), "InfiniteType" => has("InfiniteType"), - "InfiniteKind" => has("InfiniteKind"), + "InfiniteKind" => has("InfiniteKind"), "DuplicateValueDeclaration" => has("DuplicateValueDeclaration"), - "OverlappingNamesInLet" => has("OverlappingNamesInLet"), + "OverlappingNamesInLet" => has("OverlappingNamesInLet"), "CycleInTypeSynonym" => has("CycleInTypeSynonym"), "CycleInDeclaration" => has("CycleInDeclaration") || has("CycleInTypeClassDeclaration"), "CycleInTypeClassDeclaration" => has("CycleInTypeClassDeclaration"), "CycleInKindDeclaration" => has("CycleInKindDeclaration"), "UnknownImport" => has("UnknownImport"), "UnknownImportDataConstructor" => has("UnknownImportDataConstructor"), - "IncorrectConstructorArity" => has("IncorrectConstructorArity"), + "IncorrectConstructorArity" => has("IncorrectConstructorArity"), "DuplicateTypeClass" => has("DuplicateTypeClass"), "DuplicateInstance" => has("DuplicateInstance"), "DuplicateTypeArgument" => has("DuplicateTypeArgument"), @@ -689,7 +487,9 @@ fn matches_expected_error( "UnknownExport" | "UnknownExportDataConstructor" => has("UnkownExport"), "OverlappingArgNames" => has("OverlappingArgNames") || has("OverlappingPattern"), "ArgListLengthsDiffer" => has("ArityMismatch"), - "InvalidNewtypeInstance" | "CannotDeriveNewtypeForData" => has("InvalidNewtypeInstance") || has("InvalidNewtypeDerivation"), + "InvalidNewtypeInstance" | "CannotDeriveNewtypeForData" => { + has("InvalidNewtypeInstance") || has("InvalidNewtypeDerivation") + } "InvalidNewtypeDerivation" => has("InvalidNewtypeDerivation"), "OverlappingPattern" => has("OverlappingPattern"), "NonExhaustivePattern" => has("NonExhaustivePattern"), @@ -699,7 +499,7 @@ fn matches_expected_error( "InvalidOperatorInBinder" => has("InvalidOperatorInBinder"), "IncorrectAnonymousArgument" => has("IncorrectAnonymousArgument"), "IntOutOfRange" => has("IntOutOfRange"), - "UnknownClass" => has("UnknownClass") || has("NoInstanceFound"), + "UnknownClass" => has("UnknownClass"), "MissingClassMember" => has("MissingClassMember"), "ExtraneousClassMember" => has("ExtraneousClassMember"), "CannotGeneralizeRecursiveFunction" => has("CannotGeneralizeRecursiveFunction"), @@ -712,7 +512,8 @@ fn matches_expected_error( "RoleDeclarationArityMismatch" => has("RoleDeclarationArityMismatch"), "UndefinedTypeVariable" => has("UndefinedTypeVariable"), "AmbiguousTypeVariables" => has("AmbiguousTypeVariables"), - "ExpectedType" | "ExpectedWildcard" => has("UnificationError") || has("SyntaxError") || has("InvalidNewtypeInstance") || has("ExpectedType"), + "ExpectedType" => has("ExpectedType"), + "ExpectedWildcard" => has("ExpectedWildcard"), "NonAssociativeError" => has("NonAssociativeError"), "MixedAssociativityError" => has("MixedAssociativityError"), "DeprecatedFFIPrime" => has("DeprecatedFFIPrime"), @@ -722,9 +523,9 @@ fn matches_expected_error( "TransitiveExportError" | "TransitiveDctorExportError" => has("TransitiveExportError"), "OverlappingInstances" => has("OverlappingInstances"), "ExportConflict" => has("ExportConflict"), - "ScopeConflict" => has("ScopeConflict") || has("ExportConflict"), + "ScopeConflict" => has("ScopeConflict"), "OrphanInstance" => has("OrphanInstance"), - "KindsDoNotUnify" => has("KindsDoNotUnify") || has("PartiallyAppliedSynonym"), + "KindsDoNotUnify" => has("KindsDoNotUnify"), "PossiblyInfiniteInstance" => has("PossiblyInfiniteInstance"), "InvalidCoercibleInstanceDeclaration" => has("InvalidCoercibleInstanceDeclaration"), "RoleMismatch" => has("RoleMismatch"), @@ -741,14 +542,18 @@ fn matches_expected_error( "QuantificationCheckFailureInType" => has("QuantificationCheckFailureInType"), "QuantificationCheckFailureInKind" => has("QuantificationCheckFailureInKind"), "VisibleQuantificationCheckFailureInType" => has("VisibleQuantificationCheckFailureInType"), + "WildcardInTypeDefinition" => has("WildcardInTypeDefinition") || has("SyntaxError"), + "ConstraintInForeignImport" => has("ConstraintInForeignImport") || has("SyntaxError"), + "InvalidConstraintArgument" => has("InvalidConstraintArgument") || has("SyntaxError"), _ => { - eprintln!("Warning: Unrecognized expected error code '{}'. Add the appropriate error constructor with a matching error.code() implementation. Then add it to matches_expected_error match statement", expected); - false - }, + eprintln!("Warning: Unrecognized expected error code '{}'. Add the appropriate error constructor with a matching error.code() implementation. Then add it to matches_expected_error match statement", expected); + false + } } } #[test] +#[timeout(30000)] // 30 second timeout — all failing fixtures are compiled without skipping. fn build_fixture_original_compiler_failing() { let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/original-compiler/failing"); @@ -759,35 +564,16 @@ fn build_fixture_original_compiler_failing() { let units = collect_build_units(&fixtures_dir); assert!(!units.is_empty(), "Expected failing fixture build units"); - // Build support packages once to get a shared registry - let support_sources_string = collect_support_sources(); - let support_sources: Vec<(&str, &str)> = support_sources_string - .iter() - .map(|(p, s)| (p.as_str(), s.as_str())) - .collect(); - let (_, registry) = build_from_sources_with_registry(&support_sources, None); - let registry = Arc::new(registry); + // Use shared support build (built lazily on first access, shared across tests) + let registry = Arc::clone(&get_support_build().registry); - let run_all = std::env::var("RUN_ALL_FAILING").ok(); - let skip: HashSet<&str> = SKIP_FAILING_FIXTURES.iter().copied().collect(); let mut total = 0; let mut correct = 0; - let mut wrong_error = 0; + let mut wrong_errors: Vec= Vec::new(); let mut panicked = 0; - let mut skipped = 0; let mut false_passes: Vec = Vec::new(); - let mut newly_correct: Vec = Vec::new(); for (name, sources, js_sources) in &units { - let should_run = match &run_all { - Some(filter) if !filter.is_empty() => name.contains(filter.as_str()), - Some(_) => true, - None => false, - }; - if skip.contains(name.as_str()) && !should_run { - skipped += 1; - continue; - } total += 1; let expected_error = extract_expected_error(sources).unwrap_or_default(); @@ -841,9 +627,19 @@ fn build_fixture_original_compiler_failing() { ) { "correct".to_string() } else { - let build_codes: Vec = result.build_errors.iter().map(|e| e.code().to_string()).collect(); - let type_codes: Vec = type_errors.iter().map(|e| e.code().to_string()).collect(); - format!("wrong_error:expected={},build=[{}],type=[{}]", expected_error_clone, build_codes.join(","), type_codes.join(",")) + let build_codes: Vec = result + .build_errors + .iter() + .map(|e| e.code().to_string()) + .collect(); + let type_codes: Vec = + type_errors.iter().map(|e| e.code().to_string()).collect(); + format!( + "wrong_error:expected={},build=[{}],type=[{}]", + expected_error_clone, + build_codes.join(","), + type_codes.join(",") + ) } } } @@ -854,29 +650,20 @@ fn build_fixture_original_compiler_failing() { Ok(result) => { if result == "correct" { correct += 1; - if run_all.is_some() && skip.contains(name.as_str()) { - newly_correct.push(name.clone()); - } } else if result.starts_with("wrong_error") { - wrong_error += 1; - if run_all.is_none() || !skip.contains(name.as_str()) { - eprintln!(" WRONG: {} -> {}", name, result); - } else if run_all.is_some() && skip.contains(name.as_str()) { - eprintln!(" SKIP_WRONG: {} -> {}", name, result); - } + eprintln!(" WRONG: {} -> {}", name, &result); + wrong_errors.push(result); } else if result.starts_with("false_pass:") { let expected = result.strip_prefix("false_pass:").unwrap_or(""); - if run_all.is_none() || !skip.contains(name.as_str()) { - false_passes.push(format!("{} (expected {})", name, expected)); - } else if run_all.is_some() && skip.contains(name.as_str()) { - eprintln!(" SKIP_FALSEPASS: {} (expected {})", name, expected); - } + false_passes.push(format!("{} (expected {})", name, expected)); } else { panicked += 1; + eprintln!(" PANIC with result: {} - {}", name, result.clone()); } } Err(_) => { panicked += 1; + eprintln!(" PANIC: {}", name); } } } @@ -887,50 +674,560 @@ fn build_fixture_original_compiler_failing() { Correct: {}\n\ WrongError: {}\n\ Panicked: {}\n\ - FalsePass: {}\n\ - Skipped: {}", + FalsePass: {}", total, correct, - wrong_error, + wrong_errors.len(), panicked, false_passes.len(), - skipped, ); - if !newly_correct.is_empty() { - eprintln!("\n=== Newly Correct (can remove from skip list) ==="); - for name in &newly_correct { - eprintln!(" {}", name); + + assert!( + panicked == 0, + "There should be no panics" + ); + + assert!( + false_passes.len() == 0, + "There should be no false passes. Found:\n{}", + false_passes.join("\n") + ); + + assert!( + wrong_errors.len() == 0, + "The should be no wrong errors. Found:\n{}", + wrong_errors.join("\n") + ) + +} + +const MARIONETTE_REACT_BASIC_HOOKS_EXTRA_PACKAGES: &[&str] = &[ + "lists", + "ordered-collections", + "nullable", + "exceptions", + "parallel", + "transformers", + "datetime", + "aff", + "now", + "unsafe-reference", + "web-events", + "web-dom", + "web-file", + "web-storage", + "media-types", + "js-date", + "web-html", + "js-promise", + "aff-promise", + "react-basic", + "indexed-monad", + "react-basic-hooks", + "marionette", + "marionette-react-basic-hooks", +]; + +#[test] +#[timeout(20000)] +fn build_marionette_react_basic_hooks() { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + let registry = Arc::clone(&get_support_build().registry); + + let mut sources: Vec<(String, String)> = Vec::new(); + for &pkg in MARIONETTE_REACT_BASIC_HOOKS_EXTRA_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + assert!(pkg_src.exists(), "Package '{}' not found at: {}", pkg, pkg_src.display()); + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + sources.push((f.to_string_lossy().into_owned(), source)); + } + } + } + + eprintln!("Building marionette-react-basic-hooks ({} modules from {} extra packages)...", sources.len(), MARIONETTE_REACT_BASIC_HOOKS_EXTRA_PACKAGES.len()); + + let source_refs: Vec<(&str, &str)> = sources.iter().map(|(p, s)| (p.as_str(), s.as_str())).collect(); + let options = BuildOptions { module_timeout: Some(std::time::Duration::from_secs(3)), output_dir: None }; + let (result, _) = build_from_sources_with_options(&source_refs, &None, Some(registry), &options); + + let mut timeouts: Vec = Vec::new(); + let mut panics: Vec = Vec::new(); + let mut other_errors: Vec = Vec::new(); + for e in &result.build_errors { + match e { + BuildError::TypecheckTimeout { .. } => timeouts.push(format!(" {}", e)), + BuildError::TypecheckPanic { .. } => panics.push(format!(" {}", e)), + _ => other_errors.push(format!(" {}", e)), + } + } + + assert!(timeouts.is_empty(), "marionette-react-basic-hooks: {} modules timed out:\n{}", timeouts.len(), timeouts.join("\n")); + assert!(panics.is_empty(), "marionette-react-basic-hooks: modules panicked:\n{}", panics.join("\n")); + assert!(other_errors.is_empty(), "marionette-react-basic-hooks: build errors:\n{}", other_errors.join("\n")); + + let mut type_errors: Vec<(String, PathBuf, String)> = Vec::new(); + for m in &result.modules { + if !m.type_errors.is_empty() { + for e in &m.type_errors { + type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); + } } } - if !false_passes.is_empty() { - panic!( - "{} fixtures compiled cleanly but should have failed:\n {}", - false_passes.len(), - false_passes.join("\n ") + assert!( + type_errors.is_empty(), + "marionette-react-basic-hooks: {} modules have type errors:\n{}", + type_errors.len(), + type_errors.iter().map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)).collect::>().join("\n") + ); +} + +const LITERALS_EXTRA_PACKAGES: &[&str] = &[ + "literals", +]; + +#[test] +#[timeout(20000)] +fn build_literals() { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + let registry = Arc::clone(&get_support_build().registry); + + let mut sources: Vec<(String, String)> = Vec::new(); + for &pkg in LITERALS_EXTRA_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + assert!(pkg_src.exists(), "Package '{}' not found at: {}", pkg, pkg_src.display()); + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + sources.push((f.to_string_lossy().into_owned(), source)); + } + } + } + + eprintln!("Building literals ({} modules from {} extra packages)...", sources.len(), LITERALS_EXTRA_PACKAGES.len()); + + let source_refs: Vec<(&str, &str)> = sources.iter().map(|(p, s)| (p.as_str(), s.as_str())).collect(); + let options = BuildOptions { module_timeout: Some(std::time::Duration::from_secs(3)), output_dir: None }; + let (result, _) = build_from_sources_with_options(&source_refs, &None, Some(registry), &options); + + let mut timeouts: Vec = Vec::new(); + let mut panics: Vec = Vec::new(); + let mut other_errors: Vec = Vec::new(); + for e in &result.build_errors { + match e { + BuildError::TypecheckTimeout { .. } => timeouts.push(format!(" {}", e)), + BuildError::TypecheckPanic { .. } => panics.push(format!(" {}", e)), + _ => other_errors.push(format!(" {}", e)), + } + } + + assert!(timeouts.is_empty(), "literals: {} modules timed out:\n{}", timeouts.len(), timeouts.join("\n")); + assert!(panics.is_empty(), "literals: modules panicked:\n{}", panics.join("\n")); + assert!(other_errors.is_empty(), "literals: build errors:\n{}", other_errors.join("\n")); + + let mut type_errors: Vec<(String, PathBuf, String)> = Vec::new(); + for m in &result.modules { + if !m.type_errors.is_empty() { + for e in &m.type_errors { + type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); + } + } + } + + assert!( + type_errors.is_empty(), + "literals: {} modules have type errors:\n{}", + type_errors.len(), + type_errors.iter().map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)).collect::>().join("\n") + ); +} + +/// Additional packages needed to build codec-json on top of SUPPORT_PACKAGES. +const CODEC_JSON_EXTRA_PACKAGES: &[&str] = &["codec", "variant", "codec-json"]; + +#[test] +#[timeout(10000)] +fn build_codec_json() { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + + // Build on top of the shared support registry + let registry = Arc::clone(&get_support_build().registry); + + // Collect sources from the extra packages needed for codec-json + let mut sources: Vec<(String, String)> = Vec::new(); + for &pkg in CODEC_JSON_EXTRA_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + assert!( + pkg_src.exists(), + "Package '{}' not found at: {}", + pkg, + pkg_src.display() ); + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + sources.push((f.to_string_lossy().into_owned(), source)); + } + } } - if wrong_error > 0 { - panic!("{} fixtures produced wrong errors. See output for details.", wrong_error); + eprintln!( + "Building codec-json ({} modules from {} extra packages)...", + sources.len(), + CODEC_JSON_EXTRA_PACKAGES.len() + ); + + let source_refs: Vec<(&str, &str)> = sources + .iter() + .map(|(p, s)| (p.as_str(), s.as_str())) + .collect(); + + let options = BuildOptions { + module_timeout: None, + output_dir: None, + }; + let (result, _) = + build_from_sources_with_options(&source_refs, &None, Some(registry), &options); + + // Separate timeouts from other build errors + let mut timeouts: Vec = Vec::new(); + let mut other_errors: Vec = Vec::new(); + for e in &result.build_errors { + match e { + BuildError::TypecheckTimeout { .. } => timeouts.push(format!(" {}", e)), + _ => other_errors.push(format!(" {}", e)), + } + } + + assert!( + timeouts.is_empty(), + "Modules timed out:\n{}", + timeouts.join("\n") + ); + + assert!( + other_errors.is_empty(), + "Build errors in codec-json:\n{}", + other_errors.join("\n") + ); + + let mut type_errors: Vec<(String, PathBuf, String)> = Vec::new(); + let mut fails = 0; + + for m in &result.modules { + if !m.type_errors.is_empty() { + fails += 1; + for e in &m.type_errors { + type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); + } + } } + + let type_errors_str: String = type_errors + .iter() + .map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)) + .collect::>() + .join("\n"); + + assert!( + type_errors.is_empty(), + "codec-json: {}/{} modules have type errors:\n{}", + fails, + result.modules.len(), + type_errors_str + ); + + eprintln!( + "codec-json: {} modules typechecked, {} with errors", + result.modules.len(), + fails + ); +} + +/// Additional packages needed to build webb-aff-list on top of SUPPORT_PACKAGES. +const WEBB_AFF_LIST_EXTRA_PACKAGES: &[&str] = &[ + "aff", + "tailrec", + "monad-loops", + "debug", + "profunctor-lenses", + "webb-monad", + "webb-refer", + "webb-array", + "webb-mutex", + "webb-channel", + "webb-slot", + "webb-stateful", + "webb-thread", + "webb-aff-list", + "parallel", +]; + +#[test] +#[timeout(30000)] +fn build_webb_aff_list() { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + + // Build on top of the shared support registry + let registry = Arc::clone(&get_support_build().registry); + + // Collect sources from the extra packages needed for webb-aff-list + let mut sources: Vec<(String, String)> = Vec::new(); + for &pkg in WEBB_AFF_LIST_EXTRA_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + assert!( + pkg_src.exists(), + "Package '{}' not found at: {}", + pkg, + pkg_src.display() + ); + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + sources.push((f.to_string_lossy().into_owned(), source)); + } + } + } + + eprintln!( + "Building webb-aff-list ({} modules from {} extra packages)...", + sources.len(), + WEBB_AFF_LIST_EXTRA_PACKAGES.len() + ); + + let source_refs: Vec<(&str, &str)> = sources + .iter() + .map(|(p, s)| (p.as_str(), s.as_str())) + .collect(); + + let options = BuildOptions { + module_timeout: Some(std::time::Duration::from_secs(10)), + output_dir: None, + }; + let (result, _) = + build_from_sources_with_options(&source_refs, &None, Some(registry), &options); + + // Separate timeouts/panics from other build errors + let mut timeouts: Vec = Vec::new(); + let mut panics: Vec = Vec::new(); + let mut other_errors: Vec = Vec::new(); + for e in &result.build_errors { + match e { + BuildError::TypecheckTimeout { .. } => timeouts.push(format!(" {}", e)), + BuildError::TypecheckPanic { .. } => panics.push(format!(" {}", e)), + _ => other_errors.push(format!(" {}", e)), + } + } + + assert!( + timeouts.is_empty(), + "Modules exceeded typecheck timeout:\n{}", + timeouts.join("\n") + ); + + assert!( + panics.is_empty(), + "Modules panicked:\n{}", + panics.join("\n") + ); + + assert!( + other_errors.is_empty(), + "Build errors:\n{}", + other_errors.join("\n") + ); + + // Only check type errors for Webb.AffList.* modules (the target package) + let mut type_errors: Vec<(String, PathBuf, String)> = Vec::new(); + let mut fails = 0; + + for m in &result.modules { + if !m.type_errors.is_empty() { + fails += 1; + for e in &m.type_errors { + type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); + } + } + } + + let type_errors_str: String = type_errors + .iter() + .map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)) + .collect::>() + .join("\n"); + + assert!( + type_errors.is_empty(), + "type errors found. {}/{} modules have type errors:\n{}", + fails, + result.modules.len(), + type_errors_str + ); + + assert!( + type_errors.is_empty(), + "webb-aff-list: {}/{} modules have type errors:\n{}", + fails, + result.modules.len(), + type_errors_str + ); +} + +/// Additional packages needed to build halogen on top of SUPPORT_PACKAGES. +const HALOGEN_EXTRA_PACKAGES: &[&str] = &[ + "aff", + "media-types", + "js-date", + "js-promise", + "unsafe-reference", + "web-events", + "web-dom", + "web-storage", + "web-file", + "web-html", + "web-uievents", + "web-touchevents", + "web-pointerevents", + "web-clipboard", + "dom-indexed", + "nullable", + "parallel", + "freeap", + "fork", + "halogen-vdom", + "halogen-subscriptions", + "halogen", +]; + +#[test] +#[ignore] // 6/228 modules have type errors (ExportConflict, PartiallyAppliedSynonym, UnificationError) +#[timeout(30000)] +fn build_halogen() { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + + // Build on top of the shared support registry + let registry = Arc::clone(&get_support_build().registry); + + // Collect sources from the extra packages needed for halogen + let mut sources: Vec<(String, String)> = Vec::new(); + for &pkg in HALOGEN_EXTRA_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + assert!( + pkg_src.exists(), + "Package '{}' not found at: {}", + pkg, + pkg_src.display() + ); + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + sources.push((f.to_string_lossy().into_owned(), source)); + } + } + } + + eprintln!( + "Building halogen ({} modules from {} extra packages)...", + sources.len(), + HALOGEN_EXTRA_PACKAGES.len() + ); + + let source_refs: Vec<(&str, &str)> = sources + .iter() + .map(|(p, s)| (p.as_str(), s.as_str())) + .collect(); + + let options = BuildOptions { + module_timeout: Some(std::time::Duration::from_secs(5)), + output_dir: None, + }; + let (result, _) = + build_from_sources_with_options(&source_refs, &None, Some(registry), &options); + + // Separate timeouts/panics from other build errors + let mut timeouts: Vec = Vec::new(); + let mut panics: Vec = Vec::new(); + let mut other_errors: Vec = Vec::new(); + for e in &result.build_errors { + match e { + BuildError::TypecheckTimeout { .. } => timeouts.push(format!(" {}", e)), + BuildError::TypecheckPanic { .. } => panics.push(format!(" {}", e)), + _ => other_errors.push(format!(" {}", e)), + } + } + + assert!( + timeouts.is_empty(), + "Modules exceeded typecheck timeout:\n{}", + timeouts.join("\n") + ); + + assert!( + panics.is_empty(), + "Modules panicked:\n{}", + panics.join("\n") + ); + + assert!( + other_errors.is_empty(), + "Build errors:\n{}", + other_errors.join("\n") + ); + + let mut type_errors: Vec<(String, PathBuf, String)> = Vec::new(); + let mut fails = 0; + + for m in &result.modules { + if !m.type_errors.is_empty() { + fails += 1; + for e in &m.type_errors { + type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); + } + } + } + + let type_errors_str: String = type_errors + .iter() + .map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)) + .collect::>() + .join("\n"); + + assert!( + type_errors.is_empty(), + "halogen: {}/{} modules have type errors:\n{}", + fails, + result.modules.len(), + type_errors_str + ); } -#[test] #[timeout(120000)] #[ignore]// 120s timeout for the whole test + +#[test] +#[ignore] +// Heavy test (~33s release, ~300s debug, 4859 modules) +// run with: RUST_LOG=debug cargo test --test build build_all_packages -- --exact --ignored +// for release (RECOMMENDED): cargo test --release --test build build_all_packages -- --exact --ignored +#[timeout(300000)] // 300s (5 min) timeout — debug mode is ~10x slower than release fn build_all_packages() { let _ = env_logger::try_init(); let started = std::time::Instant::now(); - let packages_dir = - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); assert!(packages_dir.exists(), "packages directory not found"); - // Per-module timeout: defaults to 2s, controlled by MODULE_TIMEOUT_SECS env var + // Per-module timeout: defaults to 10s, controlled by MODULE_TIMEOUT_SECS env var. let timeout_secs: u64 = std::env::var("MODULE_TIMEOUT_SECS") .ok() .and_then(|s| s.parse().ok()) - .unwrap_or(2); + .unwrap_or(10); let options = BuildOptions { module_timeout: Some(std::time::Duration::from_secs(timeout_secs)), @@ -966,7 +1263,10 @@ fn build_all_packages() { } } - eprintln!("Discovered packages in {} seconds", started.elapsed().as_secs_f64()); + eprintln!( + "Discovered packages in {} seconds", + started.elapsed().as_secs_f64() + ); eprintln!( "Building all packages ({} packages, {} modules, timeout={}s)...", @@ -990,11 +1290,11 @@ fn build_all_packages() { let mut other_errors: Vec = Vec::new(); for e in &result.build_errors { match e { - BuildError::TypecheckTimeout { module_name, .. } => { - timeouts.push(module_name.clone()); + BuildError::TypecheckTimeout { .. } => { + timeouts.push(format!(" {}", e)); } - BuildError::TypecheckPanic { module_name, .. } => { - panics.push(module_name.clone()); + BuildError::TypecheckPanic { .. } => { + panics.push(format!(" {}", e)); } _ => { other_errors.push(format!(" {}", e)); @@ -1007,8 +1307,10 @@ fn build_all_packages() { for m in &result.modules { if !m.type_errors.is_empty() { + eprintln!("Errors in {}, {}", m.path.to_string_lossy(), m.module_name); fails += 1; for e in &m.type_errors { + eprintln!(" {}", e); type_errors.push((m.module_name.clone(), m.path.clone(), e.to_string())); } } @@ -1017,20 +1319,24 @@ fn build_all_packages() { let clean = result.modules.len() - fails; eprintln!( "Results: {} clean, {} with type errors, {} timeouts, {} panics out of {} modules", - clean, fails, timeouts.len(), panics.len(), result.modules.len() + clean, + fails, + timeouts.len(), + panics.len(), + result.modules.len() + ); + + assert!( + timeouts.len() == 0, + "Modules exceeded deadline:\n {}", + timeouts.join("\n ") + ); + + assert!( + panics.is_empty(), + "Modules panicked during typechecking:\n {}", + panics.join("\n ") ); - if !timeouts.is_empty() { - eprintln!("Timed out modules:"); - for name in &timeouts { - eprintln!(" {}", name); - } - } - if !panics.is_empty() { - eprintln!("Panicked modules:"); - for name in &panics { - eprintln!(" {}", name); - } - } assert!( other_errors.is_empty(), @@ -1038,17 +1344,54 @@ fn build_all_packages() { other_errors.join("\n") ); - let type_errors_str: String = type_errors - .iter() - .map(|(m, p, e)| format!("{} ({}): {}", m, p.to_string_lossy(), e)) - .collect::>() - .join("\n"); + // Categorize errors for diagnostics + let mut error_counts: std::collections::HashMap = + std::collections::HashMap::new(); + for m in &result.modules { + for e in &m.type_errors { + *error_counts.entry(e.code()).or_default() += 1; + } + } + if fails > 0 { + let mut sorted_counts: Vec<_> = error_counts.iter().collect(); + sorted_counts.sort_by(|a, b| b.1.cmp(a.1)); + eprintln!("\nError distribution ({} modules with errors):", fails); + for (code, count) in &sorted_counts { + eprintln!(" {:>4} {}", count, code); + } + // Show modules with errors and their error code breakdown + let mut module_errors: Vec<(String, Vec)> = Vec::new(); + for m in &result.modules { + if !m.type_errors.is_empty() { + let codes: Vec = m.type_errors.iter().map(|e| e.code()).collect(); + module_errors.push((m.module_name.clone(), codes)); + } + } + module_errors.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + eprintln!("\nModules with errors (by count):"); + for (module, codes) in module_errors.iter().take(40) { + let mut code_counts: std::collections::HashMap<&str, usize> = + std::collections::HashMap::new(); + for c in codes { + *code_counts.entry(c.as_str()).or_default() += 1; + } + let summary: Vec = code_counts + .iter() + .map(|(k, v)| format!("{}x{}", v, k)) + .collect(); + eprintln!( + " {:>3} errors {} [{}]", + codes.len(), + module, + summary.join(", ") + ); + } + } assert!( - type_errors.is_empty(), - "Type errors in packages: {}/{} modules failed:\n{}", + fails == 0, + "Type error regression: {}/{} modules had errors", fails, result.modules.len(), - type_errors_str ); } diff --git a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet.purs b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet.purs index 861a607d..fed163d7 100644 --- a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet.purs +++ b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet.purs @@ -1,6 +1,8 @@ -- @shouldFailWith OverlappingNamesInLet module Main where +import Prelude + foo = a where a :: Number diff --git a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet2.purs b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet2.purs index 98549b3b..18d3f4d7 100644 --- a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet2.purs +++ b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet2.purs @@ -1,6 +1,8 @@ -- @shouldFailWith OverlappingNamesInLet module Main where +import Prelude + foo = interrupted where interrupted true = 1 diff --git a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet3.purs b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet3.purs index 9ca900ea..6f322ac4 100644 --- a/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet3.purs +++ b/tests/fixtures/original-compiler/failing/DuplicateDeclarationsInLet3.purs @@ -2,6 +2,8 @@ -- @shouldFailWith OverlappingNamesInLet module Main where +import Prelude + -- Should see separate errors for `a` and `interrupted` foo = interrupter + a where diff --git a/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.out b/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.out deleted file mode 100644 index 7547a0b8..00000000 --- a/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.out +++ /dev/null @@ -1,24 +0,0 @@ -Error found: -in module Main -at tests/purs/failing/PolykindGeneralizationLet.purs:14:10 - 14:26 (line 14, column 10 - line 14, column 26) - - Could not match type -   -  "foo" -   - with type -   -  Int -   - -while trying to match type t0 "foo" - with type Proxy @Type Int -while checking that expression Proxy - has type Proxy @Type Int -in value declaration test - -where t0 is an unknown type - -See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information, -or to contribute content related to this error. - diff --git a/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.purs b/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.purs deleted file mode 100644 index 9192f096..00000000 --- a/tests/fixtures/original-compiler/failing/PolykindGeneralizationLet.purs +++ /dev/null @@ -1,14 +0,0 @@ --- @shouldFailWith TypesDoNotUnify -module Main where - -data Proxy a = Proxy -data F f a = F (f a) - -fproxy :: forall f a. Proxy f -> Proxy a -> Proxy (F f a) -fproxy _ _ = Proxy - -test = c - where - a = fproxy (Proxy :: _ Proxy) - b = a (Proxy :: _ Int) - c = a (Proxy :: _ "foo") diff --git a/tests/fixtures/original-compiler/failing/Whitespace1.out b/tests/fixtures/original-compiler/failing/Whitespace1.out deleted file mode 100644 index 299c3ddb..00000000 --- a/tests/fixtures/original-compiler/failing/Whitespace1.out +++ /dev/null @@ -1,10 +0,0 @@ -Error found: -at tests/purs/failing/Whitespace1.purs:5:1 - 5:2 (line 5, column 1 - line 5, column 2) - - Unable to parse module: - Illegal whitespace character U+0009 - - -See https://github.com/purescript/documentation/blob/master/errors/ErrorParsingModule.md for more information, -or to contribute content related to this error. - diff --git a/tests/fixtures/original-compiler/failing/Whitespace1.purs b/tests/fixtures/original-compiler/failing/Whitespace1.purs deleted file mode 100644 index b73805a0..00000000 --- a/tests/fixtures/original-compiler/failing/Whitespace1.purs +++ /dev/null @@ -1,5 +0,0 @@ --- @shouldFailWith ErrorParsingModule -module Main where - -test = do - test diff --git a/tests/fixtures/packages/logging/LICENSE b/tests/fixtures/packages/logging/LICENSE new file mode 100644 index 00000000..84c22d81 --- /dev/null +++ b/tests/fixtures/packages/logging/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, rightfold +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/packages/logging/README.md b/tests/fixtures/packages/logging/README.md new file mode 100644 index 00000000..caabf708 --- /dev/null +++ b/tests/fixtures/packages/logging/README.md @@ -0,0 +1,37 @@ +# purescript-logging + +Composable loggers for PureScript. + +## Usage + +A logger receives records and potentially performs some effects. You can create +a logger from any function `r -> m Unit` for any `r` and `m`. + +Unlike most other logging libraries, purescript-logger has no separate concepts +"loggers" and "handlers". Instead, loggers can be composed into larger loggers +using the `Semigroup` instance. Loggers can also be transformed using `cmap` +(for transforming records) and `cfilter` (for filtering records). An example +use case might be the following: + +```purescript +data Level = Debug | Info | Warning | Error +derive instance eqLevel :: Eq Level +derive instance ordLevel :: Ord Level + +type Entry = + { time :: DateTime + , level :: Level + , fields :: Map String String + } + +fileLogger :: Path -> Logger Effect Entry +fileLogger path = Logger \entry -> ?todo {- append entry to file -} + +main = do + let debugLogger = fileLogger "debug.log" # cfilter (\e -> e.level == Debug) + let productionLogger = fileLogger "production.log" # cfilter (\e -> e.level /= Debug) + let logger = debugLogger <> productionLogger + + time <- now + log logger {time, level: Info, fields: Map.singleton "msg" "boot"} +``` diff --git a/tests/fixtures/packages/logging/bower.json b/tests/fixtures/packages/logging/bower.json new file mode 100644 index 00000000..640e34cd --- /dev/null +++ b/tests/fixtures/packages/logging/bower.json @@ -0,0 +1,23 @@ +{ + "name": "purescript-logging", + "license": "BSD-3-Clause", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], + "dependencies": { + "purescript-prelude": "^4.0.1", + "purescript-contravariant": "^4.0.0", + "purescript-console": "^4.0.0", + "purescript-effect": "^2.0.0", + "purescript-transformers": "^4.0.0", + "purescript-tuples": "^5.0.0", + "purescript-either": "^4.0.0" + }, + "repository": { + "type": "git", + "url": "git://github.com/rightfold/purescript-logging.git" + } +} diff --git a/tests/fixtures/packages/logging/purs.json b/tests/fixtures/packages/logging/purs.json new file mode 100644 index 00000000..1f00b989 --- /dev/null +++ b/tests/fixtures/packages/logging/purs.json @@ -0,0 +1,19 @@ +{ + "name": "logging", + "version": "3.0.0", + "license": "BSD-3-Clause", + "location": { + "githubOwner": "rightfold", + "githubRepo": "purescript-logging" + }, + "ref": "v3.0.0", + "dependencies": { + "console": ">=4.0.0 <7.0.0", + "contravariant": ">=4.0.0 <7.0.0", + "effect": ">=2.0.0 <5.0.0", + "either": ">=4.0.0 <7.0.0", + "prelude": ">=4.0.1 <7.0.0", + "transformers": ">=4.0.0 <7.0.0", + "tuples": ">=5.0.0 <8.0.0" + } +} diff --git a/tests/fixtures/packages/logging/src/Control/Logger.purs b/tests/fixtures/packages/logging/src/Control/Logger.purs new file mode 100644 index 00000000..cecad14b --- /dev/null +++ b/tests/fixtures/packages/logging/src/Control/Logger.purs @@ -0,0 +1,56 @@ +module Control.Logger +( Logger(Logger) +, log +, cfilter +, hoist +) where + +import Data.Decidable (class Decidable) +import Data.Decide (class Decide) +import Data.Divide (class Divide) +import Data.Divisible (class Divisible) +import Data.Either (Either(..)) +import Data.Functor.Contravariant (class Contravariant) +import Data.Tuple (Tuple(..)) +import Prelude + +-- | A logger receives records and potentially performs some effects. +newtype Logger m r = Logger (r -> m Unit) + +instance contravariantLogger :: Contravariant (Logger m) where + cmap f (Logger l) = Logger \r -> l (f r) + +instance divideLogger :: (Apply m) => Divide (Logger m) where + divide f (Logger a) (Logger b) = + Logger \r -> case f r of Tuple r1 r2 -> a r1 *> b r2 + +instance divisibleLogger :: (Applicative m) => Divisible (Logger m) where + conquer = Logger \_ -> pure unit + +instance decideLogger :: (Apply m) => Decide (Logger m) where + choose f (Logger a) (Logger b) = + Logger \r -> case f r of + Left r' -> a r' + Right r' -> b r' + +instance decidableLogger :: (Applicative m) => Decidable (Logger m) where + lose f = Logger \r -> absurd (f r) + +instance semigroupLogger :: (Apply m) => Semigroup (Logger m r) where + append (Logger a) (Logger b) = Logger \r -> a r *> b r + +instance monoidLogger :: (Applicative m) => Monoid (Logger m r) where + mempty = Logger \_ -> pure unit + +-- | Log a record to the logger. +log :: forall m r. Logger m r -> r -> m Unit +log (Logger l) = l + +-- | Transform the logger such that it ignores records for which the predicate +-- | returns false. +cfilter :: forall m r. (Applicative m) => (r -> Boolean) -> Logger m r -> Logger m r +cfilter f (Logger l) = Logger \r -> when (f r) (l r) + +-- | Apply a natural transformation to the underlying functor. +hoist :: forall m m' r. (m ~> m') -> Logger m r -> Logger m' r +hoist f (Logger l) = Logger (f <<< l) diff --git a/tests/fixtures/packages/logging/src/Control/Logger/Console.purs b/tests/fixtures/packages/logging/src/Control/Logger/Console.purs new file mode 100644 index 00000000..7c4f805e --- /dev/null +++ b/tests/fixtures/packages/logging/src/Control/Logger/Console.purs @@ -0,0 +1,14 @@ +module Control.Logger.Console +( console +) where + +import Control.Logger (Logger(Logger)) +import Effect.Class (liftEffect, class MonadEffect) +import Effect.Console (log) + +-- | Logger that logs records to the console. +console :: forall m r + . MonadEffect m + => (r -> String) + -> Logger m r +console show = Logger \r -> liftEffect (log (show r)) diff --git a/tests/fixtures/packages/logging/src/Control/Logger/Writer.purs b/tests/fixtures/packages/logging/src/Control/Logger/Writer.purs new file mode 100644 index 00000000..88d65d5e --- /dev/null +++ b/tests/fixtures/packages/logging/src/Control/Logger/Writer.purs @@ -0,0 +1,14 @@ +module Control.Logger.Writer +( writer +) where + +import Prelude (class Monoid) +import Control.Logger (Logger(Logger)) +import Control.Monad.Writer.Class (class MonadWriter, tell) + +-- | Logger that writes records to the writer. +writer :: forall m r + . MonadWriter r m + => Monoid r + => Logger m r +writer = Logger tell diff --git a/tests/integration.rs b/tests/integration.rs index e49e5968..8f17e1cb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -150,7 +150,7 @@ fn e2e_data_constructor_nullary() { let source = "module T where\ndata Color = Red | Green | Blue\nx = Red"; assert_eq!( lookup_type(source, "x"), - Type::Con(interner::intern("Color")), + Type::con_local("Color"), ); } @@ -159,7 +159,7 @@ fn e2e_data_constructor_with_field() { let source = "module T where\ndata Box a = MkBox a\nx = MkBox 42"; assert_eq!( lookup_type(source, "x"), - Type::app(Type::Con(interner::intern("Box")), Type::int()), + Type::app(Type::con_local("Box"), Type::int()), ); } @@ -173,7 +173,7 @@ f x = case x of let ty = lookup_type(source, "f"); match ty { Type::Fun(from, to) => { - assert_eq!(*from, Type::Con(interner::intern("MyBool"))); + assert_eq!(*from, Type::con_local("MyBool")); assert_eq!(*to, Type::int()); } other => panic!("Expected MyBool -> Int, got: {}", other), diff --git a/tests/resolve.rs b/tests/resolve.rs new file mode 100644 index 00000000..50dfd9cb --- /dev/null +++ b/tests/resolve.rs @@ -0,0 +1,304 @@ +//! Integration tests for name resolution. +//! +//! Runs resolve_names on all PureScript fixture files to verify the resolver +//! handles real-world code without panicking and produces no errors. + +use std::path::{Path, PathBuf}; + +use purescript_fast_compiler::cst::Module; +use purescript_fast_compiler::parser; +use purescript_fast_compiler::typechecker::resolve::{resolve_names, ResolutionExports}; + +/// Support packages from tests/fixtures/packages used by the original compiler tests. +/// Same list as in tests/build.rs. +const SUPPORT_PACKAGES: &[&str] = &[ + "prelude", + "arrays", + "assert", + "bifunctors", + "catenable-lists", + "console", + "const", + "contravariant", + "control", + "datetime", + "distributive", + "effect", + "either", + "enums", + "exceptions", + "exists", + "filterable", + "foldable-traversable", + "foreign", + "foreign-object", + "free", + "functions", + "functors", + "gen", + "graphs", + "identity", + "integers", + "invariant", + "json", + "lazy", + "lcg", + "lists", + "maybe", + "newtype", + "nonempty", + "numbers", + "ordered-collections", + "orders", + "partial", + "profunctor", + "quickcheck", + "random", + "record", + "refs", + "safe-coerce", + "semirings", + "st", + "strings", + "tailrec", + "transformers", + "tuples", + "type-equality", + "typelevel-prelude", + "unfoldable", + "unsafe-coerce", + "validation", +]; + +fn collect_purs_files(dir: &Path, files: &mut Vec) { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_purs_files(&path, files); + } else if path.extension().is_some_and(|e| e == "purs") { + files.push(path); + } + } + } +} + +/// Parse all .purs files, returning (path, module) pairs for those that parse successfully. +fn parse_all_files(files: &[PathBuf]) -> Vec<(PathBuf, Module)> { + files + .iter() + .filter_map(|path| { + let source = std::fs::read_to_string(path).ok()?; + let module = parser::parse(&source).ok()?; + Some((path.clone(), module)) + }) + .collect() +} + +/// Parse all .purs source files from support packages. +fn parse_support_modules() -> Vec { + let packages_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + let mut modules = Vec::new(); + for &pkg in SUPPORT_PACKAGES { + let pkg_src = packages_dir.join(pkg).join("src"); + if !pkg_src.exists() { + continue; + } + let mut files = Vec::new(); + collect_purs_files(&pkg_src, &mut files); + for f in files { + if let Ok(source) = std::fs::read_to_string(&f) { + if let Ok(module) = parser::parse(&source) { + modules.push(module); + } + } + } + } + modules +} + +/// Run resolve_names on all parsed modules, collecting panics and errors. +fn resolve_all_modules( + parsed: &[(PathBuf, Module)], + exports: &ResolutionExports, +) -> (usize, Vec, Vec<(PathBuf, Vec)>) { + let mut panicked: Vec = Vec::new(); + let mut errored: Vec<(PathBuf, Vec)> = Vec::new(); + + for (path, module) in parsed { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + resolve_names(module, exports) + })); + match result { + Err(_) => { + panicked.push(path.clone()); + } + Ok(resolved) => { + if !resolved.errors.is_empty() { + let errors: Vec = + resolved.errors.iter().map(|e| e.to_string()).collect(); + errored.push((path.clone(), errors)); + } + } + } + } + + (parsed.len(), panicked, errored) +} + +// ===== Tests ===== + +/// Discover "projects" in the original-compiler passing directory. +/// Each .purs file in the root is a project; if a matching directory exists, +/// its files are companion modules. Returns (project_name, files) pairs. +fn discover_projects(fixtures_dir: &Path) -> Vec<(String, Vec)> { + let mut projects = Vec::new(); + let mut entries: Vec<_> = std::fs::read_dir(fixtures_dir) + .unwrap() + .flatten() + .collect(); + entries.sort_by_key(|e| e.path()); + + for entry in &entries { + let path = entry.path(); + if path.is_file() && path.extension().is_some_and(|e| e == "purs") { + let stem = path.file_stem().unwrap().to_string_lossy().to_string(); + let mut project_files = vec![path.clone()]; + // Check for companion directory + let companion_dir = fixtures_dir.join(&stem); + if companion_dir.is_dir() { + collect_purs_files(&companion_dir, &mut project_files); + } + projects.push((stem, project_files)); + } + } + projects +} + +#[test] +#[ignore = "Not using resolultion yet"] +fn resolve_fixture_original_compiler_passing() { + let fixtures_dir = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/original-compiler/passing"); + if !fixtures_dir.exists() { + eprintln!("Skipping: original-compiler/passing fixtures not found"); + return; + } + + let support_modules = parse_support_modules(); + let projects = discover_projects(&fixtures_dir); + assert!(!projects.is_empty(), "Expected passing fixture projects"); + + let mut total = 0; + let mut panicked: Vec = Vec::new(); + let mut errored: Vec<(PathBuf, Vec)> = Vec::new(); + + for (_project_name, files) in &projects { + let parsed = parse_all_files(files); + if parsed.is_empty() { + continue; + } + + // Build exports from support packages + this project's modules + let mut all_modules: Vec = support_modules.clone(); + all_modules.extend(parsed.iter().map(|(_, m)| m.clone())); + let exports = ResolutionExports::new(&all_modules); + + let (n, p, e) = resolve_all_modules(&parsed, &exports); + total += n; + panicked.extend(p); + errored.extend(e); + } + + if !panicked.is_empty() { + let summary: Vec = panicked + .iter() + .map(|p| format!(" {}", p.display())) + .collect(); + panic!( + "{}/{} files panicked during resolve_names:\n{}", + panicked.len(), + total, + summary.join("\n") + ); + } + + if !errored.is_empty() { + let summary: Vec = errored + .iter() + .take(20) + .map(|(p, errs)| { + format!(" {} ({} errors): {}", p.display(), errs.len(), errs[0]) + }) + .collect(); + panic!( + "{}/{} files had resolve errors:\n{}", + errored.len(), + total, + summary.join("\n") + ); + } + + eprintln!("resolve_names succeeded on {total} passing fixture files (0 panics, 0 errors)"); +} + +#[test] +#[ignore = "Not using resolultion yet"] +fn resolve_fixture_packages() { + let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/packages"); + if !fixtures_dir.exists() { + eprintln!("Skipping: packages fixtures not found"); + return; + } + + // Only collect from src/ directories (test/ files import test-only utilities) + let mut files = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&fixtures_dir) { + for entry in entries.flatten() { + let src_dir = entry.path().join("src"); + if src_dir.is_dir() { + collect_purs_files(&src_dir, &mut files); + } + } + } + files.sort(); + + assert!(!files.is_empty(), "Expected package fixture files"); + + let parsed = parse_all_files(&files); + let all_modules: Vec = parsed.iter().map(|(_, m)| m.clone()).collect(); + let exports = ResolutionExports::new(&all_modules); + let (total, panicked, errored) = resolve_all_modules(&parsed, &exports); + + let rel = |p: &Path| { + p.strip_prefix(&fixtures_dir) + .unwrap_or(p) + .display() + .to_string() + }; + + if !panicked.is_empty() { + let summary: Vec = panicked.iter().map(|p| format!(" {}", rel(p))).collect(); + panic!( + "{}/{} files panicked during resolve_names:\n{}", + panicked.len(), + total, + summary.join("\n") + ); + } + + if !errored.is_empty() { + let summary: Vec = errored + .iter() + .map(|(p, errs)| format!(" {} ({} errors): {}", rel(p), errs.len(), errs[0])) + .collect(); + panic!( + "{}/{} files had resolve errors:\n{}", + errored.len(), + total, + summary.join("\n") + ); + } + + eprintln!("resolve_names succeeded on {total} package files (0 panics, 0 errors)"); +} diff --git a/tests/typechecker_comprehensive.rs b/tests/typechecker_comprehensive.rs index 2a938d04..77ff680f 100644 --- a/tests/typechecker_comprehensive.rs +++ b/tests/typechecker_comprehensive.rs @@ -212,7 +212,7 @@ fn lit_array_empty() { let expr = parser::parse_expr("[]").unwrap(); let ty = infer_expr(&expr).unwrap(); match ty { - Type::App(f, _) => assert_eq!(*f, Type::Con(interner::intern("Array"))), + Type::App(f, _) => assert_eq!(*f, Type::prim_con("Array")), other => panic!("expected Array type, got: {}", other), } } @@ -262,7 +262,7 @@ fn constructor_nullary() { assert_module_type( "module T where\ndata Color = Red | Green | Blue\nx = Red", "x", - Type::Con(interner::intern("Color")), + Type::con_local("Color"), ); } @@ -271,7 +271,7 @@ fn constructor_unary() { assert_module_type( "module T where\ndata Box a = MkBox a\nx = MkBox 42", "x", - Type::app(Type::Con(interner::intern("Box")), Type::int()), + Type::app(Type::con_local("Box"), Type::int()), ); } @@ -281,7 +281,7 @@ fn constructor_binary() { "module T where\ndata Pair a b = MkPair a b\nx = MkPair 42 true", "x", Type::app( - Type::app(Type::Con(interner::intern("Pair")), Type::int()), + Type::app(Type::con_local("Pair"), Type::int()), Type::boolean(), ), ); @@ -293,7 +293,7 @@ fn constructor_nothing() { let source = "module T where\ndata Maybe a = Just a | Nothing\nx = Nothing"; let ty = assert_module_fn_type(source, "x"); match ty { - Type::App(f, _) => assert_eq!(*f, Type::Con(interner::intern("Maybe"))), + Type::App(f, _) => assert_eq!(*f, Type::con_local("Maybe")), other => panic!("expected Maybe ?a, got: {}", other), } } @@ -409,7 +409,7 @@ fn app_constructor() { assert_module_type( "module T where\ndata Maybe a = Just a | Nothing\nx = Just 42", "x", - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -554,7 +554,7 @@ f x = case x of match ty { Type::Fun(from, to) => { assert_eq!(*from, Type::int()); - assert_eq!(*to, Type::Con(interner::intern("MyBool"))); + assert_eq!(*to, Type::con_local("MyBool")); } other => panic!("expected Int -> MyBool, got: {}", other), } @@ -570,7 +570,7 @@ f x = case x of let ty = assert_module_fn_type(source, "f"); match ty { Type::Fun(from, to) => { - assert_eq!(*from, Type::Con(interner::intern("MyBool"))); + assert_eq!(*from, Type::con_local("MyBool")); assert_eq!(*to, Type::int()); } other => panic!("expected MyBool -> Int, got: {}", other), @@ -589,7 +589,7 @@ f x = case x of Type::Fun(from, to) => { assert_eq!( *from, - Type::app(Type::Con(interner::intern("Maybe")), Type::int()) + Type::app(Type::con_local("Maybe"), Type::int()) ); assert_eq!(*to, Type::int()); } @@ -1020,7 +1020,7 @@ fn err_module_function_wrong_return() { #[test] fn module_data_simple_enum() { let source = "module T where\ndata Color = Red | Green | Blue\nx = Green"; - assert_module_type(source, "x", Type::Con(interner::intern("Color"))); + assert_module_type(source, "x", Type::con_local("Color")); } #[test] @@ -1029,7 +1029,7 @@ fn module_data_maybe() { assert_module_type( source, "x", - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -1050,7 +1050,7 @@ y = Right true"; match x_ty { Type::App(f, _) => match f.as_ref() { Type::App(either, arg) => { - assert_eq!(**either, Type::Con(interner::intern("Either"))); + assert_eq!(**either, Type::con_local("Either")); assert_eq!(**arg, Type::int()); } other => panic!("expected Either Int _, got: {}", other), @@ -1068,7 +1068,7 @@ x = MkPair 42 true"; source, "x", Type::app( - Type::app(Type::Con(interner::intern("Pair")), Type::int()), + Type::app(Type::con_local("Pair"), Type::int()), Type::boolean(), ), ); @@ -1077,7 +1077,7 @@ x = MkPair 42 true"; #[test] fn module_newtype() { let source = "module T where\nnewtype Name = Name String\nx = Name \"Alice\""; - assert_module_type(source, "x", Type::Con(interner::intern("Name"))); + assert_module_type(source, "x", Type::con_local("Name")); } #[test] @@ -1086,7 +1086,7 @@ fn module_newtype_parameterized() { assert_module_type( source, "x", - Type::app(Type::Con(interner::intern("Wrapper")), Type::int()), + Type::app(Type::con_local("Wrapper"), Type::int()), ); } @@ -1107,14 +1107,14 @@ y = Cons 1 Nil"; // x = Nil :: List ?a (polymorphic) let x_ty = types.get(&interner::intern("x")).unwrap(); match x_ty { - Type::App(f, _) => assert_eq!(**f, Type::Con(interner::intern("List"))), + Type::App(f, _) => assert_eq!(**f, Type::con_local("List")), other => panic!("expected List type for Nil, got: {}", other), } // y = Cons 1 Nil :: List Int assert_eq!( *types.get(&interner::intern("y")).unwrap(), - Type::app(Type::Con(interner::intern("List")), Type::int()) + Type::app(Type::con_local("List"), Type::int()) ); } @@ -1133,7 +1133,7 @@ fromMaybe d x = case x of assert_eq!(**a, **result, "default and result should match"); match maybe_a.as_ref() { Type::App(f, elem) => { - assert_eq!(**f, Type::Con(interner::intern("Maybe"))); + assert_eq!(**f, Type::con_local("Maybe")); assert_eq!(**elem, **a, "Maybe elem should match default type"); } other => panic!("expected Maybe type, got: {}", other), @@ -1170,7 +1170,7 @@ in g"; source, "f", Type::fun( - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), Type::int(), ), ); @@ -1185,8 +1185,8 @@ x = Just (Just 42)"; source, "x", Type::app( - Type::Con(interner::intern("Maybe")), - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::con_local("Maybe"), + Type::app(Type::con_local("Maybe"), Type::int()), ), ); } @@ -1208,7 +1208,7 @@ f x = if true then Just x else Nothing"; match ty { Type::Fun(a, result) => match result.as_ref() { Type::App(f, elem) => { - assert_eq!(**f, Type::Con(interner::intern("Maybe"))); + assert_eq!(**f, Type::con_local("Maybe")); assert_eq!(**elem, *a, "Just x should have same type as input"); } other => panic!("expected Maybe type, got: {}", other), @@ -1225,7 +1225,7 @@ x = [Just 1, Just 2, Nothing]"; assert_module_type( source, "x", - Type::array(Type::app(Type::Con(interner::intern("Maybe")), Type::int())), + Type::array(Type::app(Type::con_local("Maybe"), Type::int())), ); } @@ -1798,7 +1798,7 @@ x = Just 42"; assert_module_type( source, "x", - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -1813,7 +1813,7 @@ x = Id 42"; assert_module_type( source, "x", - Type::app(Type::Con(interner::intern("Id")), Type::int()), + Type::app(Type::con_local("Id"), Type::int()), ); } @@ -1858,7 +1858,7 @@ x = pure 42"; let x_ty = types.get(&interner::intern("x")).unwrap(); match x_ty { Type::App(f, elem) => { - assert_eq!(**f, Type::Con(interner::intern("Effect"))); + assert_eq!(**f, Type::con_local("Effect")); assert_eq!(**elem, Type::int()); } other => panic!("expected Effect Int, got: {}", other), @@ -1876,7 +1876,7 @@ result = id (MkBox 42)"; assert_module_type( source, "result", - Type::app(Type::Con(interner::intern("Box")), Type::int()), + Type::app(Type::con_local("Box"), Type::int()), ); } @@ -2050,7 +2050,7 @@ fn record_in_array() { let ty = infer_expr(&expr).unwrap(); match ty { Type::App(f, elem) => { - assert_eq!(*f, Type::Con(interner::intern("Array"))); + assert_eq!(*f, Type::prim_con("Array")); match *elem { Type::Record(fields, None) => assert_eq!(fields.len(), 1), other => panic!("expected record element, got: {}", other), @@ -2068,7 +2068,7 @@ x = Wrap { x: 1, y: true }"; let ty = assert_module_fn_type(source, "x"); match ty { Type::App(f, arg) => { - assert_eq!(*f, Type::Con(interner::intern("Wrapper"))); + assert_eq!(*f, Type::con_local("Wrapper")); match *arg { Type::Record(fields, None) => assert_eq!(fields.len(), 2), other => panic!("expected record in Wrapper, got: {}", other), @@ -2149,16 +2149,20 @@ f = do #[test] fn do_nested_array_result() { + // bind x f = x returns its first argument, so: + // do { x <- [1,2]; [[x]] } ==> bind [1,2] (\x -> [[x]]) ==> [1,2] :: Array Int let source = "module T where bind x f = x f = do x <- [1, 2] [[x]]"; - assert_module_type(source, "f", Type::array(Type::array(Type::int()))); + assert_module_type(source, "f", Type::array(Type::int())); } #[test] fn do_with_constructor() { + // bind x f = x returns its first argument, so: + // do { x <- [1,2]; [Just x] } ==> bind [1,2] (\x -> [Just x]) ==> [1,2] :: Array Int let source = "module T where bind x f = x data Maybe a = Just a | Nothing @@ -2168,7 +2172,7 @@ f = do assert_module_type( source, "f", - Type::array(Type::app(Type::Con(interner::intern("Maybe")), Type::int())), + Type::array(Type::int()), ); } @@ -2205,7 +2209,7 @@ f = ado assert_module_type( source, "f", - Type::array(Type::app(Type::Con(interner::intern("Maybe")), Type::int())), + Type::array(Type::app(Type::con_local("Maybe"), Type::int())), ); } @@ -2265,7 +2269,7 @@ f x = case x of assert_eq!(*to, Type::int()); // from is Maybe ?a — just check it's a Maybe match *from { - Type::App(ref f, _) => assert_eq!(**f, Type::Con(interner::intern("Maybe"))), + Type::App(ref f, _) => assert_eq!(**f, Type::con_local("Maybe")), ref other => panic!("expected Maybe type, got: {}", other), } } @@ -2345,7 +2349,7 @@ f x = case x of Type::Fun(from, to) => { assert_eq!( *from, - Type::app(Type::Con(interner::intern("Maybe")), Type::boolean()) + Type::app(Type::con_local("Maybe"), Type::boolean()) ); assert_eq!(*to, Type::int()); } @@ -2380,7 +2384,7 @@ f MyFalse = 0"; let ty = assert_module_fn_type(source, "f"); match ty { Type::Fun(from, to) => { - assert_eq!(*from, Type::Con(interner::intern("MyBool"))); + assert_eq!(*from, Type::con_local("MyBool")); assert_eq!(*to, Type::int()); } other => panic!("expected MyBool -> Int, got: {}", other), @@ -2396,7 +2400,7 @@ f _ = 0"; let ty = assert_module_fn_type(source, "f"); match ty { Type::Fun(from, to) => { - assert_eq!(*from, Type::Con(interner::intern("Color"))); + assert_eq!(*from, Type::con_local("Color")); assert_eq!(*to, Type::int()); } other => panic!("expected Color -> Int, got: {}", other), @@ -2414,7 +2418,7 @@ fromJust Nothing = 0"; Type::Fun(from, to) => { assert_eq!( *from, - Type::app(Type::Con(interner::intern("Maybe")), Type::int()) + Type::app(Type::con_local("Maybe"), Type::int()) ); assert_eq!(*to, Type::int()); } @@ -2431,11 +2435,11 @@ and _ _ = MyFalse"; let ty = assert_module_fn_type(source, "and"); match ty { Type::Fun(a, inner) => { - assert_eq!(*a, Type::Con(interner::intern("MyBool"))); + assert_eq!(*a, Type::con_local("MyBool")); match *inner { Type::Fun(b, ret) => { - assert_eq!(*b, Type::Con(interner::intern("MyBool"))); - assert_eq!(*ret, Type::Con(interner::intern("MyBool"))); + assert_eq!(*b, Type::con_local("MyBool")); + assert_eq!(*ret, Type::con_local("MyBool")); } other => panic!("expected MyBool -> MyBool, got: {}", other), } @@ -2505,10 +2509,10 @@ f x = case x of // from should be Maybe (Maybe Int) match *from { Type::App(ref f, ref inner) => { - assert_eq!(**f, Type::Con(interner::intern("Maybe"))); + assert_eq!(**f, Type::con_local("Maybe")); assert_eq!( **inner, - Type::app(Type::Con(interner::intern("Maybe")), Type::int()) + Type::app(Type::con_local("Maybe"), Type::int()) ); } ref other => panic!("expected Maybe (Maybe Int), got: {}", other), @@ -2577,7 +2581,7 @@ f x = case x of Type::Fun(from, to) => { assert_eq!(*to, Type::int()); match *from { - Type::App(ref f, _) => assert_eq!(**f, Type::Con(interner::intern("Array"))), + Type::App(ref f, _) => assert_eq!(**f, Type::prim_con("Array")), ref other => panic!("expected Array type, got: {}", other), } } @@ -2815,7 +2819,7 @@ x = pure 42"; let ty = assert_module_fn_type(source, "x"); match ty { Type::App(f, arg) => { - assert_eq!(*f, Type::Con(interner::intern("IO"))); + assert_eq!(*f, Type::con_local("IO")); assert_eq!(*arg, Type::int()); } other => panic!("expected IO Int, got: {}", other), @@ -2832,10 +2836,10 @@ x = newRef 42"; let ty = assert_module_fn_type(source, "x"); match ty { Type::App(f, inner) => { - assert_eq!(*f, Type::Con(interner::intern("Effect"))); + assert_eq!(*f, Type::con_local("Effect")); match *inner { Type::App(ref g, ref arg) => { - assert_eq!(**g, Type::Con(interner::intern("Ref"))); + assert_eq!(**g, Type::con_local("Ref")); assert_eq!(**arg, Type::int()); } ref other => panic!("expected Ref Int, got: {}", other), @@ -2962,12 +2966,12 @@ branch = Branch (Leaf 1) (Leaf 2)"; assert_module_type( source, "leaf", - Type::app(Type::Con(interner::intern("Tree")), Type::int()), + Type::app(Type::con_local("Tree"), Type::int()), ); assert_module_type( source, "branch", - Type::app(Type::Con(interner::intern("Tree")), Type::int()), + Type::app(Type::con_local("Tree"), Type::int()), ); } @@ -2988,15 +2992,15 @@ z = SuccE (SuccO Zero)"; ); assert_eq!( *types.get(&interner::intern("x")).unwrap(), - Type::Con(interner::intern("Even")) + Type::con_local("Even") ); assert_eq!( *types.get(&interner::intern("y")).unwrap(), - Type::Con(interner::intern("Odd")) + Type::con_local("Odd") ); assert_eq!( *types.get(&interner::intern("z")).unwrap(), - Type::Con(interner::intern("Even")) + Type::con_local("Even") ); } @@ -3016,7 +3020,7 @@ result = mapMaybe (\\x -> x) (Just 42)"; assert_module_type( source, "result", - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -3080,7 +3084,7 @@ result = apply (\\x -> Just x) 42"; assert_module_type( source, "result", - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::con_local("Maybe"), Type::int()), ); } @@ -3135,7 +3139,7 @@ getName x = case x of let ty = assert_module_fn_type(source, "getName"); match ty { Type::Fun(from, to) => { - assert_eq!(*from, Type::Con(interner::intern("Name"))); + assert_eq!(*from, Type::con_local("Name")); assert_eq!(*to, Type::string()); } other => panic!("expected Name -> String, got: {}", other), @@ -3232,7 +3236,7 @@ f = MkPair 42"; Type::App(ref f, ref arg) => { match f.as_ref() { Type::App(pair, int_arg) => { - assert_eq!(**pair, Type::Con(interner::intern("Pair"))); + assert_eq!(**pair, Type::con_local("Pair")); assert_eq!(**int_arg, Type::int()); } other => panic!("expected Pair Int, got: {}", other), @@ -3252,7 +3256,7 @@ fn edge_array_of_functions() { let ty = infer_expr(&expr).unwrap(); match ty { Type::App(f, elem) => { - assert_eq!(*f, Type::Con(interner::intern("Array"))); + assert_eq!(*f, Type::prim_con("Array")); match *elem { Type::Fun(a, b) => assert_eq!(*a, *b, "should be array of identity-like functions"), other => panic!("expected function element, got: {}", other), @@ -3439,7 +3443,7 @@ f = f"; match ty { Type::App(f, _) => match *f { Type::App(ref op, ref left) => { - assert_eq!(**op, Type::Con(interner::intern("Pair"))); + assert_eq!(**op, Type::con_local("Pair")); assert_eq!(**left, Type::int()); } ref other => panic!("expected Pair Int _, got: {}", other), @@ -4094,8 +4098,8 @@ f x = case x of *types.get(&interner::intern("f")).unwrap(), Type::fun( Type::app( - Type::Con(interner::intern("Maybe")), - Type::Con(interner::intern("Boolean")) + Type::con_local("Maybe"), + Type::prim_con("Boolean") ), Type::int() ) @@ -4339,7 +4343,7 @@ f r = case r of let x_ty = &fields[0].1; assert_eq!( *x_ty, - Type::app(Type::Con(interner::intern("Maybe")), Type::int()) + Type::app(Type::prim_con("Maybe"), Type::int()) ); } other => panic!("expected record, got: {}", other), @@ -4592,7 +4596,7 @@ fn check_modules(sources: &[&str]) -> (HashMap, Vec) { let mut last_errors = Vec::new(); for source in sources { let module = parser::parse(source).unwrap_or_else(|e| panic!("parse failed: {}", e)); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); registry.register(&module.name.value.parts, result.exports); last_types = result.types; last_errors = result.errors; @@ -4636,7 +4640,7 @@ x = Red"; let x = interner::intern("x"); assert_eq!( *types.get(&x).unwrap(), - Type::Con(interner::intern("Color")) + Type::con_local("Color") ); } @@ -4657,7 +4661,7 @@ x = Just 42"; let x = interner::intern("x"); assert_eq!( *types.get(&x).unwrap(), - Type::app(Type::Con(interner::intern("Maybe")), Type::int()) + Type::app(Type::prim_con("Maybe"), Type::int()) ); } @@ -4797,7 +4801,7 @@ x = MyTrue"; let x = interner::intern("x"); assert_eq!( *types.get(&x).unwrap(), - Type::Con(interner::intern("MyBool")) + Type::con("A", "MyBool") ); } @@ -4820,7 +4824,7 @@ f x = case x of let f = interner::intern("f"); match types.get(&f).unwrap() { Type::Fun(from, to) => { - assert_eq!(**from, Type::Con(interner::intern("AB"))); + assert_eq!(**from, Type::con("A", "AB")); assert_eq!(**to, Type::int()); } other => panic!("expected function type, got: {}", other), @@ -4898,7 +4902,7 @@ x = toBit 1"; errors.iter().map(|e| e.to_string()).collect::>() ); let x = interner::intern("x"); - assert_eq!(*types.get(&x).unwrap(), Type::Con(interner::intern("Bit"))); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Bit")); } #[test] @@ -4957,7 +4961,7 @@ x = Wrap 42"; let x = interner::intern("x"); assert_eq!( *types.get(&x).unwrap(), - Type::app(Type::Con(interner::intern("Wrapper")), Type::int()) + Type::app(Type::con("A", "Wrapper"), Type::int()) ); } @@ -5038,7 +5042,7 @@ x = M.Just 42"; let x = interner::intern("x"); assert_eq!( *types.get(&x).unwrap(), - Type::app(Type::Con(interner::intern("Maybe")), Type::int()), + Type::app(Type::prim_con("Maybe"), Type::int()), ); } @@ -5062,7 +5066,7 @@ f x = case x of let f = interner::intern("f"); match types.get(&f).unwrap() { Type::Fun(from, to) => { - assert_eq!(**from, Type::Con(interner::intern("Color"))); + assert_eq!(**from, Type::con("A", "Color")); assert_eq!(**to, Type::int()); } other => panic!("Expected function type, got: {}", other), @@ -5713,7 +5717,7 @@ x :: String x = \"hello\""; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "unexpected errors: {:?}", @@ -5735,11 +5739,15 @@ x :: Int x = 42"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "expected no errors: bare Int should still work with import Prim as P, got: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } #[test] @@ -5751,16 +5759,20 @@ x :: P.Int x = 42"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "expected no errors: P.Int should work with import Prim as P, got: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } #[test] -fn prim_explicit_without_imports_fails () { +fn prim_explicit_without_imports_fails() { // If we explicitly import Prim but don't list some of its exports, then those exports from Prim should not be available unqualified. let source = "module T where import Prim (String) @@ -5769,11 +5781,18 @@ x ∷ Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( - result.errors.iter().any(|e| matches!(e, TypeError::UnknownType { .. })), + result + .errors + .iter() + .any(|e| matches!(e, TypeError::UnknownName { .. })), "expected error for unknown type Int when importing Prim with only String, got: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -5831,7 +5850,7 @@ import NonExistent x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result .errors @@ -6143,7 +6162,7 @@ x :: Nonexistent x = 1"; assert_module_error_kind( source, - |e| matches!(e, TypeError::UnknownType { .. }), + |e| matches!(e, TypeError::UnknownName { .. }), "UnknownType", ); } @@ -6185,7 +6204,7 @@ y = \"hello\""; let y = interner::intern("y"); assert_eq!( *types.get(&x).unwrap(), - Type::app(Type::Con(interner::intern("Array")), Type::int()) + Type::app(Type::prim_con("Array"), Type::int()) ); assert_eq!(*types.get(&y).unwrap(), Type::string()); } @@ -6344,7 +6363,9 @@ fn no_error_distinct_type_args() { let source = "module T where\ndata MyType a b = MyConstructor a b"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::DuplicateTypeArgument { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::DuplicateTypeArgument { .. })), "distinct type args should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6376,7 +6397,9 @@ class MyClassB a where bar :: a -> a"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::DuplicateTypeClass { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::DuplicateTypeClass { .. })), "distinct type classes should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6412,7 +6435,9 @@ instance myInstB :: MyClass String where foo x = x"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::DuplicateInstance { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::DuplicateInstance { .. })), "distinct instance names should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6447,7 +6472,9 @@ fn no_error_distinct_arg_names() { let source = "module T where\nf x y = x"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::OverlappingArgNames { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::OverlappingArgNames { .. })), "distinct arg names should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6477,7 +6504,9 @@ f = case Pair 1 2 of Pair x y -> x"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::OverlappingPattern { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::OverlappingPattern { .. })), "distinct pattern vars should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6524,7 +6553,10 @@ f = do Box x"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::InvalidDoBind { .. } | TypeError::InvalidDoLet { .. })), + !errors.iter().any(|e| matches!( + e, + TypeError::InvalidDoBind { .. } | TypeError::InvalidDoLet { .. } + )), "valid do block should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6566,7 +6598,9 @@ fn no_error_non_cyclic_type_synonyms() { let source = "module T where\ntype Name = String\ntype Id = Int"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::CycleInTypeSynonym { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::CycleInTypeSynonym { .. })), "non-cyclic type synonyms should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6608,7 +6642,9 @@ fn no_error_non_cyclic_superclasses() { let source = "module T where\nclass Foo a\nclass (Foo a) <= Bar a"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::CycleInTypeClassDeclaration { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::CycleInTypeClassDeclaration { .. })), "non-cyclic superclass chain should not error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6620,7 +6656,9 @@ fn no_error_superclass_referencing_external_class() { let source = "module T where\nclass (Show a) <= MyClass a"; let (_, errors) = check_module_types(source); assert!( - !errors.iter().any(|e| matches!(e, TypeError::CycleInTypeClassDeclaration { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::CycleInTypeClassDeclaration { .. })), "external superclass reference should not trigger cycle error: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6639,7 +6677,9 @@ f = 42"; let (_, errors) = check_module_types(source); // We just need to verify no import errors for Partial assert!( - !errors.iter().any(|e| matches!(e, TypeError::UnknownType { .. })), + !errors + .iter() + .any(|e| matches!(e, TypeError::UnknownType { .. })), "Partial class should be available from Prim: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); @@ -6649,19 +6689,23 @@ f = 42"; fn prim_kind_types_available() { // Kind types Type, Constraint, Symbol, Row should be available from implicit Prim import for kind_name in &["Type", "Constraint", "Symbol", "Row"] { - let source = format!( - "module T where\nforeign import data Foo :: {}", - kind_name - ); + let source = format!("module T where\nforeign import data Foo :: {}", kind_name); let module = parser::parse(&source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); // These are valid kind references; should not cause unknown type errors assert!( - !result.errors.iter().any(|e| matches!(e, TypeError::UnknownType { .. })), + !result + .errors + .iter() + .any(|e| matches!(e, TypeError::UnknownType { .. })), "Kind type {} should be available from Prim: {:?}", kind_name, - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } } @@ -6676,11 +6720,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Int classes should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6694,11 +6742,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Boolean types should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6712,11 +6764,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Coerce class should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6730,11 +6786,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Ordering types should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6748,11 +6808,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Row classes should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6766,11 +6830,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.RowList types/classes should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6784,11 +6852,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.Symbol classes should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6802,11 +6874,15 @@ x :: Int x = 1"; let module = parser::parse(source).unwrap(); let registry = ModuleRegistry::new(); - let result = purescript_fast_compiler::typechecker::check::check_module(&module, ®istry); + let result = purescript_fast_compiler::typechecker::check_module_with_registry(&module, ®istry); assert!( result.errors.is_empty(), "Prim.TypeError types/classes should be importable: {:?}", - result.errors.iter().map(|e| e.to_string()).collect::>() + result + .errors + .iter() + .map(|e| e.to_string()) + .collect::>() ); } @@ -6825,4 +6901,1047 @@ x = 1"; "Prim.Int classes should not be implicitly available: {:?}", errors.iter().map(|e| e.to_string()).collect::>() ); -} \ No newline at end of file +} + +// ═══════════════════════════════════════════════════════════════════════════ +// ADVANCED EXPORT/IMPORT FEATURES +// ═══════════════════════════════════════════════════════════════════════════ + +// ----- Hiding keyword for resolving naming conflicts ----- + +#[test] +fn hiding_resolves_naming_conflict_between_modules() { + // Two modules export the same name; use hiding to disambiguate + let m1 = "module M1 where +sameName :: Int +sameName = 1"; + let m2 = "module M2 where +sameName :: String +sameName = \"hello\""; + let consumer = "module C where +import M1 (sameName) +import M2 hiding (sameName) +x = sameName"; + let (types, errors) = check_modules(&[m1, m2, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + // sameName comes from M1 (Int), since M2's sameName is hidden + assert_eq!(*types.get(&x).unwrap(), Type::int()); +} + +#[test] +fn hiding_allows_non_hidden_names_through() { + // hiding only blocks the listed names; others come through + let a = "module A where +x :: Int +x = 1 +y :: String +y = \"hello\" +z :: Boolean +z = true"; + let b = "module B where +import A hiding (x) +a = y +b = z"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let a_sym = interner::intern("a"); + let b_sym = interner::intern("b"); + assert_eq!(*types.get(&a_sym).unwrap(), Type::string()); + assert_eq!(*types.get(&b_sym).unwrap(), Type::boolean()); +} + +#[test] +fn err_hiding_blocks_listed_name() { + // Using a hidden name should produce an error + let a = "module A where +x :: Int +x = 1 +y :: String +y = \"hello\""; + let b = "module B where +import A hiding (x) +z = x"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: x should not be in scope after hiding" + ); +} + +#[test] +fn hiding_multiple_names() { + // Hide multiple names at once + let a = "module A where +x :: Int +x = 1 +y :: String +y = \"hello\" +z :: Boolean +z = true"; + let b = "module B where +import A hiding (x, y) +w = z"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let w = interner::intern("w"); + assert_eq!(*types.get(&w).unwrap(), Type::boolean()); +} + +#[test] +fn err_hiding_multiple_names_blocks_all() { + // Both hidden names should be inaccessible + let a = "module A where +x :: Int +x = 1 +y :: String +y = \"hello\" +z :: Boolean +z = true"; + let b = "module B where +import A hiding (x, y) +w = y"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: y should not be in scope after hiding" + ); +} + +#[test] +fn hiding_data_constructors() { + // Hide a data constructor using hiding + let a = "module A where +data Color = Red | Green | Blue"; + let b = "module B where +import A hiding (Red) +x = Green"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Color")); +} + +// ----- Module aliases for resolving naming conflicts ----- + +#[test] +fn module_aliases_resolve_naming_conflict() { + // Two modules export the same name; use aliases to disambiguate + let m1 = "module M1 where +sameName :: Int +sameName = 1"; + let m2 = "module M2 where +sameName :: String +sameName = \"hello\""; + let consumer = "module C where +import M1 as M1 +import M2 as M2 +x = M1.sameName +y = M2.sameName"; + let (types, errors) = check_modules(&[m1, m2, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +#[test] +fn module_alias_qualified_data_constructors() { + // Use qualified data constructors from aliased modules + let m1 = "module M1 where +data Shape = Circle | Square"; + let m2 = "module M2 where +data Shape = Triangle | Pentagon"; + let consumer = "module C where +import M1 as M1 +import M2 as M2 +x = M1.Circle +y = M2.Triangle"; + let (types, errors) = check_modules(&[m1, m2, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::con("M1", "Shape")); + assert_eq!(*types.get(&y).unwrap(), Type::con("M2", "Shape")); +} + +#[test] +fn module_alias_qualified_function_call() { + // Call a function using a module alias + let a = "module A where +double :: Int -> Int +double x = x"; + let b = "module B where +import A as Lib +y = Lib.double 21"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + assert_eq!(*types.get(&y).unwrap(), Type::int()); +} + +#[test] +fn module_alias_pattern_match() { + // Use qualified constructors from aliased import in pattern matching + let a = "module A where +data Maybe a = Just a | Nothing"; + let b = "module B where +import A as M +f x = case x of + M.Just v -> v + M.Nothing -> 0"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let f = interner::intern("f"); + match types.get(&f).unwrap() { + Type::Fun(from, to) => { + assert_eq!(**from, Type::app(Type::prim_con("Maybe"), Type::int())); + assert_eq!(**to, Type::int()); + } + other => panic!("Expected function type, got: {}", other), + } +} + +// ----- Re-exporting modules ----- + +#[test] +fn reexport_module_consolidates_imports() { + // Module that re-exports items from multiple modules under a single alias + let m1 = "module M1 where +anInt :: Int +anInt = 1"; + let m2 = "module M2 where +aString :: String +aString = \"hello\""; + // ReExporter imports from M1 and M2 and re-exports them + let reexporter = "module ReExporter (module M1, module M2) where +import M1 +import M2"; + let consumer = "module C where +import ReExporter +x = anInt +y = aString"; + let (types, errors) = check_modules(&[m1, m2, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +#[test] +fn reexport_data_type_and_constructors() { + // Re-export a data type with its constructors + let definer = "module Definer where +data Color = Red | Green | Blue"; + let reexporter = "module ReExporter (module Definer) where +import Definer"; + let consumer = "module C where +import ReExporter +x = Red"; + let (types, errors) = check_modules(&[definer, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::con("Definer", "Color")); +} + +#[test] +fn reexport_function() { + // Re-export a function from another module + let definer = "module Definer where +double :: Int -> Int +double x = x"; + let reexporter = "module ReExporter (module Definer) where +import Definer"; + let consumer = "module C where +import ReExporter +y = double 21"; + let (types, errors) = check_modules(&[definer, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + assert_eq!(*types.get(&y).unwrap(), Type::int()); +} + +#[test] +fn reexport_type_alias() { + // Re-export a type alias from another module + let definer = "module Definer where +type Name = String +greet :: Name -> String +greet n = n"; + let reexporter = "module ReExporter (module Definer) where +import Definer"; + let consumer = "module C where +import ReExporter +x = greet \"Alice\""; + let (types, errors) = check_modules(&[definer, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::string()); +} + +#[test] +fn reexport_selective_via_alias() { + // Import specific names from different modules under a common alias, then re-export + let m1 = "module M1 where +a :: Int +a = 1 +b :: Int +b = 2"; + let m2 = "module M2 where +c :: String +c = \"hello\" +d :: String +d = \"world\""; + // Re-export only specific items: a from M1 and c from M2 + let reexporter = "module ReExporter (module Alias) where +import M1 (a) as Alias +import M2 (c) as Alias"; + let consumer = "module C where +import ReExporter +x = a +y = c"; + let (types, errors) = check_modules(&[m1, m2, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +#[test] +fn err_reexport_does_not_include_unimported() { + // Re-exporting module X only re-exports what was imported from X + let definer = "module Definer where +exported :: Int +exported = 1 +internal :: String +internal = \"secret\""; + let reexporter = "module ReExporter (module Definer) where +import Definer (exported)"; + let consumer = "module C where +import ReExporter +x = internal"; + let (_, errors) = check_modules(&[definer, reexporter, consumer]); + assert!( + !errors.is_empty(), + "expected error: internal should not be available through re-export" + ); +} + +// ----- Exporting entire current module (module Self) ----- + +#[test] +fn self_reexport_exports_all_local_definitions() { + // module A (module A) exports everything defined in A + let a = "module A (module A) where +x :: Int +x = 1 +y :: String +y = \"hello\" +z :: Boolean +z = true"; + let b = "module B where +import A +a = x +b = y +c = z"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let a_sym = interner::intern("a"); + let b_sym = interner::intern("b"); + let c_sym = interner::intern("c"); + assert_eq!(*types.get(&a_sym).unwrap(), Type::int()); + assert_eq!(*types.get(&b_sym).unwrap(), Type::string()); + assert_eq!(*types.get(&c_sym).unwrap(), Type::boolean()); +} + +#[test] +fn self_reexport_exports_data_types() { + // module A (module A) exports data types and their constructors + let a = "module A (module A) where +data Color = Red | Green | Blue"; + let b = "module B where +import A +x = Red +y = Green"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Color")); + assert_eq!(*types.get(&y).unwrap(), Type::con("A", "Color")); +} + +#[test] +fn self_reexport_exports_type_alias() { + // module A (module A) exports type aliases + let a = "module A (module A) where +type Name = String +greet :: Name -> String +greet n = n"; + let b = "module B where +import A +x = greet \"world\""; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::string()); +} + +#[test] +fn self_reexport_combined_with_module_reexport() { + // module C (module C, module A) exports both local defs and re-exports from A + let a = "module A where +fromA :: Int +fromA = 42"; + let c = "module C (module C, module A) where +import A +fromC :: String +fromC = \"local\""; + let consumer = "module D where +import C +x = fromA +y = fromC"; + let (types, errors) = check_modules(&[a, c, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +// ----- Exporting specific types and type aliases ----- + +#[test] +fn export_type_with_all_constructors() { + // module A (Color(..)) exports the type and all its constructors + let a = "module A (Color(..)) where +data Color = Red | Green | Blue"; + let b = "module B where +import A +x = Red +y = Blue"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Color")); + assert_eq!(*types.get(&y).unwrap(), Type::con("A", "Color")); +} + +#[test] +fn export_type_without_constructors() { + // module A (Color) exports the type but NOT constructors + let a = "module A (Color) where +data Color = Red | Green | Blue"; + let b = "module B where +import A +x = Red"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: Red should not be accessible when Color is exported without constructors" + ); +} + +#[test] +fn export_type_alias_explicitly() { + // module A (Name, greet) exports a type alias and a function that uses it + let a = "module A (Name, greet) where +type Name = String +greet :: Name -> String +greet n = n"; + let b = "module B where +import A +x = greet \"Alice\""; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::string()); +} + +#[test] +fn export_newtype_with_constructor() { + // Export a newtype with its constructor + let a = "module A (Wrapper(..)) where +newtype Wrapper a = Wrap a"; + let b = "module B where +import A +x = Wrap 42"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!( + *types.get(&x).unwrap(), + Type::app(Type::con("A", "Wrapper"), Type::int()) + ); +} + +#[test] +fn err_export_newtype_without_constructor() { + // Export a newtype without its constructor + let a = "module A (Wrapper) where +newtype Wrapper a = Wrap a"; + let b = "module B where +import A +x = Wrap 42"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: Wrap should not be accessible when Wrapper is exported without constructors" + ); +} + +// ----- Multi-level import chains (transitive imports through multiple modules) ----- + +#[test] +fn value_through_three_modules_via_reexport() { + // A defines a value, B re-exports it, C consumes it + let a = "module A where +original :: Int +original = 42"; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +x = original"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); +} + +#[test] +fn value_through_four_modules_via_reexport() { + // A -> B -> C -> D, each re-exporting the previous + let a = "module A where +deep :: Int +deep = 99"; + let b = "module B (module A) where +import A"; + let c = "module C (module B) where +import B"; + let d = "module D where +import C +x = deep"; + let (types, errors) = check_modules(&[a, b, c, d]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); +} + +#[test] +fn data_type_through_three_modules_via_reexport() { + // Data types and constructors flow through re-exports + let a = "module A where +data Maybe a = Just a | Nothing"; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +x = Just 42"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!( + *types.get(&x).unwrap(), + Type::app(Type::prim_con("Maybe"), Type::int()) + ); +} + +#[test] +fn type_alias_through_three_modules_via_reexport() { + // Type aliases flow through re-exports + let a = "module A where +type Name = String +mkName :: String -> Name +mkName s = s"; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +x = mkName \"Alice\""; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::string()); +} + +#[test] +fn function_through_four_modules_via_reexport() { + // Functions with type signatures flow through multiple re-exports + let a = "module A where +double :: Int -> Int +double x = x"; + let b = "module B (module A) where +import A"; + let c = "module C (module B) where +import B"; + let d = "module D where +import C +y = double 21"; + let (types, errors) = check_modules(&[a, b, c, d]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + assert_eq!(*types.get(&y).unwrap(), Type::int()); +} + +#[test] +fn multiple_values_through_chain_with_local_additions() { + // Each module in the chain adds its own values alongside re-exports + let a = "module A where +fromA :: Int +fromA = 1"; + let b = "module B (module A, module B) where +import A +fromB :: String +fromB = \"hello\""; + let c = "module C where +import B +x = fromA +y = fromB"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +#[test] +fn reexport_chain_with_data_and_values() { + // Re-export chain that includes data types, constructors, and values + let a = "module A where +data Color = Red | Green | Blue +colorName :: Color -> String +colorName c = \"color\""; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +x = Red +y = colorName Green"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Color")); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +// ----- Combining hiding with qualified imports ----- + +#[test] +fn hiding_with_another_qualified_import() { + // Use hiding on one import, qualified on another + let m1 = "module M1 where +value :: Int +value = 1"; + let m2 = "module M2 where +value :: String +value = \"hello\" +extra :: Boolean +extra = true"; + let consumer = "module C where +import M1 hiding (value) +import M2 as M2 +x = M2.value +y = M2.extra"; + let (types, errors) = check_modules(&[m1, m2, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::string()); + assert_eq!(*types.get(&y).unwrap(), Type::boolean()); +} + +// ----- Import explicit list with data constructors ----- + +#[test] +fn import_explicit_data_type_with_all_constructors() { + // import A (Color(..)) brings the type and all constructors + let a = "module A where +data Color = Red | Green | Blue"; + let b = "module B where +import A (Color(..)) +x = Red"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!(*types.get(&x).unwrap(), Type::con("A", "Color")); +} + +#[test] +fn import_explicit_data_type_without_constructors() { + // import A (Color) brings only the type, not constructors + let a = "module A where +data Color = Red | Green | Blue"; + let b = "module B where +import A (Color) +x = Red"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: Red should not be accessible when importing Color without constructors" + ); +} + +// ----- Qualified imports with explicit import lists ----- + +#[test] +fn qualified_import_explicit_list_access() { + // import A (x) as Q — x is available as Q.x + let a = "module A where +x :: Int +x = 42 +y :: String +y = \"hello\""; + let b = "module B where +import A (x) as Q +z = Q.x"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let z = interner::intern("z"); + assert_eq!(*types.get(&z).unwrap(), Type::int()); +} + +// ----- Re-export with selective imports under common alias ----- + +#[test] +fn reexport_selective_from_multiple_modules_under_alias() { + // Consolidate selective imports from multiple modules under a single alias + let m1 = "module M1 where +anInt :: Int +anInt = 1 +otherInt :: Int +otherInt = 2"; + let m2 = "module M2 where +aString :: String +aString = \"hello\" +otherString :: String +otherString = \"world\""; + let reexporter = "module ReExporter (module Alias) where +import M1 (anInt) as Alias +import M2 (aString) as Alias"; + let consumer = "module C where +import ReExporter +x = anInt +y = aString"; + let (types, errors) = check_modules(&[m1, m2, reexporter, consumer]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +#[test] +fn err_reexport_alias_does_not_leak_non_imported() { + // When re-exporting via alias, names NOT imported should not leak through + let m1 = "module M1 where +exported :: Int +exported = 1 +notExported :: String +notExported = \"secret\""; + let reexporter = "module ReExporter (module Alias) where +import M1 (exported) as Alias"; + let consumer = "module C where +import ReExporter +x = notExported"; + let (_, errors) = check_modules(&[m1, reexporter, consumer]); + assert!( + !errors.is_empty(), + "expected error: notExported should not be available through selective re-export" + ); +} + +// ----- Diamond import pattern ----- + +#[test] +fn diamond_import_pattern() { + // A defines a value; B and C both import from A; D imports from both B and C + let a = "module A where +shared :: Int +shared = 42"; + let b = "module B (module A, fromB) where +import A +fromB :: String +fromB = \"b\""; + let c = "module C (module A, fromC) where +import A +fromC :: Boolean +fromC = true"; + let d = "module D where +import B +import C +x = shared +y = fromB +z = fromC"; + let (types, errors) = check_modules(&[a, b, c, d]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + let y = interner::intern("y"); + let z = interner::intern("z"); + assert_eq!(*types.get(&x).unwrap(), Type::int()); + assert_eq!(*types.get(&y).unwrap(), Type::string()); + assert_eq!(*types.get(&z).unwrap(), Type::boolean()); +} + +// ----- Class import/export through chains ----- + +#[test] +fn class_method_through_reexport_chain() { + // Class and instance from A, re-exported through B, used in C + let a = "module A where +class Show a where + show :: a -> String +instance Show Int where + show x = \"int\""; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +y = show 42"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + assert_eq!(*types.get(&y).unwrap(), Type::string()); +} + +// ----- Export list with mixed items ----- + +#[test] +fn export_mixed_values_types_and_classes() { + // Export a mix of values, types, and classes + let a = "module A (x, Color(..), class Show, show) where +x :: Int +x = 42 +data Color = Red | Green | Blue +class Show a where + show :: a -> String +instance Show Int where + show i = \"int\" +hidden :: String +hidden = \"nope\""; + let b = "module B where +import A +y = x +z = Red +w = show 1"; + let (types, errors) = check_modules(&[a, b]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + let z = interner::intern("z"); + let w = interner::intern("w"); + assert_eq!(*types.get(&y).unwrap(), Type::int()); + assert_eq!(*types.get(&z).unwrap(), Type::con("A", "Color")); + assert_eq!(*types.get(&w).unwrap(), Type::string()); +} + +#[test] +fn err_export_mixed_hidden_value_not_accessible() { + // Value not in export list should not be accessible + let a = "module A (x, Color(..)) where +x :: Int +x = 42 +data Color = Red | Green | Blue +hidden :: String +hidden = \"nope\""; + let b = "module B where +import A +z = hidden"; + let (_, errors) = check_modules(&[a, b]); + assert!( + !errors.is_empty(), + "expected error: hidden should not be accessible" + ); +} + +// ----- Operator re-export through chain ----- + +#[test] +fn operator_reexport_through_chain() { + // A defines an operator, B re-exports, C uses it + let a = "module A where +add :: Int -> Int -> Int +add x y = x +infixl 6 add as +"; + let b = "module B (module A) where +import A"; + let c = "module C where +import B +y = 1 + 2"; + let (types, errors) = check_modules(&[a, b, c]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let y = interner::intern("y"); + assert_eq!(*types.get(&y).unwrap(), Type::int()); +} + +// ----- Newtype through re-export chain ----- + +#[test] +fn newtype_through_reexport_chain() { + // Newtype from A, re-exported through B and C, used in D + let a = "module A where +newtype Wrapper a = Wrap a"; + let b = "module B (module A) where +import A"; + let c = "module C (module B) where +import B"; + let d = "module D where +import C +x = Wrap 42"; + let (types, errors) = check_modules(&[a, b, c, d]); + assert!( + errors.is_empty(), + "unexpected errors: {:?}", + errors.iter().map(|e| e.to_string()).collect::>() + ); + let x = interner::intern("x"); + assert_eq!( + *types.get(&x).unwrap(), + Type::app(Type::con("A", "Wrapper"), Type::int()) + ); +}