diff --git a/src/ast.rs b/src/ast.rs index 3608949c..6f3709c2 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -502,6 +502,12 @@ pub enum TypeExpr { /// Type-level integer literal: 42 IntLiteral { span: Span, value: i64 }, + + /// Array pattern parsed in type context (for as-patterns via VTA) + ArrayPattern { span: Span, elements: Vec }, + + /// As-pattern parsed in type context (for nested as-patterns in VTA) + AsPattern { span: Span, name: Spanned, ty: Box }, } /// Type constraint (for type classes) @@ -612,7 +618,9 @@ impl TypeExpr { | TypeExpr::Wildcard { span, .. } | TypeExpr::Kinded { span, .. } | TypeExpr::StringLiteral { span, .. } - | TypeExpr::IntLiteral { span, .. } => *span, + | TypeExpr::IntLiteral { span, .. } + | TypeExpr::ArrayPattern { span, .. } + | TypeExpr::AsPattern { span, .. } => *span, } } } @@ -1414,13 +1422,10 @@ impl Converter { .next() .map_or(false, |c| c.is_lowercase() || c == '_') { - self.function_op_aliases.insert( - QualifiedIdent { - module: None, - name: operator.value, - } - .name, - ); + self.function_op_aliases.insert(operator.value); + } else { + // Constructor target: remove any imported function alias + self.function_op_aliases.remove(&operator.value); } } } @@ -2466,9 +2471,17 @@ impl Converter { // 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) + let key_l = if let Some(m) = operators[i].value.module { + qualified_symbol(m, operators[i].value.name) + } else { operators[i].value.name }; + let key_r = if let Some(m) = operators[i+1].value.module { + qualified_symbol(m, operators[i+1].value.name) + } else { operators[i+1].value.name }; + let (assoc_l, prec_l) = self.type_fixities.get(&key_l) + .or_else(|| 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) + let (assoc_r, prec_r) = self.type_fixities.get(&key_r) + .or_else(|| 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 { @@ -2483,7 +2496,14 @@ impl Converter { // 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() { + // For qualified operators (e.g., Hooks.<>), look up the qualified key first + let op_key = if let Some(m) = op_ref.value.module { + qualified_symbol(m, op_ref.value.name) + } else { + op_ref.value.name + }; + let target = match self.type_operators.get(&op_key).copied() + .or_else(|| self.type_operators.get(&op_ref.value.name).copied()) { Some(t) => t, None => { self.errors.push(TypeError::UndefinedVariable { @@ -2500,7 +2520,8 @@ impl Converter { name: target_qi, definition_site: def_site, }; - let (assoc, prec) = self.type_fixities.get(&op_ref.value.name) + let (assoc, prec) = self.type_fixities.get(&op_key) + .or_else(|| self.type_fixities.get(&op_ref.value.name)) .copied().unwrap_or((Associativity::Left, 9)); (ctor, op_ref.span, assoc, prec) }).collect(); @@ -2569,6 +2590,15 @@ impl Converter { span: *span, value: *value, }, + cst::TypeExpr::ArrayPattern { span, elements } => TypeExpr::ArrayPattern { + span: *span, + elements: elements.iter().map(|e| self.convert_type_expr(e)).collect(), + }, + cst::TypeExpr::AsPattern { span, name, ty } => TypeExpr::AsPattern { + span: *span, + name: name.clone(), + ty: Box::new(self.convert_type_expr(ty)), + }, } } @@ -2635,45 +2665,130 @@ impl Converter { 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, - }); + // Flatten right-recursive binder Op chain into operands and operators + let mut operands: Vec<&cst::Binder> = vec![left.as_ref()]; + let mut operators: Vec<&cst::Spanned> = vec![op]; + let mut current: &cst::Binder = right.as_ref(); + loop { + match current { + cst::Binder::Op { left: rl, op: rop, right: rr, .. } => { + operands.push(rl.as_ref()); + operators.push(rop); + current = rr.as_ref(); + } + _ => break, + } } - // 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, + operands.push(current); + + // Check for function aliases and resolve targets for each operator + struct ResolvedBinderOp { + target: QualifiedIdent, + #[allow(dead_code)] + op_span: Span, + def_site: DefinitionSite, + assoc: Associativity, + prec: u8, + } + let resolved_ops: Vec = operators.iter().map(|op_ref| { + let binder_op_key = if let Some(m) = op_ref.value.module { + qualified_symbol(m, op_ref.value.name) + } else { + op_ref.value.name + }; + // Check function alias + if self.function_op_aliases.contains(&binder_op_key) + || self.function_op_aliases.contains(&op_ref.value.name) { + self.errors.push(TypeError::InvalidOperatorInBinder { + span: op_ref.span, + op: op_ref.value.name, }); - op.value } - }; - // Propagate module qualifier (e.g. A.: → A.Cons) - if target_name.module.is_none() { - target_name.module = op.value.module; + // Resolve target + let mut target_name = match self.value_operator_targets.get(&binder_op_key) + .or_else(|| self.value_operator_targets.get(&op_ref.value.name)) { + Some(target) => *target, + None => { + self.errors.push(TypeError::UndefinedVariable { + span: op_ref.span, + name: op_ref.value.name, + }); + op_ref.value + } + }; + if target_name.module.is_none() { + target_name.module = op_ref.value.module; + } + let def_site = self.resolve_operator_target(target_name.name, op_ref.span); + let (assoc, prec) = self.value_fixities.get(&binder_op_key) + .or_else(|| self.value_fixities.get(&op_ref.value.name)) + .copied().unwrap_or((Associativity::Left, 9)); + ResolvedBinderOp { target: target_name, op_span: op_ref.span, def_site, assoc, prec } + }).collect(); + + // Convert all operands + let ast_operands: Vec = operands.iter().map(|b| self.convert_binder(b)).collect(); + + // Single operator: fast path (no rebalancing needed) + if resolved_ops.len() == 1 { + let op0 = &resolved_ops[0]; + let mut iter = ast_operands.into_iter(); + let left_b = iter.next().unwrap(); + let right_b = iter.next().unwrap(); + return Binder::Constructor { + span: *span, + name: op0.target, + args: vec![left_b, right_b], + definition_site: op0.def_site.clone(), + }; } - 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, + + // Shunting-yard: rebalance based on fixity + let mut output: Vec = Vec::new(); + let mut op_stack: Vec = Vec::new(); + let mut operand_iter = ast_operands.into_iter(); + output.push(operand_iter.next().unwrap()); + + for (i, rop) in resolved_ops.iter().enumerate() { + while let Some(&top_idx) = op_stack.last() { + let top = &resolved_ops[top_idx]; + let should_pop = match rop.assoc { + Associativity::Left => rop.prec <= top.prec, + Associativity::Right => rop.prec < top.prec, + Associativity::None => rop.prec <= top.prec, + }; + if should_pop { + op_stack.pop(); + let right_b = output.pop().unwrap(); + let left_b = output.pop().unwrap(); + output.push(Binder::Constructor { + span: *span, + name: top.target, + args: vec![left_b, right_b], + definition_site: top.def_site.clone(), + }); + } else { + break; + } + } + op_stack.push(i); + output.push(operand_iter.next().unwrap()); + } + + // Pop remaining operators + while let Some(top_idx) = op_stack.pop() { + let top = &resolved_ops[top_idx]; + let right_b = output.pop().unwrap(); + let left_b = output.pop().unwrap(); + output.push(Binder::Constructor { + span: *span, + name: top.target, + args: vec![left_b, right_b], + definition_site: top.def_site.clone(), + }); } + + output.pop().unwrap() } cst::Binder::Typed { span, binder, ty } => Binder::Typed { span: *span, diff --git a/src/build/mod.rs b/src/build/mod.rs index d39874e9..55ba6b61 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -20,7 +20,6 @@ use crate::js_ffi; use crate::typechecker::check; use crate::typechecker::registry::ModuleRegistry; use crate::typechecker::error::TypeError; -use crate::typechecker::types::Type; pub use error::BuildError; @@ -37,6 +36,14 @@ pub struct BuildOptions { /// Output directory for generated JavaScript files. /// `None` means skip codegen. `Some(path)` writes JS to `path//index.js`. pub output_dir: Option, + + /// If true, typecheck modules sequentially (one at a time) instead of in + /// parallel. Useful for debugging memory issues or non-deterministic bugs. + pub sequential: bool, + + /// If true, stop building as soon as the first error is encountered + /// (build error or type error). Useful for quick iteration. + pub fail_fast: bool, } // ===== Public types ===== @@ -44,7 +51,6 @@ pub struct BuildOptions { pub struct ModuleResult { pub path: PathBuf, pub module_name: String, - pub types: HashMap, pub type_errors: Vec, } @@ -74,6 +80,44 @@ fn is_prim_import(parts: &[Symbol]) -> bool { !parts.is_empty() && interner::symbol_eq(parts[0], "Prim") } +/// Handle a typecheck panic (timeout or other) by recording the appropriate build error. +fn handle_typecheck_panic( + build_errors: &mut Vec, + pm: &ParsedModule, + payload: Box, + elapsed: std::time::Duration, + done: usize, + total_modules: usize, + timeout: Option, +) { + 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 + ); + build_errors.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 + ); + build_errors.push(BuildError::TypecheckPanic { + path: pm.path.clone(), + module_name: pm.module_name.clone(), + }); + } +} + /// Extract the names of all `foreign import` declarations from a module. fn extract_foreign_import_names(module: &Module) -> Vec { module @@ -97,30 +141,26 @@ pub fn build(globs: &[&str], output_dir: Option) -> BuildResult { let mut build_errors = Vec::new(); // Phase 1: Glob resolution - log::info!("Phase 1: Resolving glob patterns: {:?}", globs); + log::debug!("Phase 1: Resolving glob patterns: {:?}", globs); let phase_start = Instant::now(); let paths = resolve_globs(globs, &mut build_errors); - log::info!( + log::debug!( "Phase 1 complete: found {} files in {:.2?}", paths.len(), phase_start.elapsed() ); - for path in &paths { - log::info!(" discovered: {}", path.display()); - } - // Phase 2: Read and parse - log::info!("Phase 2a: Reading source files"); + log::debug!("Phase 2a: Reading source files"); let phase_start = Instant::now(); let mut sources = Vec::new(); for path in &paths { match std::fs::read_to_string(path) { Ok(source) => { - log::info!(" read {} ({} bytes)", path.display(), source.len()); + log::debug!(" read {} ({} bytes)", path.display(), source.len()); sources.push((path.to_string_lossy().into_owned(), source)); } Err(e) => { - log::info!(" failed to read {}: {}", path.display(), e); + log::debug!(" failed to read {}: {}", path.display(), e); build_errors.push(BuildError::FileReadError { path: path.clone(), error: e.to_string(), @@ -128,25 +168,25 @@ pub fn build(globs: &[&str], output_dir: Option) -> BuildResult { } } } - log::info!( + log::debug!( "Phase 2a complete: read {} source files in {:.2?}", sources.len(), phase_start.elapsed() ); - log::info!("Phase 2b: Scanning for FFI companion .js files"); + log::debug!("Phase 2b: Scanning for FFI companion .js files"); let mut js_sources: HashMap = HashMap::new(); for (path_str, _) in &sources { let purs_path = PathBuf::from(path_str); let js_path = purs_path.with_extension("js"); if js_path.exists() { if let Ok(js_source) = std::fs::read_to_string(&js_path) { - log::info!(" found FFI companion: {}", js_path.display()); + log::debug!(" found FFI companion: {}", js_path.display()); js_sources.insert(path_str.clone(), js_source); } } } - log::info!( + log::debug!( "Phase 2b complete: found {} FFI companion files", js_sources.len() ); @@ -171,7 +211,7 @@ pub fn build(globs: &[&str], output_dir: Option) -> BuildResult { build_errors.append(&mut result.build_errors); result.build_errors = build_errors; - log::info!("Build finished in {:.2?}", build_start.elapsed()); + log::debug!("Build finished in {:.2?}", build_start.elapsed()); result } @@ -208,9 +248,10 @@ pub fn build_from_sources_with_options( ) -> (BuildResult, ModuleRegistry) { let pipeline_start = Instant::now(); let mut build_errors = Vec::new(); + let fail_fast = options.fail_fast; // Phase 2: Parse all sources (parallel) - log::info!("Phase 2c: Parsing {} source files", sources.len()); + log::debug!("Phase 2c: Parsing {} source files", sources.len()); let phase_start = Instant::now(); // Parse all sources in parallel @@ -233,6 +274,7 @@ pub fn build_from_sources_with_options( let (path, module) = match result { Ok(pair) => pair, Err(e) => { + log::debug!("Build error:\n{e}"); build_errors.push(e); continue; } @@ -246,7 +288,7 @@ pub fn build_from_sources_with_options( if !module_parts.is_empty() { let is_prim = interner::with_resolved(module_parts[0], |s| s == "Prim").unwrap_or(false); if is_prim { - log::info!(" rejected {}: Prim namespace is reserved", module_name); + log::debug!(" rejected {}: Prim namespace is reserved", module_name); build_errors.push(BuildError::CannotDefinePrimModules { module_name, path }); continue; } @@ -259,7 +301,7 @@ pub fn build_from_sources_with_options( s.chars().find(|&c| c == '\'' || c == '_') }).flatten(); if let Some(c) = invalid_char { - log::info!( + log::debug!( " rejected {}: invalid character '{}' in module name", module_name, c @@ -279,7 +321,7 @@ pub fn build_from_sources_with_options( // Check for duplicate module names if let Some(existing_path) = seen_modules.get(&module_parts) { - log::info!( + log::debug!( " rejected {}: duplicate (already at {})", module_name, existing_path.display() @@ -315,22 +357,30 @@ pub fn build_from_sources_with_options( js_source, }); } - log::info!( + log::debug!( "Phase 2c complete: parsed {} modules (rejected {}) in {:.2?}", parsed.len(), sources.len() - parsed.len(), phase_start.elapsed() ); + if fail_fast && !build_errors.is_empty() { + let registry = match start_registry { + Some(base) => ModuleRegistry::with_base(base), + None => ModuleRegistry::default(), + }; + return (BuildResult { modules: Vec::new(), build_errors }, registry); + } + // Phase 3: Build dependency graph and check for unknown imports - log::info!("Phase 3: Building dependency graph"); + log::debug!("Phase 3: Building dependency graph"); let phase_start = Instant::now(); let known_modules: HashSet> = parsed.iter().map(|p| p.module_parts.clone()).collect(); let mut registry = match start_registry { Some(base) => { - log::info!(" using base registry from support packages"); + log::debug!(" using base registry from support packages"); ModuleRegistry::with_base(base) } None => ModuleRegistry::default(), @@ -340,7 +390,7 @@ pub fn build_from_sources_with_options( for imp_parts in &pm.import_parts { let imp_name = module_name_string(imp_parts); if !known_modules.contains(imp_parts) && !registry.contains(imp_parts) { - log::info!( + log::debug!( " missing import: {} imports {} (not found)", pm.module_name, imp_name @@ -351,8 +401,6 @@ pub fn build_from_sources_with_options( path: pm.path.clone(), span: pm.module.span, }); - } else { - log::info!(" resolved import: {} -> {}", pm.module_name, imp_name); } } } @@ -365,11 +413,11 @@ pub fn build_from_sources_with_options( .collect(); // Topological sort (Kahn's algorithm) - log::info!("Phase 3b: Topological sort of {} modules", parsed.len()); + log::debug!("Phase 3b: Topological sort of {} modules", parsed.len()); let levels: Vec> = match topological_sort_levels(&parsed, &module_index) { Ok(levels) => { - log::info!(" {} dependency levels for parallel build", levels.len()); + log::debug!(" {} dependency levels for parallel build", levels.len()); levels } Err(cycle_indices) => { @@ -377,7 +425,7 @@ pub fn build_from_sources_with_options( .iter() .map(|&i| (parsed[i].module_name.clone(), parsed[i].path.clone())) .collect(); - log::info!( + log::debug!( " cycle detected among: {:?}", cycle_names .iter() @@ -393,26 +441,35 @@ pub fn build_from_sources_with_options( } } }; - log::info!( + log::debug!( "Phase 3 complete: dependency graph built in {:.2?}", phase_start.elapsed() ); - // Phase 4: Typecheck in dependency order (parallel within each level) + if fail_fast && !build_errors.is_empty() { + log::debug!("Phase 3 failed"); + return (BuildResult { modules: Vec::new(), build_errors }, registry); + } + + // Phase 4: Typecheck in dependency order let total_modules: usize = levels.iter().map(|l| l.len()).sum(); - log::info!( - "Phase 4: Typechecking {} modules ({} levels, parallel within levels)", + let sequential = options.sequential; + log::debug!( + "Phase 4: Typechecking {} modules ({} levels, {})", total_modules, levels.len(), + if sequential { "sequential" } else { "parallel within levels" }, ); let phase_start = Instant::now(); let timeout = options.module_timeout; let mut module_results = Vec::new(); // Build a rayon thread pool with large stacks for deep recursion in the typechecker. - let num_threads = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1); + let num_threads = if sequential { 1 } else { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + }; let pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .stack_size(16 * 1024 * 1024) @@ -420,21 +477,25 @@ pub fn build_from_sources_with_options( .expect("failed to build rayon thread pool"); // Scale wall-clock deadline to account for resource contention under parallel // execution (interner mutex, CPU cache pressure, memory bandwidth). - let parallel_timeout = timeout.map(|t| t * 3); - log::info!(" using {} worker threads (deadline {}s)", num_threads, - parallel_timeout.map(|t| t.as_secs()).unwrap_or(0)); + // In sequential mode, use the raw timeout since there's no contention. + let effective_timeout = if sequential { timeout } else { timeout.map(|t| t * 3) }; + log::debug!(" using {} worker threads (deadline {}s)", num_threads, + effective_timeout.map(|t| t.as_secs()).unwrap_or(0)); let mut done = 0usize; for level in &levels { - // Typecheck all modules in this level in parallel - let level_results: Vec<_> = pool.install(|| { - level.par_iter().map(|&idx| { + if sequential { + // Sequential mode: process each module inline so that each CheckResult + // (including ModuleExports) is dropped before the next module starts. + // Peak memory = 1 module's CheckResult at a time. + for &idx in level { let pm = &parsed[idx]; let tc_start = Instant::now(); - let deadline = parallel_timeout.map(|t| tc_start + t); + let deadline = effective_timeout.map(|t| tc_start + t); let check_result = std::panic::catch_unwind(AssertUnwindSafe(|| { let mod_sym = crate::interner::intern(&pm.module_name); + log::debug!("Typechecking: {}", &pm.module_name); let path_str = pm.path.to_string_lossy(); crate::typechecker::set_deadline(deadline, mod_sym, &path_str); let (ast_module, convert_errors) = crate::ast::convert(&pm.module, ®istry); @@ -447,69 +508,100 @@ pub fn build_from_sources_with_options( crate::typechecker::set_deadline(None, mod_sym, ""); result })); - (idx, check_result, tc_start.elapsed()) - }).collect() - }); - - // Register results sequentially (registry needs &mut) - for (idx, check_result, elapsed) in level_results { - let pm = &parsed[idx]; - done += 1; - match check_result { - Ok(result) => { - log::info!( - " [{}/{}] ok: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed - ); - registry.register(&pm.module_parts, result.exports); - module_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::info!( - " [{}/{}] timeout: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed + let elapsed = tc_start.elapsed(); + done += 1; + match check_result { + Ok(result) => { + log::debug!( + " [{}/{}] ok: {} ({:.2?})", + done, total_modules, pm.module_name, elapsed ); - build_errors.push(BuildError::TypecheckTimeout { + // Register exports immediately — result.exports is moved, + // then result (with its types HashMap) is dropped. + registry.register(&pm.module_parts, result.exports); + module_results.push(ModuleResult { path: pm.path.clone(), module_name: pm.module_name.clone(), - timeout_secs: timeout.unwrap().as_secs(), + type_errors: result.errors, }); - } else { - log::info!( - " [{}/{}] panic: {} ({:.2?})", - done, - total_modules, - pm.module_name, - elapsed + } + Err(payload) => { + handle_typecheck_panic( + &mut build_errors, pm, payload, elapsed, + done, total_modules, timeout, + ); + } + } + // In sequential mode, check fail_fast after each module + if fail_fast { + let has_errors = module_results.last().map_or(false, |r| !r.type_errors.is_empty()) || !build_errors.is_empty(); + if has_errors { + log::debug!("Phase 4: fail_fast triggered after module, stopping"); + break; + } + } + } + } else { + // Parallel mode: collect all results for the level, then register sequentially. + let level_results: Vec<_> = pool.install(|| { + level.par_iter().map(|&idx| { + let pm = &parsed[idx]; + let tc_start = Instant::now(); + let deadline = effective_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); + let (ast_module, convert_errors) = crate::ast::convert(&pm.module, ®istry); + let mut result = check::check_module(&ast_module, ®istry); + if !convert_errors.is_empty() { + let mut all_errors = convert_errors; + all_errors.extend(result.errors); + result.errors = all_errors; + } + crate::typechecker::set_deadline(None, mod_sym, ""); + result + })); + (idx, check_result, tc_start.elapsed()) + }).collect() + }); + + // Register results sequentially (registry needs &mut) + for (idx, check_result, elapsed) in level_results { + let pm = &parsed[idx]; + done += 1; + match check_result { + Ok(result) => { + log::debug!( + " [{}/{}] ok: {} ({:.2?})", + done, total_modules, pm.module_name, elapsed ); - build_errors.push(BuildError::TypecheckPanic { + registry.register(&pm.module_parts, result.exports); + module_results.push(ModuleResult { path: pm.path.clone(), module_name: pm.module_name.clone(), + type_errors: result.errors, }); } + Err(payload) => { + handle_typecheck_panic( + &mut build_errors, pm, payload, elapsed, + done, total_modules, timeout, + ); + } } } } + // After each dependency level, check if fail_fast should stop + if fail_fast { + let err_count = module_results.iter().filter(|r| !r.type_errors.is_empty()).count(); + if !build_errors.is_empty() || err_count > 0 { + log::debug!("Phase 4: fail_fast triggered after level ({} done, {} with errors), stopping", done, err_count); + break; + } + } } - log::info!( + log::debug!( "Phase 4 complete: typechecked {} modules in {:.2?}", module_results.len(), phase_start.elapsed() @@ -517,7 +609,7 @@ pub fn build_from_sources_with_options( // Phase 5: FFI validation (only when JS sources were provided) if js_sources.is_some() { - log::info!("Phase 5: FFI validation"); + log::debug!("Phase 5: FFI validation"); let phase_start = Instant::now(); let mut ffi_checked = 0; for pm in &parsed { @@ -526,7 +618,7 @@ pub fn build_from_sources_with_options( match (&pm.js_source, has_foreign) { (Some(js_src), _) => { - log::info!( + log::debug!( " validating FFI for {} ({} foreign imports)", pm.module_name, foreign_names.len() @@ -536,12 +628,12 @@ pub fn build_from_sources_with_options( Ok(info) => { let ffi_errors = js_ffi::validate_foreign_module(&foreign_names, &info); if ffi_errors.is_empty() { - log::info!(" FFI OK for {}", pm.module_name); + log::debug!(" FFI OK for {}", pm.module_name); } for err in ffi_errors { match err { js_ffi::FfiError::DeprecatedFFICommonJSModule => { - log::info!( + log::debug!( " FFI error in {}: deprecated CommonJS module", pm.module_name ); @@ -553,7 +645,7 @@ pub fn build_from_sources_with_options( ); } js_ffi::FfiError::MissingFFIImplementations { missing } => { - log::info!( + log::debug!( " FFI error in {}: missing implementations: {:?}", pm.module_name, missing @@ -565,7 +657,7 @@ pub fn build_from_sources_with_options( }); } js_ffi::FfiError::UnusedFFIImplementations { unused } => { - log::info!( + log::debug!( " FFI error in {}: unused implementations: {:?}", pm.module_name, unused @@ -595,7 +687,7 @@ pub fn build_from_sources_with_options( ); } js_ffi::FfiError::ParseError { message } => { - log::info!( + log::debug!( " FFI parse error in {}: {}", pm.module_name, message @@ -610,7 +702,7 @@ pub fn build_from_sources_with_options( } } Err(msg) => { - log::info!(" FFI parse error in {}: {}", pm.module_name, msg); + log::debug!(" FFI parse error in {}: {}", pm.module_name, msg); build_errors.push(BuildError::FFIParseError { module_name: pm.module_name.clone(), path: pm.path.clone(), @@ -620,7 +712,7 @@ pub fn build_from_sources_with_options( } } (None, true) => { - log::info!( + log::debug!( " missing FFI companion for {} ({} foreign imports)", pm.module_name, foreign_names.len() @@ -633,7 +725,7 @@ pub fn build_from_sources_with_options( (None, false) => {} } } - log::info!( + log::debug!( "Phase 5 complete: validated {} FFI modules in {:.2?}", ffi_checked, phase_start.elapsed() @@ -642,7 +734,7 @@ pub fn build_from_sources_with_options( // Phase 6: Code generation (only when output_dir is specified) if let Some(ref output_dir) = options.output_dir { - log::info!("Phase 6: JavaScript code generation to {}", output_dir.display()); + log::debug!("Phase 6: JavaScript code generation to {}", output_dir.display()); let phase_start = Instant::now(); let mut codegen_count = 0; @@ -655,7 +747,7 @@ pub fn build_from_sources_with_options( for pm in &parsed { if !ok_modules.contains(&pm.module_name) { - log::info!(" skipping {} (has type errors)", pm.module_name); + log::debug!(" skipping {} (has type errors)", pm.module_name); continue; } @@ -663,14 +755,14 @@ pub fn build_from_sources_with_options( let module_exports = match registry.lookup(&pm.module_parts) { Some(exports) => exports, None => { - log::info!(" skipping {} (no exports in registry)", pm.module_name); + log::debug!(" skipping {} (no exports in registry)", pm.module_name); continue; } }; let has_ffi = pm.js_source.is_some(); - log::info!(" generating JS for {}", pm.module_name); + log::debug!(" generating JS for {}", pm.module_name); let js_module = crate::codegen::js::module_to_js( &pm.module, &pm.module_name, @@ -685,7 +777,7 @@ pub fn build_from_sources_with_options( // Write output//index.js let module_dir = output_dir.join(&pm.module_name); if let Err(e) = std::fs::create_dir_all(&module_dir) { - log::info!(" failed to create dir {}: {}", module_dir.display(), e); + log::debug!(" failed to create dir {}: {}", module_dir.display(), e); build_errors.push(BuildError::FileReadError { path: module_dir.clone(), error: format!("Failed to create output directory: {e}"), @@ -695,40 +787,40 @@ pub fn build_from_sources_with_options( let index_path = module_dir.join("index.js"); if let Err(e) = std::fs::write(&index_path, &js_text) { - log::info!(" failed to write {}: {}", index_path.display(), e); + log::debug!(" failed to write {}: {}", index_path.display(), e); build_errors.push(BuildError::FileReadError { path: index_path, error: format!("Failed to write JS output: {e}"), }); continue; } - log::info!(" wrote {} ({} bytes)", index_path.display(), js_text.len()); + log::debug!(" wrote {} ({} bytes)", index_path.display(), js_text.len()); // Copy FFI companion file if let Some(ref js_src) = pm.js_source { let foreign_path = module_dir.join("foreign.js"); if let Err(e) = std::fs::write(&foreign_path, js_src) { - log::info!(" failed to write {}: {}", foreign_path.display(), e); + log::debug!(" failed to write {}: {}", foreign_path.display(), e); build_errors.push(BuildError::FileReadError { path: foreign_path, error: format!("Failed to write foreign JS: {e}"), }); continue; } - log::info!(" copied foreign.js for {}", pm.module_name); + log::debug!(" copied foreign.js for {}", pm.module_name); } codegen_count += 1; } - log::info!( + log::debug!( "Phase 6 complete: generated JS for {} modules in {:.2?}", codegen_count, phase_start.elapsed() ); } - log::info!( + log::debug!( "Build pipeline finished in {:.2?} ({} modules, {} errors)", pipeline_start.elapsed(), module_results.len(), @@ -924,10 +1016,6 @@ mod tests { assert_eq!(result.modules[1].module_name, "B"); assert!(result.modules[0].type_errors.is_empty()); assert!(result.modules[1].type_errors.is_empty()); - // B should have y :: Int - let y_sym = interner::intern("y"); - let y_ty = result.modules[1].types.get(&y_sym).expect("y not found"); - assert_eq!(*y_ty, Type::int()); } #[test] diff --git a/src/cst.rs b/src/cst.rs index bf8a2d08..6dec05f2 100644 --- a/src/cst.rs +++ b/src/cst.rs @@ -688,6 +688,15 @@ pub enum TypeExpr { /// Type-level integer literal: 42 IntLiteral { span: Span, value: i64 }, + + /// Array pattern parsed in type context (for as-patterns via VTA): + /// `name@{ field: [a, b] }` parses the `[a, b]` as this variant. + /// Only meaningful when converted back to a binder via type_to_binder. + ArrayPattern { span: Span, elements: Vec }, + + /// As-pattern parsed in type context (for nested as-patterns in VTA): + /// `name@{ user: x@{ id } }` parses `x@{ id }` as this variant. + AsPattern { span: Span, name: Spanned, ty: Box }, } /// Type constraint (for type classes) @@ -864,17 +873,42 @@ pub fn expr_to_binder(expr: Expr) -> Result { }) } Expr::VisibleTypeApp { span, func, ty } => { - let name_ident = match *func { + match *func { Expr::Var { name: qi, span: ns, .. - } => Spanned::new(qi.name, ns), - _ => return Err(format!("expected variable name in as-pattern")), - }; - Ok(Binder::As { - span, - name: name_ident, - binder: Box::new(type_to_binder(ty)?), - }) + } => { + // Simple as-pattern: name@pattern + Ok(Binder::As { + span, + name: Spanned::new(qi.name, ns), + binder: Box::new(type_to_binder(ty)?), + }) + } + Expr::App { func: ctor_func, arg: last_arg, .. } => { + // Constructor application with as-pattern on last arg: + // (Constructor arg1 lastArg@{ pattern }) parsed as + // VisibleTypeApp(App(App(Con, arg1), Var(lastArg)), RecordType) + // Convert last arg to as-pattern binder + let as_name = match *last_arg { + Expr::Var { name: qi, span: ns, .. } => Spanned::new(qi.name, ns), + _ => return Err(format!("expected variable name in as-pattern")), + }; + let as_binder = Binder::As { + span, + name: as_name, + binder: Box::new(type_to_binder(ty)?), + }; + // Convert the constructor application head to a binder, then add the as-pattern arg + match expr_to_binder(*ctor_func)? { + Binder::Constructor { name, mut args, .. } => { + args.push(as_binder); + Ok(Binder::Constructor { span, name, args }) + } + _ => Err(format!("expected constructor application in as-pattern")), + } + } + _ => Err(format!("expected variable name in as-pattern")), + } } Expr::Wildcard { span } => Ok(Binder::Wildcard { span }), _other => Err(format!("expression cannot be used as a binder")), @@ -948,10 +982,57 @@ pub fn type_to_binder(ty: TypeExpr) -> Result { op, right: Box::new(type_to_binder(*right)?), }), + TypeExpr::ArrayPattern { span, elements } => { + let binders: Result, String> = elements.into_iter().map(type_to_binder).collect(); + Ok(Binder::Array { span, elements: binders? }) + } + TypeExpr::AsPattern { span, name, ty } => { + let binder = type_to_binder(*ty)?; + Ok(Binder::As { span, name, binder: Box::new(binder) }) + } _ => Err(format!("type expression cannot be used as a binder")), } } +/// Convert an expression to a type expression (for VTA reinterpretation). +/// When the parser produces `AsPattern(name, arg)` in expression context, +/// the typechecker needs to convert `arg` to a `TypeExpr` for visible type application. +pub fn expr_to_type_expr(expr: &Expr) -> Result { + match expr { + Expr::Var { span, name } => Ok(TypeExpr::Var { + span: *span, + name: Spanned::new(name.name, *span), + }), + Expr::Constructor { span, name } => Ok(TypeExpr::Constructor { + span: *span, + name: name.clone(), + }), + Expr::App { span, func, arg } => Ok(TypeExpr::App { + span: *span, + constructor: Box::new(expr_to_type_expr(func)?), + arg: Box::new(expr_to_type_expr(arg)?), + }), + Expr::Parens { span, expr } => Ok(TypeExpr::Parens { + span: *span, + ty: Box::new(expr_to_type_expr(expr)?), + }), + Expr::Hole { span, name } => Ok(TypeExpr::Hole { + span: *span, + name: *name, + }), + Expr::Wildcard { span } => Ok(TypeExpr::Wildcard { span: *span }), + Expr::Literal { span, lit: Literal::String(s) } => Ok(TypeExpr::StringLiteral { + span: *span, + value: s.clone(), + }), + Expr::Literal { span, lit: Literal::Int(n) } => Ok(TypeExpr::IntLiteral { + span: *span, + value: *n, + }), + _ => Err(format!("expression cannot be converted to type")), + } +} + /// Helper type for values with spans #[derive(Debug, Clone, PartialEq)] pub struct Spanned { @@ -1048,7 +1129,9 @@ impl TypeExpr { | TypeExpr::TypeOp { span, .. } | TypeExpr::Kinded { span, .. } | TypeExpr::StringLiteral { span, .. } - | TypeExpr::IntLiteral { span, .. } => *span, + | TypeExpr::IntLiteral { span, .. } + | TypeExpr::ArrayPattern { span, .. } + | TypeExpr::AsPattern { span, .. } => *span, } } } diff --git a/src/lexer/layout.rs b/src/lexer/layout.rs index 7f396ae1..79a316d0 100644 --- a/src/lexer/layout.rs +++ b/src/lexer/layout.rs @@ -305,13 +305,14 @@ pub fn process_layout(raw_tokens: Vec<(RawToken, Span)>, source: &str) -> Vec expr \n >>> f` = `(case _ of P -> expr) >>> f` + // e.g. `tryRethrow do \n ... \n <|> pure ""` = `(tryRethrow do { ... }) <|> pure ""` // 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 { + if matches!(delim, LayoutDelim::LytOf | LayoutDelim::LytDo | LayoutDelim::LytAdo) && is_operator_token && !last_was_operator { result.push((Token::RBrace, dummy_span)); stack.pop(); // Continue loop to check enclosing blocks @@ -329,7 +330,7 @@ pub fn process_layout(raw_tokens: Vec<(RawToken, Span)>, source: &str) -> Vec) -> Vec { break; } // Contextual keywords used as identifiers after module qualifier - Token::As | Token::Hiding => { + Token::As | Token::Hiding | Token::Export => { let kw_str = match &tokens[j + 1].0 { Token::As => "as", Token::Hiding => "hiding", + Token::Export => "export", _ => unreachable!(), }; let name = interner::intern(kw_str); diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 9adf727b..24741c18 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -112,6 +112,8 @@ Export: Export = { "type" "(" ":" ")" => Export::TypeOp(crate::interner::intern(":")), "type" "(" "~" ")" => Export::TypeOp(crate::interner::intern("~")), "as" => Export::Value(crate::interner::intern("as")), + "export" => Export::Value(crate::interner::intern("export")), + "hiding" => Export::Value(crate::interner::intern("hiding")), }; DataMembersOpt: Option = { @@ -331,6 +333,15 @@ GuardAppExpr: Expr = { arg: Box::new(arg), } }, + // Visible type application in guard context: expr @Type + // Uses AtomicTypeExprBase to avoid shift/reduce conflict with AsPattern's @ + "@" => { + Expr::VisibleTypeApp { + span: Span::new(start, end), + func: Box::new(func), + ty: arg, + } + }, }; // Guard item: either boolean condition or pattern bind (GuardExpr <- GuardExpr) @@ -977,8 +988,9 @@ AppExpr: Expr = { arg: Box::new(arg), } }, - // Visible type application / as-pattern: expr @arg - "@" => { + // Visible type application: expr @Type + // Uses AtomicTypeExprBase to avoid shift/reduce conflict with AsPattern's @ + "@" => { Expr::VisibleTypeApp { span: Span::new(start, end), func: Box::new(func), @@ -1937,7 +1949,22 @@ TypeAppExpr: TypeExpr = { } }; +// AtomicTypeExpr includes AsPattern for nested as-patterns in type context. +// VTA rules use AtomicTypeExprBase instead to avoid @-ambiguity. AtomicTypeExpr: TypeExpr = { + => <>, + // As-pattern in type context (for nested as-patterns in VTA): + // `name@{ user: x@{ id } }` parses `x@{ id }` as AsPattern. + "@" => { + TypeExpr::AsPattern { + span: Span::new(start, end), + name, + ty: Box::new(ty), + } + }, +}; + +AtomicTypeExprBase: TypeExpr = { => { TypeExpr::Var { span: Span::new(start, end), @@ -2041,6 +2068,14 @@ AtomicTypeExpr: TypeExpr = { is_record: false, } }, + // Array pattern in type context (for as-patterns via VTA): + // `name@{ field: [a, b] }` parses the `[a, b]` as ArrayPattern. + "[" > "]" => { + TypeExpr::ArrayPattern { + span: Span::new(start, end), + elements, + } + }, // Type keyword as type constructor (for kinds like `Row Type`) "type" => { TypeExpr::Constructor { @@ -2144,6 +2179,16 @@ TypeField: TypeField = { ty, } }, + // Single ":" variant for as-pattern records parsed as types in @ context: + // `state@{ context: { eventId } }` parses as VisibleTypeApp with RecordType + // where fields use ":" (binder syntax) instead of "::" (type syntax). + ":" => { + TypeField { + span: Span::new(start, end), + label, + ty, + } + }, // Shorthand: just a label without :: (for as-pattern records parsed as types in @ context) => { TypeField { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d80de3f7..209e7f0c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1458,6 +1458,105 @@ unexpected_token ); } + #[test] + fn test_parse_as_pattern_record() { + // as-pattern in record binder field: { payload: p@{ x } } + let source = "module Test where\ntest { payload: p@{ x } } = p\n"; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse as-pattern in record: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_toplevel() { + // as-pattern at top-level binder: f c@{ x } = c + let source = "module Test where\ntest c@{ x } = c\n"; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse as-pattern: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_nested_record() { + // nested record field with as-pattern: { payload: payload@{ item_name, pendingUpgrade } } + let source = "module Test where\nhandlerProgram { payload: payload@{ item_name, pendingUpgrade }, params: { eventId }, userId, email } = userId\n"; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse nested as-pattern: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_constructor_record() { + // Constructor wrapping as-pattern: (Client client@{ vat_code }) + let source = "module Test where\ntest (Client client@{ vat_code }) = client\n"; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse ctor as-pattern: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_in_do_bind_simple() { + // Simple as-pattern in do-bind + let source = r#"module Test where +test = do + c@{ x } <- f y + pure c +"#; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse simple as-pattern in do-bind: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_in_do_bind_ctor() { + // as-pattern with constructor wrapper in do-bind + let source = r#"module Test where +test = do + (Client c@{ x }) <- f y + pure c +"#; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse ctor as-pattern in do-bind: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_in_guard() { + // as-pattern inside a case guard: | Just sm@{ sessionId } <- expr -> + let source = r#"module Test where +test x + | Just sm@{ sessionId } <- getModel x = sm + | otherwise = x +"#; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse as-pattern in guard: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_guard_record_colon() { + // as-pattern with : field separator in guard pattern + let source = r#"module Test where +test x + | Just sm@{ sessionId: Just sid } <- getModel x = sid + | otherwise = x +"#; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse as-pattern with : in guard: {:?}", result.err()); + } + + #[test] + fn test_parse_as_pattern_complex() { + // Complex case from OA codebase + let source = r#"module Test where +handlerProgram { payload: payload@{ item_name, pendingUpgrade }, params: { eventId }, userId, email } = do + let x = payload.amount + pure x +"#; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse complex as-pattern: {:?}", result.err()); + } + + #[test] + fn test_parse_export_as_value() { + let source = "module Test (export) where\nexport = 42\n"; + let result = parse(source); + assert!(result.is_ok(), "Failed to parse 'export' as value: {:?}", result.err()); + } + // ===== Error Cases ===== #[test] diff --git a/src/typechecker/check.rs b/src/typechecker/check.rs index 480bb4a7..1bbbf620 100644 --- a/src/typechecker/check.rs +++ b/src/typechecker/check.rs @@ -310,6 +310,9 @@ fn is_non_nominal_instance_head( /// Like `is_non_nominal_instance_head`, but only rejects open records — /// function types are allowed in instance heads (e.g. `Fn1 a b = a -> b`). +/// Only triggers when the alias body is DIRECTLY a Record type (e.g. `type X r = {x :: Int | r}`), +/// not when it becomes a Record through further row-alias expansion (e.g. `type Links r = A + B + r`). +/// This avoids false positives for row-kind type aliases used as class parameters. fn is_non_nominal_instance_head_record_only( ty: &Type, type_aliases: &HashMap, Type)>, @@ -317,11 +320,19 @@ fn is_non_nominal_instance_head_record_only( 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, + // Extract the synonym name from the head + fn get_head_name(ty: &Type) -> Option { + match ty { + Type::Con(name) => Some(name.name), + Type::App(f, _) => get_head_name(f), + _ => None, + } } + let Some(name) = get_head_name(ty) else { return false }; + let Some((_params, body)) = type_aliases.get(&name) else { return false }; + // Only reject if the alias body is a truly open record (has Var/Unif tail). + // Row composition aliases and closed records (body ending in Record(_, None)) are valid. + has_open_row_tail(body) } /// Check if a type contains a record with an open row variable tail. @@ -343,6 +354,26 @@ fn has_open_record_row(ty: &Type) -> bool { /// 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). +/// Check if a record type has a truly open row tail (Var or Unif). +/// `Record(_, Some(Record(_, None)))` is closed despite nested `Some`. +/// This happens when row composition aliases like `{ | FixtureEnvRow () }` expand. +fn has_open_row_tail(ty: &Type) -> bool { + match ty { + Type::Record(_, None) => false, + Type::Record(_, Some(tail)) => row_tail_is_open(tail), + _ => false, + } +} + +fn row_tail_is_open(tail: &Type) -> bool { + match tail { + Type::Var(_) | Type::Unif(_) => true, + Type::Record(_, None) => false, + Type::Record(_, Some(inner)) => row_tail_is_open(inner), + _ => true, // conservative: App/Con/etc treated as potentially open + } +} + fn is_non_nominal_for_derive( ty: &Type, type_aliases: &HashMap, Type)>, @@ -350,8 +381,8 @@ fn is_non_nominal_for_derive( is_newtype: bool, ) -> bool { if is_newtype { - // derive newtype: only reject open records - if matches!(ty, Type::Record(_, Some(_))) { + // derive newtype: only reject truly open records (with Var/Unif tail) + if has_open_row_tail(ty) { return true; } } else { @@ -375,7 +406,7 @@ fn is_non_nominal_for_derive( if !is_also_data_type { let expanded = expand_type_aliases_limited(ty, type_aliases, 0); if is_newtype { - if matches!(&expanded, Type::Record(_, Some(_))) { + if has_open_row_tail(&expanded) { return true; } } else { @@ -401,6 +432,23 @@ fn has_synonym_head(ty: &Type, type_aliases: &HashMap, Type } } +/// Collect all type constructor names (Con) referenced in a type. +/// Used to determine which type_con_arities entries are needed for +/// alias expansion disambiguation. +fn collect_type_con_names_from_type(ty: &Type, names: &mut HashSet) { + match ty { + Type::Con(qi) => { names.insert(qi.name); } + Type::App(f, a) => { collect_type_con_names_from_type(f, names); collect_type_con_names_from_type(a, names); } + Type::Fun(a, b) => { collect_type_con_names_from_type(a, names); collect_type_con_names_from_type(b, names); } + Type::Forall(_, body) => { collect_type_con_names_from_type(body, names); } + Type::Record(fields, tail) => { + for (_, t) in fields { collect_type_con_names_from_type(t, names); } + if let Some(t) = tail { collect_type_con_names_from_type(t, names); } + } + _ => {} + } +} + /// Expand type aliases with a depth limit to prevent stack overflow. /// Uses exact arity matching (args == params) for safety. pub fn expand_type_aliases_limited( @@ -409,7 +457,7 @@ pub fn expand_type_aliases_limited( depth: u32, ) -> Type { let mut expanding = HashSet::new(); - expand_type_aliases_limited_inner(ty, type_aliases, None, depth, &mut expanding) + expand_type_aliases_limited_inner(ty, type_aliases, None, depth, &mut expanding, None) } /// Expand type aliases with over-saturation support and data-type disambiguation. @@ -460,11 +508,11 @@ fn expand_type_aliases_limited_with_arities( Some(type_con_arities), depth, &mut expanding, + None, ) } -/// 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, @@ -479,17 +527,81 @@ fn type_contains_con_name(ty: &Type, name: &QualifiedIdent) -> bool { } } +/// Check if a type contains Con(name) applied to exactly `expected_args` arguments. +/// Arity-aware version for self-referential alias detection at export time. +fn contains_self_referential_usage_in_type(ty: &Type, name: Symbol, expected_args: usize) -> bool { + match ty { + Type::Con(n) => n.name == name && expected_args == 0, + Type::App(_, _) => { + 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(); + } + if let Type::Con(n) = head { + if n.name == name && args.len() == expected_args { + return true; + } + } + contains_self_referential_usage_in_type(head, name, expected_args) + || args.iter().any(|a| contains_self_referential_usage_in_type(a, name, expected_args)) + } + Type::Fun(from, to) => { + contains_self_referential_usage_in_type(from, name, expected_args) + || contains_self_referential_usage_in_type(to, name, expected_args) + } + Type::Forall(_, body) => contains_self_referential_usage_in_type(body, name, expected_args), + Type::Record(fields, tail) => { + fields.iter().any(|(_, t)| contains_self_referential_usage_in_type(t, name, expected_args)) + || tail.as_ref().map_or(false, |t| contains_self_referential_usage_in_type(t, name, expected_args)) + } + _ => false, + } +} + +/// Check if a type alias body is a simple re-export: `type X a b = M.X a b` +/// where M is a qualified import alias and X matches the alias name. +fn is_alias_reexport(body: &Type, alias_name: Symbol, params: &[Symbol]) -> bool { + let mut head = body; + let mut app_args = Vec::new(); + while let Type::App(f, a) = head { + app_args.push(a.as_ref()); + head = f.as_ref(); + } + if let Type::Con(qi_name) = head { + if qi_name.module.is_some() && qi_name.name == alias_name && app_args.len() == params.len() { + // For zero-param aliases, body is just Con(M.X) + if params.is_empty() { + return true; + } + // For parameterized aliases, each arg must be a matching Var + return app_args.iter().rev().zip(params.iter()).all(|(arg, param)| { + matches!(arg, Type::Var(v) if *v == *param) + }); + } + } + 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. +/// +/// `con_zero_blockers`: optional set of alias names to block in the zero-arg Con path. +/// Used during export expansion to prevent wrong expansion of alias names that collide +/// with data types from different modules (e.g. `type GqlError = { ... }` alias vs +/// `data GqlError = ...`). Only blocks standalone `Con(X)` expansion, NOT `App(Con(X), ...)` +/// expansion, so over-saturated usage (like `POST url input output`) still works. fn expand_type_aliases_limited_inner( ty: &Type, type_aliases: &HashMap, Type)>, type_con_arities: Option<&HashMap>, depth: u32, expanding: &mut HashSet, + con_zero_blockers: Option<&HashSet>, ) -> Type { if depth > 200 || type_aliases.is_empty() { return ty.clone(); @@ -551,6 +663,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ) }) .collect(); @@ -564,6 +677,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ); expanding.remove(name); let mut result = expanded_head; @@ -598,6 +712,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ); if is_self_ref { expanding.remove(name); @@ -620,6 +735,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ) }) .collect(); @@ -631,6 +747,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ), }; let mut result = expanded_head; @@ -648,6 +765,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ), expand_type_aliases_limited_inner( b, @@ -655,6 +773,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ), ), Type::Record(fields, tail) => { @@ -669,6 +788,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ), ) }) @@ -680,6 +800,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, )) }); Type::Record(fields, tail) @@ -692,6 +813,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, )), ), Type::Con(name) => { @@ -710,6 +832,13 @@ fn expand_type_aliases_limited_inner( if !expanding.contains(&expand_key) { if let Some((params, body)) = type_aliases.get(&lookup_key) { if params.is_empty() { + // Check zero-arg blockers: skip expansion if this alias name + // was marked as colliding with a data type at the call site. + if let Some(blockers) = con_zero_blockers { + if blockers.contains(&name.name) { + return ty.clone(); + } + } expanding.insert(expand_key); let result = expand_type_aliases_limited_inner( body, @@ -717,6 +846,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ); expanding.remove(&expand_key); return result; @@ -737,6 +867,7 @@ fn expand_type_aliases_limited_inner( type_con_arities, depth + 1, expanding, + con_zero_blockers, ); expanding.remove(&expand_key); return result; @@ -866,15 +997,23 @@ fn check_partially_applied_synonyms_inner( }; if let Some((params, _)) = alias_entry { if args.len() < params.len() { - errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); - return; + // Before flagging as partially applied, check if the name also refers + // to a data type with a compatible arity. If so, the user is referencing + // the data type, not the alias (name collision between modules). + let is_data_type = lookup_type_con_arity(type_con_arities, name) + .map_or(false, |arity| args.len() <= arity); + if !is_data_type { + errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); + return; + } } else if args.len() > params.len() { - // 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). + // Over-saturation: the alias might expand to a type that takes more args. + // E.g., `type POST = Route "POST"` where Route is a 2-param data type. + // `POST "/path"` expands to `Route "POST" "/path"` which is valid. + // Only report KAM if we know this name is a data type with insufficient + // arity; otherwise assume the alias expansion will accommodate the args. let arity_ok = lookup_type_con_arity(type_con_arities, name) - .map_or(false, |arity| args.len() <= arity); + .map_or(true, |arity| args.len() <= arity); if !arity_ok { errors.push(TypeError::KindArityMismatch { span, @@ -936,7 +1075,13 @@ fn check_partially_applied_synonyms_inner( }; if let Some((params, _)) = alias_entry { if !params.is_empty() { - errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); + // Check if the name also refers to a data type (any arity). + // Data types can be used bare (partially applied) for higher-kinded + // usage, so if the name resolves to a data type, skip the PAS check. + let is_data_type = lookup_type_con_arity(type_con_arities, name).is_some(); + if !is_data_type { + errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); + } } } } @@ -1273,12 +1418,15 @@ pub(super) fn prim_submodule_exports(module_name: &crate::cst::ModuleName) -> Mo // class Coercible (no user-visible methods) exports.instances.insert(unqualified_ident("Coercible"), Vec::new()); exports.class_param_counts.insert(unqualified_ident("Coercible"), 2); - // Coercible :: Type -> Type -> Constraint + // Coercible :: forall k. k -> k -> Constraint use crate::typechecker::types::Type as CoerceType; - let ct = CoerceType::kind_type(); + let k_var = intern("k"); + let k = CoerceType::Var(k_var); let cc = CoerceType::kind_constraint(); exports.class_type_kinds.insert(intern("Coercible"), - CoerceType::fun(ct.clone(), CoerceType::fun(ct, cc))); + CoerceType::Forall(vec![(k_var, false)], Box::new( + CoerceType::fun(k.clone(), CoerceType::fun(k, cc)) + ))); } "Int" => { // Compiler-solved type classes for type-level Ints @@ -1883,7 +2031,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .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(None, prim, &mut env, &mut ctx, None, &HashSet::new(), &HashSet::new()); + import_all(None, prim, &mut env, &mut ctx, None, &HashSet::new(), &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"); @@ -1913,6 +2061,23 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { &mut errors, ); + // Build canonical → import-alias mapping for the unifier. + // This allows try_expand_alias to resolve canonical qualified types + // (e.g. Components.AskForReview.Model) back to import-alias keys + // (e.g. AskForReview.Model) for alias lookup. + for import_decl in &module.imports { + if let Some(ref alias) = import_decl.qualified { + let canonical = module_name_to_symbol(&import_decl.module); + let alias_sym = module_name_to_symbol(alias); + ctx.state.canonical_to_qualifier.insert(canonical, alias_sym); + // Also register qualifier → qualifier (self-mapping) so try_expand_alias + // recognizes the import alias as a known module when defined_types + // canonicalization uses the qualifier (e.g. Con("Card.Action")). + ctx.state.canonical_to_qualifier.entry(alias_sym) + .or_insert(alias_sym); + } + } + // Pre-populate class param counts from imported class methods and class definitions. for (_method, (class_name, tvs)) in &ctx.class_methods { class_param_counts @@ -1951,6 +2116,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .entry(qi(*class_name)) .or_insert_with(|| fd.clone()); } + // Import superclass constraints for transitively expanding "given" constraints + for (class_name, sc_info) in &exports.class_superclasses { + class_superclasses.entry(*class_name).or_insert_with(|| sc_info.clone()); + } } } @@ -2872,7 +3041,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { types, .. } => { - let class_kind = match ks.lookup_class_kind_fresh(class_name.name) { + // For qualified class names (e.g., Route.Route from + // `import OaRouteClass as Route`), try the composite key + // first since class kinds are registered under qualified keys. + let class_kind_raw = if let Some(m) = class_name.module { + let qualified = crate::interner::intern_qualified(m, class_name.name); + ks.lookup_class_kind_fresh(qualified) + .or_else(|| ks.lookup_class_kind_fresh(class_name.name)) + } else { + ks.lookup_class_kind_fresh(class_name.name) + }; + let class_kind = match class_kind_raw { Some(k) => kind::instantiate_kind(&mut ks, &k), None => continue, }; @@ -2978,6 +3157,39 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ctx.newtype_names.insert(qi(name.value)); } } + // Pre-register local type aliases so that PAS checks during data constructor + // processing see the correct alias arity. Without this, when a data declaration + // appears before a type alias in source order (e.g. `data Action = ... Input ...` + // before `type Input = { ... }`), the PAS check finds an imported parametric alias + // instead of the local 0-param one, producing a false PartiallyAppliedSynonym error. + for decl in &module.decls { + if let Decl::TypeAlias { name, type_vars, ty, .. } = decl { + if let Ok(body_ty) = convert_type_expr(ty, &type_ops) { + let params: Vec = type_vars.iter().map(|tv| tv.value).collect(); + // For re-exported aliases like `type X = M.X`, resolve the body + // using the already-imported expanded alias instead of storing the + // unexpandable qualified reference. + let resolved_body = if is_alias_reexport(&body_ty, name.value, ¶ms) { + if let Some((existing_params, existing_body)) = ctx.state.type_aliases.get(&name.value) { + if existing_params.len() == params.len() && !matches!(existing_body, Type::Con(_)) { + existing_body.clone() + } else { + body_ty + } + } else { + body_ty + } + } else { + body_ty + }; + ctx.state.type_aliases.insert(name.value, (params, resolved_body)); + if matches!(ty, TypeExpr::Record { .. }) { + ctx.record_type_aliases.insert(qi(name.value)); + } + } + } + } + // Pass 1: Collect type signatures and data constructors for decl in &module.decls { super::check_deadline(); @@ -3377,8 +3589,28 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Track methods with extra typeclass constraints (e.g. Applicative m =>). // These get implicit dictionary parameters, making them functions even // with 0 explicit binders (prevents false CycleInDeclaration). - if has_any_constraint(&member.ty).is_some() { - ctx.constrained_class_methods.insert(member.name.value); + // Extract method-level constraint class names for current_given_expanded + { + let mut constraint_classes = Vec::new(); + fn extract_constraint_classes(ty: &crate::ast::TypeExpr, out: &mut Vec) { + match ty { + crate::ast::TypeExpr::Constrained { constraints, ty, .. } => { + for c in constraints { + out.push(c.class.name); + } + extract_constraint_classes(ty, out); + } + crate::ast::TypeExpr::Forall { ty, .. } => { + extract_constraint_classes(ty, out); + } + _ => {} + } + } + extract_constraint_classes(&member.ty, &mut constraint_classes); + if !constraint_classes.is_empty() { + ctx.constrained_class_methods.insert(member.name.value); + ctx.method_own_constraints.insert(member.name.value, constraint_classes); + } } match convert_type_expr(&member.ty, &type_ops) { Ok(member_ty) => { @@ -3474,16 +3706,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } } - // Check for non-nominal types in instance heads: type synonyms that - // 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). + // Check for type synonyms expanding to open records in instance heads. + // E.g. `type X r = {x :: Int | r}; instance Show (X r)` is invalid. + // Only check the LAST type (the main instance head), not class parameters — + // row types are valid as class parameters (e.g. `instance SSTLinks RowAlias (SomeType m)`). if inst_ok { - for inst_ty in &inst_types { - if is_non_nominal_instance_head_record_only(inst_ty, &ctx.state.type_aliases) { + if let Some(last_ty) = inst_types.last() { + if is_non_nominal_instance_head_record_only(last_ty, &ctx.state.type_aliases) { errors.push(TypeError::InvalidInstanceHead { span: *span }); inst_ok = false; - break; } } } @@ -3902,9 +4133,36 @@ 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) = + if let Ok(mut sig_ty) = convert_type_expr(ty, &type_ops) { + // Replace wildcard `_` type vars with fresh unif vars. + // PureScript allows `_` in instance method type annotations + // meaning "infer this part" (e.g. `foo :: _ (NT m Aff)`). + let wildcard_sym = crate::interner::intern("_"); + fn replace_wildcards(ty: &Type, wildcard: Symbol, ctx: &mut InferCtx) -> Type { + match ty { + Type::Var(v) if *v == wildcard => Type::Unif(ctx.state.fresh_var()), + Type::Fun(a, b) => Type::fun( + replace_wildcards(a, wildcard, ctx), + replace_wildcards(b, wildcard, ctx), + ), + Type::App(f, a) => Type::app( + replace_wildcards(f, wildcard, ctx), + replace_wildcards(a, wildcard, ctx), + ), + Type::Forall(vars, body) => Type::Forall( + vars.clone(), + Box::new(replace_wildcards(body, wildcard, ctx)), + ), + Type::Record(fields, tail) => Type::Record( + fields.iter().map(|(l, t)| (*l, replace_wildcards(t, wildcard, ctx))).collect(), + tail.as_ref().map(|t| Box::new(replace_wildcards(t, wildcard, ctx))), + ), + other => other.clone(), + } + } + sig_ty = replace_wildcards(&sig_ty, wildcard_sym, &mut ctx); // Unify the declared instance sig with the class-derived type if let Err(e) = ctx.state.unify(*sig_span, &sig_ty, &expected_ty) @@ -4091,7 +4349,22 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }); } } - ctx.state.type_aliases.insert(name.value, (params, body_ty)); + // For re-exported aliases, resolve the body using the + // already-imported expanded alias. + let resolved_body = if is_alias_reexport(&body_ty, name.value, ¶ms) { + if let Some((existing_params, existing_body)) = ctx.state.type_aliases.get(&name.value) { + if existing_params.len() == params.len() && !matches!(existing_body, Type::Con(_)) { + existing_body.clone() + } else { + body_ty + } + } else { + body_ty + } + } else { + body_ty + }; + ctx.state.type_aliases.insert(name.value, (params, resolved_body)); // Track if this is a record-kind alias (body is { } syntax, kind Type) if matches!(ty, TypeExpr::Record { .. }) { ctx.record_type_aliases.insert(qi(name.value)); @@ -5032,7 +5305,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .. } = decl { - if ctx.ctor_details.contains_key(&target) { + if ctx.ctor_details.contains_key(&target) + || ctx.ctor_details.contains_key(&qi(target.name)) + { // Constructor target: remove any inherited function alias flag ctx.function_op_aliases.remove(&qi(operator.value)); } else { @@ -5058,9 +5333,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // - `bottom = Date bottom bottom bottom` → head is `Date` (constructor) → ok // - `pos = pos <<< lower` → Op expression, no app head → ok // - // 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). + // We also exclude names that exist as top-level values in the module + // or were imported as standalone (non-class-method) values, + // since the RHS refers to the top-level/imported function, not the sibling method + // (e.g. `chooseInt = chooseInt` delegates to a top-level function, + // and `eventIsArchived = eventIsArchived` delegates to an imported function). let top_level_values: HashSet = module .decls .iter() @@ -5070,6 +5347,62 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { _ => None, }) .collect(); + // Collect non-class-method values imported from other modules. + // These take precedence over sibling instance method names on the RHS, + // so `eventIsArchived = eventIsArchived` (imported value) is NOT a cycle. + let imported_non_class_values: HashSet = { + let mut set = HashSet::new(); + for import_decl in &module.imports { + // Skip qualified imports — they don't introduce unqualified names + if import_decl.qualified.is_some() { + continue; + } + let prim_sub; + let module_exports = if is_prim_module(&import_decl.module) { + continue; // Prim doesn't have relevant values for this check + } else if is_prim_submodule(&import_decl.module) { + prim_sub = prim_submodule_exports(&import_decl.module); + &prim_sub + } else { + match registry.lookup(&import_decl.module.parts) { + Some(exports) => exports, + None => continue, + } + }; + match &import_decl.imports { + None => { + // Open import: all non-class-method values are imported + for name in module_exports.values.keys() { + if !module_exports.class_methods.contains_key(name) { + set.insert(name.name); + } + } + } + Some(ImportList::Explicit(items)) => { + for item in items { + if let Import::Value(sym) = item { + // Explicitly imported value — check it's not a class method + // in the source module (e.g. `import Prelude (f)` where f + // is a class method should not count). + let qi_sym = qi(*sym); + if !module_exports.class_methods.contains_key(&qi_sym) { + set.insert(*sym); + } + } + } + } + Some(ImportList::Hiding(_)) => { + // Hiding import: all non-class-method, non-hidden values + for name in module_exports.values.keys() { + if !module_exports.class_methods.contains_key(name) { + set.insert(name.name); + } + } + } + } + } + set + }; let mut cycle_methods: HashSet = HashSet::new(); for group in &instance_method_groups { let sibling_set: HashSet = group.iter().copied().collect(); @@ -5085,10 +5418,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if ctx.constrained_class_methods.contains(name) { continue; } - // Check if the application head is a sibling method name + // Check if the application head is a sibling method name. + // Exclude names that are top-level values in the module OR imported + // as standalone (non-class-method) values — in both cases the RHS refers + // to the existing function, not the sibling instance method. 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) + sibling_set.contains(&head) + && !top_level_values.contains(&head) + && !imported_non_class_values.contains(&head) } else { false } @@ -5135,6 +5473,37 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let prev_given = ctx.given_class_names.clone(); ctx.scoped_type_vars.extend(inst_scoped); ctx.given_class_names.extend(inst_given); + // Set per-function given classes for instance method body + ctx.current_given_expanded.clear(); + for gcn in &ctx.given_class_names { + ctx.current_given_expanded.insert(gcn.name); + let mut stack = vec![gcn.name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if ctx.current_given_expanded.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + // Also include method-level constraints from the class definition (e.g. IxBind f =>) + if let Some(constraint_classes) = ctx.method_own_constraints.get(name) { + for &cls_name in constraint_classes { + ctx.current_given_expanded.insert(cls_name); + let mut stack = vec![cls_name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if ctx.current_given_expanded.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + } if let Err(e) = check_value_decl( &mut ctx, &env, @@ -5426,6 +5795,40 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { var }; + // Set per-function given classes: the calling function's own signature + // constraints (with transitive superclass expansion) so that deferred + // constraints for transitively-given classes are filtered at push time. + ctx.current_given_expanded.clear(); + if let Some(fn_constraints) = ctx.signature_constraints.get(&qualified).cloned() { + for (cn, _) in &fn_constraints { + ctx.current_given_expanded.insert(cn.name); + let mut stack = vec![cn.name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if ctx.current_given_expanded.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + } + // Also include given_class_names (from enclosing instance declarations) + for gcn in &ctx.given_class_names { + ctx.current_given_expanded.insert(gcn.name); + let mut stack = vec![gcn.name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if ctx.current_given_expanded.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + // Save constraint count before inference for AmbiguousTypeVariables detection let constraint_start = ctx.deferred_constraints.len(); @@ -6275,6 +6678,46 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + // Pre-compute the set of "given" class names from instance declarations (given_class_names) + // including transitive superclasses. Used by Pass 2.5 (sig_deferred_constraints). + let mut given_classes_expanded: HashSet = HashSet::new(); + for gcn in &ctx.given_class_names { + given_classes_expanded.insert(gcn.name); + let mut stack = vec![gcn.name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if given_classes_expanded.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + + // Extended set for Pass 3 (deferred_constraints from class method instantiation). + // Includes everything from given_classes_expanded PLUS classes from all function + // signature_constraints. When a class method is called from a function that doesn't + // have the class in its own signature, the constraint gets type-var args after + // generalization. If any function in the module declares that class, the constraint + // shouldn't trigger false-positive chain ambiguity errors. + let mut given_classes_expanded_for_deferred: HashSet = given_classes_expanded.clone(); + for constraints in ctx.signature_constraints.values() { + for (class_name, _) in constraints { + given_classes_expanded_for_deferred.insert(class_name.name); + let mut stack = vec![class_name.name]; + while let Some(cls) = stack.pop() { + if let Some((_, sc_constraints)) = class_superclasses.get(&qi(cls)) { + for (sc_class, _) in sc_constraints { + if given_classes_expanded_for_deferred.insert(sc_class.name) { + stack.push(sc_class.name); + } + } + } + } + } + } + // Pass 2.5: Check signature-propagated constraints for zero-instance classes. // These constraints come from type signatures (e.g. `Foo a => ...`) and are only // checked for the case where the class has absolutely zero instances, since our @@ -6291,9 +6734,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { 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. + // Skip if the class is a "given" constraint from an enclosing function signature + // (including transitive superclasses). These constraints are declared requirements + // that callers must satisfy — they shouldn't be checked for local instances. + let is_given_by_signature = given_classes_expanded.contains(&class_name.name); + if is_given_by_signature { + continue; + } + // If all args are unsolved, the constraint may be satisfied at a downstream call + // site. Only fire when at least one arg is concrete and there are no type vars. if !all_pure_unif && !has_type_vars { // Skip compiler-magic classes that are resolved without explicit instances let class_str = crate::interner::resolve(class_name.name).unwrap_or_default(); @@ -6447,6 +6896,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(); @@ -6476,10 +6926,7 @@ 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() - .any(|constraints| constraints.iter().any(|(cn, _)| cn == class_name)); + let is_given = given_classes_expanded_for_deferred.contains(&class_name.name); if !is_given { if let Some(known) = lookup_instances(&instances, class_name) { let has_concrete_instance = known.iter().any(|(inst_types, _)| { @@ -6511,12 +6958,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { && !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)); + // Skip if the class is "given" by an enclosing function's type signature + // (including transitive superclasses). + let is_given = given_classes_expanded_for_deferred.contains(&class_name.name); if is_given { continue; } @@ -6595,10 +7039,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { 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 + // Skip Coercible solving when unif vars are in structural positions + // (bare Unif args, or inside App/Fun args). The solver can't handle + // partial types like Coercible ?543 (Array X) from GraphQL queries. + // But keep solving when unif vars are only in row tails — the solver + // CAN determine coercibility from the record field structure alone. + let has_structural_unif = zonked_args .iter() - .all(|t| !ctx.state.free_unif_vars(t).is_empty()); - if both_have_unif { + .any(|t| has_unif_outside_row_tails(t)); + if has_structural_unif { continue; } match solve_coercible( @@ -6663,10 +7112,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // 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)); + let is_given = given_classes_expanded_for_deferred.contains(&class_name.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 @@ -6780,8 +7226,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // If the class itself is not known (not in any instance map and no // methods registered), produce UnknownClass instead of NoInstanceFound. - let class_is_known = instances.contains_key(class_name) - || ctx.class_methods.values().any(|(cn, _)| cn == class_name); + // Use lookup_instances for qualified fallback (e.g. SimpleJson.WriteForeign → WriteForeign). + let class_is_known = lookup_instances(&instances, class_name).is_some() + || ctx.class_methods.values().any(|(cn, _)| cn == class_name || cn.name == class_name.name); if !class_is_known { errors.push(TypeError::UnknownClass { span: *span, @@ -7264,6 +7711,9 @@ 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(); + // Build a filtered alias map for export expansion that excludes aliases from + // qualified imports that collide with data types. This prevents wrong expansion + // when e.g. `type GqlError = { ... }` alias (from a qualified import) would let mut export_data_constructors: HashMap> = HashMap::new(); let mut export_ctor_details: HashMap, Vec)> = HashMap::new(); for type_name in &declared_types { @@ -7314,8 +7764,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { HashMap::new(); for (class_name, insts) in &instances { // Export all instances (both for local and imported classes) since instances - // are globally visible in PureScript - export_instances.insert(*class_name, insts.clone()); + // are globally visible in PureScript. + // Expand type aliases in instance types so that importing modules can match + // against concrete types even without the alias in scope. + // E.g. `MonadAsk PayloadEnv PayloadM` → `MonadAsk { logger :: ..., ... } PayloadM` + let expanded_insts: Vec<_> = insts.iter().map(|(types, constraints)| { + let expanded_types: Vec = types.iter().map(|t| { + expand_type_aliases_limited(t, &ctx.state.type_aliases, 0) + }).collect(); + (expanded_types, constraints.clone()) + }).collect(); + export_instances.insert(*class_name, expanded_insts); } let mut export_type_operators: HashMap = HashMap::new(); @@ -7350,8 +7809,36 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // 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. + // + // Two-tier check: only include aliases where the DIRECT body (before transitive + // alias expansion) contains Con(name) with matching arity. Aliases where the + // self-reference only appears after transitive expansion (e.g., + // `type Model = ModelExt(...)` where ModelExt body contains `AskForReview.Model` + // data type that became Con("Model") after qualifier stripping) are excluded IF + // a data type with the same name and arity exists. This allows one level of + // expansion at export time. Downstream modules still inherit the self_referential + // flag (from self_referential_aliases export), so their self_ref_qis prevents + // re-expansion of the inner Con(name) — no cross-module type growth. let self_ref_qis: HashSet = ctx.state.self_referential_aliases .iter() + .filter(|&&name| { + // Check if the DIRECT body contains the self-reference + if let Some((params, body)) = ctx.state.type_aliases.get(&name) { + let param_count = params.len(); + if contains_self_referential_usage_in_type(body, name, param_count) { + // Direct self-reference → truly self-referential, keep in set + return true; + } + // Indirect only → check for data type collision + let has_data_type_collision = ctx.type_con_arities.iter() + .any(|(k, &arity)| k.name == name && arity == param_count); + // If collision exists, exclude from set (allow expansion) + !has_data_type_collision + } else { + // Alias not found (shouldn't happen), keep in set for safety + true + } + }) .map(|s| qi(*s)) .collect(); @@ -7359,13 +7846,110 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // 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). + // Only expand bodies of LOCALLY-DEFINED aliases. Imported alias bodies should + // already be expanded from their source module. Re-expanding imported alias bodies + // can cause name collisions (e.g. `type GqlError = { ... }` alias from one module + // incorrectly expanding `Con(GqlError)` data type references in another alias body). + // Also filter out aliases that were only imported via qualified imports — these should + // not be re-exported since qualified imports don't make names available unqualified. + // + // Compute zero-arg blocker set for export alias body expansion. + // Block zero-param alias expansion when the name appears as a type constructor + // in IMPORTED (non-locally-defined) alias bodies. These type constructors were + // already "resolved" in the source module — re-expanding them with a different + // alias from the current module causes type mismatches across module boundaries. + // E.g., `EventRec` from Data.Event contains `ProgramType` (a data type there), + // but Effect.Update.Fn also imports `type ProgramType = { ... }` (a record alias). + // Without blocking, the data type ref gets expanded as the record alias on export. + // + // Compute two related blocker sets: + // 1. con_zero_blockers: for expand_type_aliases_limited_inner (existing mechanism) + // 2. zonk_con_blockers: for zonk_ref's Type::Con branch (new mechanism) + // + // Both block zero-param alias expansion when the name collides with a data type + // from a different module. The difference: con_zero_blockers is checked during + // expand_type_aliases_limited_inner, zonk_con_blockers during zonk. + // + // To determine genuine data type collisions (vs blocked-alias cascades), check + // the registry's type_con_arities: if a name exists as a data type in ANY + // imported module, it's a genuine collision. Names that only appear because + // a previous module's con_zero_blockers blocked expansion are NOT in any + // module's type_con_arities. + let con_zero_blockers: HashSet = { + // Start with the original qualified-import-based blockers + let mut blockers: HashSet = ctx + .qualified_import_unqual_aliases + .iter() + .filter(|name| ctx.type_con_arities.iter().any(|(k, &v)| k.name == **name && v == 0)) + .copied() + .collect(); + // Collect type constructor names from imported (non-locally-defined) alias bodies + // that are GENUINELY data types in some registry module. + let mut imported_body_cons: HashSet = HashSet::new(); + for (name, (_params, body)) in &ctx.state.type_aliases { + if has_type_alias_def.contains(name) { + continue; // Skip locally-defined aliases + } + collect_type_con_names_from_type(body, &mut imported_body_cons); + } + // Only block when the data type is actually in scope (present in ctx.type_con_arities + // under the unqualified key). Previously we collected ALL data types from ALL imported + // modules' type_con_arities, which was too broad — e.g. `data Time` from Data.Time + // would block `type Time = Number` from Signal.Time even when Data.Time wasn't imported. + for con_name in &imported_body_cons { + // Only block if: + // 1. There's a zero-param alias with this name in the current module + // 2. The name is a genuine data type actually in scope (in type_con_arities under unqualified key) + if let Some((params, _)) = ctx.state.type_aliases.get(con_name) { + if params.is_empty() + && ctx.type_con_arities.contains_key(&qi(*con_name)) + && !has_type_alias_def.contains(con_name) + { + blockers.insert(*con_name); + } + } + } + blockers + }; + // Build reverse qualifier map: canonical module path → import alias. + // Used to de-canonicalize type constructors in imported alias bodies before + // expansion, so references like `Components.AskForReview.Model` can be found + // under their import-alias key `AskForReview.Model`. + let reverse_qualifier_map: HashMap = module.imports.iter() + .filter_map(|import_decl| { + let alias = import_decl.qualified.as_ref()?; + let mod_sym = module_name_to_symbol(&import_decl.module); + let alias_sym = module_name_to_symbol(alias); + Some((mod_sym, alias_sym)) + }) + .collect(); let export_type_aliases: HashMap, Type)> = ctx .state .type_aliases .iter() + .filter(|(name, _)| { + // Keep locally-defined aliases always + if has_type_alias_def.contains(name) { + return true; + } + // Exclude aliases that came only from qualified imports + !ctx.qualified_import_unqual_aliases.contains(name) + }) .map(|(name, (params, 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); + let expanded_body = if has_type_alias_def.contains(name) { + // De-canonicalize type constructors in the body before expansion, + // so that `Components.AskForReview.Model` becomes `AskForReview.Model` + // which can be found in type_aliases under the import-alias key. + let body = if !reverse_qualifier_map.is_empty() { + resolve_type_qualifiers(body, &reverse_qualifier_map) + } else { + body.clone() + }; + let mut expanding = self_ref_qis.clone(); + expand_type_aliases_limited_inner(&body, &ctx.state.type_aliases, Some(&ctx.type_con_arities), 0, &mut expanding, Some(&con_zero_blockers)) + } else { + body.clone() + }; (qi(*name), (params.iter().map(|p| qi(*p)).collect(), expanded_body)) }) .collect(); @@ -7377,8 +7961,17 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // 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() { + // Set zonk_con_blockers on the UnifyState so that zonk_ref's Type::Con branch + // skips expansion of zero-arg aliases that genuinely collide with data types. + ctx.state.zonk_con_blockers = con_zero_blockers.clone(); + for (_val_name, scheme) in local_values.iter_mut() { scheme.ty = ctx.state.zonk(scheme.ty.clone()); + // De-canonicalize type constructors before expansion so that canonical + // qualifiers (e.g. `Components.AskForReview.Model`) can be found under + // their import-alias keys (e.g. `AskForReview.Model`) in type_aliases. + if !reverse_qualifier_map.is_empty() { + scheme.ty = resolve_type_qualifiers(&scheme.ty, &reverse_qualifier_map); + } let mut expanding = self_ref_qis.clone(); scheme.ty = expand_type_aliases_limited_inner( &scheme.ty, @@ -7386,6 +7979,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { Some(&ctx.type_con_arities), 0, &mut expanding, + Some(&con_zero_blockers), ); // Replace any remaining unsolved Unif vars with fresh named type variables. // These can occur for unsolved row tails in open records (e.g. `{ x :: Int | ?331 }`) @@ -7404,6 +7998,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + // Clear zonk_con_blockers after export-time zonking is done + ctx.state.zonk_con_blockers.clear(); + // Build origin maps: all locally-defined names have origin = this module let current_mod_sym = module_name_to_symbol(&module.name.value); let mut value_origins: HashMap = HashMap::new(); @@ -7496,6 +8093,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { (name.name, strip_kind_qualifiers(&generalized)) }) .collect(), + class_superclasses: class_superclasses.clone(), + method_own_constraints: ctx.method_own_constraints.iter().map(|(k, v)| (qi(*k), v.clone())).collect(), }; // Ensure operator targets (e.g. Tuple for /\) are included in exported values and @@ -7526,6 +8125,40 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + // Add constructor schemes to exported values so that `Type(..)` exports and + // downstream `import M (Type(..))` can find constructor type schemes. + // Constructors are registered in `env` during type checking but not in `local_values`. + for (_type_name, ctors) in &module_exports.data_constructors.clone() { + for ctor in ctors { + if !module_exports.values.contains_key(ctor) { + if let Some(scheme) = env.lookup(ctor.name) { + let mut scheme = scheme.clone(); + 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, + None, + ); + let mut unif_to_var: HashMap = HashMap::new(); + collect_unif_var_ids(&scheme.ty, &mut unif_to_var); + if !unif_to_var.is_empty() { + scheme.ty = replace_unif_with_vars(&scheme.ty, &unif_to_var); + for var_name in unif_to_var.values() { + if !scheme.forall_vars.contains(var_name) { + scheme.forall_vars.push(*var_name); + } + } + } + module_exports.values.insert(*ctor, scheme); + } + } + } + } + // 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 @@ -7897,6 +8530,51 @@ fn maybe_qualify_qualified_ident( } } +/// Canonicalize unqualified type constructor references in an alias body. +/// For qualified imports (`import M as Q`), alias bodies may contain +/// `Type::Con(QualifiedIdent { module: None, name: "Bar" })` — unqualified +/// references to types from the source module. This function sets the module +/// qualifier to the source module's canonical name so that `try_expand_alias` +/// can find them via canonical key or canonical_to_qualifier fallback, and +/// so they won't be confused with local aliases of the same name. +fn canonicalize_alias_body_types( + ty: &Type, + source_module: Symbol, + exported_type_names: &HashSet, + exclude_name: Option, +) -> Type { + match ty { + Type::Con(qi) if qi.module.is_none() + && exported_type_names.contains(&qi.name) + && exclude_name.map_or(true, |ex| ex != qi.name) => { + Type::Con(QualifiedIdent { module: Some(source_module), name: qi.name }) + } + Type::App(f, a) => { + Type::App( + Box::new(canonicalize_alias_body_types(f, source_module, exported_type_names, exclude_name)), + Box::new(canonicalize_alias_body_types(a, source_module, exported_type_names, exclude_name)), + ) + } + Type::Fun(a, b) => { + Type::Fun( + Box::new(canonicalize_alias_body_types(a, source_module, exported_type_names, exclude_name)), + Box::new(canonicalize_alias_body_types(b, source_module, exported_type_names, exclude_name)), + ) + } + Type::Forall(vars, body) => { + Type::Forall(vars.clone(), Box::new(canonicalize_alias_body_types(body, source_module, exported_type_names, exclude_name))) + } + Type::Record(fields, tail) => { + let new_fields: Vec<_> = fields.iter() + .map(|(label, ty)| (*label, canonicalize_alias_body_types(ty, source_module, exported_type_names, exclude_name))) + .collect(); + let new_tail = tail.as_ref().map(|t| Box::new(canonicalize_alias_body_types(t, source_module, exported_type_names, exclude_name))); + Type::Record(new_fields, new_tail) + } + _ => ty.clone(), + } +} + type InstanceMap = HashMap, Vec<(QualifiedIdent, Vec)>)>>; /// Look up instances for a class, falling back to unqualified name if needed. @@ -7965,6 +8643,63 @@ fn process_imports( }) .collect(); + // Pre-scan all imports to collect type alias names that will be imported + // into the unqualified namespace. This is needed so qualified imports can + // detect name collisions with type aliases even when the alias-providing + // import appears later in the import list. Without this, import ordering + // affects whether defined_types qualifies value scheme type constructors, + // causing incorrect alias expansion (e.g., `Expiry` data type in a qualified + // import's value scheme gets alias-expanded to `{ expiresIn :: Int }` from + // an unqualified type alias import that hasn't been processed yet). + let all_alias_names: HashSet = { + let mut names = local_type_alias_names.clone(); + for import_decl in &module.imports { + // Only unqualified imports add aliases to the unqualified namespace + if import_decl.qualified.is_some() { + continue; + } + let prim_sub_pre; + let module_exports_pre = if is_prim_module(&import_decl.module) { + prim + } else if is_prim_submodule(&import_decl.module) { + prim_sub_pre = prim_submodule_exports(&import_decl.module); + &prim_sub_pre + } else { + match registry.lookup(&import_decl.module.parts) { + Some(exports) => exports, + None => continue, + } + }; + match &import_decl.imports { + None => { + // import M — all type aliases imported unqualified + for name in module_exports_pre.type_aliases.keys() { + names.insert(name.name); + } + } + Some(ImportList::Explicit(items)) => { + // import M (x, y, ...) — check which are type aliases + for item in items { + let sym = import_name(item); + if module_exports_pre.type_aliases.keys().any(|n| n.name == sym) { + names.insert(sym); + } + } + } + Some(ImportList::Hiding(items)) => { + // import M hiding (x, y) — all aliases except hidden + let hidden_pre: HashSet = items.iter().map(|i| import_name(i)).collect(); + for name in module_exports_pre.type_aliases.keys() { + if !hidden_pre.contains(&name.name) { + names.insert(name.name); + } + } + } + } + } + names + }; + // 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 @@ -8080,10 +8815,26 @@ fn process_imports( ctx.state.self_referential_aliases.extend(&module_exports.self_referential_aliases); } + // Compute canonical_origins for explicit/hiding import paths: maps unqualified + // type names to their origin module when they collide with LOCAL type aliases. + // Use all_alias_names (not just local_type_alias_names) for consistency with + // import_all's canonical_origins. Without this, import_item value schemes have + // bare type names while import_all alias bodies have canonicalized names, causing + // unification mismatches (e.g., Time vs Data.Time.Time). + let import_canonical_origins: Option> = { + let mut origins: HashMap = HashMap::new(); + for (&name, &origin) in &module_exports.type_origins { + if all_alias_names.contains(&name) { + origins.insert(name, origin); + } + } + if origins.is_empty() { None } else { Some(origins) } + }; + match &import_decl.imports { None => { // import M — everything unqualified; import M as Q — everything qualified only - import_all(Some(import_decl.module.clone()), module_exports, env, ctx, qualifier, &local_type_alias_names, &local_data_type_names); + import_all(Some(import_decl.module.clone()), module_exports, env, ctx, qualifier, &all_alias_names, &local_type_alias_names, &local_data_type_names); } Some(ImportList::Explicit(items)) => { // import M (x) — listed items unqualified @@ -8105,12 +8856,35 @@ fn process_imports( qualifier, import_decl.span, errors, + &import_canonical_origins, ); } + // Import type_con_arities from the source module for names referenced + // in imported alias bodies. This ensures data type arities are known + // for alias expansion disambiguation (e.g., `type GqlData = RemoteData GqlError` + // where GqlError is a data type in the source module but also an alias + // from a different qualified import in the consuming module). + { + let mut alias_body_names: HashSet = HashSet::new(); + for item in items { + let item_name = import_name(item); + let item_qi = qi(item_name); + if let Some(alias) = module_exports.type_aliases.get(&item_qi) { + collect_type_con_names_from_type(&alias.1, &mut alias_body_names); + } + } + if !alias_body_names.is_empty() { + for (name, arity) in &module_exports.type_con_arities { + if alias_body_names.contains(&name.name) { + ctx.type_con_arities.entry(maybe_qualify_qualified_ident(*name, qualifier)).or_insert(*arity); + } + } + } + } } 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, &local_data_type_names); + import_all_except(Some(import_decl.module.clone()), module_exports, &hidden, env, ctx, instances, qualifier, &local_data_type_names, &import_canonical_origins); } } } @@ -8205,12 +8979,13 @@ fn canonicalize_scheme_type_cons(scheme: &Scheme, canonical_origins: &HashMap, + from: Option, exports: &ModuleExports, env: &mut Env, ctx: &mut InferCtx, qualifier: Option, - local_type_alias_names: &HashSet, + all_alias_names: &HashSet, + _local_type_alias_names: &HashSet, local_data_type_names: &HashSet, ) { // For qualified imports, qualify imported type constructors defined in the source @@ -8219,11 +8994,15 @@ fn import_all( // `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). + // Uses all_alias_names (including imported aliases) for ordering independence. let defined_types: Option<(HashSet, Symbol)> = qualifier.and_then(|q| { - let mod_sym = _from.as_ref().map(module_name_to_symbol)?; + 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)) + .filter(|(&name, _)| { + ctx.state.type_aliases.contains_key(&name) + || all_alias_names.contains(&name) + }) .map(|(&name, _)| name) .collect(); if dt.is_empty() { None } else { Some((dt, q)) } @@ -8232,12 +9011,24 @@ fn import_all( // 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)`. + // If the exporting module itself defines a name as a type alias, its value schemes + // use the alias, not the data type. Don't canonicalize in that case — canonicalizing + // would turn alias references into data type references (e.g., Time=Number into + // Data.Time.Time, or ResponseUpdate into its qualified form). 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 the exporting module itself has this as a type alias, its value + // schemes use the alias meaning. Don't canonicalize. + if exports.type_aliases.iter().any(|(k, _)| k.name == name) { + continue; } + let has_alias_collision = ctx.state.type_aliases.contains_key(&name) + || all_alias_names.contains(&name); + if !has_alias_collision { + continue; + } + origins.insert(name, origin); } if origins.is_empty() { None } else { Some(origins) } }; @@ -8290,14 +9081,11 @@ fn import_all( for (name, details) in &exports.ctor_details { 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); - } + // Qualified import: store under qualified key only (e.g. M.Leaf) + // Don't insert unqualified — qualified imports don't make names + // available unqualified, and doing so overwrites correct entries + // from explicit unqualified imports (e.g. Left from Data.Either). + ctx.ctor_details.insert(QualifiedIdent { module: Some(q), name: name.name }, entry); } else { ctx.ctor_details.insert(*name, entry); } @@ -8330,25 +9118,91 @@ fn import_all( for name in &exports.constrained_class_methods { ctx.constrained_class_methods.insert(name.name); } + for (name, constraints) in &exports.method_own_constraints { + ctx.method_own_constraints.entry(name.name).or_insert_with(|| constraints.clone()); + } + // For qualified imports, build the set of type names that ORIGINATE from the source + // module. We only canonicalize these in alias bodies — re-exported types (like String, + // Maybe from Prim) should stay unqualified to avoid OaComponents.Table.String mismatches. + let (source_module_sym, exported_type_names) = if qualifier.is_some() { + let mod_sym = from.as_ref().map(module_name_to_symbol); + let mut type_names: HashSet = HashSet::new(); + if let Some(mod_sym) = mod_sym { + for (&name, &origin) in &exports.type_origins { + if origin == mod_sym { + type_names.insert(name); + } + } + } + (mod_sym, type_names) + } else { + (None, HashSet::new()) + }; for (name, alias) in &exports.type_aliases { 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())); + // Canonicalize alias body with canonical_origins to prevent local aliases + // from intercepting type constructor references in imported alias bodies. + // E.g., an alias body containing `Time` (the Data.Time data type) must not + // be expanded by a local `type Time = Number` alias. + let body_canonicalized = if let Some(co) = &canonical_origins { + canonicalize_type_cons(&alias.1, co) + } else { + alias.1.clone() + }; + if qualifier.is_none() { + // Unqualified import: register under unqualified key as before. + // Don't register if it collides with a locally-defined data/newtype name. + let collides_with_local_data = local_data_type_names.contains(&name.name); + if !collides_with_local_data { + ctx.state.type_aliases.insert(name.name, (sym_params.clone(), body_canonicalized.clone())); + ctx.qualified_import_unqual_aliases.remove(&name.name); + } } + // For qualified imports, canonicalize alias body so unqualified type refs + // from the source module use the canonical module name. This allows + // try_expand_alias to find them via canonical_to_qualifier fallback. + let body_for_qualified = if let Some(mod_sym) = source_module_sym { + canonicalize_alias_body_types(&body_canonicalized, mod_sym, &exported_type_names, Some(name.name)) + } else { + body_canonicalized.clone() + }; let qualified_name = maybe_qualify_symbol(name.name, qualifier); - // Also store under qualified key so alias expansion can disambiguate + // 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.state.type_aliases.insert(qualified_name, (sym_params.clone(), body_for_qualified.clone())); ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(*name, qualifier)); } + // Register under canonical qualified key (origin_module.name) so alias expansion + // works after canonicalize_type_cons qualifies type constructors to avoid + // local alias collisions. E.g., Con("Model") canonicalized to + // Con("AdminDashboard.Model.Model") needs to find the alias under that key. + // Skip canonical key registration for zero-param aliases: their body often + // references the canonical form of the same name (e.g. type X = Canon.X a b c), + // creating a self-referential zero-arg alias under the canonical key. + if !sym_params.is_empty() { + if let Some(co) = &canonical_origins { + if let Some(&origin) = co.get(&name.name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + let body = if qualifier.is_some() { body_for_qualified.clone() } else { body_canonicalized.clone() }; + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), body)); + } + } + } + // Also register under defined_types qualified key for qualified imports. + if let Some((dt, q)) = &defined_types { + if dt.contains(&name.name) { + let q_str = crate::interner::resolve(*q).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let dt_key = crate::interner::intern(&format!("{}.{}", q_str, name_str)); + let body = if qualifier.is_some() { body_for_qualified.clone() } else { body_canonicalized.clone() }; + ctx.state.type_aliases.entry(dt_key) + .or_insert((sym_params.clone(), body)); + } + } } for (name, arity) in &exports.type_con_arities { ctx.type_con_arities.insert(maybe_qualify_qualified_ident(*name, qualifier), *arity); @@ -8391,6 +9245,7 @@ fn import_item( qualifier: Option, import_span: crate::span::Span, errors: &mut Vec, + canonical_origins: &Option>, ) { match item { Import::Value(name) => { @@ -8408,8 +9263,16 @@ fn import_item( } if let Some(scheme) = exports.values.get(&name_qi) { // Explicit imports always win — the user specifically asked for this value. - // Values are already alias-expanded at export time. - env.insert_scheme(maybe_qualify_symbol(*name, qualifier), scheme.clone()); + // Canonicalize type constructors that collide with local type aliases + // to prevent incorrect alias expansion. E.g., if the local module defines + // `type File = { ... }`, imported `getMediaType :: File -> String` must + // have its `File` qualified to `Web.File.File.File` to avoid expansion. + let scheme = if let Some(co) = canonical_origins { + canonicalize_scheme_type_cons(scheme, co) + } else { + scheme.clone() + }; + env.insert_scheme(maybe_qualify_symbol(*name, qualifier), scheme); } // Instances are imported centrally in process_imports with module-level dedup. // Import fixity if this is an operator @@ -8425,6 +9288,9 @@ fn import_item( if exports.constrained_class_methods.contains(&name_qi) { ctx.constrained_class_methods.insert(*name); } + if let Some(constraints) = exports.method_own_constraints.get(&name_qi) { + ctx.method_own_constraints.entry(*name).or_insert_with(|| constraints.clone()); + } // Import ctor_details if this is a constructor alias (e.g. `:|` for `NonEmpty`) 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())); @@ -8501,7 +9367,12 @@ fn import_item( for ctor in &import_ctors { if let Some(scheme) = exports.values.get(ctor) { - env.insert_scheme(maybe_qualify_symbol(ctor.name, qualifier), scheme.clone()); + let scheme = if let Some(co) = canonical_origins { + canonicalize_scheme_type_cons(scheme, co) + } else { + scheme.clone() + }; + env.insert_scheme(maybe_qualify_symbol(ctor.name, qualifier), scheme); } } // Import ctor_details for ALL constructors when at least some are imported, @@ -8516,13 +9387,8 @@ fn import_item( 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); - } + // Qualified import: store under qualified key only + ctx.ctor_details.insert(QualifiedIdent { module: Some(q), name: ctor.name }, entry); } else { ctx.ctor_details.insert(*ctor, entry); } @@ -8532,22 +9398,103 @@ fn import_item( // 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_qi) { - let sym_alias = (alias.0.iter().map(|p| p.name).collect(), alias.1.clone()); - ctx.state.type_aliases.insert(*name, sym_alias.clone()); + let sym_params: Vec = alias.0.iter().map(|p| p.name).collect(); + if qualifier.is_none() { + let body = if let Some(co) = canonical_origins { + canonicalize_type_cons(&alias.1, co) + } else { + alias.1.clone() + }; + ctx.state.type_aliases.insert(*name, (sym_params.clone(), body)); + ctx.qualified_import_unqual_aliases.remove(name); + } if let Some(q) = qualifier { + // Canonicalize body for qualified import + let mod_sym = module_name_to_symbol(_module_name); + let mut type_names: HashSet = HashSet::new(); + for (&n, &origin) in &exports.type_origins { + if origin == mod_sym { type_names.insert(n); } + } + let body = canonicalize_alias_body_types(&alias.1, mod_sym, &type_names, Some(*name)); let qualified_name = maybe_qualify_symbol(*name, Some(q)); - ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.state.type_aliases.insert(qualified_name, (sym_params.clone(), body.clone())); ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(name_qi, Some(q))); + // Register under canonical key + if let Some(co) = canonical_origins { + if let Some(&origin) = co.get(name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(*name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), body)); + } + } + } else { + // Register under canonical key (unqualified import) + // Skip for zero-param aliases to avoid self-referential expansion loops. + if !sym_params.is_empty() { + if let Some(co) = canonical_origins { + if let Some(&origin) = co.get(name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(*name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), alias.1.clone())); + } + } + } } } } else if let Some(alias) = exports.type_aliases.get(&name_qi) { // Type alias import - let sym_alias = (alias.0.iter().map(|p| p.name).collect(), alias.1.clone()); - ctx.state.type_aliases.insert(*name, sym_alias.clone()); + let sym_params: Vec = alias.0.iter().map(|p| p.name).collect(); + if qualifier.is_none() { + // Canonicalize alias body to avoid collisions with local type aliases. + // E.g., `type HasuraClient = Client ...` where `Client` is from + // GraphQL.Client.Types — if the importing module also defines + // `type Client = { ... }`, the unqualified `Client` in the alias body + // must be qualified to prevent incorrect expansion. + let body = if let Some(co) = canonical_origins { + canonicalize_type_cons(&alias.1, co) + } else { + alias.1.clone() + }; + ctx.state.type_aliases.insert(*name, (sym_params.clone(), body)); + ctx.qualified_import_unqual_aliases.remove(name); + } if qualifier.is_some() { + // Canonicalize body for qualified import + let mod_sym = module_name_to_symbol(_module_name); + let alias_names: HashSet = exports.type_aliases.keys().map(|k| k.name).collect(); + let body = canonicalize_alias_body_types(&alias.1, mod_sym, &alias_names, Some(*name)); let qualified_name = maybe_qualify_symbol(*name, qualifier); - ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.state.type_aliases.insert(qualified_name, (sym_params.clone(), body.clone())); ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(name_qi, qualifier)); + // Register under canonical key (skip zero-param to avoid self-ref loops) + if !sym_params.is_empty() { + if let Some(co) = canonical_origins { + if let Some(&origin) = co.get(name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(*name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), body)); + } + } + } + } else { + // Register under canonical key (unqualified import, skip zero-param) + if !sym_params.is_empty() { + if let Some(co) = canonical_origins { + if let Some(&origin) = co.get(name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(*name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), alias.1.clone())); + } + } + } } } else { errors.push(TypeError::UnknownImport { @@ -8577,6 +9524,9 @@ fn import_item( if exports.constrained_class_methods.contains(method_name) { ctx.constrained_class_methods.insert(method_name.name); } + if let Some(constraints) = exports.method_own_constraints.get(method_name) { + ctx.method_own_constraints.entry(method_name.name).or_insert_with(|| constraints.clone()); + } // 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. @@ -8593,8 +9543,19 @@ fn import_item( 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) { - let _arity = alias.0.len(); - ctx.state.type_aliases.insert(target.name, (alias.0.iter().map(|p| p.name).collect(), alias.1.clone())); + let sym_params: Vec = alias.0.iter().map(|p| p.name).collect(); + if qualifier.is_none() { + ctx.state.type_aliases.insert(target.name, (sym_params.clone(), alias.1.clone())); + } else { + let mod_sym = module_name_to_symbol(_module_name); + let mut type_names: HashSet = HashSet::new(); + for (&n, &origin) in &exports.type_origins { + if origin == mod_sym { type_names.insert(n); } + } + let body = canonicalize_alias_body_types(&alias.1, mod_sym, &type_names, Some(target.name)); + let qualified_name = maybe_qualify_symbol(target.name, qualifier); + ctx.state.type_aliases.insert(qualified_name, (sym_params, body)); + } } } else { errors.push(TypeError::UnknownImport { @@ -8609,6 +9570,7 @@ fn import_item( /// Import all names except those in the hidden set. /// If `qualifier` is Some, env entries are stored with qualified keys. fn import_all_except( + from: Option, exports: &ModuleExports, hidden: &HashSet, env: &mut Env, @@ -8616,6 +9578,7 @@ fn import_all_except( _instances: &mut HashMap, Vec<(QualifiedIdent, Vec)>)>>, qualifier: Option, local_data_type_names: &HashSet, + canonical_origins: &Option>, ) { // Import class method info first so we can detect conflicts for (name, info) in &exports.class_methods { @@ -8633,8 +9596,13 @@ fn import_all_except( { continue; } - // Values are already alias-expanded at export time. - env.insert_scheme(maybe_qualify_symbol(name.name, qualifier), scheme.clone()); + // Canonicalize type constructors that collide with local type aliases + let scheme = if let Some(co) = canonical_origins { + canonicalize_scheme_type_cons(scheme, co) + } else { + scheme.clone() + }; + env.insert_scheme(maybe_qualify_symbol(name.name, qualifier), scheme); } } for (name, ctors) in &exports.data_constructors { @@ -8646,7 +9614,12 @@ fn import_all_except( for ctor in ctors { if !hidden.contains(&ctor.name) { if let Some(details) = exports.ctor_details.get(ctor) { - ctx.ctor_details.insert(*ctor, (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone())); + let entry = (details.0, details.1.iter().map(|s| s.name).collect(), details.2.clone()); + if let Some(q) = qualifier { + ctx.ctor_details.insert(QualifiedIdent { module: Some(q), name: ctor.name }, entry); + } else { + ctx.ctor_details.insert(*ctor, entry); + } } } } @@ -8686,21 +9659,70 @@ fn import_all_except( ctx.constrained_class_methods.insert(name.name); } } + for (name, constraints) in &exports.method_own_constraints { + if !hidden.contains(&name.name) { + ctx.method_own_constraints.entry(name.name).or_insert_with(|| constraints.clone()); + } + } + // For qualified imports, build set of type names originating from source module. + let (source_module_sym, exported_type_names) = if qualifier.is_some() { + let mod_sym = from.as_ref().map(module_name_to_symbol); + let mut type_names: HashSet = HashSet::new(); + if let Some(mod_sym) = mod_sym { + for (&name, &origin) in &exports.type_origins { + if origin == mod_sym { + type_names.insert(name); + } + } + } + (mod_sym, type_names) + } else { + (None, HashSet::new()) + }; for (name, alias) in &exports.type_aliases { 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()); + let sym_params: Vec = alias.0.iter().map(|p| p.name).collect(); + // Canonicalize alias body with canonical_origins to prevent local aliases + // from intercepting type constructor references in imported alias bodies. + let body_canonicalized = if let Some(co) = canonical_origins { + canonicalize_type_cons(&alias.1, co) + } else { + alias.1.clone() + }; + if qualifier.is_none() { + // Unqualified import: register under unqualified key. + let collides_with_local_data = local_data_type_names.contains(&name.name); + if !collides_with_local_data { + ctx.state.type_aliases.insert(name.name, (sym_params.clone(), body_canonicalized.clone())); + ctx.qualified_import_unqual_aliases.remove(&name.name); + } } + // Canonicalize alias body for qualified imports. + let body_for_qualified = if let Some(mod_sym) = source_module_sym { + canonicalize_alias_body_types(&body_canonicalized, mod_sym, &exported_type_names, Some(name.name)) + } else { + body_canonicalized.clone() + }; if qualifier.is_some() { let qualified_name = maybe_qualify_symbol(name.name, qualifier); - ctx.state.type_aliases.insert(qualified_name, sym_alias); + ctx.state.type_aliases.insert(qualified_name, (sym_params.clone(), body_for_qualified.clone())); ctx.qualified_type_alias_names.insert(maybe_qualify_qualified_ident(*name, qualifier)); } + // Register under canonical qualified key so alias expansion works after + // canonicalize_type_cons qualifies type constructors. + // Skip for zero-param aliases to avoid self-referential expansion loops. + if !sym_params.is_empty() { + if let Some(co) = canonical_origins { + if let Some(&origin) = co.get(&name.name) { + let origin_str = crate::interner::resolve(origin).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let canonical_key = crate::interner::intern(&format!("{}.{}", origin_str, name_str)); + let body = if qualifier.is_some() { body_for_qualified.clone() } else { body_canonicalized.clone() }; + ctx.state.type_aliases.entry(canonical_key) + .or_insert((sym_params.clone(), body)); + } + } + } } } for (name, arity) in &exports.type_con_arities { @@ -8952,6 +9974,9 @@ fn filter_exports( if all.constrained_class_methods.contains(&name_qi) { result.constrained_class_methods.insert(name_qi); } + if let Some(constraints) = all.method_own_constraints.get(&name_qi) { + result.method_own_constraints.insert(name_qi, constraints.clone()); + } // Also export ctor_details if this is a constructor alias (e.g. `:|`) if let Some(details) = all.ctor_details.get(&name_qi) { result.ctor_details.insert(name_qi, details.clone()); @@ -9049,6 +10074,9 @@ fn filter_exports( if all.constrained_class_methods.contains(method_name) { result.constrained_class_methods.insert(*method_name); } + if let Some(constraints) = all.method_own_constraints.get(method_name) { + result.method_own_constraints.insert(*method_name, constraints.clone()); + } } } // Export instances for this class @@ -9081,10 +10109,11 @@ fn filter_exports( result.values.insert(*name, scheme.clone()); } for (name, ctors) in &all.data_constructors { - result.data_constructors.insert(*name, ctors.clone()); + // Don't overwrite entries already set by explicit Export::Type + result.data_constructors.entry(*name).or_insert_with(|| ctors.clone()); } for (name, details) in &all.ctor_details { - result.ctor_details.insert(*name, details.clone()); + result.ctor_details.entry(*name).or_insert_with(|| details.clone()); } for (name, info) in &all.class_methods { result.class_methods.insert(*name, info.clone()); @@ -9110,6 +10139,9 @@ fn filter_exports( for name in &all.constrained_class_methods { result.constrained_class_methods.insert(*name); } + for (name, constraints) in &all.method_own_constraints { + result.method_own_constraints.insert(*name, constraints.clone()); + } for (name, alias) in &all.type_aliases { result.type_aliases.insert(*name, alias.clone()); } @@ -9270,10 +10302,15 @@ fn filter_exports( type_origins.insert(name.name, (origin, import_qual, is_local_def)); } } - result.data_constructors.insert(*name, ctors.clone()); + // Don't overwrite data_constructors already set by an explicit + // Export::Type — the explicit export has the correct constructor + // list for the locally-defined type, while a module re-export + // may carry a same-named type from a different module. + result.data_constructors.entry(*name).or_insert_with(|| ctors.clone()); } for (name, details) in &mod_exports.ctor_details { - result.ctor_details.insert(*name, details.clone()); + // Don't overwrite ctor_details already set by Export::Type + result.ctor_details.entry(*name).or_insert_with(|| details.clone()); } for (name, target) in &mod_exports.type_operators { let imported = filter @@ -9325,8 +10362,15 @@ fn filter_exports( for name in &mod_exports.constrained_class_methods { result.constrained_class_methods.insert(*name); } + for (name, constraints) in &mod_exports.method_own_constraints { + result.method_own_constraints.insert(*name, constraints.clone()); + } for (name, alias) in &mod_exports.type_aliases { - result.type_aliases.insert(*name, alias.clone()); + // Don't overwrite locally-defined aliases with re-exported ones. + // E.g. `module Table (module ColFilterControls, Input, ...)` should + // keep Table's own `Input` (7 params) rather than overwriting it + // with ColFilterControls' `Input` (3 params). + result.type_aliases.entry(*name).or_insert_with(|| alias.clone()); } for (name, count) in &mod_exports.class_param_counts { result.class_param_counts.insert(*name, *count); @@ -9418,6 +10462,7 @@ fn filter_exports( result.signature_constraints = all.signature_constraints.clone(); result.partial_dischargers = all.partial_dischargers.clone(); result.type_con_arities = all.type_con_arities.clone(); + result.method_own_constraints = all.method_own_constraints.clone(); result } @@ -9552,9 +10597,25 @@ fn check_multi_eq_exhaustiveness( // 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`). + // + // Similarly, if any other unconditional equation at this position + // doesn't itself contain array patterns, the array binder is covered. + // E.g., `f { success: [] } = ...; f { error: Nothing } = ...` — the + // second equation covers all cases regardless of `success`, so the + // array binder in the first doesn't cause partiality. + let array_covered_by_other_eq = decls.iter().any(|decl| { + if let Decl::Value { binders, guarded, .. } = decl { + if is_unconditional_for_exhaustiveness(guarded) { + if let Some(binder) = binders.get(idx) { + return !contains_inherently_partial_binder(binder); + } + } + } + false + }); let is_known_adt = extract_type_con(param_ty) .map_or(false, |tn| ctx.data_constructors.contains_key(&tn)); - if !is_known_adt { + if !is_known_adt && !array_covered_by_other_eq { let partial_sym = crate::interner::intern("Partial"); errors.push(TypeError::NoInstanceFound { span, @@ -10799,9 +11860,11 @@ fn check_instance_depth( // 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) + // Use lookup_instances for qualified fallback (e.g. SimpleJson.WriteForeign → WriteForeign). if depth > 0 { if let Some(kc) = known_classes { - if !kc.contains(class_name) && !instances.contains_key(class_name) { + let kc_known = kc.contains(class_name) || (class_name.module.is_some() && kc.iter().any(|k| k.name == class_name.name)); + if !kc_known && lookup_instances(instances, class_name).is_none() { return InstanceResult::UnknownClass(*class_name); } } @@ -10903,7 +11966,7 @@ fn check_instance_depth( .iter() .map(|t| { let mut expanding = HashSet::new(); - expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding, None) }) .collect(); @@ -10918,7 +11981,7 @@ fn check_instance_depth( .iter() .map(|t| { let mut expanding = HashSet::new(); - expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding, None) }) .collect(); if expanded_inst_types.len() != expanded_args.len() { @@ -10944,26 +12007,28 @@ fn check_instance_depth( let substituted_args: Vec = c_args.iter().map(|t| apply_var_subst(&subst, t)).collect(); let has_unbound_vars = substituted_args.iter().any(|t| { - fn has_var_not_in_subst(ty: &Type, subst: &HashMap) -> bool { + fn has_var_or_unif(ty: &Type, subst: &HashMap) -> bool { match ty { Type::Var(v) => !subst.contains_key(v), + // Unification variables are unresolved — can't check constraints involving them + Type::Unif(_) => true, Type::App(f, a) => { - has_var_not_in_subst(f, subst) || has_var_not_in_subst(a, subst) + has_var_or_unif(f, subst) || has_var_or_unif(a, subst) } Type::Fun(a, b) => { - has_var_not_in_subst(a, subst) || has_var_not_in_subst(b, subst) + has_var_or_unif(a, subst) || has_var_or_unif(b, subst) } - Type::Forall(_, body) => has_var_not_in_subst(body, subst), + Type::Forall(_, body) => has_var_or_unif(body, subst), Type::Record(fields, tail) => { - fields.iter().any(|(_, t)| has_var_not_in_subst(t, subst)) + fields.iter().any(|(_, t)| has_var_or_unif(t, subst)) || tail .as_ref() - .map_or(false, |t| has_var_not_in_subst(t, subst)) + .map_or(false, |t| has_var_or_unif(t, subst)) } _ => false, } } - has_var_not_in_subst(t, &subst) + has_var_or_unif(t, &subst) }); if has_unbound_vars { continue; @@ -11081,7 +12146,7 @@ fn has_matching_instance_depth( .iter() .map(|t| { let mut expanding = HashSet::new(); - expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding, None) }) .collect(); @@ -11098,7 +12163,7 @@ fn has_matching_instance_depth( .iter() .map(|t| { let mut expanding = HashSet::new(); - expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding) + expand_type_aliases_limited_inner(t, type_aliases, type_con_arities, 0, &mut expanding, None) }) .collect(); if expanded_inst_types.len() != expanded_args.len() { @@ -12535,6 +13600,25 @@ enum CoercibleResult { KindMismatch, } +/// Returns true if the type has unif vars outside of record row tail positions. +/// Used to decide whether to skip Coercible solving in the has_unsolved block: +/// - Unif vars in structural positions (bare, App args) → skip (can't solve yet) +/// - Unif vars only in row tails → don't skip (solver can still determine coercibility) +fn has_unif_outside_row_tails(ty: &Type) -> bool { + match ty { + Type::Unif(_) => true, + Type::Con(_) | Type::Var(_) | Type::TypeString(_) | Type::TypeInt(_) => false, + Type::App(f, a) => has_unif_outside_row_tails(f) || has_unif_outside_row_tails(a), + Type::Fun(a, b) => has_unif_outside_row_tails(a) || has_unif_outside_row_tails(b), + Type::Record(fields, _tail) => { + // Fields must not have unif vars in structural positions. + // The tail itself is a row tail position, so unif vars there are fine. + fields.iter().any(|(_, ft)| has_unif_outside_row_tails(ft)) + } + Type::Forall(_, body) => has_unif_outside_row_tails(body), + } +} + /// Solve a `Coercible a b` constraint. /// Uses role-based decomposition and newtype unwrapping. /// `givens` are Coercible pairs assumed to hold (from the function's signature constraints). @@ -13403,6 +14487,7 @@ fn check_class_param_kind_consistency( None => return Ok(()), }; + // Extract the class parameter kind (strip the result Constraint). // E.g., (ix -> ix -> Type -> Type) -> Constraint → ix -> ix -> Type -> Type let param_kind = match class_kind { diff --git a/src/typechecker/convert.rs b/src/typechecker/convert.rs index 9d37520f..2d99f45e 100644 --- a/src/typechecker/convert.rs +++ b/src/typechecker/convert.rs @@ -95,7 +95,7 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap { + TypeExpr::Row { fields, tail, is_record, .. } => { let field_types: Vec<_> = fields .iter() .map(|f| { @@ -103,6 +103,15 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap>()?; + // Normalize bare row `(| r)` (not record `{ | r }`) with no fields to + // just the tail variable. This ensures that `IProp (| r) i` passes the + // row variable directly to the alias, rather than wrapping it in an extra + // Record layer that prevents unification with bare type variables. + if !*is_record && field_types.is_empty() { + if let Some(t) = tail { + return convert_type_expr(t, type_ops); + } + } let tail_ty = tail .as_ref() .map(|t| convert_type_expr(t, type_ops)) @@ -134,6 +143,12 @@ pub fn convert_type_expr(ty: &TypeExpr, type_ops: &HashMap { + Ok(Type::Var(crate::interner::intern("_"))) + } + } } diff --git a/src/typechecker/error.rs b/src/typechecker/error.rs index a51c261b..0675689d 100644 --- a/src/typechecker/error.rs +++ b/src/typechecker/error.rs @@ -55,7 +55,7 @@ pub enum TypeError { /// No instance found for a type class constraint #[error("No type class instance was found for {class_name} {args} at {span}", - args = type_args.iter().map(|ty| format!("{}", ty)).collect::>().join(" ") + args = type_args.iter().map(|ty| format!("{}", ty)).collect::>().join(" "), )] NoInstanceFound { span: Span, diff --git a/src/typechecker/infer.rs b/src/typechecker/infer.rs index 8f3f298d..979c84c5 100644 --- a/src/typechecker/infer.rs +++ b/src/typechecker/infer.rs @@ -9,6 +9,43 @@ use crate::typechecker::error::TypeError; use crate::typechecker::types::{Role, Scheme, Type}; use crate::typechecker::unify::UnifyState; +/// Convert an ast::Expr to an ast::TypeExpr for VTA reinterpretation. +/// Returns None if the expression can't be converted to a type. +fn expr_to_type_expr(expr: &Expr) -> Option { + use crate::ast::TypeExpr; + use crate::ast::DefinitionSite; + use crate::cst::Spanned; + match expr { + Expr::Var { span, name, .. } => Some(TypeExpr::Var { + span: *span, + name: Spanned::new(name.name, *span), + }), + Expr::Constructor { span, name, .. } => Some(TypeExpr::Constructor { + span: *span, + name: name.clone(), + definition_site: DefinitionSite::Local(*span), + }), + Expr::App { span, func, arg } => Some(TypeExpr::App { + span: *span, + constructor: Box::new(expr_to_type_expr(func)?), + arg: Box::new(expr_to_type_expr(arg)?), + }), + Expr::Hole { span, name } => Some(TypeExpr::Hole { + span: *span, + name: *name, + }), + Expr::Literal { span, lit: Literal::String(s) } => Some(TypeExpr::StringLiteral { + span: *span, + value: s.clone(), + }), + Expr::Literal { span, lit: Literal::Int(n) } => Some(TypeExpr::IntLiteral { + span: *span, + value: *n, + }), + _ => None, + } +} + /// Check if a binder introduces reserved do-notation names (`bind` or `discard`). fn check_do_reserved_names(binder: &Binder) -> Result<(), TypeError> { if let Binder::Var { name, .. } = binder { @@ -65,6 +102,10 @@ pub struct InferCtx { /// These get implicit dictionary parameters, making them functions even with 0 explicit binders. /// Used to avoid false CycleInDeclaration errors for instance methods. pub constrained_class_methods: HashSet, + /// Method-level constraint class names from class definitions. + /// Maps method name → set of constraint class names from the method type. + /// Used to set current_given_expanded for instance method body checks. + pub method_own_constraints: HashMap>, /// Whether we're checking a full module (enables scope checks for desugared names) pub module_mode: bool, /// Names that are ambiguous due to being imported from multiple modules. @@ -105,6 +146,11 @@ pub struct InferCtx { /// 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, + /// Classes given by the current function's own signature constraints (with transitive + /// superclass expansion). Set before each function body check, cleared after. + /// Used to filter sig_deferred_constraints at push time: if a called function's + /// constraint is already given by the caller's own signature, don't defer it. + pub current_given_expanded: 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 @@ -134,6 +180,12 @@ pub struct InferCtx { /// Non-exhaustive pattern errors collected during case expression inference. /// Consumed by check.rs to emit NonExhaustivePattern errors. pub non_exhaustive_errors: Vec, + /// Alias names that were inserted into `state.type_aliases` under unqualified keys + /// solely because of qualified imports (e.g. `import M as Q`). These should NOT be + /// re-exported, since in PureScript qualified imports don't make names available + /// unqualified. If a subsequent unqualified import provides the same alias name, + /// it's removed from this set. + pub qualified_import_unqual_aliases: HashSet, } impl InferCtx { @@ -152,6 +204,7 @@ impl InferCtx { value_fixities: HashMap::new(), function_op_aliases: HashSet::new(), constrained_class_methods: HashSet::new(), + method_own_constraints: HashMap::new(), module_mode: false, scope_conflicts: HashSet::new(), operator_class_targets: HashMap::new(), @@ -162,6 +215,7 @@ impl InferCtx { sig_deferred_constraints: Vec::new(), chained_classes: HashSet::new(), given_class_names: HashSet::new(), + current_given_expanded: HashSet::new(), type_roles: HashMap::new(), newtype_names: HashSet::new(), scoped_type_vars: HashSet::new(), @@ -171,6 +225,7 @@ impl InferCtx { class_param_app_args: HashMap::new(), class_method_schemes: HashMap::new(), non_exhaustive_errors: Vec::new(), + qualified_import_unqual_aliases: HashSet::new(), } } @@ -279,10 +334,15 @@ impl InferCtx { } 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 { - span: other.span(), - feature: format!("inference for this expression form"), - }), + Expr::AsPattern { span, name, pattern } => { + match expr_to_type_expr(pattern) { + Some(ty_expr) => self.infer_visible_type_app(env, *span, name, &ty_expr), + None => Err(TypeError::NotImplemented { + span: *span, + feature: format!("as-pattern in expression context"), + }), + } + } } } @@ -424,7 +484,9 @@ impl InferCtx { } } - if !self.given_class_names.contains(&class_name) { + if !self.given_class_names.contains(&class_name) + && !self.current_given_expanded.contains(&class_name.name) + { self.deferred_constraints.push((span, class_name, constraint_types)); } @@ -456,7 +518,11 @@ impl InferCtx { ); if has_solver { self.deferred_constraints.push((span, *class_name, subst_args)); - } else { + } else if !self.current_given_expanded.contains(&class_name.name) { + // Only defer if the class is NOT given by the calling + // function's own signature constraints (including + // transitive superclasses). If given, the caller's own + // callers will satisfy it — no need to check instances. self.sig_deferred_constraints.push((span, *class_name, subst_args)); } } @@ -670,9 +736,16 @@ impl InferCtx { remaining = *ret; } _ => { + // remaining is not a Fun (e.g. a unif var from a wildcard `_`). + // Decompose it: unify remaining = param_ty -> ?ret so that + // the signature correctly captures all lambda parameters. let param_ty = Type::Unif(self.state.fresh_var()); + let ret_ty = Type::Unif(self.state.fresh_var()); + let fun_ty = Type::fun(param_ty.clone(), ret_ty.clone()); + let _ = self.state.unify(binder.span(), &remaining, &fun_ty); self.infer_binder(&mut current_env, binder, ¶m_ty)?; param_types.push(param_ty); + remaining = ret_ty; } } } @@ -776,9 +849,10 @@ impl InferCtx { _ => false, }; let saved_partial = if discharges_partial { - let saved = self.has_partial_lambda; + let saved_flag = self.has_partial_lambda; + let saved_errors = std::mem::take(&mut self.non_exhaustive_errors); self.has_partial_lambda = false; - Some(saved) + Some((saved_flag, saved_errors)) } else { None }; @@ -788,9 +862,10 @@ impl InferCtx { let pre_arg_var_count = self.state.var_count(); let arg_ty = self.infer(env, arg)?; - // Restore has_partial_lambda if we saved it for a Partial-discharging function - if let Some(saved) = saved_partial { - self.has_partial_lambda = saved; + // Restore has_partial_lambda and discard non-exhaustive errors from inside unsafePartial + if let Some((saved_flag, saved_errors)) = saved_partial { + self.has_partial_lambda = saved_flag; + self.non_exhaustive_errors = saved_errors; } // Higher-rank type checking: when the function expects a polymorphic argument @@ -1470,6 +1545,12 @@ impl InferCtx { Expr::VisibleTypeApp { span, func, ty } => { self.infer_visible_type_app(env, *span, func, ty) } + Expr::AsPattern { span, name, pattern } => { + match expr_to_type_expr(pattern) { + Some(ty_expr) => self.infer_visible_type_app(env, *span, name, &ty_expr), + None => self.infer(env, expr), + } + } other => self.infer(env, other), } } @@ -2232,22 +2313,40 @@ impl InferCtx { result: &Expr, ) -> Result { let functor_ty = Type::Unif(self.state.fresh_var()); - let mut current_env = env.child(); + // In ado (applicative do), `<-` bindings are independent — each `<-` expression + // runs in the applicative context and can only see the outer env + `let` bindings, + // NOT other `<-` bindings. `let` bindings CAN see prior `<-` bindings (they get + // moved into the result expression in the desugaring). The `in` expression sees all. + // + // expr_env: for inferring `<-` expressions (accumulates only `let` bindings) + // result_env: for `let` bindings and the `in` expression (accumulates everything) + let mut expr_env = env.child(); + let mut result_env = env.child(); for stmt in statements { match stmt { crate::ast::DoStatement::Bind { binder, expr, .. } => { - let expr_ty = self.infer(¤t_env, expr)?; + // Infer the expression in expr_env (no <- bindings visible) + let expr_ty = self.infer(&expr_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)?; + // Add binder to result_env only (visible in `let` and `in`) + self.infer_binder(&mut result_env, binder, &inner_ty)?; } crate::ast::DoStatement::Let { bindings, .. } => { - self.process_let_bindings(&mut current_env, bindings)?; + // Let bindings can see prior <- bindings, so process in result_env. + // Then copy newly added names to expr_env for subsequent <- expressions. + let before: std::collections::HashSet = result_env.top_bindings().keys().copied().collect(); + self.process_let_bindings(&mut result_env, bindings)?; + for (name, scheme) in result_env.top_bindings() { + if !before.contains(name) { + expr_env.insert_scheme(*name, scheme.clone()); + } + } } crate::ast::DoStatement::Discard { expr, .. } => { - let expr_ty = self.infer(¤t_env, expr)?; + let expr_ty = self.infer(&expr_env, expr)?; let discard_inner = Type::Unif(self.state.fresh_var()); let expected = Type::app(functor_ty.clone(), discard_inner); self.state.unify(span, &expr_ty, &expected)?; @@ -2255,7 +2354,7 @@ impl InferCtx { } } - let result_ty = self.infer(¤t_env, result)?; + let result_ty = self.infer(&result_env, result)?; Ok(Type::app(functor_ty, result_ty)) } @@ -2295,25 +2394,11 @@ impl InferCtx { Ok(()) } 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 }; - 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 { - span: *span, - name: name.name, - expected: expected_arity, - found: args.len(), - }); - } - } - // Look up constructor type in env (handle qualified names) let lookup_result = env.lookup(lookup_name); match lookup_result { @@ -2321,6 +2406,24 @@ impl InferCtx { let mut ctor_ty = self.instantiate(scheme); ctor_ty = self.instantiate_forall_type(ctor_ty)?; + // Count the expected arity from the constructor type + let mut expected_arity = 0; + { + let mut t = &ctor_ty; + while let Type::Fun(_, ret) = t { + expected_arity += 1; + t = ret; + } + } + if args.len() != expected_arity { + return Err(TypeError::IncorrectConstructorArity { + span: *span, + name: name.name, + expected: expected_arity, + found: args.len(), + }); + } + // Peel off argument types for arg_binder in args { match ctor_ty { @@ -2569,8 +2672,15 @@ pub fn classify_binder(binder: &Binder, has_catchall: &mut bool, covered: &mut V Binder::Typed { binder: inner, .. } => { classify_binder(inner, has_catchall, covered); } - Binder::Array { .. } | Binder::Record { .. } => { - // These don't contribute to constructor exhaustiveness + Binder::Record { .. } => { + // Record patterns are irrefutable — they match any value of the record type. + // Treat them as catchalls so the exhaustiveness checker doesn't falsely report + // missing constructors when a type alias (e.g. `type Input = { ... }`) collides + // with a data type of the same name in data_constructors. + *has_catchall = true; + } + Binder::Array { .. } => { + // Array patterns don't contribute to constructor exhaustiveness } } } @@ -2755,25 +2865,25 @@ pub fn check_exhaustiveness( // 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 - }; + // may have been overwritten by the wrong type. Detect this by checking if ALL + // covered constructors appear in all_ctors; if some don't, we have a name collision + // with partial overlap (e.g. both Action types have "Init" but different other ctors). + let all_covered_match = !covered.is_empty() && covered.iter().all(|c| all_ctors.iter().any(|ac| ac.name == *c)); + if !all_covered_match && !covered.is_empty() { + // The data_constructors entry for this type name has been overwritten by a + // different type from another module (name collision). Since we can't reliably + // determine the correct constructor set, bail out rather than report false + // non-exhaustive pattern errors. + return None; + } + + // After resolution, re-check: if none of the covered constructors appear in + // all_ctors, the data_constructors lookup found a wrong type (name collision + // between modules, e.g. Modal.Output vs some other Output). Bail out rather + // than reporting false NonExhaustivePattern errors. + if !covered.is_empty() && !covered.iter().any(|c| all_ctors.iter().any(|ac| ac.name == *c)) { + return None; + } // 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, @@ -2806,6 +2916,17 @@ pub fn check_exhaustiveness( .collect(); if !missing_at_this_level.is_empty() { + // Before reporting, check if the covered constructors completely cover any + // known type's constructor set. This handles name collisions where + // data_constructors returns the wrong type's constructors (e.g., two modules + // export types with overlapping constructor names like Action). + if !resolved_covered.is_empty() { + for (_, ctors) in data_constructors { + if !ctors.is_empty() && ctors.iter().all(|c| resolved_covered.contains(&c.name)) { + return None; // Exhaustive for this type + } + } + } // Missing constructors — report them let missing_strs: Vec = missing_at_this_level .iter() @@ -2827,7 +2948,14 @@ pub fn check_exhaustiveness( Some(d) => d, None => continue, }; - let (_parent_type, type_var_syms, field_types) = details; + let (parent_type, type_var_syms, field_types) = details; + + // Verify the ctor_details entry belongs to the correct parent type. + // Name collisions (e.g. `ResponseUpdate` ctor in both `ResponseUpdate` data type + // and `Output` data type) can cause ctor_details to contain the wrong entry. + if parent_type.name != type_name.name { + continue; + } // Only recurse into single-field constructors if field_types.len() != 1 { @@ -2901,73 +3029,6 @@ fn expr_references_name(expr: &Expr, target: Symbol, _let_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. diff --git a/src/typechecker/kind.rs b/src/typechecker/kind.rs index 16449e43..49eaf781 100644 --- a/src/typechecker/kind.rs +++ b/src/typechecker/kind.rs @@ -143,14 +143,17 @@ impl KindState { /// Unify two kinds, mapping unification errors to kind-specific errors. 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::KindsDoNotUnify { span, expected, found } - } - TypeError::InfiniteType { span, var, ty } => { - TypeError::InfiniteKind { span, var, ty } + self.state.unify(span, expected, found).map_err(|e| { + // Debug: log "Type vs (Row Type)" errors with caller tag + match e { + TypeError::UnificationError { span, expected, found } => { + TypeError::KindsDoNotUnify { span, expected, found } + } + TypeError::InfiniteType { span, var, ty } => { + TypeError::InfiniteKind { span, var, ty } + } + other => other, } - other => other, }) } @@ -694,8 +697,11 @@ pub fn infer_kind( }; if let Some(tail_expr) = tail { let tail_kind = infer_kind(ks, tail_expr, type_var_kinds, type_ops, self_type)?; - if !*is_record { - // Bare row: tail should have kind Row k + if *is_record { + // Record tails always have kind Row Type (Record :: Row Type -> Type) + let row_type = Type::kind_row_of(Type::kind_type()); + ks.unify_kinds(*span, &row_type, &tail_kind)?; + } else { let row_k = Type::kind_row_of(elem_kind.clone()); ks.unify_kinds(*span, &row_k, &tail_kind)?; } @@ -723,6 +729,9 @@ pub fn infer_kind( TypeExpr::Wildcard { .. } => Ok(ks.fresh_kind_var()), TypeExpr::Hole { .. } => Ok(ks.fresh_kind_var()), + + // Array/As patterns in type context — only used for binder conversion, give fresh kind + TypeExpr::ArrayPattern { .. } | TypeExpr::AsPattern { .. } => Ok(ks.fresh_kind_var()), } } @@ -1777,8 +1786,46 @@ pub fn check_inferred_type_kind( let remapped = remap_unif_vars(kind, &mut old_to_new, &mut ks); ks.type_kinds.insert(name, remapped); } - let _ = infer_runtime_kind(ty, &mut ks, span)?; - Ok(()) + match infer_runtime_kind(ty, &mut ks, span) { + Ok(_) => Ok(()), + Err(TypeError::KindsDoNotUnify { span, expected, found }) => { + // This check's purpose is to catch type-level literals used in wrong + // positions (e.g. "foo" -> String where "foo" :: Symbol should be :: Type). + // Only report errors that involve Symbol or Int kinds (the literal kinds). + // Other KDU errors are likely false positives from incomplete kind info + // (e.g. Row/Type confusion from our Type::Record representation, or + // unknown type constructors getting fresh kind vars). + fn involves_literal_kind(ty: &Type) -> bool { + match ty { + Type::Con(name) => { + crate::interner::resolve(name.name) + .map_or(false, |n| n == "Symbol" || n == "Int") + } + Type::App(f, a) => involves_literal_kind(f) || involves_literal_kind(a), + Type::Fun(from, to) => involves_literal_kind(from) || involves_literal_kind(to), + _ => false, + } + } + fn has_unresolved_kind_var(ty: &Type) -> bool { + match ty { + Type::Unif(_) => true, + Type::App(f, a) => has_unresolved_kind_var(f) || has_unresolved_kind_var(a), + Type::Fun(from, to) => has_unresolved_kind_var(from) || has_unresolved_kind_var(to), + _ => false, + } + } + // Suppress errors with unresolved kind variables — they indicate incomplete + // kind inference and the mismatch may resolve with more information. + if has_unresolved_kind_var(&expected) || has_unresolved_kind_var(&found) { + Ok(()) + } else if involves_literal_kind(&expected) || involves_literal_kind(&found) { + Err(TypeError::KindsDoNotUnify { span, expected, found }) + } else { + Ok(()) + } + } + Err(e) => Err(e), + } } /// Public wrapper for `infer_runtime_kind` for use in constraint resolution kind checks. diff --git a/src/typechecker/registry.rs b/src/typechecker/registry.rs index 0fb44011..28e1a955 100644 --- a/src/typechecker/registry.rs +++ b/src/typechecker/registry.rs @@ -70,6 +70,12 @@ pub struct ModuleExports { /// Pre-computed self-referential type aliases from this module. /// Imported at import time to avoid recomputing from scratch. pub self_referential_aliases: HashSet, + /// Class superclass constraints: class_name → (type_var_names, [(superclass_class, superclass_args)]) + /// Used to transitively expand "given" constraints from type signatures. + pub class_superclasses: HashMap, Vec<(QualifiedIdent, Vec)>)>, + /// Method-level constraint class names from class definitions. + /// Maps method name → constraint class names. Used for current_given_expanded in instance methods. + pub method_own_constraints: HashMap>, } /// Registry of compiled modules, used to resolve imports. diff --git a/src/typechecker/resolve.rs b/src/typechecker/resolve.rs index ff904dd1..03761ed9 100644 --- a/src/typechecker/resolve.rs +++ b/src/typechecker/resolve.rs @@ -1279,6 +1279,14 @@ fn walk_type_expr(r: &mut Resolver, ty: &TypeExpr, type_vars: &HashSet) walk_type_expr(r, kind, type_vars); } TypeExpr::StringLiteral { .. } | TypeExpr::IntLiteral { .. } => {} + TypeExpr::ArrayPattern { elements, .. } => { + for elem in elements { + walk_type_expr(r, elem, type_vars); + } + } + TypeExpr::AsPattern { ty, .. } => { + walk_type_expr(r, ty, type_vars); + } } } diff --git a/src/typechecker/unify.rs b/src/typechecker/unify.rs index 4da077ae..d1903032 100644 --- a/src/typechecker/unify.rs +++ b/src/typechecker/unify.rs @@ -16,7 +16,10 @@ use std::collections::HashSet; /// 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, + // Only unqualified Con references count as self-referential. + // Qualified references like `AskForReview.Model` are different types, + // not self-references to the local `Model` alias. + Type::Con(n) => n.name == name && n.module.is_none() && expected_args == 0, Type::App(_, _) => { // Collect the full App spine let mut head = ty; @@ -25,10 +28,16 @@ fn contains_self_referential_usage(ty: &Type, name: Symbol, expected_args: usize args.push(a.as_ref()); head = f.as_ref(); } - // Check if this App chain is headed by Con(name) with exactly expected_args + // Check if this App chain is headed by Con(name) if let Type::Con(n) = head { - if n.name == name && args.len() == expected_args { - return true; + if n.name == name && n.module.is_none() { + // For zero-param aliases, ANY occurrence of the name in the body is + // self-referential — zonk_ref expands bare Con eagerly, then recurses + // into App components, reaching the inner bare Con again (infinite loop). + // For non-zero-param aliases, only matching arg counts are self-referential. + if expected_args == 0 || args.len() == expected_args { + return true; + } } } // Recurse into head and all args @@ -127,6 +136,17 @@ pub struct UnifyState { /// 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, + /// Maps canonical module paths to import-alias module names. + /// E.g. "Components.AskForReview" → "AskForReview". + /// Used in `try_expand_alias` to resolve canonical qualified type constructors + /// (from exported types) back to import-alias-qualified alias keys. + pub canonical_to_qualifier: std::collections::HashMap, + /// Zero-arg alias names that genuinely collide with data types from different modules. + /// When set (non-empty), zonk_ref's Type::Con branch skips expansion for these names. + /// Only populated temporarily during export-time zonking. Unlike con_zero_blockers + /// (which scans imported alias bodies), this uses registry type_con_arities to verify + /// that the name is actually a data type in some module (not just a blocked alias). + pub zonk_con_blockers: std::collections::HashSet, } impl UnifyState { @@ -140,6 +160,8 @@ impl UnifyState { self_referential_aliases: std::collections::HashSet::new(), qualifier_to_canonical: std::collections::HashMap::new(), type_con_arities: std::collections::HashMap::new(), + canonical_to_qualifier: std::collections::HashMap::new(), + zonk_con_blockers: std::collections::HashSet::new(), } } @@ -295,7 +317,12 @@ impl UnifyState { // 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) } + if expanded == *ty { None } else { + // Recursively zonk the expanded body so that nested aliases + // (e.g. zero-arg aliases like `ResponseUpdate` inside the + // body of a multi-arg alias like `HTML`) are also expanded. + Some(self.zonk(expanded)) + } } else { None } @@ -308,7 +335,10 @@ impl UnifyState { } // Try alias expansion on the full rebuilt type if self.is_alias_app_non_self_referential(&result) { - Some(self.try_expand_alias(result)) + let expanded = self.try_expand_alias(result); + // Recursively zonk so nested aliases in the expanded body + // are also expanded. + Some(self.zonk(expanded)) } else { Some(result) } @@ -363,6 +393,11 @@ impl UnifyState { if *sym == wk.function { return Some(Type::Con(wk.arrow)); } + // Skip expansion if this name is in the zonk_con_blockers set + // (zero-arg alias genuinely colliding with a data type from a different module). + if !self.zonk_con_blockers.is_empty() && self.zonk_con_blockers.contains(&sym.name) { + 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). @@ -373,11 +408,24 @@ impl UnifyState { } else { sym.name }; - let is_zero_arg = self.type_aliases.get(&alias_key).map_or(false, |(params, _)| params.is_empty()); + 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) } + if expanded == *ty { + None + } else if contains_self_referential_usage(&expanded, sym.name, 0) { + // Runtime detection: the expanded body still references the + // same name — this alias is self-referential through imports. + // Mark it to prevent future expansion attempts. + self.self_referential_aliases.insert(sym.name); + None + } else { + // Recursively zonk the expanded body so that nested aliases + // within the zero-arg alias body are also expanded. + Some(self.zonk(expanded)) + } } else { None } @@ -420,7 +468,11 @@ impl UnifyState { } fn unify_inner(&mut self, span: Span, t1: &Type, t2: &Type) -> Result<(), TypeError> { - if self.unify_depth > 500 { + if self.unify_depth > 800 { + // Even at extreme depth, identical types should unify trivially + if t1 == t2 { + return Ok(()); + } return Err(TypeError::UnificationError { span, expected: t1.clone(), @@ -478,8 +530,7 @@ impl UnifyState { // 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 + self.try_expand_alias(t1.clone()) } else { t1 }; @@ -689,6 +740,22 @@ impl UnifyState { self.unify_records(span, fields1, tail1, &empty, &Some(Box::new((**row).clone())), &t1, &t2) } + // App(f, a) vs Record — when f is not Con("Record") (e.g. a unif var), + // convert the Record to App(Con("Record"), row) and unify structurally. + // This handles `g row` matching `{ fields }` by solving g = Record. + (Type::App(f, a), Type::Record(fields, tail)) => { + let record_con = Type::Con(WELL_KNOWN.record); + self.unify(span, f, &record_con)?; + let row = Type::Record(fields.clone(), tail.clone()); + self.unify(span, a, &row) + } + (Type::Record(fields, tail), Type::App(f, a)) => { + let record_con = Type::Con(WELL_KNOWN.record); + self.unify(span, f, &record_con)?; + let row = Type::Record(fields.clone(), tail.clone()); + self.unify(span, a, &row) + } + // Forall types: instantiate with fresh vars and unify bodies (Type::Forall(vars, body), _) => { let instantiated = self.instantiate_forall(vars, body); @@ -897,23 +964,40 @@ impl UnifyState { 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 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(); - let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); - self.type_aliases.get(&qualified) + crate::interner::intern(&format!("{}.{}", mod_str, name_str)) } else { - self.type_aliases.get(&name.name) + name.name }; - return alias_entry - .map_or(false, |(params, _)| params.len() == arg_count); + // Check self-referential using the proper alias key (qualified when + // available) to avoid false positives: "AskForReview.Model" should NOT + // be blocked just because a different "Model" alias is self-referential. + if self.self_referential_aliases.contains(&alias_key) { + return false; + } + if let Some((params, _)) = self.type_aliases.get(&alias_key) { + return params.len() == arg_count; + } + // Try canonical → import-alias mapping (e.g. "Components.AskForReview.Model" + // → "AskForReview.Model") before falling back to unqualified. + if let Some(module) = name.module { + if let Some(&alias_mod) = self.canonical_to_qualifier.get(&module) { + let alias_mod_str = crate::interner::resolve(alias_mod).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let import_key = crate::interner::intern(&format!("{}.{}", alias_mod_str, name_str)); + if self.self_referential_aliases.contains(&import_key) { + return false; + } + if let Some((params, _)) = self.type_aliases.get(&import_key) { + return params.len() == arg_count; + } + } + } + // Do NOT fall back to unqualified — the existing try_expand_alias + // unqualified fallback handles that with proper cycle guards. + return false; } _ => return false, } @@ -935,21 +1019,38 @@ impl UnifyState { } } 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 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(); - let qualified = crate::interner::intern(&format!("{}.{}", mod_str, name_str)); - self.type_aliases.get(&qualified) + crate::interner::intern(&format!("{}.{}", mod_str, name_str)) } else { - self.type_aliases.get(&name.name) + name.name }; - if let Some((params, _)) = alias_entry { + if self.self_referential_aliases.contains(&alias_key) { + return 0; + } + if let Some((params, _)) = self.type_aliases.get(&alias_key) { if params.len() > applied { return params.len() - applied; } + return 0; + } + // Try canonical → import-alias mapping + if let Some(module) = name.module { + if let Some(&alias_mod) = self.canonical_to_qualifier.get(&module) { + let alias_mod_str = crate::interner::resolve(alias_mod).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let import_key = crate::interner::intern(&format!("{}.{}", alias_mod_str, name_str)); + if self.self_referential_aliases.contains(&import_key) { + return 0; + } + if let Some((params, _)) = self.type_aliases.get(&import_key) { + if params.len() > applied { + return params.len() - applied; + } + return 0; + } + } } } 0 @@ -959,6 +1060,7 @@ impl UnifyState { if self.type_aliases.is_empty() { return ty; } + super::check_deadline(); // Collect the head constructor and arguments from nested App chains let mut args = Vec::new(); let mut head = &ty; @@ -986,11 +1088,61 @@ impl UnifyState { if self.expanding_aliases.contains(&alias_key) { return ty; } - // 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(); + // For qualified keys, if the exact qualified form isn't found, fall back + // to the unqualified name. This handles cross-module canonicalization where + // module A canonicalizes Con("Model") to Con("AdminDashboard.Model.Model"), + // but module B (which inherits the qualified form) doesn't have the alias + // under the qualified key — only under the unqualified "Model" key. + // When falling back, also check the unqualified name in expanding_aliases + // to prevent cycles (e.g. `type Number = P.Number` where P.Number falls + // back to unqualified "Number"). + let (actual_alias_key, alias_entry) = if let Some(entry) = self.type_aliases.get(&alias_key).cloned() { + (alias_key, Some(entry)) + } else if let Some(module) = name.module { + // Try mapping canonical module path → import-alias-qualified key. + // E.g. "Components.AskForReview.Model" → "AskForReview.Model" when + // `import Components.AskForReview as AskForReview`. + let import_alias_result = self.canonical_to_qualifier.get(&module).and_then(|&alias_mod| { + let alias_mod_str = crate::interner::resolve(alias_mod).unwrap_or_default(); + let name_str = crate::interner::resolve(name.name).unwrap_or_default(); + let import_key = crate::interner::intern(&format!("{}.{}", alias_mod_str, name_str)); + if self.expanding_aliases.contains(&import_key) { + return None; + } + self.type_aliases.get(&import_key).cloned().map(|entry| (import_key, entry)) + }); + if let Some((key, entry)) = import_alias_result { + (key, Some(entry)) + } else if self.canonical_to_qualifier.contains_key(&module) { + // Module is known (in canonical_to_qualifier) but alias not found + // under the import-qualified key. This type was explicitly qualified + // (e.g. canonicalized to avoid local alias collision). Don't fall + // back to unqualified — that would incorrectly expand a data type + // constructor using a local alias of the same name. + (alias_key, None) + } else if name.module.is_some() { + // The Con has a module qualifier but neither the exact qualified key + // nor any import-alias-qualified key matched an alias. The module isn't + // in canonical_to_qualifier either. This means the type comes from a + // module we don't have a direct import relationship with (e.g., it + // appeared in an imported type scheme). Don't fall back to unqualified — + // that would incorrectly expand a foreign data type (like + // `GraphQL.Client.Types.Client`) using a local alias of the same name. + (alias_key, None) + } else { + // Unqualified Con: check cycle guard then try alias expansion + if self.expanding_aliases.contains(&name.name) { + return ty; + } + if let Some(entry) = self.type_aliases.get(&name.name).cloned() { + (name.name, Some(entry)) + } else { + (alias_key, None) + } + } + } else { + (alias_key, None) + }; if let Some((params, body)) = alias_entry { // Args collected in reverse order (outermost last) args.reverse(); @@ -999,22 +1151,9 @@ impl UnifyState { // 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) { + if self.self_referential_aliases.contains(&actual_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 @@ -1027,9 +1166,27 @@ impl UnifyState { for extra_arg in &args[params.len()..] { expanded = Type::App(Box::new(expanded), Box::new((*extra_arg).clone())); } - self.expanding_aliases.push(alias_key); + self.expanding_aliases.push(actual_alias_key); + // Also guard the original canonical key and unqualified name + // to prevent re-entry when the body re-introduces either form. + let guard_canonical = alias_key != actual_alias_key; + if guard_canonical { + self.expanding_aliases.push(alias_key); + } + // Guard unqualified name too to prevent cross-module expansion + // of a different alias with the same unqualified name. + let guard_unqual = name.module.is_some() && name.name != actual_alias_key && name.name != alias_key; + if guard_unqual { + self.expanding_aliases.push(name.name); + } // Recursively expand nested aliases in the result let result = self.try_expand_alias(expanded); + if guard_unqual { + self.expanding_aliases.pop(); + } + if guard_canonical { + self.expanding_aliases.pop(); + } self.expanding_aliases.pop(); return result; } diff --git a/tests/build.rs b/tests/build.rs index 1729fbb0..a27b9a16 100644 --- a/tests/build.rs +++ b/tests/build.rs @@ -415,7 +415,7 @@ fn build_fixture_original_compiler_passing() { .collect(); assert!( - !failures.is_empty(), + failures.is_empty(), "{}/{} build units failed:\n\n{}", failures.len(), total, @@ -709,7 +709,7 @@ fn build_fixture_original_compiler_failing() { // 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 +#[timeout(120000)] // 120s timeout — debug mode is ~10x slower than release fn build_all_packages() { let _ = env_logger::try_init(); let started = std::time::Instant::now(); @@ -726,6 +726,8 @@ fn build_all_packages() { let options = BuildOptions { module_timeout: Some(std::time::Duration::from_secs(timeout_secs)), output_dir: None, + sequential: false, + fail_fast: false, }; // Discover all packages with src/ directories @@ -892,9 +894,9 @@ fn build_all_packages() { // run with: RUST_LOG=debug cargo test --test build build_from_sources -- --exact --ignored -// for release (RECOMMENDED): cargo test --release --test build build_from_sources -- --exact --ignored +// for release (RECOMMENDED): RUST_LOG=debug FAIL_FAST=1 cargo test --release --test build build_from_sources -- --exact --ignored --no-capture #[test] -#[ignore] // This is for manually invocation with +#[ignore] // This is for manually invocation #[timeout(600000)] // 10 min timeout fn build_from_sources() { @@ -904,7 +906,8 @@ fn build_from_sources() { let application_dir = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("CARGO_MANIFEST_DIR has no parent") - .join("application"); + .join("application-copy/application"); + assert!( application_dir.exists(), "OA application directory not found at: {}", @@ -917,11 +920,16 @@ fn build_from_sources() { let timeout_secs: u64 = std::env::var("MODULE_TIMEOUT_SECS") .ok() .and_then(|s| s.parse().ok()) - .unwrap_or(30); + .unwrap_or(60); + + let sequential = std::env::var("SEQUENTIAL").is_ok(); + let fail_fast = std::env::var("FAIL_FAST").is_ok(); let options = BuildOptions { module_timeout: Some(std::time::Duration::from_secs(timeout_secs)), - output_dir: None + output_dir: None, + sequential, + fail_fast, }; // Step 1: Glob all patterns to collect file paths @@ -986,12 +994,15 @@ fn build_from_sources() { for e in &result.build_errors { match e { BuildError::TypecheckTimeout { .. } => { + eprintln!("TypecheckTimeout: {e}"); timeouts.push(format!(" {}", e)); } BuildError::TypecheckPanic { .. } => { + eprintln!("TypecheckPanic: {e}"); panics.push(format!(" {}", e)); } _ => { + eprintln!("Other error: {e}"); other_errors.push(format!(" {}", e)); } } @@ -1021,6 +1032,16 @@ fn build_from_sources() { result.modules.len() ); + assert!(timeouts.is_empty(), "No timeouts"); + assert!(panics.is_empty(), "No panics"); + if !other_errors.is_empty() { + eprintln!("\n{} other build errors:", other_errors.len()); + for e in &other_errors { + eprintln!("{}", e); + } + } + // Note: other_errors (parse failures, missing modules) are expected and not asserted. + // Error distribution let mut error_counts: std::collections::HashMap = std::collections::HashMap::new(); @@ -1036,6 +1057,150 @@ fn build_from_sources() { for (code, count) in &sorted_counts { eprintln!(" {:>4} {}", count, code); } + // Count modules by exclusive error type + { + let mut nep_only = 0; + let mut uv_only = 0; + let mut ue_only = 0; + let mut nif_only = 0; + for m in &result.modules { + if !m.type_errors.is_empty() { + let codes: std::collections::HashSet = m.type_errors.iter().map(|e| e.code()).collect(); + if codes.len() == 1 { + let code = codes.into_iter().next().unwrap(); + match code.as_str() { + "NonExhaustivePattern" => nep_only += 1, + "UndefinedVariable" => uv_only += 1, + "UnificationError" => ue_only += 1, + "NoInstanceFound" => nif_only += 1, + _ => {} + } + } + } + } + let mut kam_only = 0; + let mut kdu_only = 0; + let mut mnf_only = 0; + let mut sc_only = 0; + let mut ica_only = 0; + for m in &result.modules { + if !m.type_errors.is_empty() { + let codes: std::collections::HashSet = m.type_errors.iter().map(|e| e.code()).collect(); + if codes.len() == 1 { + match codes.iter().next().unwrap().as_str() { + "KindArityMismatch" => kam_only += 1, + "KindsDoNotUnify" => kdu_only += 1, + "ModuleNotFound" => mnf_only += 1, + "ScopeConflict" => sc_only += 1, + "IncorrectConstructorArity" => ica_only += 1, + _ => {} + } + } + } + } + eprintln!(" Single-error-type modules: NEP={}, UV={}, UE={}, NIF={}, KAM={}, KDU={}, MNF={}, SC={}, ICA={}", nep_only, uv_only, ue_only, nif_only, kam_only, kdu_only, mnf_only, sc_only, ica_only); + } + // KDU pattern breakdown — write to file to avoid OOM with --nocapture + { + use std::io::Write; + let mut kdu_patterns: std::collections::HashMap = std::collections::HashMap::new(); + for m in &result.modules { + for e in &m.type_errors { + if let purescript_fast_compiler::typechecker::error::TypeError::KindsDoNotUnify { expected, found, .. } = e { + let pattern = format!("{} vs {}", expected, found); + *kdu_patterns.entry(pattern).or_default() += 1; + } + } + } + if !kdu_patterns.is_empty() { + if let Ok(mut f) = std::fs::File::create(concat!(env!("CARGO_MANIFEST_DIR"), "/kdu_patterns.txt")) { + let mut sorted: Vec<_> = kdu_patterns.iter().collect(); + sorted.sort_by(|a, b| b.1.cmp(a.1)); + let _ = writeln!(f, "KDU pattern breakdown:"); + for (pattern, count) in &sorted { + let _ = writeln!(f, " {:>4} {}", count, pattern); + } + } + } + } + // Show first 40 NEP errors + let mut nep_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.contains("cover all inputs") { + eprintln!(" NEP in {}: {}", mod_name, &err_str[..std::cmp::min(150, err_str.len())]); + nep_count += 1; + if nep_count >= 40 { break; } + } + } + // Show first 30 KDU errors with module names + let mut kdu_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.starts_with("Could not match kind") { + eprintln!(" KDU in {}: {}", mod_name, &err_str[..std::cmp::min(120, err_str.len())]); + kdu_count += 1; + if kdu_count >= 30 { break; } + } + } + // Show all PartiallyAppliedSynonym errors + for (mod_name, _path, err_str) in &type_errors { + if err_str.contains("partially applied") { + eprintln!(" PAS in {}: {}", mod_name, err_str); + } + } + // Show first 20 IncorrectConstructorArity errors + let mut ica_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.starts_with("Constructor") && err_str.contains("expects") { + eprintln!(" ICA in {}: {}", mod_name, &err_str[..std::cmp::min(120, err_str.len())]); + ica_count += 1; + if ica_count >= 20 { break; } + } + } + // Show first 30 InvalidInstanceHead errors + let mut iih_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.contains("Invalid instance head") || err_str.contains("instance head") { + eprintln!(" IIH in {}: {}", mod_name, &err_str[..std::cmp::min(200, err_str.len())]); + iih_count += 1; + if iih_count >= 30 { break; } + } + } + // Show first 30 UndefinedVariable errors + let mut uv_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.starts_with("Unknown value") { + eprintln!(" UV in {}: {}", mod_name, &err_str[..std::cmp::min(120, err_str.len())]); + uv_count += 1; + if uv_count >= 30 { break; } + } + } + // Show first 30 UnificationError messages + let mut ue_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.starts_with("Could not match type") { + eprintln!(" UE in {}: {}", mod_name, &err_str[..std::cmp::min(200, err_str.len())]); + ue_count += 1; + if ue_count >= 120 { break; } + } + } + // Show first 20 UnknownName errors + let mut un_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.starts_with("Unknown") { + eprintln!(" UN in {}: {}", mod_name, &err_str[..std::cmp::min(120, err_str.len())]); + un_count += 1; + if un_count >= 20 { break; } + } + } + // Show first 20 KindArityMismatch errors + let mut kam_count = 0; + for (mod_name, _path, err_str) in &type_errors { + if err_str.contains("expected") && err_str.contains("arguments") && err_str.contains("type") { + eprintln!(" KAM in {}: {}", mod_name, &err_str[..std::cmp::min(150, err_str.len())]); + kam_count += 1; + if kam_count >= 20 { break; } + } + } } assert!( @@ -1049,4 +1214,19 @@ fn build_from_sources() { "Modules panicked during typechecking:\n{}", panics.join("\n") ); + + + // Note: other_errors (parse failures, module-not-found) are expected — + // not all PureScript syntax is supported by the parser yet. + + assert!( + fails == 0, + "Type errors: {} modules with errors\n\ + Error distribution:\n{}", + fails, + error_counts.iter() + .map(|(code, count)| format!(" {:>4} {}", count, code)) + .collect::>() + .join("\n") + ); } diff --git a/tests/fixtures/original-compiler/failing/CtorArityTooFew.purs b/tests/fixtures/original-compiler/failing/CtorArityTooFew.purs new file mode 100644 index 00000000..855d2b3a --- /dev/null +++ b/tests/fixtures/original-compiler/failing/CtorArityTooFew.purs @@ -0,0 +1,7 @@ +-- @shouldFailWith IncorrectConstructorArity +module Main where + +data Pair a b = Pair a b + +test :: forall a b. Pair a b -> a +test (Pair x) = x diff --git a/tests/fixtures/original-compiler/failing/DeriveInstanceRecordAlias.purs b/tests/fixtures/original-compiler/failing/DeriveInstanceRecordAlias.purs new file mode 100644 index 00000000..591f5e12 --- /dev/null +++ b/tests/fixtures/original-compiler/failing/DeriveInstanceRecordAlias.purs @@ -0,0 +1,9 @@ +-- @shouldFailWith InvalidInstanceHead +module Main where + +import Prelude + +type MyRec r = { x :: Int | r } + +instance showMyRec :: Show (MyRec r) where + show _ = "" diff --git a/tests/fixtures/original-compiler/failing/NoInstanceMixedUnif.purs b/tests/fixtures/original-compiler/failing/NoInstanceMixedUnif.purs new file mode 100644 index 00000000..eacfedc1 --- /dev/null +++ b/tests/fixtures/original-compiler/failing/NoInstanceMixedUnif.purs @@ -0,0 +1,10 @@ +-- @shouldFailWith NoInstanceFound +module Main where + +class Foo a b | a -> b + +bar :: forall a. Foo Int a => Int -> Int +bar x = x + +test :: Int +test = bar 0 diff --git a/tests/fixtures/original-compiler/failing/QualifiedAliasShadow.purs b/tests/fixtures/original-compiler/failing/QualifiedAliasShadow.purs new file mode 100644 index 00000000..6c17d6b1 --- /dev/null +++ b/tests/fixtures/original-compiler/failing/QualifiedAliasShadow.purs @@ -0,0 +1,8 @@ +-- @shouldFailWith UnknownName +module Main where + +import Lib as Q + +-- Event should NOT be available unqualified since we used `import Lib as Q` +x :: Event +x = { name: "test" } diff --git a/tests/fixtures/original-compiler/failing/QualifiedAliasShadow/Lib.purs b/tests/fixtures/original-compiler/failing/QualifiedAliasShadow/Lib.purs new file mode 100644 index 00000000..c7ce8b76 --- /dev/null +++ b/tests/fixtures/original-compiler/failing/QualifiedAliasShadow/Lib.purs @@ -0,0 +1,5 @@ +module Lib where + +type Event = { name :: String } + +data Foo = Foo diff --git a/tests/fixtures/original-compiler/passing/AdoScopingIndependent.purs b/tests/fixtures/original-compiler/passing/AdoScopingIndependent.purs new file mode 100644 index 00000000..834a79c2 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/AdoScopingIndependent.purs @@ -0,0 +1,38 @@ +module Main where + +import Prelude + +-- Tests that ado `<-` bindings don't shadow each other. +-- In ado, each `<-` expression is independent (applicative). +-- `let` bindings CAN see prior `<-` bindings. + +data Maybe a = Nothing | Just a + +instance functorMaybe :: Functor Maybe where + map _ Nothing = Nothing + map f (Just a) = Just (f a) + +instance applyMaybe :: Apply Maybe where + apply (Just f) (Just a) = Just (f a) + apply _ _ = Nothing + +-- x is a `<-` binding, y is a `let` that depends on x, +-- both should be available in `in`. +test :: Maybe Int +test = ado + x <- Just 1 + let y = x + 1 + in x + y + +-- Two `<-` bindings that rebind the same name as a local let. +-- The second `<-` expression should NOT see the first `<-` binding. +type Result = { a :: Int, b :: Int } + +outerVal :: Int +outerVal = 42 + +testNoShadow :: Maybe Result +testNoShadow = ado + x <- Just 1 + z <- Just outerVal -- should see outerVal, not be affected by x binding + in { a: x, b: z } diff --git a/tests/fixtures/original-compiler/passing/AppRecordUnify.purs b/tests/fixtures/original-compiler/passing/AppRecordUnify.purs new file mode 100644 index 00000000..9eae1903 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/AppRecordUnify.purs @@ -0,0 +1,9 @@ +module Main where + +-- Tests that App(f, a) can unify with Record types. +-- When f is a unif var, it should solve to Record. +apply :: forall f a. f a -> f a +apply x = x + +test :: { x :: Int } +test = apply { x: 42 } diff --git a/tests/fixtures/original-compiler/passing/CoercibleNestedConstructors.purs b/tests/fixtures/original-compiler/passing/CoercibleNestedConstructors.purs new file mode 100644 index 00000000..baa16e16 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/CoercibleNestedConstructors.purs @@ -0,0 +1,29 @@ +module Main where + +import Safe.Coerce (coerce) +import Prim.Coerce (class Coercible) + +-- Regression test: Coercible solver correctly handles coerce through +-- nested type constructors with newtype arguments. Previously, when +-- unsolved unification variables appeared in type constructor argument +-- positions (e.g. Map ?a (Array ?b)), the solver would incorrectly +-- emit TypesDoNotUnify or NoInstanceFound instead of deferring. + +newtype Id a = Id a + +newtype N = N String + +data Pair a b = Pair a b +type role Pair representational representational + +-- Coerce through nested type constructors (similar to Map Id (Array Response)) +unwrapPair :: Pair (Id Int) (Array N) -> Pair Int (Array String) +unwrapPair = coerce + +-- Coerce with inferred intermediate types: +-- Here the Coercible constraint is generated before the full type is known, +-- exercising the deferred constraint path. +convert :: Pair (Id Int) (Array N) -> Pair Int (Array String) +convert x = + let result = coerce x + in result diff --git a/tests/fixtures/original-compiler/passing/CoerciblePolykinded.purs b/tests/fixtures/original-compiler/passing/CoerciblePolykinded.purs new file mode 100644 index 00000000..1c63f618 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/CoerciblePolykinded.purs @@ -0,0 +1,19 @@ +module Main where + +import Safe.Coerce (coerce) +import Prim.Coerce (class Coercible) + +-- Regression test: Coercible should be polykinded (forall k. k -> k -> Constraint), +-- not just (Type -> Type -> Constraint). This allows coercing between newtypes +-- used at higher kinds. + +newtype MyFunctor f a = MyFunctor (f a) + +myCoerce :: forall a b. Coercible a b => a -> b +myCoerce = coerce + +-- Coercing a value at Type kind +newtype Name = Name String + +getName :: Name -> String +getName = myCoerce diff --git a/tests/fixtures/original-compiler/passing/ConZeroBlockerExport.purs b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport.purs new file mode 100644 index 00000000..ee0e0f38 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport.purs @@ -0,0 +1,12 @@ +module Main where + +import ConZeroBlockerExport.DataModule (T(..)) +import ConZeroBlockerExport.Middle (event) + +-- Regression test: when Middle module imports both `type T = { ... }` +-- (alias) and `data T` (data type), its exported value scheme for +-- `event` must preserve the data type reference, not expand it to the record. +-- This module then uses the data type T without conflict. + +test :: T +test = event { name: "test", pt: PT "hello" 42 } diff --git a/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/AliasModule.purs b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/AliasModule.purs new file mode 100644 index 00000000..19fb049d --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/AliasModule.purs @@ -0,0 +1,4 @@ +module ConZeroBlockerExport.AliasModule where + +-- Zero-param type alias +type T = { name :: String, id :: Int } diff --git a/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/DataModule.purs b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/DataModule.purs new file mode 100644 index 00000000..bf735616 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/DataModule.purs @@ -0,0 +1,4 @@ +module ConZeroBlockerExport.DataModule where + +-- Data type with same unqualified name +data T = PT String Int diff --git a/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/Middle.purs b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/Middle.purs new file mode 100644 index 00000000..3875997a --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ConZeroBlockerExport/Middle.purs @@ -0,0 +1,14 @@ +module ConZeroBlockerExport.Middle where + +import ConZeroBlockerExport.AliasModule (T) +import ConZeroBlockerExport.DataModule (T(..)) as D + +-- This module imports BOTH the alias and the data type. +-- The alias (record) is in type_aliases; the data type is in type_con_arities. +-- con_zero_blockers should prevent the alias from expanding T +-- in the exported value scheme for `event`, preserving the data type ref. + +type EventRec = { name :: String, pt :: D.T } + +event :: EventRec -> D.T +event r = r.pt diff --git a/tests/fixtures/original-compiler/passing/DeriveNewtypeClosedRecordAlias.purs b/tests/fixtures/original-compiler/passing/DeriveNewtypeClosedRecordAlias.purs new file mode 100644 index 00000000..995570de --- /dev/null +++ b/tests/fixtures/original-compiler/passing/DeriveNewtypeClosedRecordAlias.purs @@ -0,0 +1,17 @@ +module Main where + +-- Regression test: a closed record type alias using row composition +-- should not be rejected as an open record in instance heads. +-- Previously, { | Row () } expanded to Record([], Some(Record(fields, Some(Record([], None))))) +-- which was incorrectly flagged as an open record due to the Some(_) tail. + +class MonadAsk r m where + ask :: m r + +data FixM a = FixM a + +type EnvRow r = (name :: String, count :: Int | r) +type Env = { | EnvRow () } + +instance MonadAsk Env FixM where + ask = FixM { name: "", count: 0 } diff --git a/tests/fixtures/original-compiler/passing/GivenConstraintAbstract.purs b/tests/fixtures/original-compiler/passing/GivenConstraintAbstract.purs new file mode 100644 index 00000000..3405a42a --- /dev/null +++ b/tests/fixtures/original-compiler/passing/GivenConstraintAbstract.purs @@ -0,0 +1,9 @@ +module Main where + +class Foo a + +-- The Foo constraint is fully abstract (all type vars). +-- It should be treated as "given" by the signature and +-- not produce NoInstanceFound even though Foo has no instances. +bar :: forall a. Foo a => a -> Int +bar _ = 0 diff --git a/tests/fixtures/original-compiler/passing/GivenConstraintScoped.purs b/tests/fixtures/original-compiler/passing/GivenConstraintScoped.purs new file mode 100644 index 00000000..c38020b9 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/GivenConstraintScoped.purs @@ -0,0 +1,10 @@ +module Main where + +import GivenConstraintScoped.Lib (class Cl, member) + +-- Regression test: per-function given class scoping. +-- `handler` has `Cl m =>` in its signature, so Super (superclass of Cl) +-- is transitively "given" — calling member (needs Super) should work. + +handler :: forall m. Cl m => String -> m String +handler key = member key diff --git a/tests/fixtures/original-compiler/passing/GivenConstraintScoped/Lib.purs b/tests/fixtures/original-compiler/passing/GivenConstraintScoped/Lib.purs new file mode 100644 index 00000000..7a141b91 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/GivenConstraintScoped/Lib.purs @@ -0,0 +1,6 @@ +module GivenConstraintScoped.Lib where + +class Super m where + member :: String -> m String + +class Super m <= Cl m diff --git a/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision.purs b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision.purs new file mode 100644 index 00000000..94b2ec6d --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision.purs @@ -0,0 +1,15 @@ +module Main where + +import Prelude + +import ImportedAliasDataTypeCollision.Signal (Time) +import ImportedAliasDataTypeCollision.Lib (mkTime) + +-- Regression test: importing `type Time = Number` (alias) from Signal +-- while also importing values from Lib that use `Time` (data type). +-- canonical_origins must NOT rewrite the data type reference in Lib's +-- exports to a qualified form that then mismatches with the alias. + +-- Time here is the alias (= Number) +delay :: Time -> Time +delay t = t + 1.0 diff --git a/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/DataTime.purs b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/DataTime.purs new file mode 100644 index 00000000..8fdbb358 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/DataTime.purs @@ -0,0 +1,4 @@ +module ImportedAliasDataTypeCollision.DataTime where + +-- data type with same unqualified name (like Data.Time) +data Time = Time Int Int diff --git a/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Lib.purs b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Lib.purs new file mode 100644 index 00000000..20442a60 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Lib.purs @@ -0,0 +1,8 @@ +module ImportedAliasDataTypeCollision.Lib where + +import ImportedAliasDataTypeCollision.DataTime (Time(..)) + +-- This module uses Time as the DATA TYPE. +-- Its exported value schemes contain Time (the data type). +mkTime :: Int -> Int -> Time +mkTime = Time diff --git a/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Signal.purs b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Signal.purs new file mode 100644 index 00000000..69e03312 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ImportedAliasDataTypeCollision/Signal.purs @@ -0,0 +1,4 @@ +module ImportedAliasDataTypeCollision.Signal where + +-- type alias: Time = Number (like Signal.Time) +type Time = Number diff --git a/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData.purs b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData.purs new file mode 100644 index 00000000..6a09e1fe --- /dev/null +++ b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData.purs @@ -0,0 +1,26 @@ +module Main where + +import Component as Component +import Shared (ModelExt) + +-- This module defines a local `type Model` alias AND imports `Component` +-- which has `data Model`. The local alias must expand correctly: +-- con_zero_blockers must not block locally-defined aliases. + +type Model = ModelExt (page :: String, component :: Component.Model) + +-- LazyUpdate references Model in its body — this exercises alias expansion +-- of the local zero-param alias through another local alias. +type LazyUpdate = Model -> String + +update :: LazyUpdate +update m = m.name + +getName :: Model -> String +getName m = m.name + +getCount :: Model -> Int +getCount m = m.count + +getPage :: Model -> String +getPage m = m.page diff --git a/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Component.purs b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Component.purs new file mode 100644 index 00000000..33c77c81 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Component.purs @@ -0,0 +1,11 @@ +module Component where + +-- A module that defines `data Model` — a data type, not a type alias. +-- When another module imports this AND defines its own `type Model = ...` alias, +-- the local alias must still expand correctly (not be blocked by con_zero_blockers). + +data Model = Init | Ready String + +render :: Model -> String +render Init = "loading" +render (Ready s) = s diff --git a/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Shared.purs b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Shared.purs new file mode 100644 index 00000000..ba6d9d2f --- /dev/null +++ b/tests/fixtures/original-compiler/passing/LocalAliasNotBlockedByImportedData/Shared.purs @@ -0,0 +1,5 @@ +module Shared where + +-- A type alias imported by the main module that references `Model` in its body. +-- This simulates alias bodies from imported modules containing Con("Model"). +type ModelExt r = { name :: String, count :: Int | r } diff --git a/tests/fixtures/original-compiler/passing/MethodConstraintGiven.purs b/tests/fixtures/original-compiler/passing/MethodConstraintGiven.purs new file mode 100644 index 00000000..176aae93 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/MethodConstraintGiven.purs @@ -0,0 +1,19 @@ +module Main where + +import MethodConstraintGiven.Lib (class IxBind, class IxMonad, class IxApply, ibind, ipure) + +-- Regression test: when an instance method has extra typeclass constraints +-- (beyond the instance head), those constraints should be treated as "given" +-- within the method body. Here, IxApply has superclasses IxBind and IxMonad, +-- so ibind and ipure should be available in the default implementation. + +newtype Wrapper a = Wrapper a + +instance IxBind Wrapper where + ibind (Wrapper a) f = f a + +instance IxMonad Wrapper where + ipure = Wrapper + +instance IxApply Wrapper where + iapply (Wrapper f) (Wrapper a) = ipure (f a) diff --git a/tests/fixtures/original-compiler/passing/MethodConstraintGiven/Lib.purs b/tests/fixtures/original-compiler/passing/MethodConstraintGiven/Lib.purs new file mode 100644 index 00000000..88bf6c44 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/MethodConstraintGiven/Lib.purs @@ -0,0 +1,10 @@ +module MethodConstraintGiven.Lib where + +class IxBind f where + ibind :: forall a b. f a -> (a -> f b) -> f b + +class IxMonad f where + ipure :: forall a. a -> f a + +class (IxBind f, IxMonad f) <= IxApply f where + iapply :: forall a b. f (a -> b) -> f a -> f b diff --git a/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion.purs b/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion.purs new file mode 100644 index 00000000..0c86cac6 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion.purs @@ -0,0 +1,11 @@ +module Main where + +import Lib as L + +-- Qualified alias should expand correctly +x :: L.Alias +x = 42 + +-- Qualified record alias should also work +y :: L.Rec +y = { x: 1, y: "hello" } diff --git a/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion/Lib.purs b/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion/Lib.purs new file mode 100644 index 00000000..f573a330 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/QualifiedAliasExpansion/Lib.purs @@ -0,0 +1,5 @@ +module Lib where + +type Alias = Int + +type Rec = { x :: Int, y :: String } diff --git a/tests/fixtures/original-compiler/passing/RecordBinderExhaustive.purs b/tests/fixtures/original-compiler/passing/RecordBinderExhaustive.purs new file mode 100644 index 00000000..9dea2ba3 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/RecordBinderExhaustive.purs @@ -0,0 +1,9 @@ +module Main where + +type Input = { x :: Int, y :: String } + +-- Record patterns are irrefutable and should not trigger +-- non-exhaustive pattern warnings even when a data type +-- with the same name exists in data_constructors. +getX :: Input -> Int +getX { x } = x diff --git a/tests/fixtures/original-compiler/passing/SelfRefAliasQualified.purs b/tests/fixtures/original-compiler/passing/SelfRefAliasQualified.purs new file mode 100644 index 00000000..e706d85f --- /dev/null +++ b/tests/fixtures/original-compiler/passing/SelfRefAliasQualified.purs @@ -0,0 +1,11 @@ +module Main where + +import Lib as Lib + +-- Local alias named "Model" that references Lib.Model. +-- The qualified reference Lib.Model should NOT be treated as +-- self-referential just because the unqualified name matches. +type Model = Lib.Model + +test :: Model +test = { x: 42 } diff --git a/tests/fixtures/original-compiler/passing/SelfRefAliasQualified/Lib.purs b/tests/fixtures/original-compiler/passing/SelfRefAliasQualified/Lib.purs new file mode 100644 index 00000000..29c93375 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/SelfRefAliasQualified/Lib.purs @@ -0,0 +1,3 @@ +module Lib where + +type Model = { x :: Int } diff --git a/tests/fixtures/original-compiler/passing/SuperclassGiven.purs b/tests/fixtures/original-compiler/passing/SuperclassGiven.purs new file mode 100644 index 00000000..31ce4548 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/SuperclassGiven.purs @@ -0,0 +1,11 @@ +module Main where + +import SuperclassGiven.Lib (class Cl, member) + +-- Regression test: when a function's type signature has `Cl m =>`, +-- and Cl has superclass `Super m`, calling Super methods should not +-- produce NoInstanceFound errors. The Super constraint is transitively +-- "given" through the Cl superclass chain. + +handler :: forall m. Cl m => String -> m Int +handler key = member { key } diff --git a/tests/fixtures/original-compiler/passing/SuperclassGiven/Lib.purs b/tests/fixtures/original-compiler/passing/SuperclassGiven/Lib.purs new file mode 100644 index 00000000..7c3d5a55 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/SuperclassGiven/Lib.purs @@ -0,0 +1,6 @@ +module SuperclassGiven.Lib where + +class Super m where + member :: { key :: String } -> m Int + +class Super m <= Cl m diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule.purs new file mode 100644 index 00000000..53e0a721 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule.purs @@ -0,0 +1,14 @@ +module Main where + +import Data as D +import Types (A) +import Consumer (consume) + +-- Multiple modules import the same zero-param alias that +-- references a qualified data type with the same name. +-- All modules must expand the alias correctly. +make :: A +make = D.A 42 "hello" + +test :: Int +test = consume make diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Consumer.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Consumer.purs new file mode 100644 index 00000000..133c923e --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Consumer.purs @@ -0,0 +1,11 @@ +module Consumer where + +import Data as D +import Types (A) + +-- A second module importing the same zero-param alias. +-- This must also expand correctly — the fix must not +-- mark the alias as self-referential globally. +consume :: A -> Int +consume (D.A n _) = n +consume D.B = 0 diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Data.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Data.purs new file mode 100644 index 00000000..cfb85c0e --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Data.purs @@ -0,0 +1,3 @@ +module Data where + +data A a b = A a b | B diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Types.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Types.purs new file mode 100644 index 00000000..26386b11 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasMultiModule/Types.purs @@ -0,0 +1,6 @@ +module Types where + +import Data as D + +-- Zero-param alias: same unqualified name as the data type +type A = D.A Int String diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified.purs new file mode 100644 index 00000000..df4bcc63 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified.purs @@ -0,0 +1,11 @@ +module Main where + +import Lib as R +import Types (T) + +-- Import a zero-param alias that expands to a qualified data type +-- with the same unqualified name, applied to concrete type arguments. +-- The alias should expand correctly without infinite loops, +-- and the expanded type should unify with the data constructors. +test :: T +test = R.TA 42 true diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Lib.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Lib.purs new file mode 100644 index 00000000..4d1ffadf --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Lib.purs @@ -0,0 +1,11 @@ +module Lib where + +data T a b c + = TA a c + | TB b a c + +type A = Int + +type B = String + +type C = Boolean diff --git a/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Types.purs b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Types.purs new file mode 100644 index 00000000..c059d7f9 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZeroParamAliasQualified/Types.purs @@ -0,0 +1,10 @@ +module Types where + +import Lib as R + +-- Zero-param alias whose body is a qualified reference to a data type +-- with the SAME unqualified name, applied to concrete type arguments. +-- This must NOT cause an infinite expansion loop: +-- expanding T -> R.T A B C +-- must not re-expand R.T as the alias again. +type T = R.T R.A R.B R.C diff --git a/tests/fixtures/original-compiler/passing/ZonkConBlockerExport.purs b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport.purs new file mode 100644 index 00000000..01aa6ec5 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport.purs @@ -0,0 +1,17 @@ +module Main where + +import DataModule (ProgramType(..), showPT) +import AliasModule as A + +-- Uses the data type ProgramType (not the alias). +-- When exporting, zonk_con_blockers must prevent the alias from +-- being expanded incorrectly in the exported type scheme. +usePT :: ProgramType -> String +usePT pt = showPT pt + +mkTalk :: ProgramType +mkTalk = Talk + +-- Also use the alias through its qualified name +mkAlias :: A.ProgramType +mkAlias = { name: "test", code: 1 } diff --git a/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/AliasModule.purs b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/AliasModule.purs new file mode 100644 index 00000000..4725a2b2 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/AliasModule.purs @@ -0,0 +1,6 @@ +module AliasModule where + +-- Defines a type alias with the SAME unqualified name as the data type +-- in DataModule. This simulates the real-world scenario where +-- `type ProgramType = { name :: String }` collides with `data ProgramType`. +type ProgramType = { name :: String, code :: Int } diff --git a/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/DataModule.purs b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/DataModule.purs new file mode 100644 index 00000000..81eaf369 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ZonkConBlockerExport/DataModule.purs @@ -0,0 +1,9 @@ +module DataModule where + +-- Defines a data type `ProgramType` +data ProgramType = Talk | Poster | Workshop + +showPT :: ProgramType -> String +showPT Talk = "talk" +showPT Poster = "poster" +showPT Workshop = "workshop"