diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd89f05b..ef61ee72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,11 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable + - name: Set up Node.js (for codegen execution tests) + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Cache cargo registry & build artifacts uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index cd208ebe..a4ffd6b3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ Thumbs.db # Test files /test.purs *.purs.bak +/output diff --git a/src/codegen/js.rs b/src/codegen/js.rs index 3cdc3f53..e8191097 100644 --- a/src/codegen/js.rs +++ b/src/codegen/js.rs @@ -4,8 +4,8 @@ /// pretty-printed to ES module JavaScript. Mirrors the original PureScript /// compiler's Language.PureScript.CodeGen.JS module. -use std::cell::Cell; -use std::collections::{HashMap, HashSet}; +use std::cell::{Cell, RefCell}; +use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; use crate::cst::*; use crate::interner::{self, Symbol}; @@ -22,12 +22,12 @@ struct CodegenCtx<'a> { /// This module's exports (from typechecking) exports: &'a ModuleExports, /// Registry of all typechecked modules - #[allow(dead_code)] registry: &'a ModuleRegistry, /// Module name as dot-separated string (e.g. "Data.Maybe") #[allow(dead_code)] module_name: &'a str, /// Module name parts as symbols + #[allow(dead_code)] module_parts: &'a [Symbol], /// Set of names that are newtypes (newtype constructor erasure) newtype_names: &'a HashSet, @@ -39,10 +39,27 @@ struct CodegenCtx<'a> { function_op_aliases: &'a HashSet, /// Names of foreign imports in this module foreign_imports: HashSet, + /// Names declared locally in this module (values, constructors, instances) + local_names: HashSet, /// Import map: module_parts → JS variable name import_map: HashMap, String>, + /// Unqualified name → source module parts (for cross-module reference resolution) + name_source: HashMap>, + /// Operator → (target_module_parts, target_function_name) for resolving operators + operator_targets: HashMap>, Symbol)>, /// Counter for generating fresh variable names fresh_counter: Cell, + /// Maps class method symbol → dict expression, set while generating constrained function bodies. + /// e.g. `myShow` → `JsExpr::Var("dictMyShow")` when inside `showValue :: MyShow a => a -> String`. + /// For superclass methods, stores chained accessors like `dictAlternative.Applicative0().Apply0()`. + constraint_dicts: RefCell>, + /// Maps class name → dict expression. Used by `find_dict_for_class` to resolve dicts + /// for classes that may have no methods (e.g. `Alternative` which only has superclass constraints). + class_dicts: RefCell>, + /// Queue of per-call-site resolved dicts. Unlike `class_dicts` (which is reusable for + /// constraint dicts), this queue is consumed: each call to `find_dict_for_class` pops + /// the next dict for that class. This handles cases like two `show` calls at different types. + resolved_dict_queue: RefCell>>, } impl<'a> CodegenCtx<'a> { @@ -62,36 +79,53 @@ pub fn module_to_js( registry: &ModuleRegistry, has_ffi: bool, ) -> JsModule { - let mut ctx = CodegenCtx { - module, - exports, - registry, - module_name, - module_parts, - newtype_names: &exports.newtype_names, - ctor_details: &exports.ctor_details, - data_constructors: &exports.data_constructors, - function_op_aliases: &exports.function_op_aliases, - foreign_imports: HashSet::new(), - import_map: HashMap::new(), - fresh_counter: Cell::new(0), - }; + // Phase 1: Collect local names + let mut local_names: HashSet = HashSet::new(); + let mut foreign_imports_set: HashSet = HashSet::new(); + for decl in &module.decls { + match decl { + Decl::Value { name, .. } => { local_names.insert(name.value); } + Decl::Data { constructors, .. } => { + for ctor in constructors { + local_names.insert(ctor.name.value); + } + } + Decl::Newtype { constructor, .. } => { local_names.insert(constructor.value); } + Decl::Foreign { name, .. } => { + local_names.insert(name.value); + foreign_imports_set.insert(name.value); + } + Decl::Instance { name: Some(n), .. } => { local_names.insert(n.value); } + Decl::Class { members, .. } => { + for member in members { + local_names.insert(member.name.value); + } + } + _ => {} + } + } - let mut exported_names: Vec = Vec::new(); - let mut foreign_re_exports: Vec = Vec::new(); + // Phase 2: Build name resolution map (unqualified name → source module) + let mut name_source: HashMap> = HashMap::new(); + let mut import_map: HashMap, String> = HashMap::new(); + let mut operator_targets: HashMap>, Symbol)> = HashMap::new(); - // Collect foreign imports + // Collect operator → target from local fixity declarations for decl in &module.decls { - if let Decl::Foreign { name, .. } = decl { - ctx.foreign_imports.insert(name.value); + if let Decl::Fixity { target, operator, is_type: false, .. } = decl { + let target_module = target.module.map(|m| { + let mod_str = interner::resolve(m).unwrap_or_default(); + mod_str.split('.').map(|s| interner::intern(s)).collect::>() + }); + operator_targets.insert(operator.value, (target_module, target.name)); } } - // Build import statements + // Build import map and resolve unqualified names from imports let mut imports = Vec::new(); for imp in &module.imports { let parts = &imp.module.parts; - // Skip Prim imports + // Skip Prim imports (built-in types, no JS module) if !parts.is_empty() { let first = interner::resolve(parts[0]).unwrap_or_default(); if first == "Prim" { @@ -99,27 +133,213 @@ pub fn module_to_js( } } // Skip self-imports - if *parts == ctx.module_parts { + if *parts == module_parts { continue; } - if ctx.import_map.contains_key(parts) { - continue; + + // Register import JS variable name + if !import_map.contains_key(parts) { + let js_name = module_name_to_js(parts); + let mod_name_str = parts + .iter() + .map(|s| interner::resolve(*s).unwrap_or_default()) + .collect::>() + .join("."); + let path = format!("../{mod_name_str}/index.js"); + + imports.push(JsStmt::Import { + name: js_name.clone(), + path, + }); + import_map.insert(parts.clone(), js_name); } - let js_name = module_name_to_js(parts); - let mod_name_str = parts - .iter() - .map(|s| interner::resolve(*s).unwrap_or_default()) - .collect::>() - .join("."); - let path = format!("../{mod_name_str}/index.js"); + // Resolve names from this import into name_source map + if let Some(imp_exports) = registry.lookup(parts) { + match &imp.imports { + None => { + // Open import: `import Data.Maybe` — all exported names come from here + // Only register if not already claimed by a more specific import + for name in imp_exports.values.keys() { + if !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + for name in imp_exports.ctor_details.keys() { + if !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + for name in imp_exports.class_methods.keys() { + if !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + // Also import operator targets and register their target functions + for (op, target) in &imp_exports.operator_targets { + operator_targets.entry(*op).or_insert_with(|| target.clone()); + // Ensure operator target functions are resolvable via name_source + let (_, target_fn) = target; + if !local_names.contains(target_fn) { + name_source.entry(*target_fn).or_insert_with(|| parts.clone()); + } + } + } + Some(ImportList::Explicit(items)) => { + for item in items { + match item { + Import::Value(name) => { + if !local_names.contains(name) { + name_source.insert(*name, parts.clone()); + } + // Operators are parsed as Import::Value (e.g. `(<$>)`). + // If this name is an operator with a target, propagate it. + if let Some(target) = imp_exports.operator_targets.get(name) { + operator_targets.entry(*name).or_insert_with(|| target.clone()); + } + } + Import::Type(type_name, members) => { + // The type itself doesn't produce JS, but its constructors do + if let Some(DataMembers::All) = members { + if let Some(ctors) = imp_exports.data_constructors.get(type_name) { + for ctor in ctors { + if !local_names.contains(ctor) { + name_source.insert(*ctor, parts.clone()); + } + } + } + } else if let Some(DataMembers::Explicit(ctors)) = members { + for ctor in ctors { + if !local_names.contains(ctor) { + name_source.insert(*ctor, parts.clone()); + } + } + } + } + Import::Class(class_name) => { + // Import class methods + for (method, (cls, _)) in &imp_exports.class_methods { + if *cls == *class_name && !local_names.contains(method) { + name_source.insert(*method, parts.clone()); + } + } + } + Import::TypeOp(_) => {} + } + } + } + Some(ImportList::Hiding(items)) => { + let hidden: HashSet = items.iter().filter_map(|item| { + match item { + Import::Value(name) => Some(*name), + _ => None, + } + }).collect(); + for name in imp_exports.values.keys() { + if !hidden.contains(name) && !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + for name in imp_exports.ctor_details.keys() { + if !hidden.contains(name) && !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + for name in imp_exports.class_methods.keys() { + if !hidden.contains(name) && !local_names.contains(name) { + name_source.entry(*name).or_insert_with(|| parts.clone()); + } + } + // Propagate operator targets that aren't hidden + for (op, target) in &imp_exports.operator_targets { + if !hidden.contains(op) { + operator_targets.entry(*op).or_insert_with(|| target.clone()); + } + } + } + } + } + } - imports.push(JsStmt::Import { - name: js_name.clone(), - path, - }); - ctx.import_map.insert(parts.clone(), js_name); + // Supplement operator_targets from operator_class_targets (more thoroughly propagated). + // This ensures operators like <$> → map resolve even if operator_targets propagation missed them. + for (op, method) in &exports.operator_class_targets { + operator_targets.entry(*op).or_insert_with(|| (None, *method)); } + for imp in &module.imports { + if let Some(imp_exports) = registry.lookup(&imp.module.parts) { + for (op, method) in &imp_exports.operator_class_targets { + operator_targets.entry(*op).or_insert_with(|| (None, *method)); + } + } + } + + // Add imports for instance-defining modules referenced by resolved_dicts. + // Instances like showArray (from Data.Show) may not be directly imported but + // are needed for nested dictionary applications. + { + fn collect_instance_names(dict: &crate::typechecker::check::DictExpr, names: &mut Vec) { + use crate::typechecker::check::DictExpr; + match dict { + DictExpr::Var(name) => names.push(*name), + DictExpr::App(name, args) => { + names.push(*name); + for arg in args { + collect_instance_names(arg, names); + } + } + } + } + let mut needed_names = Vec::new(); + for (_, dicts) in &exports.resolved_dicts { + for (_, dict_expr) in dicts { + collect_instance_names(dict_expr, &mut needed_names); + } + } + for inst_name in &needed_names { + if let Some(mod_parts) = exports.instance_modules.get(inst_name) { + if !import_map.contains_key(mod_parts) && *mod_parts != module_parts { + let js_name = module_name_to_js(mod_parts); + let mod_name_str = mod_parts + .iter() + .map(|s| interner::resolve(*s).unwrap_or_default()) + .collect::>() + .join("."); + let path = format!("../{mod_name_str}/index.js"); + imports.push(JsStmt::Import { + name: js_name.clone(), + path, + }); + import_map.insert(mod_parts.clone(), js_name); + } + } + } + } + + let ctx = CodegenCtx { + module, + exports, + registry, + module_name, + module_parts, + newtype_names: &exports.newtype_names, + ctor_details: &exports.ctor_details, + data_constructors: &exports.data_constructors, + function_op_aliases: &exports.function_op_aliases, + foreign_imports: foreign_imports_set, + local_names, + import_map, + name_source, + operator_targets, + fresh_counter: Cell::new(0), + constraint_dicts: RefCell::new(HashMap::new()), + class_dicts: RefCell::new(HashMap::new()), + resolved_dict_queue: RefCell::new(HashMap::new()), + }; + + + let mut exported_names: Vec = Vec::new(); + let mut foreign_re_exports: Vec = Vec::new(); // Generate body declarations let mut body = Vec::new(); @@ -182,7 +402,29 @@ pub fn module_to_js( let stmts = gen_instance_decl(&ctx, decl); body.extend(stmts); } - DeclGroup::Class(_) | DeclGroup::TypeAlias | DeclGroup::Fixity + DeclGroup::Class(decl) => { + // Generate accessor functions for class methods. + // e.g. `class Semigroupoid a where compose :: ...` produces: + // var compose = function(dict) { return dict["compose"]; }; + if let Decl::Class { members, .. } = decl { + for member in members { + let method_js = ident_to_js(member.name.value); + let accessor = JsExpr::Function( + None, + vec!["dict".to_string()], + vec![JsStmt::Return(JsExpr::Indexer( + Box::new(JsExpr::Var("dict".to_string())), + Box::new(JsExpr::StringLit(method_js.clone())), + ))], + ); + body.push(JsStmt::VarDecl(method_js.clone(), Some(accessor))); + if is_exported(&ctx, member.name.value) { + exported_names.push(method_js); + } + } + } + } + DeclGroup::TypeAlias | DeclGroup::Fixity | DeclGroup::TypeSig | DeclGroup::ForeignData | DeclGroup::Derive | DeclGroup::KindSig => { // These produce no JS output @@ -190,6 +432,84 @@ pub fn module_to_js( } } + // Topologically sort declarations so each binding appears after the + // bindings it references (fixes issues like Partial where `crash` calls + // `crashWith` which must be defined first). + let mut body = topo_sort_body(body); + + // Generate re-export bindings for names exported from this module but not + // locally defined. When a module re-exports names from other modules + // (e.g. Data.Ord re-exports GT from Data.Ordering), we create local + // bindings so that references like `Data_Ord.GT` resolve at runtime. + { + let mut already_exported: HashSet = + exported_names.iter().cloned().collect(); + let already_foreign: HashSet = + foreign_re_exports.iter().cloned().collect(); + + let mut seen: HashSet = HashSet::new(); + + let re_export_candidates: Vec = exports + .values + .keys() + .chain(exports.ctor_details.keys()) + .copied() + .collect(); + + for name in re_export_candidates { + if !seen.insert(name) { + continue; + } + if ctx.local_names.contains(&name) || ctx.foreign_imports.contains(&name) { + continue; + } + + // If this is an operator, re-export its target function instead. + // Operators are syntactic aliases (e.g. `<#>` → `mapFlipped`), so we + // generate `var mapFlipped = Data_Functor.mapFlipped;` rather than the + // broken `var $less$hash$greater = Data_Functor.$less$hash$greater;`. + if let Some((target_module, target_fn)) = ctx.operator_targets.get(&name) { + let target_js = ident_to_js(*target_fn); + if already_exported.contains(&target_js) + || already_foreign.contains(&target_js) + { + continue; + } + // Use the operator's original module if available, else name_source + let source = target_module.as_ref() + .or_else(|| ctx.name_source.get(&name)); + if let Some(source_parts) = source { + if let Some(js_mod) = ctx.import_map.get(source_parts) { + body.push(JsStmt::VarDecl( + target_js.clone(), + Some(JsExpr::ModuleAccessor(js_mod.clone(), target_js.clone())), + )); + already_exported.insert(target_js.clone()); + exported_names.push(target_js); + } + } + continue; + } + + let js_name = ident_to_js(name); + if already_exported.contains(&js_name) + || already_foreign.contains(&js_name) + { + continue; + } + if let Some(source_parts) = ctx.name_source.get(&name) { + if let Some(js_mod) = ctx.import_map.get(source_parts) { + body.push(JsStmt::VarDecl( + js_name.clone(), + Some(JsExpr::ModuleAccessor(js_mod.clone(), js_name.clone())), + )); + already_exported.insert(js_name.clone()); + exported_names.push(js_name); + } + } + } + } + let foreign_module_path = if has_ffi { Some("./foreign.js".to_string()) } else { @@ -205,6 +525,154 @@ pub fn module_to_js( } } +// ===== Topological sort for declaration ordering ===== + +/// Collect all `JsExpr::Var` references in a JS expression. +fn collect_expr_var_refs(expr: &JsExpr, refs: &mut HashSet) { + match expr { + JsExpr::Var(name) => { refs.insert(name.clone()); } + JsExpr::Function(_, _, body) => { + for stmt in body { collect_stmt_var_refs(stmt, refs); } + } + JsExpr::App(callee, args) => { + collect_expr_var_refs(callee, refs); + for arg in args { collect_expr_var_refs(arg, refs); } + } + JsExpr::ArrayLit(elems) => { + for e in elems { collect_expr_var_refs(e, refs); } + } + JsExpr::ObjectLit(fields) => { + for (_, v) in fields { collect_expr_var_refs(v, refs); } + } + JsExpr::Indexer(a, b) | JsExpr::Binary(_, a, b) | JsExpr::InstanceOf(a, b) => { + collect_expr_var_refs(a, refs); + collect_expr_var_refs(b, refs); + } + JsExpr::Unary(_, e) => collect_expr_var_refs(e, refs), + JsExpr::New(ctor, args) => { + collect_expr_var_refs(ctor, refs); + for a in args { collect_expr_var_refs(a, refs); } + } + JsExpr::Ternary(c, t, e) => { + collect_expr_var_refs(c, refs); + collect_expr_var_refs(t, refs); + collect_expr_var_refs(e, refs); + } + JsExpr::NumericLit(_) | JsExpr::IntLit(_) | JsExpr::StringLit(_) + | JsExpr::BoolLit(_) | JsExpr::ModuleAccessor(_, _) | JsExpr::RawJs(_) => {} + } +} + +/// Collect all `JsExpr::Var` references in a JS statement. +fn collect_stmt_var_refs(stmt: &JsStmt, refs: &mut HashSet) { + match stmt { + JsStmt::Expr(e) | JsStmt::Return(e) | JsStmt::Throw(e) => { + collect_expr_var_refs(e, refs); + } + JsStmt::VarDecl(_, Some(e)) => collect_expr_var_refs(e, refs), + JsStmt::VarDecl(_, None) | JsStmt::ReturnVoid | JsStmt::Comment(_) => {} + JsStmt::Assign(target, value) => { + collect_expr_var_refs(target, refs); + collect_expr_var_refs(value, refs); + } + JsStmt::If(cond, then_stmts, else_stmts) => { + collect_expr_var_refs(cond, refs); + for s in then_stmts { collect_stmt_var_refs(s, refs); } + if let Some(els) = else_stmts { + for s in els { collect_stmt_var_refs(s, refs); } + } + } + JsStmt::Block(stmts) => { + for s in stmts { collect_stmt_var_refs(s, refs); } + } + JsStmt::For(_, init, bound, body) => { + collect_expr_var_refs(init, refs); + collect_expr_var_refs(bound, refs); + for s in body { collect_stmt_var_refs(s, refs); } + } + JsStmt::ForIn(_, obj, body) => { + collect_expr_var_refs(obj, refs); + for s in body { collect_stmt_var_refs(s, refs); } + } + JsStmt::While(cond, body) => { + collect_expr_var_refs(cond, refs); + for s in body { collect_stmt_var_refs(s, refs); } + } + JsStmt::Import { .. } | JsStmt::Export(_) | JsStmt::ExportFrom(_, _) + | JsStmt::RawJs(_) => {} + } +} + +/// Topologically sort body statements so that each `var` declaration appears +/// after the declarations it references. Uses Kahn's algorithm with +/// source-order tie-breaking (BTreeSet). Cycles (from mutually recursive +/// functions) are broken by falling back to source order. +fn topo_sort_body(body: Vec) -> Vec { + let n = body.len(); + if n <= 1 { return body; } + + // Map: var name → index of the VarDecl that defines it + let mut name_to_idx: HashMap<&str, usize> = HashMap::new(); + for (i, stmt) in body.iter().enumerate() { + if let JsStmt::VarDecl(name, _) = stmt { + name_to_idx.insert(name.as_str(), i); + } + } + + // Build dependency graph: dep_sets[i] = set of indices that stmt i depends on + let mut in_degree = vec![0usize; n]; + let mut dependents: Vec> = vec![Vec::new(); n]; + + for (i, stmt) in body.iter().enumerate() { + let mut refs = HashSet::new(); + collect_stmt_var_refs(stmt, &mut refs); + let mut seen_deps: HashSet = HashSet::new(); + for r in &refs { + if let Some(&j) = name_to_idx.get(r.as_str()) { + if j != i && seen_deps.insert(j) { + in_degree[i] += 1; + dependents[j].push(i); + } + } + } + } + + // Kahn's algorithm — BTreeSet ensures we always pick the lowest original + // index first among nodes with zero in-degree (preserves source order). + let mut available: BTreeSet = BTreeSet::new(); + for i in 0..n { + if in_degree[i] == 0 { + available.insert(i); + } + } + + let mut order = Vec::with_capacity(n); + while let Some(&i) = available.iter().next() { + available.remove(&i); + order.push(i); + for &dep in &dependents[i] { + in_degree[dep] -= 1; + if in_degree[dep] == 0 { + available.insert(dep); + } + } + } + + // Cycle fallback: add remaining nodes in source order + if order.len() < n { + let in_order: HashSet = order.iter().copied().collect(); + for i in 0..n { + if !in_order.contains(&i) { + order.push(i); + } + } + } + + // Reorder body according to topological order + let mut slots: Vec> = body.into_iter().map(Some).collect(); + order.into_iter().map(|i| slots[i].take().unwrap()).collect() +} + // ===== Declaration groups ===== #[allow(dead_code)] @@ -280,6 +748,10 @@ fn collect_decl_groups(decls: &[Decl]) -> Vec> { // ===== Export checking ===== fn is_exported(ctx: &CodegenCtx, name: Symbol) -> bool { + // Instances are always exported in PureScript (globally visible) + if ctx.exports.instance_modules.contains_key(&name) { + return true; + } match &ctx.module.exports { None => true, // No export list means export everything Some(export_list) => { @@ -319,9 +791,208 @@ fn is_exported(ctx: &CodegenCtx, name: Symbol) -> bool { } } +// ===== Constraint dictionary helpers ===== + +/// Look up the superclasses of a class from local exports or imported modules. +/// Returns Vec<(superclass_class_name, index)> matching the purs accessor convention. +fn lookup_superclasses(ctx: &CodegenCtx, class_name: Symbol) -> Vec<(Symbol, usize)> { + if let Some(supers) = ctx.exports.class_superclasses.get(&class_name) { + return supers.clone(); + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some(supers) = imp_exports.class_superclasses.get(&class_name) { + return supers.clone(); + } + } + } + Vec::new() +} + +/// Recursively populate `dicts` with method → dict_expr mappings for `class_name` +/// and all its transitive superclasses. +/// +/// For the root constraint, `dict_expr` is `JsExpr::Var("dictAlternative")`. +/// For superclasses, it chains accessors: `dictAlternative["Applicative0"]()`. +fn resolve_superclass_chain( + ctx: &CodegenCtx, + class_name: Symbol, + dict_expr: JsExpr, + dicts: &mut HashMap, + class_dicts: &mut HashMap, + visited: &mut HashSet, +) { + if !visited.insert(class_name) { + return; + } + + // Map the class itself to its dict expression (for classes with no methods, like Alternative) + class_dicts.entry(class_name).or_insert_with(|| dict_expr.clone()); + + // Map all methods of this class to dict_expr + for (method, (cls, _)) in &ctx.exports.class_methods { + if *cls == class_name { + dicts.entry(*method).or_insert_with(|| dict_expr.clone()); + } + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + for (method, (cls, _)) in &imp_exports.class_methods { + if *cls == class_name { + dicts.entry(*method).or_insert_with(|| dict_expr.clone()); + } + } + } + } + + // Recurse into superclasses + let superclasses = lookup_superclasses(ctx, class_name); + for (sc_class, sc_index) in superclasses { + let sc_name_str = interner::resolve(sc_class).unwrap_or_default(); + let accessor_name = format!("{sc_name_str}{sc_index}"); + // Build: dict_expr["Superclass0"]() + let sc_expr = JsExpr::App( + Box::new(JsExpr::Indexer( + Box::new(dict_expr.clone()), + Box::new(JsExpr::StringLit(accessor_name)), + )), + vec![], + ); + resolve_superclass_chain(ctx, sc_class, sc_expr, dicts, class_dicts, visited); + } +} + +/// Populate `ctx.constraint_dicts` from a list of (class_name, type_args) constraints. +/// Returns the ordered list of dict parameter names to prepend to the function. +/// +/// For each constraint, generates a parameter name like `dictMyShow` and recursively maps +/// all methods of the class and its superclasses to the appropriate dict expressions. +fn setup_constraint_dicts( + ctx: &CodegenCtx, + constraints: &[(Symbol, Vec)], +) -> Vec { + let mut dict_params: Vec = Vec::new(); + let mut dict_name_counts: HashMap = HashMap::new(); + let mut new_dicts: HashMap = HashMap::new(); + + for (class_name, _type_args) in constraints { + let class_str = interner::resolve(*class_name).unwrap_or_default(); + let base_name = format!("dict{class_str}"); + let count = dict_name_counts.entry(base_name.clone()).or_insert(0); + let dict_var = if *count == 0 { + base_name.clone() + } else { + format!("{base_name}{count}") + }; + *count += 1; + dict_params.push(dict_var.clone()); + + let dict_expr = JsExpr::Var(dict_var); + let mut visited = HashSet::new(); + let mut new_class_dicts: HashMap = HashMap::new(); + resolve_superclass_chain(ctx, *class_name, dict_expr, &mut new_dicts, &mut new_class_dicts, &mut visited); + ctx.class_dicts.borrow_mut().extend(new_class_dicts); + } + + *ctx.constraint_dicts.borrow_mut() = new_dicts; + dict_params +} + +/// Same as `setup_constraint_dicts` but takes CST `Constraint` objects (for instance declarations). +fn setup_constraint_dicts_from_cst( + ctx: &CodegenCtx, + constraints: &[crate::cst::Constraint], +) -> Vec { + let mut dict_params: Vec = Vec::new(); + let mut dict_name_counts: HashMap = HashMap::new(); + let mut new_dicts: HashMap = HashMap::new(); + + for constraint in constraints { + let class_name = constraint.class.name; + let class_str = interner::resolve(class_name).unwrap_or_default(); + let base_name = format!("dict{class_str}"); + let count = dict_name_counts.entry(base_name.clone()).or_insert(0); + let dict_var = if *count == 0 { + base_name.clone() + } else { + format!("{base_name}{count}") + }; + *count += 1; + dict_params.push(dict_var.clone()); + + let dict_expr = JsExpr::Var(dict_var); + let mut visited = HashSet::new(); + let mut new_class_dicts: HashMap = HashMap::new(); + resolve_superclass_chain(ctx, class_name, dict_expr, &mut new_dicts, &mut new_class_dicts, &mut visited); + ctx.class_dicts.borrow_mut().extend(new_class_dicts); + } + + *ctx.constraint_dicts.borrow_mut() = new_dicts; + dict_params +} + +/// Wrap a JS expression in curried dict-parameter lambdas. +/// `dict_params = ["dictShow", "dictEq"]` wraps `expr` as: +/// `function(dictShow) { return function(dictEq) { return expr; }; }` +fn wrap_expr_with_dict_params(expr: JsExpr, dict_params: &[String]) -> JsExpr { + let mut result = expr; + for dict_var in dict_params.iter().rev() { + result = JsExpr::Function( + None, + vec![dict_var.clone()], + vec![JsStmt::Return(result)], + ); + } + result +} + // ===== Value declarations ===== fn gen_value_decl(ctx: &CodegenCtx, name: Symbol, decls: &[&Decl]) -> Vec { + // Set up constraint dicts if this function has type class constraints. + let dict_params = match ctx.exports.signature_constraints.get(&name) { + Some(constraints) if !constraints.is_empty() => { + setup_constraint_dicts(ctx, constraints) + } + _ => vec![], + }; + + // Set up concrete instance dicts for bindings that use class methods at concrete types. + // e.g., `throw = compose throwException error` resolves Semigroupoid to semigroupoidFn + // Uses a queue so that multiple uses of the same class at different types (e.g. show on + // Array Int and show on Int) each get the correct dict in call-site order. + if let Some(concrete_dicts) = ctx.exports.resolved_dicts.get(&name) { + let mut queue = ctx.resolved_dict_queue.borrow_mut(); + for (class_name, dict) in concrete_dicts { + let inst_expr = dict_expr_to_js(ctx, dict); + queue.entry(*class_name).or_insert_with(VecDeque::new).push_back(inst_expr); + } + } + + let stmts = gen_value_decl_inner(ctx, name, decls); + + // Clear constraint dicts after body generation. + ctx.constraint_dicts.borrow_mut().clear(); + ctx.class_dicts.borrow_mut().clear(); + ctx.resolved_dict_queue.borrow_mut().clear(); + + if dict_params.is_empty() { + return stmts; + } + + // Wrap each generated VarDecl's expression with the dict param lambdas. + stmts + .into_iter() + .map(|stmt| match stmt { + JsStmt::VarDecl(n, Some(expr)) => { + JsStmt::VarDecl(n, Some(wrap_expr_with_dict_params(expr, &dict_params))) + } + other => other, + }) + .collect() +} + +fn gen_value_decl_inner(ctx: &CodegenCtx, name: Symbol, decls: &[&Decl]) -> Vec { let js_name = ident_to_js(name); if decls.len() == 1 { @@ -561,7 +1232,15 @@ fn gen_newtype_decl(_ctx: &CodegenCtx, decl: &Decl) -> Vec { // ===== Instance declarations ===== fn gen_instance_decl(ctx: &CodegenCtx, decl: &Decl) -> Vec { - let Decl::Instance { name, members, .. } = decl else { return vec![] }; + let Decl::Instance { name, members, constraints, types, .. } = decl else { return vec![] }; + + // Set up constraint dicts for instance constraints (e.g. `instance (Eq a) => Show (Maybe a)`). + // This lets method bodies inside the instance dispatch to constraint dicts automatically. + let dict_params = if !constraints.is_empty() { + setup_constraint_dicts_from_cst(ctx, constraints) + } else { + vec![] + }; // Instances become object literals with method implementations let instance_name = match name { @@ -569,73 +1248,571 @@ fn gen_instance_decl(ctx: &CodegenCtx, decl: &Decl) -> Vec { None => ctx.fresh_name("instance_"), }; - let mut fields = Vec::new(); + // Extract head type constructor from instance types (for instance-aware dict composition) + let head_type_con = types.first().and_then(|t| extract_head_type_con_from_cst(t)); + + // Group member equations by method name to handle multi-equation instance methods. + // e.g. `describe true = "true"; describe false = "false"` → single `describe` field. + let mut method_groups: HashMap> = HashMap::new(); + let mut method_order: Vec = Vec::new(); for member in members { - if let Decl::Value { name: method_name, binders, guarded, where_clause, .. } = member { - let method_js = ident_to_js(method_name.value); - let method_expr = if binders.is_empty() && where_clause.is_empty() { - gen_guarded_expr(ctx, guarded) + if let Decl::Value { name: method_name, .. } = member { + if !method_groups.contains_key(&method_name.value) { + method_order.push(method_name.value); + } + method_groups.entry(method_name.value).or_default().push(member); + } + } + + let mut fields = Vec::new(); + for method_name_sym in method_order { + let method_js = ident_to_js(method_name_sym); + let method_decls = method_groups.remove(&method_name_sym).unwrap_or_default(); + + // Check for method-level constraints (e.g. `eq1 :: forall a. Eq a => ...`) + let method_constraints = lookup_method_constraints(ctx, method_name_sym); + + let method_expr = if !method_constraints.is_empty() { + // This method has its own constraints — needs dict param wrapping. + // Save current dicts (instance-level) + let saved_dicts = ctx.constraint_dicts.borrow().clone(); + let saved_class_dicts = ctx.class_dicts.borrow().clone(); + + // Set up method constraint dicts + let method_dict_params = setup_constraint_dicts(ctx, &method_constraints); + + // Instance-aware dict composition: for each method constraint class C, + // look up instance (C, head_type_con) to build composed dicts. + // e.g., for `Eq a =>` in `instance Eq1 Array`, look up (Eq, Array) → eqArray, + // then map Eq methods to `eqArray(dictEq)["method"]` instead of `dictEq["method"]`. + if let Some(htc) = head_type_con { + for (i, (class_name, _)) in method_constraints.iter().enumerate() { + if i < method_dict_params.len() { + if let Some(inst_name_sym) = lookup_instance_name(ctx, *class_name, htc) { + let inst_ref = resolve_name_to_js(ctx, inst_name_sym); + let dict_param_var = JsExpr::Var(method_dict_params[i].clone()); + let composed_dict = JsExpr::App(Box::new(inst_ref), vec![dict_param_var]); + + // Remap all methods of this class to the composed dict + let mut overrides: HashMap = HashMap::new(); + for (method, (cls, _)) in &ctx.exports.class_methods { + if *cls == *class_name { + overrides.insert(*method, composed_dict.clone()); + } + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + for (method, (cls, _)) in &imp_exports.class_methods { + if *cls == *class_name { + overrides.insert(*method, composed_dict.clone()); + } + } + } + } + ctx.constraint_dicts.borrow_mut().extend(overrides); + + // Also update class_dicts for this class + ctx.class_dicts.borrow_mut().insert(*class_name, composed_dict); + } + } + } + } + + // Merge back instance-level dicts (don't override method-level dicts) + { + let mut dicts = ctx.constraint_dicts.borrow_mut(); + for (k, v) in &saved_dicts { + dicts.entry(*k).or_insert_with(|| v.clone()); + } + } + { + let mut cd = ctx.class_dicts.borrow_mut(); + for (k, v) in &saved_class_dicts { + cd.entry(*k).or_insert_with(|| v.clone()); + } + } + + // Generate the method body + let body_expr = gen_instance_method_body(ctx, &method_js, &method_decls); + + // Wrap in method constraint lambdas + let wrapped = wrap_expr_with_dict_params(body_expr, &method_dict_params); + + // Restore original dicts + *ctx.constraint_dicts.borrow_mut() = saved_dicts; + *ctx.class_dicts.borrow_mut() = saved_class_dicts; + + wrapped + } else { + gen_instance_method_body(ctx, &method_js, &method_decls) + }; + + fields.push((method_js, method_expr)); + } + + // Clear constraint dicts after building the instance body. + ctx.constraint_dicts.borrow_mut().clear(); + ctx.class_dicts.borrow_mut().clear(); + + let obj = JsExpr::ObjectLit(fields); + + // Wrap in dict-param lambdas if the instance has constraints. + let instance_expr = if dict_params.is_empty() { + obj + } else { + wrap_expr_with_dict_params(obj, &dict_params) + }; + + vec![JsStmt::VarDecl(instance_name, Some(instance_expr))] +} + +/// Generate the JS expression for a single instance method body (shared between +/// constrained and unconstrained method paths). +fn gen_instance_method_body(ctx: &CodegenCtx, method_js: &str, method_decls: &[&Decl]) -> JsExpr { + if method_decls.len() == 1 { + if let Decl::Value { binders, guarded, where_clause, .. } = method_decls[0] { + if binders.is_empty() && where_clause.is_empty() { + return gen_guarded_expr(ctx, guarded); } else if where_clause.is_empty() { let body_stmts = gen_guarded_expr_stmts(ctx, guarded); - gen_curried_function(ctx, binders, body_stmts) + return gen_curried_function(ctx, binders, body_stmts); } else { let mut iife_body = Vec::new(); gen_let_bindings(ctx, where_clause, &mut iife_body); if binders.is_empty() { let expr = gen_guarded_expr(ctx, guarded); iife_body.push(JsStmt::Return(expr)); - JsExpr::App( + return JsExpr::App( Box::new(JsExpr::Function(None, vec![], iife_body)), vec![], - ) + ); } else { let body_stmts = gen_guarded_expr_stmts(ctx, guarded); iife_body.extend(body_stmts); - gen_curried_function_from_stmts(ctx, binders, iife_body) + return gen_curried_function_from_stmts(ctx, binders, iife_body); } - }; - fields.push((method_js, method_expr)); + } } + return JsExpr::Var("undefined".to_string()); + } + // Multi-equation: reuse gen_multi_equation and extract the function expression. + let stmts = gen_multi_equation(ctx, method_js, method_decls); + match stmts.into_iter().next() { + Some(JsStmt::VarDecl(_, Some(expr))) => expr, + _ => JsExpr::Var("undefined".to_string()), } +} - let obj = JsExpr::ObjectLit(fields); - vec![JsStmt::VarDecl(instance_name, Some(obj))] +/// Resolve a name to a JS expression (local var or imported module accessor). +/// Unlike gen_qualified_ref, this does NOT check constraint_dicts (used for instance names). +fn resolve_name_to_js(ctx: &CodegenCtx, name: Symbol) -> JsExpr { + let js_name = ident_to_js(name); + if ctx.local_names.contains(&name) { + return JsExpr::Var(js_name); + } + if let Some(source_parts) = ctx.name_source.get(&name) { + if let Some(js_mod) = ctx.import_map.get(source_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + // Check instance_modules for the defining module of instance dictionaries. + // Instances aren't in name_source because they're implicitly imported with classes. + if let Some(mod_parts) = ctx.exports.instance_modules.get(&name) { + if let Some(js_mod) = ctx.import_map.get(mod_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + // Fallback: search imported modules' instance registries + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some(mod_parts) = imp_exports.instance_modules.get(&name) { + if let Some(js_mod) = ctx.import_map.get(mod_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + } + } + JsExpr::Var(js_name) +} + +// ===== Call-site dictionary passing ===== + +/// Look up the type constraints for a function from local exports or imported modules. +fn lookup_function_constraints( + ctx: &CodegenCtx, + name: Symbol, +) -> Vec<(Symbol, Vec)> { + // Check local module first + if let Some(constraints) = ctx.exports.signature_constraints.get(&name) { + return constraints.clone(); + } + // Only look up constraints from the specific module that provides this name. + // This avoids cross-module name collisions (e.g. Data.Array.length vs + // Data.Foldable.length having different constraints). + if let Some(source_parts) = ctx.name_source.get(&name) { + if let Some(imp_exports) = ctx.registry.lookup(source_parts) { + if let Some(constraints) = imp_exports.signature_constraints.get(&name) { + return constraints.clone(); + } + } + } + Vec::new() +} + +/// Convert a typechecker DictExpr into a JS expression. +/// `DictExpr::Var(showInt)` → `Prelude.showInt` +/// `DictExpr::App(showArray, [Var(showInt)])` → `Prelude.showArray(Prelude.showInt)` +fn dict_expr_to_js(ctx: &CodegenCtx, dict: &crate::typechecker::check::DictExpr) -> JsExpr { + use crate::typechecker::check::DictExpr; + match dict { + DictExpr::Var(name) => resolve_name_to_js(ctx, *name), + DictExpr::App(name, args) => { + let mut result = resolve_name_to_js(ctx, *name); + for arg in args { + result = JsExpr::App(Box::new(result), vec![dict_expr_to_js(ctx, arg)]); + } + result + } + } +} + +/// Find a dictionary expression for a given class by checking class_dicts first (handles +/// classes with no methods like Alternative), then falling back to searching constraint_dicts +/// for any method belonging to that class. +fn find_dict_for_class(ctx: &CodegenCtx, class_name: Symbol) -> Option { + // Direct class→dict lookup (handles constraint dicts from setup_constraint_dicts) + { + let cd = ctx.class_dicts.borrow(); + if let Some(dict_expr) = cd.get(&class_name) { + return Some(dict_expr.clone()); + } + } + + // Per-call-site resolved dicts (consume from queue in order) + { + let mut queue = ctx.resolved_dict_queue.borrow_mut(); + if let Some(q) = queue.get_mut(&class_name) { + if let Some(dict_expr) = q.pop_front() { + return Some(dict_expr); + } + } + } + + // Fallback: find via method mapping + let dicts = ctx.constraint_dicts.borrow(); + for (method, (cls, _)) in &ctx.exports.class_methods { + if *cls == class_name { + if let Some(dict_expr) = dicts.get(method) { + return Some(dict_expr.clone()); + } + } + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + for (method, (cls, _)) in &imp_exports.class_methods { + if *cls == class_name { + if let Some(dict_expr) = dicts.get(method) { + return Some(dict_expr.clone()); + } + } + } + } + } + None +} + +/// If `name` refers to a constrained function, wrap `base_expr` with dictionary argument +/// applications. For example, `many` with constraints `[Alternative, Lazy]` becomes +/// `many(dictAlternative)(dictLazy)`. +fn maybe_insert_dict_args(ctx: &CodegenCtx, name: Symbol, base_expr: JsExpr) -> JsExpr { + // Don't insert dicts for class methods when inside a constrained function + // (the method is already dispatched through dict.method via constraint_dicts) + { + let dicts = ctx.constraint_dicts.borrow(); + if dicts.contains_key(&name) { + return base_expr; + } + } + + // Look up if this function has constraints from its type signature + let mut constraints = lookup_function_constraints(ctx, name); + + // Deduplicate constraints by class name (duplicates accumulate through import chains) + { + let mut seen_classes = HashSet::new(); + constraints.retain(|(class_name, _)| seen_classes.insert(*class_name)); + } + + // If no signature constraints found, check if it's a class method. + // Class methods take a dict argument for their parent class. + if constraints.is_empty() { + if let Some((class_name, class_tvs)) = ctx.exports.class_methods.get(&name) { + constraints = vec![(*class_name, class_tvs.iter().map(|v| crate::typechecker::types::Type::Var(*v)).collect())]; + } else { + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some((class_name, class_tvs)) = imp_exports.class_methods.get(&name) { + constraints = vec![(*class_name, class_tvs.iter().map(|v| crate::typechecker::types::Type::Var(*v)).collect())]; + break; + } + } + } + } + } + + if constraints.is_empty() { + return base_expr; + } + + // Try to resolve each constraint to a dictionary expression from current scope + let mut result = base_expr; + for (class_name, _type_args) in &constraints { + if let Some(dict_expr) = find_dict_for_class(ctx, *class_name) { + result = JsExpr::App(Box::new(result), vec![dict_expr]); + } + // If we can't find a dict, skip it — partial application will still + // be correct for the dicts we can resolve. + } + result +} + +// ===== Method constraint helpers ===== + +/// Look up the method-level constraints for a class method from local exports or imported modules. +/// Returns the constraints from the class method's own type signature (e.g., `Eq a =>` on `eq1`). +fn lookup_method_constraints( + ctx: &CodegenCtx, + method_name: Symbol, +) -> Vec<(Symbol, Vec)> { + if let Some(mc) = ctx.exports.method_constraints.get(&method_name) { + return mc.clone(); + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some(mc) = imp_exports.method_constraints.get(&method_name) { + return mc.clone(); + } + } + } + Vec::new() +} + +/// Look up an instance name from the instance registry. +/// Returns the instance variable name for a given (class, head_type_constructor) pair. +/// e.g., `(Eq, Array) → eqArray` +fn lookup_instance_name( + ctx: &CodegenCtx, + class_name: Symbol, + type_con: Symbol, +) -> Option { + let key = (class_name, type_con); + if let Some(name) = ctx.exports.instance_registry.get(&key) { + return Some(*name); + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some(name) = imp_exports.instance_registry.get(&key) { + return Some(*name); + } + } + } + None +} + +/// Extract the head type constructor from a CST TypeExpr. +/// e.g., `Array a` → `Array`, `Int` → `Int` +fn extract_head_type_con_from_cst(ty: &crate::cst::TypeExpr) -> Option { + match ty { + crate::cst::TypeExpr::Constructor { name, .. } => Some(name.name), + crate::cst::TypeExpr::App { constructor, .. } => extract_head_type_con_from_cst(constructor), + crate::cst::TypeExpr::Parens { ty, .. } => extract_head_type_con_from_cst(ty), + _ => None, + } +} + +// ===== Operator re-association ===== + +/// Get the fixity (associativity, precedence) for an operator. +fn get_fixity(ctx: &CodegenCtx, op_name: Symbol) -> (Associativity, u8) { + if let Some((assoc, prec)) = ctx.exports.value_fixities.get(&op_name) { + return (*assoc, *prec); + } + for imp in &ctx.module.imports { + if let Some(imp_exports) = ctx.registry.lookup(&imp.module.parts) { + if let Some((assoc, prec)) = imp_exports.value_fixities.get(&op_name) { + return (*assoc, *prec); + } + } + } + (Associativity::Left, 9) +} + +/// Resolve an operator to its JS expression with dict args applied. +fn gen_op_with_dicts(ctx: &CodegenCtx, op: &crate::cst::QualifiedIdent) -> JsExpr { + let op_ref = resolve_operator(ctx, op); + if let Some((_, target_fn)) = ctx.operator_targets.get(&op.name) { + maybe_insert_dict_args(ctx, *target_fn, op_ref) + } else { + op_ref + } +} + +/// Check if an operator is `$` (apply) or `#` (applyFlipped) which should be inlined +/// as direct function application rather than generating `apply(f)(x)`. +fn is_inline_apply_op(ctx: &CodegenCtx, op_name: Symbol) -> Option { + // Returns Some(false) for `$` (normal order: left(right)) + // Returns Some(true) for `#` (flipped order: right(left)) + // Returns None for other operators + if let Some((_, target_fn)) = ctx.operator_targets.get(&op_name) { + let name = interner::resolve(*target_fn).unwrap_or_default(); + // Only inline operators that target regular functions, not class methods. + // operator_class_targets contains operators whose targets are class methods + // (e.g., <*> → Control.Apply.apply). Operators NOT in this map target regular + // functions (e.g., $ → Data.Function.apply) and are safe to inline. + let is_class_op = ctx.exports.operator_class_targets.contains_key(&op_name) + || ctx.module.imports.iter().any(|imp| { + ctx.registry.lookup(&imp.module.parts) + .map_or(false, |e| e.operator_class_targets.contains_key(&op_name)) + }); + if is_class_op { + return None; + } + match name.as_str() { + "apply" => return Some(false), // $ : f $ x → f(x) + "applyFlipped" => return Some(true), // # : x # f → f(x) + _ => {} + } + } + None +} + +/// Apply a binary operator: `op(left)(right)`, with inlining for `$` and `#`. +fn apply_binop_js(ctx: &CodegenCtx, op_name: Symbol, op_expr: JsExpr, left: JsExpr, right: JsExpr) -> JsExpr { + if let Some(flipped) = is_inline_apply_op(ctx, op_name) { + if flipped { + // `#` (applyFlipped): `x # f` → `f(x)` + return JsExpr::App(Box::new(right), vec![left]); + } else { + // `$` (apply): `f $ x` → `f(x)` + return JsExpr::App(Box::new(left), vec![right]); + } + } + JsExpr::App( + Box::new(JsExpr::App(Box::new(op_expr), vec![left])), + vec![right], + ) +} + +/// Generate JS for an operator chain, using shunting-yard to handle precedence. +/// The parser produces right-associative chains: `a + b * c` → `Op(+, a, Op(*, b, c))`. +/// We flatten and re-associate based on fixity to get the correct grouping. +fn gen_op_chain( + ctx: &CodegenCtx, + left: &Expr, + op: &crate::cst::Spanned, + right: &Expr, +) -> JsExpr { + // Flatten the right-associative chain + let mut operands: Vec<&Expr> = vec![left]; + let mut operators: Vec<&crate::cst::Spanned> = vec![op]; + let mut current = right; + while let Expr::Op { left: rl, op: rop, right: rr, .. } = current { + operands.push(rl.as_ref()); + operators.push(rop); + current = rr.as_ref(); + } + operands.push(current); + + // Fast path: single binary operator (no re-association needed) + if operators.len() == 1 { + let op_js = gen_op_with_dicts(ctx, &operators[0].value); + let l = gen_expr(ctx, operands[0]); + let r = gen_expr(ctx, operands[1]); + return apply_binop_js(ctx, operators[0].value.name, op_js, l, r); + } + + // Generate JS for all operands and operators + let operand_js: Vec = operands.iter().map(|e| gen_expr(ctx, e)).collect(); + let op_js: Vec = operators.iter().map(|o| gen_op_with_dicts(ctx, &o.value)).collect(); + + // Shunting-yard: re-associate based on precedence + let mut output: Vec = Vec::new(); + let mut op_stack: Vec = Vec::new(); // indices into operators/op_js + + output.push(operand_js[0].clone()); + + for i in 0..operators.len() { + let (assoc_i, prec_i) = get_fixity(ctx, operators[i].value.name); + + while let Some(&top_idx) = op_stack.last() { + let (_assoc_top, prec_top) = get_fixity(ctx, operators[top_idx].value.name); + let should_pop = prec_top > prec_i + || (prec_top == prec_i && assoc_i == Associativity::Left); + if should_pop { + op_stack.pop(); + let right_val = output.pop().unwrap(); + let left_val = output.pop().unwrap(); + let result = apply_binop_js(ctx, operators[top_idx].value.name, op_js[top_idx].clone(), left_val, right_val); + output.push(result); + } else { + break; + } + } + + op_stack.push(i); + output.push(operand_js[i + 1].clone()); + } + + // Pop remaining operators + while let Some(top_idx) = op_stack.pop() { + let right_val = output.pop().unwrap(); + let left_val = output.pop().unwrap(); + let result = apply_binop_js(ctx, operators[top_idx].value.name, op_js[top_idx].clone(), left_val, right_val); + output.push(result); + } + + output.pop().unwrap() } // ===== Expression translation ===== fn gen_expr(ctx: &CodegenCtx, expr: &Expr) -> JsExpr { match expr { - Expr::Var { name, .. } => gen_qualified_ref(ctx, name), + Expr::Var { name, .. } => { + let base = gen_qualified_ref(ctx, name); + maybe_insert_dict_args(ctx, name.name, base) + } Expr::Constructor { name, .. } => { let ctor_name = name.name; - // Check if nullary (use .value) or n-ary (use .create) - if let Some((_, _, fields)) = ctx.ctor_details.get(&ctor_name) { - if fields.is_empty() { - // Nullary: Ctor.value - let base = gen_qualified_ref_raw(ctx, name); - JsExpr::Indexer( - Box::new(base), - Box::new(JsExpr::StringLit("value".to_string())), - ) - } else { - // N-ary: Ctor.create - let base = gen_qualified_ref_raw(ctx, name); - JsExpr::Indexer( - Box::new(base), - Box::new(JsExpr::StringLit("create".to_string())), - ) - } - } else if ctx.newtype_names.contains(&ctor_name) { - // Newtype constructor: Ctor.create (identity) - let base = gen_qualified_ref_raw(ctx, name); + let base = gen_qualified_ref(ctx, name); + + // Determine accessor: .value for nullary, .create for n-ary + let is_nullary = if let Some((_, _, fields)) = ctx.ctor_details.get(&ctor_name) { + fields.is_empty() + } else { + // Check in imported modules + is_nullary_ctor_from_imports(ctx, ctor_name) + }; + + let is_newtype = ctx.newtype_names.contains(&ctor_name) + || is_newtype_from_imports(ctx, ctor_name); + + if is_newtype { JsExpr::Indexer( Box::new(base), Box::new(JsExpr::StringLit("create".to_string())), ) + } else if is_nullary { + JsExpr::Indexer( + Box::new(base), + Box::new(JsExpr::StringLit("value".to_string())), + ) } else { - gen_qualified_ref_raw(ctx, name) + JsExpr::Indexer( + Box::new(base), + Box::new(JsExpr::StringLit("create".to_string())), + ) } } @@ -658,17 +1835,17 @@ fn gen_expr(ctx: &CodegenCtx, expr: &Expr) -> JsExpr { } Expr::Op { left, op, right, .. } => { - // Resolve operator to function application: op(left)(right) - let op_ref = gen_qualified_ref(ctx, &op.value); - let l = gen_expr(ctx, left); - let r = gen_expr(ctx, right); - JsExpr::App( - Box::new(JsExpr::App(Box::new(op_ref), vec![l])), - vec![r], - ) + gen_op_chain(ctx, left, op, right) } - Expr::OpParens { op, .. } => gen_qualified_ref(ctx, &op.value), + Expr::OpParens { op, .. } => { + let op_ref = resolve_operator(ctx, &op.value); + if let Some((_, target_fn)) = ctx.operator_targets.get(&op.value.name) { + maybe_insert_dict_args(ctx, *target_fn, op_ref) + } else { + op_ref + } + } Expr::If { cond, then_expr, else_expr, .. } => { let c = gen_expr(ctx, cond); @@ -772,58 +1949,177 @@ fn gen_literal(ctx: &CodegenCtx, lit: &Literal) -> JsExpr { // ===== Qualified references ===== +/// Resolve a qualified or unqualified reference to a JS expression. +/// For unqualified names, uses the name resolution map to find the source module. fn gen_qualified_ref(ctx: &CodegenCtx, qident: &QualifiedIdent) -> JsExpr { let name = qident.name; + let js_name = ident_to_js(name); + + if let Some(mod_sym) = &qident.module { + // Already qualified — resolve through import map + return resolve_module_ref(ctx, *mod_sym, &js_name); + } - // Check if it's a foreign import in the current module - if qident.module.is_none() && ctx.foreign_imports.contains(&name) { - let js_name = ident_to_js(name); + // Unqualified: check local first, then imports + + // Foreign imports: $foreign.name + if ctx.foreign_imports.contains(&name) { return JsExpr::ModuleAccessor("$foreign".to_string(), js_name); } - gen_qualified_ref_raw(ctx, qident) + // Constraint dicts: if inside a constrained function and this name is a class method, + // emit `dictExpr["methodName"]` instead of a bare reference. + { + let dicts = ctx.constraint_dicts.borrow(); + if let Some(dict_expr) = dicts.get(&name) { + return JsExpr::Indexer( + Box::new(dict_expr.clone()), + Box::new(JsExpr::StringLit(js_name)), + ); + } + } + + // Local names: bare variable + if ctx.local_names.contains(&name) { + return JsExpr::Var(js_name); + } + + // Cross-module: look up in name resolution map + if let Some(source_parts) = ctx.name_source.get(&name) { + if let Some(js_mod) = ctx.import_map.get(source_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + + // Fallback: emit as bare variable (may be a lambda-bound or let-bound name) + JsExpr::Var(js_name) } -fn gen_qualified_ref_raw(ctx: &CodegenCtx, qident: &QualifiedIdent) -> JsExpr { - let js_name = ident_to_js(qident.name); +/// Check if a constructor from an imported module is nullary. +fn is_nullary_ctor_from_imports(ctx: &CodegenCtx, ctor_name: Symbol) -> bool { + if let Some(source_parts) = ctx.name_source.get(&ctor_name) { + if let Some(exports) = ctx.registry.lookup(source_parts) { + if let Some((_, _, fields)) = exports.ctor_details.get(&ctor_name) { + return fields.is_empty(); + } + } + } + // Default: assume non-nullary (safer — produces .create) + false +} - match &qident.module { - None => JsExpr::Var(js_name), - Some(mod_sym) => { - // Look up the module in import map - // The module qualifier is a single symbol containing the alias - let mod_str = interner::resolve(*mod_sym).unwrap_or_default(); - // Find the actual import by looking at qualified imports - for imp in &ctx.module.imports { - if let Some(ref qual) = imp.qualified { - let qual_str = qual.parts - .iter() - .map(|s| interner::resolve(*s).unwrap_or_default()) - .collect::>() - .join("."); - if qual_str == mod_str { - if let Some(js_mod) = ctx.import_map.get(&imp.module.parts) { - return JsExpr::ModuleAccessor(js_mod.clone(), js_name); - } - } - } - // Also check if module name directly matches - let imp_name = imp.module.parts - .iter() - .map(|s| interner::resolve(*s).unwrap_or_default()) - .collect::>() - .join("."); - if imp_name == mod_str { - if let Some(js_mod) = ctx.import_map.get(&imp.module.parts) { - return JsExpr::ModuleAccessor(js_mod.clone(), js_name); - } +/// Check if a constructor from an imported module belongs to a sum type (multiple constructors). +fn is_sum_ctor_from_imports(ctx: &CodegenCtx, ctor_name: Symbol) -> bool { + if let Some(source_parts) = ctx.name_source.get(&ctor_name) { + if let Some(exports) = ctx.registry.lookup(source_parts) { + if let Some((parent, _, _)) = exports.ctor_details.get(&ctor_name) { + return exports.data_constructors + .get(parent) + .map_or(false, |ctors| ctors.len() > 1); + } + } + } + // Default: assume sum type (safer — produces instanceof check) + true +} + +/// Check if a constructor from an imported module is a newtype. +fn is_newtype_from_imports(ctx: &CodegenCtx, ctor_name: Symbol) -> bool { + if let Some(source_parts) = ctx.name_source.get(&ctor_name) { + if let Some(exports) = ctx.registry.lookup(source_parts) { + return exports.newtype_names.contains(&ctor_name); + } + } + false +} + +/// Resolve an operator to its target function reference. +/// Operators like `<>` are resolved to their aliased function (e.g., `append`) +/// via the module's fixity declarations. +fn resolve_operator(ctx: &CodegenCtx, op_qident: &QualifiedIdent) -> JsExpr { + let op_name = op_qident.name; + + // Look up in operator targets map (built from fixity declarations) + if let Some((target_module, target_fn)) = ctx.operator_targets.get(&op_name) { + let js_name = ident_to_js(*target_fn); + + // Check constraint dicts: operator may alias a class method (e.g. `<>` → `append`). + { + let dicts = ctx.constraint_dicts.borrow(); + if let Some(dict_expr) = dicts.get(target_fn) { + return JsExpr::Indexer( + Box::new(dict_expr.clone()), + Box::new(JsExpr::StringLit(js_name)), + ); + } + } + + if let Some(mod_parts) = target_module { + // Explicitly qualified target: e.g., `Data.Semigroup.append` + if let Some(js_mod) = ctx.import_map.get(mod_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + + // Unqualified target: resolve through name_source + if ctx.local_names.contains(target_fn) { + return JsExpr::Var(js_name); + } + + if let Some(source_parts) = ctx.name_source.get(target_fn) { + if let Some(js_mod) = ctx.import_map.get(source_parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name); + } + } + + // Fallback: search imported modules for the target function. + // This handles operators whose targets are re-exported through intermediary + // modules (e.g., `<#>` → `mapFlipped` via Prelude re-exporting Data.Functor). + return resolve_name_to_js(ctx, *target_fn); + } + + // If the operator is already qualified (e.g., from a qualified import), resolve directly + if op_qident.module.is_some() { + return gen_qualified_ref(ctx, op_qident); + } + + // Fallback: emit the mangled operator name as a variable reference + // This handles cases where the operator is locally defined or lambda-bound + JsExpr::Var(ident_to_js(op_name)) +} + +/// Resolve a module qualifier symbol to a JS module accessor. +fn resolve_module_ref(ctx: &CodegenCtx, mod_sym: Symbol, js_name: &str) -> JsExpr { + let mod_str = interner::resolve(mod_sym).unwrap_or_default(); + // Check qualified imports (import X as Y) + for imp in &ctx.module.imports { + if let Some(ref qual) = imp.qualified { + let qual_str = qual.parts + .iter() + .map(|s| interner::resolve(*s).unwrap_or_default()) + .collect::>() + .join("."); + if qual_str == mod_str { + if let Some(js_mod) = ctx.import_map.get(&imp.module.parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name.to_string()); } } - // Fallback: use the module name directly - let js_mod = any_name_to_js(&mod_str.replace('.', "_")); - JsExpr::ModuleAccessor(js_mod, js_name) + } + // Also check direct module name match + let imp_name = imp.module.parts + .iter() + .map(|s| interner::resolve(*s).unwrap_or_default()) + .collect::>() + .join("."); + if imp_name == mod_str { + if let Some(js_mod) = ctx.import_map.get(&imp.module.parts) { + return JsExpr::ModuleAccessor(js_mod.clone(), js_name.to_string()); + } } } + // Fallback + let js_mod = any_name_to_js(&mod_str.replace('.', "_")); + JsExpr::ModuleAccessor(js_mod, js_name.to_string()) } // ===== Guarded expressions ===== @@ -1143,8 +2439,10 @@ fn gen_binder_match( Binder::Constructor { name, args, .. } => { let ctor_name = name.name; - // Check if this is a newtype constructor (erased) - if ctx.newtype_names.contains(&ctor_name) { + // Check if this is a newtype constructor (erased) — local or imported + let is_newtype = ctx.newtype_names.contains(&ctor_name) + || is_newtype_from_imports(ctx, ctor_name); + if is_newtype { if args.len() == 1 { return gen_binder_match(ctx, &args[0], scrutinee); } @@ -1160,11 +2458,12 @@ fn gen_binder_match( .get(parent) .map_or(false, |ctors| ctors.len() > 1) } else { - false + // Check imported modules + is_sum_ctor_from_imports(ctx, ctor_name) }; if is_sum { - let ctor_ref = gen_qualified_ref_raw(ctx, name); + let ctor_ref = gen_qualified_ref(ctx, name); conditions.push(JsExpr::InstanceOf( Box::new(scrutinee.clone()), Box::new(ctor_ref), @@ -1530,14 +2829,16 @@ fn gen_curried_lambda(params: &[String], body: JsExpr) -> JsExpr { result } -fn make_qualified_ref(_ctx: &CodegenCtx, qual_mod: Option<&Ident>, name: &str) -> JsExpr { +fn make_qualified_ref(ctx: &CodegenCtx, qual_mod: Option<&Ident>, name: &str) -> JsExpr { if let Some(mod_sym) = qual_mod { let mod_str = interner::resolve(*mod_sym).unwrap_or_default(); let js_mod = any_name_to_js(&mod_str.replace('.', "_")); JsExpr::ModuleAccessor(js_mod, any_name_to_js(name)) } else { - // Unqualified: look for it in scope - JsExpr::Var(any_name_to_js(name)) + // Resolve through name_source for proper module qualification + let sym = interner::intern(name); + let base = resolve_name_to_js(ctx, sym); + maybe_insert_dict_args(ctx, sym, base) } } diff --git a/src/typechecker/check.rs b/src/typechecker/check.rs index 1579d7e1..b16b6fd4 100644 --- a/src/typechecker/check.rs +++ b/src/typechecker/check.rs @@ -449,7 +449,10 @@ fn check_partially_applied_synonyms_inner( } // Check if head is a partially or over-applied synonym if let Type::Con(name) = head { - if let Some((params, _)) = type_aliases.get(name) { + // If the name is known as a non-alias type (data/foreign), prefer that + // interpretation over a same-named alias from another module. + // After alias expansion, remaining Type::Con refs are non-alias types. + if let Some((params, _)) = type_aliases.get(name).filter(|_| !type_con_arities.contains_key(name)) { if args.len() < params.len() { errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); return; @@ -483,7 +486,8 @@ fn check_partially_applied_synonyms_inner( } } Type::Con(name) => { - if let Some((params, _)) = type_aliases.get(name) { + // Skip alias check if this name is also a known non-alias type + if let Some((params, _)) = type_aliases.get(name).filter(|_| !type_con_arities.contains_key(name)) { if !params.is_empty() { errors.push(TypeError::PartiallyAppliedSynonym { span, name: *name }); } @@ -756,6 +760,17 @@ fn collect_binder_vars(binder: &Binder, seen: &mut HashMap>) { } } +/// A resolved dictionary expression for codegen. Represents how to build a type class +/// dictionary at a specific use site, including nested instance applications. +/// E.g., `showArray(showInt)` becomes `App(showArray, [Var(showInt)])`. +#[derive(Debug, Clone)] +pub enum DictExpr { + /// A simple instance with no constraints: e.g., `showInt`, `semiringInt` + Var(Symbol), + /// An instance applied to sub-dictionaries: e.g., `showArray(showInt)` + App(Symbol, Vec), +} + /// Exported information from a type-checked module, available for import by other modules. #[derive(Debug, Clone, Default)] pub struct ModuleExports { @@ -778,6 +793,17 @@ pub struct ModuleExports { /// Class methods whose declared type has extra constraints (e.g. `Applicative m =>`). /// Used for CycleInDeclaration detection across module boundaries. pub constrained_class_methods: HashSet, + /// Actual constraints from class method type signatures. + /// e.g. `eq1 → [(Eq, [a])]` for `eq1 :: forall a. Eq a => f a -> f a -> Boolean`. + /// Used by codegen to wrap instance method bodies with dict parameters. + pub method_constraints: HashMap)>>, + /// Instance name registry: (class_name, head_type_constructor) → instance_variable_name. + /// e.g. `(Eq, Array) → eqArray`. Used by codegen for instance-aware dict composition. + pub instance_registry: HashMap<(Symbol, Symbol), Symbol>, + /// Instance defining modules: instance_name → module_parts. + /// Tracks which module originally declared each instance, so codegen can reference + /// the correct JS module (not a re-exporting module). + pub instance_modules: HashMap>, /// Type aliases: alias_name → (params, body_type) pub type_aliases: HashMap, Type)>, /// Class definitions: class_name → param_count (for arity checking and orphan detection) @@ -811,6 +837,16 @@ pub struct ModuleExports { /// e.g. `unsafePartial :: (Partial => a) -> a`. These discharge Partial /// when applied to a partial expression. pub partial_dischargers: HashSet, + /// Value-level operator → target function name mapping (for codegen). + /// e.g. `<>` → `append`, `<<<` → `compose` + pub operator_targets: HashMap>, Symbol)>, + /// Superclass relationships: class_name → [(superclass_class_name, index)]. + /// Index matches the purs compiler accessor convention: `Applicative0`, `Plus1`. + pub class_superclasses: HashMap>, + /// Concrete dict resolutions: binding_name → [(class_name, dict_expr)]. + /// Records which concrete instance dictionaries should be passed at call sites + /// for bindings that use class methods at concrete types. + pub resolved_dicts: HashMap>, } /// Registry of compiled modules, used to resolve imports. @@ -925,6 +961,15 @@ fn is_prim_module(module_name: &crate::cst::ModuleName) -> bool { && crate::interner::resolve(module_name.parts[0]).unwrap_or_default() == "Prim" } +/// Check if a deferred class constraint should be silently skipped in Pass 3. +/// - `IsSymbol`: compiler-solved for any type-level string literal. +/// - `Bind`: synthesized by do-notation desugaring; always resolvable when the +/// Bind class is imported (standalone tests without Control.Bind skip this). +fn is_skip_deferred_class(class_name: Symbol) -> bool { + let name = crate::interner::resolve(class_name).unwrap_or_default(); + matches!(name.as_str(), "IsSymbol" | "Bind") +} + /// Check if a CST ModuleName is a Prim submodule (e.g. Prim.Coerce, Prim.Row). fn is_prim_submodule(module_name: &crate::cst::ModuleName) -> bool { module_name.parts.len() >= 2 @@ -990,13 +1035,14 @@ fn prim_submodule_exports(module_name: &crate::cst::ModuleName) -> ModuleExports exports.class_param_counts.insert(intern("RowToList"), 2); } "Symbol" => { - // classes: Append, Compare, Cons - for class in &["Append", "Compare", "Cons"] { + // classes: Append, Compare, Cons, IsSymbol + for class in &["Append", "Compare", "Cons", "IsSymbol"] { exports.instances.insert(intern(class), Vec::new()); } exports.class_param_counts.insert(intern("Append"), 3); exports.class_param_counts.insert(intern("Compare"), 3); exports.class_param_counts.insert(intern("Cons"), 3); + exports.class_param_counts.insert(intern("IsSymbol"), 1); } "TypeError" => { // classes: Fail, Warn; type constructors: Text, Beside, Above, Quote, QuoteLabel @@ -1338,6 +1384,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ctx.module_mode = true; let mut env = Env::new(); let mut signatures: HashMap = HashMap::new(); + // Track which signature_constraints entries are from local type signatures + // (vs imported from other modules). Only local entries should be exported. + let mut local_sig_constraint_names: HashSet = HashSet::new(); let mut result_types: HashMap = HashMap::new(); let mut errors: Vec = Vec::new(); @@ -1426,6 +1475,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } + // Process imports: bring imported names into scope let explicitly_imported_types = process_imports( module, @@ -1491,9 +1541,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { ctx.known_types.insert(name.value); ctx.type_con_arities.insert(name.value, type_vars.len()); } - Decl::ForeignData { name, .. } => { + Decl::ForeignData { name, kind, .. } => { ctx.known_types.insert(name.value); - // Foreign data arity is unknown without kind annotation; skip + ctx.type_con_arities.insert(name.value, count_kind_arity(kind)); } Decl::TypeAlias { name, span, .. } => { ctx.known_types.insert(name.value); @@ -2139,6 +2189,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } ctx.signature_constraints.insert(name.value, sig_constraints); + local_sig_constraint_names.insert(name.value); } } Err(e) => errors.push(e), @@ -2433,6 +2484,10 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // with 0 explicit binders (prevents false CycleInDeclaration). if has_any_constraint(&member.ty).is_some() { ctx.constrained_class_methods.insert(member.name.value); + let mc = extract_type_signature_constraints(&member.ty, &type_ops, &ctx.known_types); + if !mc.is_empty() { + ctx.method_constraints.insert(member.name.value, mc); + } } match convert_type_expr(&member.ty, &type_ops, &ctx.known_types) { Ok(member_ty) => { @@ -2751,6 +2806,14 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { .or_default() .push((inst_types.clone(), has_kind_ann, types.clone())); registered_instances.push((*span, class_name.name, inst_types.clone())); + // Populate instance name registry for codegen instance resolution. + if let Some(iname) = inst_name { + if let Some(head_con) = extract_head_type_con(&inst_types) { + ctx.instance_registry.insert((class_name.name, head_con), iname.value); + } + // Track which module defines this instance for codegen module resolution + ctx.instance_modules.insert(iname.value, module.name.value.parts.clone()); + } instances .entry(class_name.name) .or_default() @@ -3786,6 +3849,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } else { ctx.function_op_aliases.insert(operator.value); } + // Track operator → target function name (all operators) + ctx.operator_function_targets.insert(operator.value, target.name); // Track operator → class method target for deferred constraint tracking. // Local fixity overrides imported mapping, so remove if new target isn't a class method. if ctx.class_methods.contains_key(&target.name) { @@ -4125,6 +4190,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Save constraint count before inference for AmbiguousTypeVariables detection let constraint_start = ctx.deferred_constraints.len(); + // Track which binding is being checked so deferred constraints record the binding name + ctx.current_binding_name = Some(*name); + if decls.len() == 1 { // Single equation if let Decl::Value { @@ -4182,7 +4250,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } for i in constraint_start..ctx.deferred_constraints.len() { - let (_, c_class_i, _) = ctx.deferred_constraints[i]; + let (_, c_class_i, _, _) = ctx.deferred_constraints[i]; if c_class_i != compare_sym { continue; } for arg in &ctx.deferred_constraints[i].2 { let z = ctx.state.zonk(arg.clone()); @@ -4192,7 +4260,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } } for i in constraint_start..ctx.deferred_constraints.len() { - let (c_span, c_class, _) = ctx.deferred_constraints[i]; + let (c_span, c_class, _, _) = ctx.deferred_constraints[i]; if c_class != compare_sym { continue; } let zonked: Vec = ctx.deferred_constraints[i].2.iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4233,7 +4301,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { Vec::new() }; for i in constraint_start..ctx.deferred_constraints.len() { - let (c_span, c_class, _) = ctx.deferred_constraints[i]; + let (c_span, c_class, _, _) = ctx.deferred_constraints[i]; if c_class != lacks_sym { continue; } let zonked: Vec = ctx.deferred_constraints[i].2.iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4305,7 +4373,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }) .unwrap_or(false); for i in constraint_start..ctx.deferred_constraints.len() { - let (c_span, c_class, _) = ctx.deferred_constraints[i]; + let (c_span, c_class, _, _) = ctx.deferred_constraints[i]; if c_class != coercible_sym { continue; } let zonked: Vec = ctx.deferred_constraints[i].2.iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4571,7 +4639,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { }) .unwrap_or(false); for i in constraint_start..ctx.deferred_constraints.len() { - let (c_span, c_class, _) = ctx.deferred_constraints[i]; + let (c_span, c_class, _, _) = ctx.deferred_constraints[i]; if c_class != coercible_sym { continue; } let zonked: Vec = ctx.deferred_constraints[i].2.iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4713,6 +4781,8 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } ctx.scoped_type_vars = prev_scoped_multi; } + + ctx.current_binding_name = None; } // Deferred generalization for mutual recursion SCC @@ -4811,7 +4881,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // 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 // instance resolution may not handle complex imported instances correctly. - for (span, class_name, type_args) in &ctx.sig_deferred_constraints { + for (span, class_name, type_args, _binding_name) in &ctx.sig_deferred_constraints { let zonked_args: Vec = type_args .iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4866,7 +4936,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { for _ in 0..3 { let mut solved_any = false; for i in 0..ctx.deferred_constraints.len() { - let (span, class_name, _) = ctx.deferred_constraints[i]; + let (span, class_name, _, _) = ctx.deferred_constraints[i]; let class_str = crate::interner::resolve(class_name).unwrap_or_default(); let zonked_args: Vec = ctx.deferred_constraints[i].2.iter() .map(|t| ctx.state.zonk(t.clone())) @@ -4937,13 +5007,15 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } // Pass 3: Check deferred type class constraints - for (span, class_name, type_args) in &ctx.deferred_constraints { + for (span, class_name, type_args, binding_name) in &ctx.deferred_constraints { super::check_deadline(); let zonked_args: Vec = type_args .iter() .map(|t| ctx.state.zonk(t.clone())) .collect(); + + // Skip if any arg still contains unsolved unification variables or type variables // (polymorphic usage — no concrete instance needed). // We check deeply since unif vars can be nested inside App, e.g. Show ((?1 ?2) ?2). @@ -5176,9 +5248,14 @@ 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. + // Exception: compiler-solved/synthesized classes (IsSymbol, Bind from + // do-notation) are silently skipped when not imported. let class_is_known = instances.contains_key(class_name) || ctx.class_methods.values().any(|(cn, _)| cn == class_name); if !class_is_known { + if is_skip_deferred_class(*class_name) { + continue; + } errors.push(TypeError::UnknownClass { span: *span, name: *class_name, @@ -5186,6 +5263,20 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { } else { match check_instance_depth(&instances, &ctx.state.type_aliases, class_name, &zonked_args, 0) { InstanceResult::Match => { + // Record concrete dict resolution for codegen + if let Some(bname) = binding_name { + if let Some(dict_expr) = resolve_dict_expr( + *class_name, + &zonked_args, + &instances, + &ctx.instance_registry, + &ctx.state.type_aliases, + 0, + ) { + ctx.resolved_dicts.entry(*bname).or_insert_with(Vec::new) + .push((*class_name, dict_expr)); + } + } // Kind-check the constraint type against the class's kind signature. // This catches cases like IxFunctor (Indexed Array) where the class // kind constrains f :: ix -> ix -> Type -> Type, but the concrete @@ -5230,7 +5321,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { // Also check operator-deferred constraints for PossiblyInfiniteInstance only. // We don't check NoInstanceFound for operator constraints because our instance // resolver can't handle all valid cases (e.g., structural record Eq). - for (span, class_name, type_args) in &ctx.op_deferred_constraints { + for (span, class_name, type_args, binding_name) in &ctx.op_deferred_constraints { let zonked_args: Vec = type_args .iter() .map(|t| ctx.state.zonk(t.clone())) @@ -5238,9 +5329,24 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let has_unsolved = zonked_args.iter().any(|t| { !ctx.state.free_unif_vars(t).is_empty() || contains_type_var(t) }); + if has_unsolved { continue; } + // Record concrete dict resolution for operator constraints + if let Some(bname) = binding_name { + if let Some(dict_expr) = resolve_dict_expr( + *class_name, + &zonked_args, + &instances, + &ctx.instance_registry, + &ctx.state.type_aliases, + 0, + ) { + ctx.resolved_dicts.entry(*bname).or_insert_with(Vec::new) + .push((*class_name, dict_expr)); + } + } if let InstanceResult::DepthExceeded = check_instance_depth(&instances, &ctx.state.type_aliases, class_name, &zonked_args, 0) { errors.push(TypeError::PossiblyInfiniteInstance { span: *span, @@ -5613,6 +5719,7 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { let mut export_type_operators: HashMap = HashMap::new(); let mut export_value_fixities: HashMap = HashMap::new(); let mut export_function_op_aliases: HashSet = HashSet::new(); + let mut export_operator_targets: HashMap>, Symbol)> = HashMap::new(); for decl in &module.decls { if let Decl::Fixity { associativity, @@ -5631,6 +5738,12 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { if !ctx.ctor_details.contains_key(&target.name) { export_function_op_aliases.insert(operator.value); } + // Store operator → (module, target_fn) for codegen + let target_module = target.module.map(|m| { + let mod_str = crate::interner::resolve(m).unwrap_or_default(); + mod_str.split('.').map(|s| crate::interner::intern(s)).collect::>() + }); + export_operator_targets.insert(operator.value, (target_module, target.name)); } } } @@ -5652,6 +5765,11 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { scheme.ty = ctx.state.zonk(scheme.ty.clone()); } + // Collect locally-defined value names for filtering imported signature_constraints + // that shadow local definitions (e.g. Data.Foldable.length constraints should not + // appear on Data.Array.length which is a constraint-free foreign import). + let local_value_names: HashSet = local_values.keys().copied().collect(); + // 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(); @@ -5680,6 +5798,9 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { value_fixities: export_value_fixities, function_op_aliases: export_function_op_aliases, constrained_class_methods: ctx.constrained_class_methods.clone(), + method_constraints: ctx.method_constraints.clone(), + instance_registry: ctx.instance_registry.clone(), + instance_modules: ctx.instance_modules.clone(), type_aliases: export_type_aliases, class_param_counts: class_param_counts.clone(), value_origins, @@ -5690,14 +5811,28 @@ pub fn check_module(module: &Module, registry: &ModuleRegistry) -> CheckResult { type_con_arities: ctx.type_con_arities.clone(), type_roles: ctx.type_roles.clone(), newtype_names: ctx.newtype_names.clone(), - signature_constraints: ctx.signature_constraints.clone(), + signature_constraints: ctx.signature_constraints.iter() + .filter(|(name, _)| { + // Include if: (a) it has a local type signature, OR + // (b) it's a re-export (not locally defined, so no shadowing issue) + local_sig_constraint_names.contains(name) || !local_value_names.contains(name) + }) + .map(|(name, constraints)| (*name, constraints.clone())) + .collect(), partial_dischargers: ctx.partial_dischargers.clone(), + operator_targets: export_operator_targets, type_kinds: saved_type_kinds.iter() .filter(|(name, _)| local_type_names.contains(name)) .map(|(&name, kind)| (name, generalize_kind_for_export(kind))) .collect(), + class_superclasses: class_superclasses.iter() + .map(|(name, (_tvs, scs))| (*name, scs.iter().enumerate() + .map(|(i, (sc, _))| (*sc, i)).collect())) + .collect(), + resolved_dicts: ctx.resolved_dicts.clone(), }; + // Debug: check resolved_dicts for Main // 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 @@ -6020,6 +6155,14 @@ fn process_imports( } } } + // Also import instance_registry and instance_modules so codegen + // can resolve concrete dict expressions (e.g., bindEffect, applicativeEffect). + for (key, inst_name) in &module_exports.instance_registry { + ctx.instance_registry.insert(*key, *inst_name); + } + for (inst_name, mod_parts) in &module_exports.instance_modules { + ctx.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } } Some(ImportList::Hiding(items)) => { let hidden: HashSet = items.iter().map(|i| import_name(i)).collect(); @@ -6081,9 +6224,22 @@ fn import_all( for (op, target) in &exports.operator_class_targets { ctx.operator_class_targets.insert(*op, *target); } + // Populate operator → target function mapping for all operators + for (op, (_, target_fn)) in &exports.operator_targets { + ctx.operator_function_targets.insert(*op, *target_fn); + } for name in &exports.constrained_class_methods { ctx.constrained_class_methods.insert(*name); } + for (name, mc) in &exports.method_constraints { + ctx.method_constraints.insert(*name, mc.clone()); + } + for (key, inst_name) in &exports.instance_registry { + ctx.instance_registry.insert(*key, *inst_name); + } + for (inst_name, mod_parts) in &exports.instance_modules { + ctx.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } for (name, alias) in &exports.type_aliases { ctx.state.type_aliases.insert(*name, alias.clone()); ctx.known_types.insert(maybe_qualify(*name, qualifier)); @@ -6101,13 +6257,23 @@ fn import_all( ctx.partial_dischargers.insert(*name); } for (name, constraints) in &exports.signature_constraints { - // Only import Coercible constraints from other modules (other constraints - // are handled locally via extract_type_signature_constraints on CST types) - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); - if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + // Import all signature constraints from other modules. + // Non-Coercible constraints (Functor, Semigroupoid, etc.) are needed for + // operator targets — when an operator like `<#>` targets a constrained function + // like `mapFlipped :: Functor f => ...`, the constraint must be available so it + // can be deferred and resolved to a concrete instance dict for codegen. + if !constraints.is_empty() { + ctx.signature_constraints.entry(*name).or_default().extend(constraints.iter().cloned()); + } + } + // Import operator target functions' signature constraints. + // When an operator like `<#>` targets `mapFlipped`, its constraints must be + // available keyed by the target function name for deferred constraint resolution. + for (_op, (_, target_fn)) in &exports.operator_targets { + if let Some(constraints) = exports.signature_constraints.get(target_fn) { + if !constraints.is_empty() { + ctx.signature_constraints.entry(*target_fn).or_default().extend(constraints.iter().cloned()); + } } } } @@ -6162,20 +6328,33 @@ fn import_item( if let Some(target) = exports.operator_class_targets.get(name) { ctx.operator_class_targets.insert(*name, *target); } + if let Some((_, target_fn)) = exports.operator_targets.get(name) { + ctx.operator_function_targets.insert(*name, *target_fn); + } if exports.constrained_class_methods.contains(name) { ctx.constrained_class_methods.insert(*name); } + if let Some(mc) = exports.method_constraints.get(name) { + ctx.method_constraints.insert(*name, mc.clone()); + } // Import ctor_details if this is a constructor alias (e.g. `:|` for `NonEmpty`) if let Some(details) = exports.ctor_details.get(name) { ctx.ctor_details.insert(*name, details.clone()); } - // Import signature constraints for Coercible propagation (only Coercible) + // Import signature constraints (needed for operator dict resolution in codegen) if let Some(constraints) = exports.signature_constraints.get(name) { - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); - if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + if !constraints.is_empty() { + ctx.signature_constraints.entry(*name).or_default().extend(constraints.iter().cloned()); + } + } + // When importing an operator, also import its target function's signature constraints. + // E.g., importing `<#>` (target: `mapFlipped`) should bring in `mapFlipped`'s + // `Functor f` constraint so it can be deferred during operator usage. + if let Some((_, target_fn)) = exports.operator_targets.get(name) { + if let Some(constraints) = exports.signature_constraints.get(target_fn) { + if !constraints.is_empty() { + ctx.signature_constraints.entry(*target_fn).or_default().extend(constraints.iter().cloned()); + } } } // Import partial discharger info (functions with Partial in param position) @@ -6258,6 +6437,18 @@ fn import_item( if exports.constrained_class_methods.contains(method_name) { ctx.constrained_class_methods.insert(*method_name); } + if let Some(mc) = exports.method_constraints.get(method_name) { + ctx.method_constraints.insert(*method_name, mc.clone()); + } + } + } + // Import instance registry entries for this class + for ((cn, tc), inst_name) in &exports.instance_registry { + if cn == name { + ctx.instance_registry.insert((*cn, *tc), *inst_name); + if let Some(mod_parts) = exports.instance_modules.get(inst_name) { + ctx.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } } } if let Some(insts) = exports.instances.get(name) { @@ -6350,11 +6541,27 @@ fn import_all_except( ctx.operator_class_targets.insert(*op, *target); } } + for (op, (_, target_fn)) in &exports.operator_targets { + if !hidden.contains(op) { + ctx.operator_function_targets.insert(*op, *target_fn); + } + } for name in &exports.constrained_class_methods { if !hidden.contains(name) { ctx.constrained_class_methods.insert(*name); } } + for (name, mc) in &exports.method_constraints { + if !hidden.contains(name) { + ctx.method_constraints.insert(*name, mc.clone()); + } + } + for (key, inst_name) in &exports.instance_registry { + ctx.instance_registry.insert(*key, *inst_name); + } + for (inst_name, mod_parts) in &exports.instance_modules { + ctx.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } for (name, alias) in &exports.type_aliases { if !hidden.contains(name) { ctx.state.type_aliases.insert(*name, alias.clone()); @@ -6374,12 +6581,17 @@ fn import_all_except( } } for (name, constraints) in &exports.signature_constraints { - if !hidden.contains(name) { - let coercible_only: Vec<_> = constraints.iter().filter(|(cn, _)| { - crate::interner::resolve(*cn).unwrap_or_default() == "Coercible" - }).cloned().collect(); - if !coercible_only.is_empty() { - ctx.signature_constraints.entry(*name).or_default().extend(coercible_only); + if !hidden.contains(name) && !constraints.is_empty() { + ctx.signature_constraints.entry(*name).or_default().extend(constraints.iter().cloned()); + } + } + // Import operator target functions' signature constraints + for (op, (_, target_fn)) in &exports.operator_targets { + if !hidden.contains(op) { + if let Some(constraints) = exports.signature_constraints.get(target_fn) { + if !constraints.is_empty() { + ctx.signature_constraints.entry(*target_fn).or_default().extend(constraints.iter().cloned()); + } } } } @@ -6549,6 +6761,9 @@ fn filter_exports( if all.constrained_class_methods.contains(name) { result.constrained_class_methods.insert(*name); } + if let Some(mc) = all.method_constraints.get(name) { + result.method_constraints.insert(*name, mc.clone()); + } // Also export ctor_details if this is a constructor alias (e.g. `:|`) if let Some(details) = all.ctor_details.get(name) { result.ctor_details.insert(*name, details.clone()); @@ -6601,12 +6816,24 @@ fn filter_exports( if all.constrained_class_methods.contains(method_name) { result.constrained_class_methods.insert(*method_name); } + if let Some(mc) = all.method_constraints.get(method_name) { + result.method_constraints.insert(*method_name, mc.clone()); + } } } // Export instances for this class if let Some(insts) = all.instances.get(name) { result.instances.insert(*name, insts.clone()); } + // Export instance registry entries for this class + for ((cn, tc), inst_name) in &all.instance_registry { + if cn == name { + result.instance_registry.insert((*cn, *tc), *inst_name); + if let Some(mod_parts) = all.instance_modules.get(inst_name) { + result.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } + } + } // Export class param count (needed for orphan detection and arity checking) if let Some(count) = all.class_param_counts.get(name) { result.class_param_counts.insert(*name, *count); @@ -6614,6 +6841,9 @@ fn filter_exports( if let Some(fd) = all.class_fundeps.get(name) { result.class_fundeps.insert(*name, fd.clone()); } + if let Some(supers) = all.class_superclasses.get(name) { + result.class_superclasses.insert(*name, supers.clone()); + } } Export::TypeOp(name) => { if let Some(target) = all.type_operators.get(name) { @@ -6652,6 +6882,15 @@ fn filter_exports( for name in &all.constrained_class_methods { result.constrained_class_methods.insert(*name); } + for (name, mc) in &all.method_constraints { + result.method_constraints.insert(*name, mc.clone()); + } + for (key, inst_name) in &all.instance_registry { + result.instance_registry.insert(*key, *inst_name); + } + for (inst_name, mod_parts) in &all.instance_modules { + result.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } for (name, alias) in &all.type_aliases { result.type_aliases.insert(*name, alias.clone()); } @@ -6661,6 +6900,12 @@ fn filter_exports( for (name, fd) in &all.class_fundeps { result.class_fundeps.insert(*name, fd.clone()); } + for (op, target) in &all.operator_targets { + result.operator_targets.insert(*op, target.clone()); + } + for (name, supers) in &all.class_superclasses { + result.class_superclasses.insert(*name, supers.clone()); + } continue; } // Re-export everything from the named module. @@ -6674,6 +6919,12 @@ fn filter_exports( .map(|q| module_name_to_symbol(q) == reexport_mod_sym) .unwrap_or(false); if matches_module || matches_alias { + // Skip qualified-only imports (e.g. `import M as Q` with no + // explicit import list). These don't bring names into unqualified + // scope, so `module M` should not re-export through them. + if import_decl.qualified.is_some() && import_decl.imports.is_none() && !matches_alias { + continue; + } // Look up from registry; also check Prim submodules let prim_sub; let full_exports = if is_prim_module(&import_decl.module) { @@ -6738,8 +6989,6 @@ fn filter_exports( } else { value_origins.insert(*name, origin); } - } - if imported { result.values.insert(*name, scheme.clone()); } } @@ -6799,6 +7048,15 @@ fn filter_exports( for name in &mod_exports.constrained_class_methods { result.constrained_class_methods.insert(*name); } + for (name, mc) in &mod_exports.method_constraints { + result.method_constraints.insert(*name, mc.clone()); + } + for (key, inst_name) in &mod_exports.instance_registry { + result.instance_registry.insert(*key, *inst_name); + } + for (inst_name, mod_parts) in &mod_exports.instance_modules { + result.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } for (name, alias) in &mod_exports.type_aliases { result.type_aliases.insert(*name, alias.clone()); } @@ -6808,6 +7066,12 @@ fn filter_exports( for (name, fd) in &mod_exports.class_fundeps { result.class_fundeps.insert(*name, fd.clone()); } + for (op, target) in &mod_exports.operator_targets { + result.operator_targets.insert(*op, target.clone()); + } + for (name, supers) in &mod_exports.class_superclasses { + result.class_superclasses.insert(*name, supers.clone()); + } } } } @@ -6858,6 +7122,23 @@ 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(); + for (name, mc) in &all.method_constraints { + result.method_constraints.entry(*name).or_insert_with(|| mc.clone()); + } + for (key, inst_name) in &all.instance_registry { + result.instance_registry.entry(*key).or_insert(*inst_name); + } + for (inst_name, mod_parts) in &all.instance_modules { + result.instance_modules.entry(*inst_name).or_insert_with(|| mod_parts.clone()); + } + for (name, supers) in &all.class_superclasses { + result.class_superclasses.entry(*name).or_insert_with(|| supers.clone()); + } + result.resolved_dicts = all.resolved_dicts.clone(); + // Propagate operator_targets so re-exporting modules preserve operator → function mappings + for (op, target) in &all.operator_targets { + result.operator_targets.entry(*op).or_insert_with(|| target.clone()); + } result } @@ -8678,8 +8959,8 @@ fn apply_var_subst(subst: &HashMap, ty: &Type) -> Type { fn check_cannot_generalize_recursive( state: &mut crate::typechecker::unify::UnifyState, _env: &Env, - deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], - op_deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], + deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec, Option)], + op_deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec, Option)], name: Symbol, span: crate::ast::span::Span, zonked_ty: &Type, @@ -8695,7 +8976,7 @@ fn check_cannot_generalize_recursive( // Check if any of those vars appear in deferred constraints (from infer_var) // or op deferred constraints (from infer_op_binary) - for (_, _, constraint_args) in deferred_constraints.iter().chain(op_deferred_constraints.iter()) { + for (_, _, constraint_args, _) in deferred_constraints.iter().chain(op_deferred_constraints.iter()) { for arg in constraint_args { let free_in_constraint: HashSet = state.free_unif_vars(arg).into_iter().collect(); @@ -8720,7 +9001,7 @@ fn check_cannot_generalize_recursive( /// false positives from partially resolved constraints. fn check_ambiguous_type_variables( state: &mut crate::typechecker::unify::UnifyState, - deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec)], + deferred_constraints: &[(crate::ast::span::Span, Symbol, Vec, Option)], constraint_start: usize, span: crate::ast::span::Span, zonked_ty: &Type, @@ -8738,7 +9019,7 @@ fn check_ambiguous_type_variables( } // Check only constraints added during THIS binding's inference - for (_, _, constraint_args) in deferred_constraints.iter().skip(constraint_start) { + for (_, _, constraint_args, _) in deferred_constraints.iter().skip(constraint_start) { let mut ambiguous_names: Vec = Vec::new(); let mut has_resolved = false; for arg in constraint_args { @@ -9439,7 +9720,8 @@ fn types_structurally_equal(a: &Type, b: &Type) -> bool { } /// Walks through Forall → Constrained patterns, converting constraint args to internal Types. -/// Skips Partial and Warn (which are handled separately). +/// Skips compiler-magic classes (Warn, Union, etc.) that are resolved by special solvers. +/// Note: Partial is NOT skipped — it needs dict parameters to prevent eager evaluation. pub(crate) fn extract_type_signature_constraints( ty: &crate::cst::TypeExpr, type_ops: &HashMap, @@ -9456,8 +9738,9 @@ pub(crate) fn extract_type_signature_constraints( let class_str = crate::interner::resolve(c.class.name).unwrap_or_default(); // Skip compiler-magic classes that don't have explicit instances. // These are resolved by special solvers or auto-satisfied. + // Note: Partial is NOT in this list — it needs dict params for lazy evaluation. let is_magic = matches!(class_str.as_str(), - "Partial" | "Warn" + "Warn" | "Union" | "Cons" | "RowToList" | "CompareSymbol" ); @@ -9768,6 +10051,143 @@ fn kind_collect_type_vars_shared(ty: &Type, seen: &mut std::collections::HashSet } } +/// Extract the head type constructor from a list of instance type arguments. +/// e.g. for `instance Eq (Array a)`, types = [App(Con(Array), Var(a))], returns Some(Array). +/// For `instance Eq Int`, types = [Con(Int)], returns Some(Int). +fn extract_head_type_con(types: &[Type]) -> Option { + fn head_con(ty: &Type) -> Option { + match ty { + Type::Con(s) => Some(*s), + Type::App(f, _) => head_con(f), + _ => None, + } + } + types.first().and_then(|t| head_con(t)) +} + +/// Recursively resolve a type class constraint to a DictExpr. +/// Given a constraint like `Show (Array Int)`, finds the matching instance (`showArray`), +/// then recursively resolves any instance constraints (e.g., `Show Int → showInt`), +/// producing `DictExpr::App(showArray, [DictExpr::Var(showInt)])`. +fn resolve_dict_expr( + class_name: Symbol, + type_args: &[Type], + instances: &HashMap, Vec<(Symbol, Vec)>)>>, + instance_registry: &HashMap<(Symbol, Symbol), Symbol>, + type_aliases: &HashMap, Type)>, + depth: usize, +) -> Option { + if depth > 10 { + return None; // prevent infinite recursion + } + + let head_con = extract_head_type_con(type_args)?; + let inst_name = instance_registry.get(&(class_name, head_con))?; + + // Find the instance entry to get its constraints + let class_instances = instances.get(&class_name)?; + let mut inst_constraints = None; + for (inst_types, constraints) in class_instances { + // Check if this instance matches our type args by matching head constructors + if let Some(inst_head) = extract_head_type_con(inst_types) { + if inst_head == head_con { + inst_constraints = Some((inst_types, constraints)); + break; + } + } + } + + let (inst_types, constraints) = inst_constraints?; + + if constraints.is_empty() { + return Some(DictExpr::Var(*inst_name)); + } + + // Build a substitution from instance type variables to concrete types + let mut subst: HashMap = HashMap::new(); + build_type_subst(inst_types, type_args, &mut subst); + + // Resolve each constraint with the substitution applied + let mut sub_dicts = Vec::new(); + for (constraint_class, constraint_args) in constraints { + let concrete_args: Vec = constraint_args + .iter() + .map(|t| apply_type_subst(t, &subst)) + .collect(); + + // Check if the constraint args are fully resolved (no remaining type vars) + if concrete_args.iter().any(|t| type_has_vars(t)) { + // Can't resolve — return simple Var as fallback + return Some(DictExpr::Var(*inst_name)); + } + + match resolve_dict_expr( + *constraint_class, + &concrete_args, + instances, + instance_registry, + type_aliases, + depth + 1, + ) { + Some(sub_dict) => sub_dicts.push(sub_dict), + None => return Some(DictExpr::Var(*inst_name)), // fallback + } + } + + Some(DictExpr::App(*inst_name, sub_dicts)) +} + +/// Match instance types against concrete types to build a variable substitution. +/// E.g., matching `[App(Con(Array), Var(a))]` against `[App(Con(Array), Con(Int))]` +/// produces `{a → Con(Int)}`. +fn build_type_subst(pattern: &[Type], concrete: &[Type], subst: &mut HashMap) { + for (p, c) in pattern.iter().zip(concrete.iter()) { + build_type_subst_single(p, c, subst); + } +} + +fn build_type_subst_single(pattern: &Type, concrete: &Type, subst: &mut HashMap) { + match (pattern, concrete) { + (Type::Var(v), _) => { + subst.insert(*v, concrete.clone()); + } + (Type::App(pf, pa), Type::App(cf, ca)) => { + build_type_subst_single(pf, cf, subst); + build_type_subst_single(pa, ca, subst); + } + (Type::Fun(pa, pb), Type::Fun(ca, cb)) => { + build_type_subst_single(pa, ca, subst); + build_type_subst_single(pb, cb, subst); + } + _ => {} // Con, etc. — nothing to bind + } +} + +/// Apply a type variable substitution to a type. +fn apply_type_subst(ty: &Type, subst: &HashMap) -> Type { + match ty { + Type::Var(v) => subst.get(v).cloned().unwrap_or_else(|| ty.clone()), + Type::App(f, a) => Type::App( + Box::new(apply_type_subst(f, subst)), + Box::new(apply_type_subst(a, subst)), + ), + Type::Fun(a, b) => Type::Fun( + Box::new(apply_type_subst(a, subst)), + Box::new(apply_type_subst(b, subst)), + ), + Type::Forall(vars, body) => { + // Don't substitute bound variables + let any_bound = vars.iter().any(|(v, _)| subst.contains_key(v)); + if any_bound { + ty.clone() + } else { + Type::Forall(vars.clone(), Box::new(apply_type_subst(body, subst))) + } + } + _ => ty.clone(), + } +} + /// Check if a type expression has any type class constraint (at the top level, under forall/parens). fn has_any_constraint(ty: &crate::cst::TypeExpr) -> Option { use crate::cst::TypeExpr; diff --git a/src/typechecker/infer.rs b/src/typechecker/infer.rs index d54127af..428e2ee7 100644 --- a/src/typechecker/infer.rs +++ b/src/typechecker/infer.rs @@ -28,9 +28,9 @@ pub struct InferCtx { /// Map from method name → (class_name, class_type_vars). /// Set by check_module before typechecking value declarations. pub class_methods: HashMap)>, - /// Deferred type class constraints: (span, class_name, [type_args as unif vars]). + /// Deferred type class constraints: (span, class_name, [type_args], binding_name). /// Checked after inference to verify instances exist. - pub deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec, Option)>, /// Map from type constructor name → list of data constructor names. /// Used for exhaustiveness checking of case expressions. pub data_constructors: HashMap>, @@ -62,6 +62,16 @@ 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, + /// Actual constraints extracted from class method type signatures. + /// e.g. for `eq1 :: forall a. Eq a => f a -> f a -> Boolean`, stores `eq1 → [(Eq, [a])]`. + /// Used by codegen to wrap instance method bodies with dict parameters. + pub method_constraints: HashMap)>>, + /// Instance name registry: (class_name, head_type_constructor) → instance_variable_name. + /// e.g. `(Eq, Array) → eqArray`. Used by codegen for instance-aware dict composition. + pub instance_registry: HashMap<(Symbol, Symbol), Symbol>, + /// Instance defining modules: instance_name → module_parts. + /// Tracks which module originally declared each instance for codegen. + pub instance_modules: 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. @@ -70,10 +80,13 @@ pub struct InferCtx { /// Map from operator → class method target name (e.g. `<>` → `append`). /// Used for tracking deferred constraints on operator usage. pub operator_class_targets: HashMap, + /// Map from operator → target function name (ALL operators, not just class methods). + /// e.g. `<#>` → `mapFlipped`, `>>>` → `composeFlipped` + pub operator_function_targets: HashMap, /// Deferred constraints from operator usage (e.g. `<>` → Semigroup constraint). /// Only used for CannotGeneralizeRecursiveFunction detection, NOT for instance /// resolution (the instance matcher can't handle complex nested types). - pub op_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub op_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec, Option)>, /// Map from class name → (type_vars, fundeps as (lhs_indices, rhs_indices)). /// Used for fundep-aware orphan instance checking. pub class_fundeps: HashMap, Vec<(Vec, Vec)>)>, @@ -87,7 +100,7 @@ pub struct InferCtx { /// Deferred constraints from signature propagation (separate from main deferred_constraints). /// These are only checked for zero-instance classes, since our instance resolution /// may not handle complex imported instances correctly. - pub sig_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec)>, + pub sig_deferred_constraints: Vec<(crate::ast::span::Span, Symbol, Vec, Option)>, /// Classes with instance chains (else keyword). Used to route chained class constraints /// to deferred_constraints for proper chain ambiguity checking. pub chained_classes: std::collections::HashSet, @@ -122,6 +135,13 @@ pub struct InferCtx { /// fresh unif vars for these args so that at constraint resolution time we can /// check kind consistency between the class kind signature and the concrete types. pub class_param_app_args: HashMap>, + /// Currently-being-checked top-level value binding name. + /// Used to tag deferred constraints so we can associate resolved instances with bindings. + pub current_binding_name: Option, + /// Per-binding resolved concrete instance dictionaries. + /// Maps binding_name → [(class_name, dict_expr)]. + /// Populated during Pass 3 constraint resolution when a concrete type resolves to an instance. + pub resolved_dicts: HashMap>, } impl InferCtx { @@ -140,9 +160,13 @@ impl InferCtx { value_fixities: HashMap::new(), function_op_aliases: HashSet::new(), constrained_class_methods: HashSet::new(), + method_constraints: HashMap::new(), + instance_registry: HashMap::new(), + instance_modules: HashMap::new(), module_mode: false, scope_conflicts: HashSet::new(), operator_class_targets: HashMap::new(), + operator_function_targets: HashMap::new(), op_deferred_constraints: Vec::new(), class_fundeps: HashMap::new(), has_non_exhaustive_pattern_guards: false, @@ -157,6 +181,8 @@ impl InferCtx { has_partial_lambda: false, partial_dischargers: HashSet::new(), class_param_app_args: HashMap::new(), + current_binding_name: None, + resolved_dicts: HashMap::new(), } } @@ -415,7 +441,7 @@ impl InferCtx { } if !self.given_class_names.contains(&class_name) { - self.deferred_constraints.push((span, class_name, constraint_types)); + self.deferred_constraints.push((span, class_name, constraint_types, self.current_binding_name)); } return Ok(result); @@ -447,9 +473,9 @@ impl InferCtx { | "Coercible" | "Nub" ); if has_solver { - self.deferred_constraints.push((span, *class_name, subst_args)); + self.deferred_constraints.push((span, *class_name, subst_args, self.current_binding_name)); } else { - self.sig_deferred_constraints.push((span, *class_name, subst_args)); + self.sig_deferred_constraints.push((span, *class_name, subst_args, self.current_binding_name)); } } } @@ -1153,7 +1179,7 @@ impl InferCtx { } } if ok { - self.deferred_constraints.push((constraint.span, constraint.class.name, args)); + self.deferred_constraints.push((constraint.span, constraint.class.name, args, self.current_binding_name)); } } self.extract_inline_annotation_constraints(ty, span); @@ -1328,7 +1354,7 @@ impl InferCtx { } } - self.deferred_constraints.push((span, class_name, constraint_types)); + self.deferred_constraints.push((span, class_name, constraint_types, self.current_binding_name)); } Ok(ty) @@ -1477,19 +1503,87 @@ impl InferCtx { let first_hole_ty = if first_is_hole { Some(operand_types[0].clone()) } else { None }; let last_hole_ty = if last_is_hole { Some(operand_types[operand_types.len() - 1].clone()) } else { None }; - // Look up and instantiate all operator types + // Look up and instantiate all operator types, pushing deferred constraints let mut op_types: Vec = Vec::new(); for op in &operators { + let op_name = op.value.name; let op_lookup = if let Some(module) = op.value.module { - let qual_sym = Self::qualified_symbol(module, op.value.name); + let qual_sym = Self::qualified_symbol(module, op_name); env.lookup(qual_sym) } else { - env.lookup(op.value.name) + env.lookup(op_name) }; let op_ty = match op_lookup { Some(scheme) => { let ty = self.instantiate(scheme); - self.instantiate_forall_type(ty)? + // Check for class method constraints (same as infer_op_binary) + let class_info = self.class_methods.get(&op_name).cloned() + .or_else(|| { + self.operator_class_targets.get(&op_name) + .and_then(|target| self.class_methods.get(target).cloned()) + }); + if let Some((class_name, class_tvs)) = class_info { + if let Type::Forall(vars, body) = &ty { + let var_names: Vec = vars.iter().map(|&(v, _)| v).collect(); + let is_class_forall = !class_tvs.is_empty() + && var_names.len() >= class_tvs.len() + && var_names[..class_tvs.len()] == class_tvs[..]; + if is_class_forall { + let subst: HashMap = vars + .iter() + .map(|&(v, _)| (v, Type::Unif(self.state.fresh_var()))) + .collect(); + let result = self.apply_symbol_subst(&subst, body); + let result = self.instantiate_forall_type(result)?; + let constraint_types: Vec = class_tvs + .iter() + .filter_map(|tv| subst.get(tv).cloned()) + .collect(); + self.op_deferred_constraints.push((span, class_name, constraint_types, self.current_binding_name)); + result + } else { + self.instantiate_forall_type(ty)? + } + } else { + self.instantiate_forall_type(ty)? + } + } else { + // Check signature constraints for non-class-method operators + let target_fn = self.operator_function_targets.get(&op_name).copied() + .unwrap_or(op_name); + let sig_constraints = self.signature_constraints.get(&target_fn).cloned() + .unwrap_or_default(); + if !sig_constraints.is_empty() { + if let Type::Forall(vars, body) = &ty { + let subst: HashMap = vars + .iter() + .map(|&(v, _)| (v, Type::Unif(self.state.fresh_var()))) + .collect(); + let result = self.apply_symbol_subst(&subst, body); + let result = self.instantiate_forall_type(result)?; + for (class_name, class_tvs) in &sig_constraints { + let constraint_types: Vec = class_tvs + .iter() + .filter_map(|t| { + if let Type::Var(tv) = t { + subst.get(tv).cloned() + } else { + Some(self.apply_symbol_subst(&subst, t)) + } + }) + .collect(); + if !constraint_types.is_empty() { + self.op_deferred_constraints.push((span, *class_name, constraint_types, self.current_binding_name)); + } + } + result + } else { + self.instantiate_forall_type(ty)? + } + } else { + self.instantiate_forall_type(ty)? + } + } } None => { return Err(TypeError::UndefinedVariable { @@ -1672,7 +1766,7 @@ impl InferCtx { .iter() .filter_map(|tv| subst.get(tv).cloned()) .collect(); - self.op_deferred_constraints.push((span, class_name, constraint_types)); + self.op_deferred_constraints.push((span, class_name, constraint_types, self.current_binding_name)); result } else { self.instantiate_forall_type(ty)? @@ -1681,7 +1775,42 @@ impl InferCtx { self.instantiate_forall_type(ty)? } } else { - self.instantiate_forall_type(ty)? + // Not a class method — check if the operator target has signature constraints. + // Functions like `mapFlipped :: Functor f => ...` need their constraints deferred. + let target_fn = self.operator_function_targets.get(&op_name).copied() + .unwrap_or(op_name); + let sig_constraints = self.signature_constraints.get(&target_fn).cloned() + .unwrap_or_default(); + if !sig_constraints.is_empty() { + if let Type::Forall(vars, body) = &ty { + let subst: HashMap = vars + .iter() + .map(|&(v, _)| (v, Type::Unif(self.state.fresh_var()))) + .collect(); + let result = self.apply_symbol_subst(&subst, body); + let result = self.instantiate_forall_type(result)?; + for (class_name, class_tvs) in &sig_constraints { + let constraint_types: Vec = class_tvs + .iter() + .filter_map(|t| { + if let Type::Var(tv) = t { + subst.get(tv).cloned() + } else { + Some(self.apply_symbol_subst(&subst, t)) + } + }) + .collect(); + if !constraint_types.is_empty() { + self.op_deferred_constraints.push((span, *class_name, constraint_types, self.current_binding_name)); + } + } + result + } else { + self.instantiate_forall_type(ty)? + } + } else { + self.instantiate_forall_type(ty)? + } } } None => { @@ -2296,6 +2425,12 @@ impl InferCtx { if env.lookup(bind_sym).is_none() { return Err(TypeError::UndefinedVariable { span, name: bind_sym }); } + // Defer a Bind constraint so codegen can resolve the concrete dictionary + // (e.g., bindEffect for Effect monad). Do-notation desugars to `bind` calls + // but doesn't go through normal variable inference, so we must explicitly + // create the constraint here. + let bind_class = crate::interner::intern("Bind"); + self.deferred_constraints.push((span, bind_class, vec![monad_ty.clone()], self.current_binding_name)); } // Check that `discard` is in scope when do-notation has non-last discards diff --git a/src/typechecker/kind.rs b/src/typechecker/kind.rs index ea09ab18..ae5ef270 100644 --- a/src/typechecker/kind.rs +++ b/src/typechecker/kind.rs @@ -723,9 +723,19 @@ pub fn infer_data_kind( } } - // Check deferred quantification (forall vars with unsolved kinds) - if let Some(err) = ks.check_deferred_quantification() { - return Err(err); + // Check deferred quantification (forall vars with unsolved kinds). + // Exclude unif vars that appear in the data type's own type parameter kinds, + // since those are legitimately polymorphic and determined by the outer context + // (same logic as class method kind checking). + { + let mut exclude_ids: HashSet = HashSet::new(); + for tv in type_vars { + let zonked = ks.zonk_kind(var_kinds[&tv.value].clone()); + collect_unif_var_ids(&zonked, &mut exclude_ids); + } + if let Some(err) = ks.check_deferred_quantification_excluding(&exclude_ids) { + return Err(err); + } } // Build the overall kind: k1 -> k2 -> ... -> Type @@ -764,9 +774,17 @@ pub fn infer_newtype_kind( let field_kind = infer_kind(ks, field_ty, &var_kinds, type_ops, Some(name))?; ks.unify_kinds(span, &k_type, &field_kind)?; - // Check deferred quantification (forall vars with unsolved kinds) - if let Some(err) = ks.check_deferred_quantification() { - return Err(err); + // Check deferred quantification (forall vars with unsolved kinds). + // Exclude unif vars from the newtype's own type parameter kinds. + { + let mut exclude_ids: HashSet = HashSet::new(); + for tv in type_vars { + let zonked = ks.zonk_kind(var_kinds[&tv.value].clone()); + collect_unif_var_ids(&zonked, &mut exclude_ids); + } + if let Some(err) = ks.check_deferred_quantification_excluding(&exclude_ids) { + return Err(err); + } } // Build kind: k1 -> k2 -> ... -> Type diff --git a/tests/codegen.rs b/tests/codegen.rs index 8e9e91dc..71770037 100644 --- a/tests/codegen.rs +++ b/tests/codegen.rs @@ -78,6 +78,66 @@ fn codegen_fixture_with_js(purs_source: &str, js_source: Option<&str>) -> String codegen::printer::print_module(&js_module) } +/// Build a multi-module project and return the generated JS for a specific target module. +fn codegen_fixture_multi(sources: &[(&str, &str)], target_purs: &str) -> String { + let (result, registry) = + build_from_sources_with_js(sources, &None, None); + + assert!( + result.build_errors.is_empty(), + "Build errors: {:?}", + result + .build_errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + + for module in &result.modules { + assert!( + module.type_errors.is_empty(), + "Type errors in {}: {:?}", + module.module_name, + module + .type_errors + .iter() + .map(|e| e.to_string()) + .collect::>() + ); + } + + // Find and parse the target module + let target_source = sources + .iter() + .find(|(name, _)| *name == target_purs) + .expect("Target module not found in sources") + .1; + + let parsed_module = purescript_fast_compiler::parse(target_source).expect("Parse failed"); + let module_parts: Vec<_> = parsed_module.name.value.parts.clone(); + + let module_name = module_parts + .iter() + .map(|s| purescript_fast_compiler::interner::resolve(*s).unwrap_or_default()) + .collect::>() + .join("."); + + let exports = registry + .lookup(&module_parts) + .expect("Module not found in registry"); + + let js_module = codegen::js::module_to_js( + &parsed_module, + &module_name, + &module_parts, + exports, + ®istry, + false, + ); + + codegen::printer::print_module(&js_module) +} + /// Validate that a JS string is syntactically valid by parsing with SWC. fn assert_valid_js(js: &str, context: &str) { use swc_common::{FileName, SourceMap, sync::Lrc}; @@ -138,6 +198,23 @@ macro_rules! codegen_test_with_ffi { }; } +macro_rules! codegen_test_multi { + ($name:ident, $dir:expr, $target:expr, [$( $file:expr ),+ $(,)?]) => { + #[test] + fn $name() { + let sources = vec![ + $( + ($file, include_str!(concat!("fixtures/codegen/", $dir, "/", $file))), + )+ + ]; + let js = codegen_fixture_multi(&sources, $target); + assert!(!js.is_empty(), "Generated JS should not be empty"); + assert_valid_js(&js, $dir); + insta::assert_snapshot!(concat!("codegen_", $dir), js); + } + }; +} + codegen_test!(codegen_literals, "Literals"); codegen_test!(codegen_functions, "Functions"); codegen_test!(codegen_data_constructors, "DataConstructors"); @@ -150,4 +227,164 @@ codegen_test!(codegen_case_expressions, "CaseExpressions"); codegen_test!(codegen_negate_and_unary, "NegateAndUnary"); codegen_test!(codegen_reserved_words, "ReservedWords"); codegen_test!(codegen_instance_dictionaries, "InstanceDictionaries"); +codegen_test!(codegen_constrained_functions, "ConstrainedFunctions"); +codegen_test!(codegen_instance_constraints, "InstanceConstraints"); codegen_test_with_ffi!(codegen_foreign_import, "ForeignImport"); +codegen_test!(codegen_operator_resolution_local, "OperatorResolutionLocal"); +codegen_test_multi!(codegen_operator_explicit_import, "OperatorExplicitImport", "App.purs", ["MyLib.purs", "App.purs"]); +codegen_test_multi!(codegen_operator_hiding_import, "OperatorHidingImport", "App.purs", ["MyLib.purs", "App.purs"]); +codegen_test_multi!(codegen_operator_module_reexport, "OperatorModuleReexport", "App.purs", ["MyLib.purs", "MyPrelude.purs", "App.purs"]); +codegen_test_multi!(codegen_superclass_dict, "SuperclassDict", "TestSuperclass.purs", ["MyFunctor.purs", "MyApply.purs", "TestSuperclass.purs"]); +codegen_test!(codegen_call_site_dict_passing, "CallSiteDictPassing"); +codegen_test_multi!(codegen_superclass_dict_deep, "SuperclassDictDeep", "TestDeep.purs", ["MyFunctor.purs", "MyApply.purs", "MyAlternative.purs", "MyPrelude.purs", "TestDeep.purs"]); +codegen_test_multi!(codegen_method_constraints, "MethodConstraints", "MyEq1.purs", ["MyEq.purs", "MyEq1.purs", "TestMethodConstraints.purs"]); + +// ===== Node.js execution tests ===== +// These tests verify that the generated JS actually runs correctly by executing +// it with Node.js and checking assertions via process.exit codes. + +/// Generate JS from PureScript, append a test harness, write to a temp file, and run with Node. +fn run_js_with_assertions(purs_source: &str, test_script: &str) { + let js = codegen_fixture(purs_source); + assert_valid_js(&js, "run_test"); + + // Strip the ES module export line so we can run it as a plain script + let js_without_exports: String = js + .lines() + .filter(|line| !line.starts_with("export {") && !line.starts_with("import ")) + .collect::>() + .join("\n"); + + let full_script = format!("{js_without_exports}\n\n// Test assertions\n{test_script}"); + + let tmp_dir = std::env::temp_dir().join("pfc_test"); + let _ = std::fs::create_dir_all(&tmp_dir); + static TEST_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let id = TEST_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let tmp_file = tmp_dir.join(format!("test_{}_{}.mjs", std::process::id(), id)); + std::fs::write(&tmp_file, &full_script).expect("Failed to write temp JS file"); + + let output = std::process::Command::new("node") + .arg(&tmp_file) + .output() + .expect("Failed to run node"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if !output.status.success() { + let _ = std::fs::remove_file(&tmp_file); + panic!( + "Node.js execution failed (exit code {:?}):\n\ + --- stdout ---\n{}\n\ + --- stderr ---\n{}\n\ + --- generated JS ---\n{}", + output.status.code(), + stdout, + stderr, + full_script + ); + } + + let _ = std::fs::remove_file(&tmp_file); +} + +#[test] +fn node_run_literals() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunLiterals.purs"), + r#" +if (anInt !== 42) throw new Error("anInt should be 42, got " + anInt); +if (aString !== "hello") throw new Error("aString should be hello"); +if (aBool !== true) throw new Error("aBool should be true"); +if (anArray.length !== 3) throw new Error("anArray should have length 3"); +if (anArray[0] !== 1) throw new Error("anArray[0] should be 1"); +"#, + ); +} + +#[test] +fn node_run_functions() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunFunctions.purs"), + r#" +if (identity(42) !== 42) throw new Error("identity(42) should be 42"); +if (identity("hello") !== "hello") throw new Error("identity('hello') should be 'hello'"); +if (constFunc(1)(2) !== 1) throw new Error("constFunc(1)(2) should be 1"); +if (apply(identity)(99) !== 99) throw new Error("apply(identity)(99) should be 99"); +"#, + ); +} + +#[test] +fn node_run_patterns() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunPatterns.purs"), + r#" +var n = Nothing.value; +var j = Just.create(42); +if (fromMaybe(0)(n) !== 0) throw new Error("fromMaybe(0)(Nothing) should be 0"); +if (fromMaybe(0)(j) !== 42) throw new Error("fromMaybe(0)(Just 42) should be 42"); +if (isJust(n) !== false) throw new Error("isJust(Nothing) should be false"); +if (isJust(j) !== true) throw new Error("isJust(Just 42) should be true"); +"#, + ); +} + +#[test] +fn node_run_records() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunRecords.purs"), + r#" +var p = mkPerson("Alice")(30); +if (getName(p) !== "Alice") throw new Error("getName should be Alice"); +if (getAge(p) !== 30) throw new Error("getAge should be 30"); +if (p.name !== "Alice") throw new Error("p.name should be Alice"); +if (p.age !== 30) throw new Error("p.age should be 30"); +"#, + ); +} + +#[test] +fn node_run_newtype() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunNewtype.purs"), + r#" +var n = mkName("Bob"); +if (n !== "Bob") throw new Error("newtype should be erased, got " + JSON.stringify(n)); +var unwrapped = unwrapName("Charlie"); +if (unwrapped !== "Charlie") throw new Error("unwrapName should pass through, got " + unwrapped); +"#, + ); +} + +#[test] +fn node_run_let_where() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunLetWhere.purs"), + r#" +if (letSimple !== 42) throw new Error("letSimple should be 42, got " + letSimple); +if (whereSimple !== 99) throw new Error("whereSimple should be 99, got " + whereSimple); +"#, + ); +} + +#[test] +fn node_run_constrained_functions() { + run_js_with_assertions( + include_str!("fixtures/codegen/RunConstrainedFunctions.purs"), + r#" +// showDesc dispatches through the dict parameter +if (showDesc(describableBool)(true) !== "true") + throw new Error("showDesc(describableBool)(true) should be 'true', got " + showDesc(describableBool)(true)); +if (showDesc(describableBool)(false) !== "false") + throw new Error("showDesc(describableBool)(false) should be 'false'"); +if (showDesc(describableInt)(42) !== "int") + throw new Error("showDesc(describableInt)(42) should be 'int', got " + showDesc(describableInt)(42)); +// The dict parameter is a plain object with the method +if (typeof describableBool.describe !== "function") + throw new Error("describableBool.describe should be a function"); +"#, + ); +} + diff --git a/tests/fixtures/codegen/CallSiteDictPassing.purs b/tests/fixtures/codegen/CallSiteDictPassing.purs new file mode 100644 index 00000000..b6740691 --- /dev/null +++ b/tests/fixtures/codegen/CallSiteDictPassing.purs @@ -0,0 +1,11 @@ +module CallSiteDictPassing where + +class MyShow a where + myShow :: a -> String + +myShowThing :: forall a. MyShow a => a -> String +myShowThing x = myShow x + +-- Should pass dictMyShow to myShowThing at the call site +wrapper :: forall a. MyShow a => a -> String +wrapper x = myShowThing x diff --git a/tests/fixtures/codegen/ConstrainedFunctions.purs b/tests/fixtures/codegen/ConstrainedFunctions.purs new file mode 100644 index 00000000..130cba1b --- /dev/null +++ b/tests/fixtures/codegen/ConstrainedFunctions.purs @@ -0,0 +1,28 @@ +-- | Tests for typeclass dictionary passing in generated JavaScript. +-- | Constrained functions must receive a dictionary argument for each constraint, +-- | and class method calls inside must be dispatched through the dict. +module ConstrainedFunctions where + +class Describable a where + describe :: a -> String + +class MyEq a where + myEq :: a -> a -> Boolean + +instance describableBool :: Describable Boolean where + describe true = "true" + describe false = "false" + +instance describableInt :: Describable Int where + describe _ = "int" + +instance myEqInt :: MyEq Int where + myEq _ _ = true + +-- Simple single-constraint function +showDesc :: forall a. Describable a => a -> String +showDesc x = describe x + +-- Multiple constraints: both dicts are threaded as separate parameters +describeTwo :: forall a. Describable a => MyEq a => a -> a -> String +describeTwo x _ = describe x diff --git a/tests/fixtures/codegen/InstanceConstraints.purs b/tests/fixtures/codegen/InstanceConstraints.purs new file mode 100644 index 00000000..1069c473 --- /dev/null +++ b/tests/fixtures/codegen/InstanceConstraints.purs @@ -0,0 +1,21 @@ +-- | Tests for instances that themselves have constraints, +-- | e.g. `instance (MyShow a) => MyShow (Maybe a) where ...` +-- | The instance value becomes a function taking the constraint dict. +module InstanceConstraints where + +data Maybe a = Nothing | Just a + +class MyShow a where + myShow :: a -> String + +instance myShowInt :: MyShow Int where + myShow _ = "int" + +-- This instance requires MyShow a, so it becomes: +-- var myShowMaybe = function(dictMyShow) { return { myShow: ... }; } +instance myShowMaybe :: MyShow a => MyShow (Maybe a) where + myShow Nothing = "nothing" + myShow (Just x) = myShow x + +showMaybeValue :: forall a. MyShow a => Maybe a -> String +showMaybeValue x = myShow x diff --git a/tests/fixtures/codegen/MethodConstraints/MyEq.purs b/tests/fixtures/codegen/MethodConstraints/MyEq.purs new file mode 100644 index 00000000..628bbd26 --- /dev/null +++ b/tests/fixtures/codegen/MethodConstraints/MyEq.purs @@ -0,0 +1,10 @@ +module MyEq where + +class MyEq a where + myEq :: a -> a -> Boolean + +instance myEqInt :: MyEq Int where + myEq x y = true + +instance myEqArray :: MyEq a => MyEq (Array a) where + myEq xs ys = true diff --git a/tests/fixtures/codegen/MethodConstraints/MyEq1.purs b/tests/fixtures/codegen/MethodConstraints/MyEq1.purs new file mode 100644 index 00000000..bb0b0b3d --- /dev/null +++ b/tests/fixtures/codegen/MethodConstraints/MyEq1.purs @@ -0,0 +1,9 @@ +module MyEq1 where + +import MyEq (class MyEq, myEq) + +class MyEq1 f where + myEq1 :: forall a. MyEq a => f a -> f a -> Boolean + +instance myEq1Array :: MyEq1 Array where + myEq1 = myEq diff --git a/tests/fixtures/codegen/MethodConstraints/TestMethodConstraints.purs b/tests/fixtures/codegen/MethodConstraints/TestMethodConstraints.purs new file mode 100644 index 00000000..3ebe8339 --- /dev/null +++ b/tests/fixtures/codegen/MethodConstraints/TestMethodConstraints.purs @@ -0,0 +1,8 @@ +module TestMethodConstraints where + +import MyEq (class MyEq) +import MyEq1 (class MyEq1, myEq1) + +-- Use eq1 in a constrained function to verify dict passing works +testEq1 :: forall a. MyEq a => Array a -> Array a -> Boolean +testEq1 xs ys = myEq1 xs ys diff --git a/tests/fixtures/codegen/OperatorExplicitImport/App.purs b/tests/fixtures/codegen/OperatorExplicitImport/App.purs new file mode 100644 index 00000000..249d1b93 --- /dev/null +++ b/tests/fixtures/codegen/OperatorExplicitImport/App.purs @@ -0,0 +1,6 @@ +module App where + +import MyLib (class MyAppend, (<>)) + +joined :: forall a. MyAppend a => a -> a -> a +joined x y = x <> y diff --git a/tests/fixtures/codegen/OperatorExplicitImport/MyLib.purs b/tests/fixtures/codegen/OperatorExplicitImport/MyLib.purs new file mode 100644 index 00000000..57b854ab --- /dev/null +++ b/tests/fixtures/codegen/OperatorExplicitImport/MyLib.purs @@ -0,0 +1,6 @@ +module MyLib where + +class MyAppend a where + myAppend :: a -> a -> a + +infixr 5 myAppend as <> diff --git a/tests/fixtures/codegen/OperatorHidingImport/App.purs b/tests/fixtures/codegen/OperatorHidingImport/App.purs new file mode 100644 index 00000000..8f8bd390 --- /dev/null +++ b/tests/fixtures/codegen/OperatorHidingImport/App.purs @@ -0,0 +1,6 @@ +module App where + +import MyLib hiding (unused) + +joined :: forall a. MyAppend a => a -> a -> a +joined x y = x <> y diff --git a/tests/fixtures/codegen/OperatorHidingImport/MyLib.purs b/tests/fixtures/codegen/OperatorHidingImport/MyLib.purs new file mode 100644 index 00000000..db597d92 --- /dev/null +++ b/tests/fixtures/codegen/OperatorHidingImport/MyLib.purs @@ -0,0 +1,9 @@ +module MyLib where + +class MyAppend a where + myAppend :: a -> a -> a + +infixr 5 myAppend as <> + +unused :: Int +unused = 0 diff --git a/tests/fixtures/codegen/OperatorModuleReexport/App.purs b/tests/fixtures/codegen/OperatorModuleReexport/App.purs new file mode 100644 index 00000000..51dc126b --- /dev/null +++ b/tests/fixtures/codegen/OperatorModuleReexport/App.purs @@ -0,0 +1,6 @@ +module App where + +import MyPrelude (class MyAppend, (<>)) + +joined :: forall a. MyAppend a => a -> a -> a +joined x y = x <> y diff --git a/tests/fixtures/codegen/OperatorModuleReexport/MyLib.purs b/tests/fixtures/codegen/OperatorModuleReexport/MyLib.purs new file mode 100644 index 00000000..57b854ab --- /dev/null +++ b/tests/fixtures/codegen/OperatorModuleReexport/MyLib.purs @@ -0,0 +1,6 @@ +module MyLib where + +class MyAppend a where + myAppend :: a -> a -> a + +infixr 5 myAppend as <> diff --git a/tests/fixtures/codegen/OperatorModuleReexport/MyPrelude.purs b/tests/fixtures/codegen/OperatorModuleReexport/MyPrelude.purs new file mode 100644 index 00000000..d6e54868 --- /dev/null +++ b/tests/fixtures/codegen/OperatorModuleReexport/MyPrelude.purs @@ -0,0 +1,3 @@ +module MyPrelude (module MyLib) where + +import MyLib diff --git a/tests/fixtures/codegen/OperatorResolutionLocal.purs b/tests/fixtures/codegen/OperatorResolutionLocal.purs new file mode 100644 index 00000000..2a13c44f --- /dev/null +++ b/tests/fixtures/codegen/OperatorResolutionLocal.purs @@ -0,0 +1,12 @@ +module TestOperator where + +class MyAppend a where + myAppend :: a -> a -> a + +infixr 5 myAppend as <> + +instance myAppendInt :: MyAppend Int where + myAppend x _ = x + +joined :: forall a. MyAppend a => a -> a -> a +joined x y = x <> y diff --git a/tests/fixtures/codegen/RunConstrainedFunctions.purs b/tests/fixtures/codegen/RunConstrainedFunctions.purs new file mode 100644 index 00000000..3daa7c79 --- /dev/null +++ b/tests/fixtures/codegen/RunConstrainedFunctions.purs @@ -0,0 +1,15 @@ +-- | Node.js-executable tests for typeclass dictionary dispatch. +module RunConstrainedFunctions where + +class Describable a where + describe :: a -> String + +instance describableBool :: Describable Boolean where + describe true = "true" + describe false = "false" + +instance describableInt :: Describable Int where + describe _ = "int" + +showDesc :: forall a. Describable a => a -> String +showDesc x = describe x diff --git a/tests/fixtures/codegen/RunFunctions.purs b/tests/fixtures/codegen/RunFunctions.purs new file mode 100644 index 00000000..22343557 --- /dev/null +++ b/tests/fixtures/codegen/RunFunctions.purs @@ -0,0 +1,10 @@ +module RunFunctions where + +identity :: forall a. a -> a +identity x = x + +constFunc :: forall a b. a -> b -> a +constFunc x _ = x + +apply :: forall a b. (a -> b) -> a -> b +apply f x = f x diff --git a/tests/fixtures/codegen/RunLetWhere.purs b/tests/fixtures/codegen/RunLetWhere.purs new file mode 100644 index 00000000..de971761 --- /dev/null +++ b/tests/fixtures/codegen/RunLetWhere.purs @@ -0,0 +1,9 @@ +module RunLetWhere where + +letSimple :: Int +letSimple = let x = 42 in x + +whereSimple :: Int +whereSimple = result + where + result = 99 diff --git a/tests/fixtures/codegen/RunLiterals.purs b/tests/fixtures/codegen/RunLiterals.purs new file mode 100644 index 00000000..28116136 --- /dev/null +++ b/tests/fixtures/codegen/RunLiterals.purs @@ -0,0 +1,13 @@ +module RunLiterals where + +anInt :: Int +anInt = 42 + +aString :: String +aString = "hello" + +aBool :: Boolean +aBool = true + +anArray :: Array Int +anArray = [1, 2, 3] diff --git a/tests/fixtures/codegen/RunNewtype.purs b/tests/fixtures/codegen/RunNewtype.purs new file mode 100644 index 00000000..aa5b7d6b --- /dev/null +++ b/tests/fixtures/codegen/RunNewtype.purs @@ -0,0 +1,9 @@ +module RunNewtype where + +newtype Name = Name String + +mkName :: String -> Name +mkName s = Name s + +unwrapName :: Name -> String +unwrapName (Name s) = s diff --git a/tests/fixtures/codegen/RunPatterns.purs b/tests/fixtures/codegen/RunPatterns.purs new file mode 100644 index 00000000..2d8f7ea8 --- /dev/null +++ b/tests/fixtures/codegen/RunPatterns.purs @@ -0,0 +1,13 @@ +module RunPatterns where + +data Maybe a = Nothing | Just a + +fromMaybe :: forall a. a -> Maybe a -> a +fromMaybe def m = case m of + Nothing -> def + Just x -> x + +isJust :: forall a. Maybe a -> Boolean +isJust m = case m of + Nothing -> false + Just _ -> true diff --git a/tests/fixtures/codegen/RunRecords.purs b/tests/fixtures/codegen/RunRecords.purs new file mode 100644 index 00000000..845cb4ab --- /dev/null +++ b/tests/fixtures/codegen/RunRecords.purs @@ -0,0 +1,12 @@ +module RunRecords where + +type Person = { name :: String, age :: Int } + +mkPerson :: String -> Int -> Person +mkPerson n a = { name: n, age: a } + +getName :: Person -> String +getName p = p.name + +getAge :: Person -> Int +getAge p = p.age diff --git a/tests/fixtures/codegen/SuperclassDict/MyApply.purs b/tests/fixtures/codegen/SuperclassDict/MyApply.purs new file mode 100644 index 00000000..284f8a3a --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDict/MyApply.purs @@ -0,0 +1,6 @@ +module MyApply where + +import MyFunctor (class MyFunctor) + +class MyFunctor f <= MyApply f where + myApply :: forall a b. f (a -> b) -> f a -> f b diff --git a/tests/fixtures/codegen/SuperclassDict/MyFunctor.purs b/tests/fixtures/codegen/SuperclassDict/MyFunctor.purs new file mode 100644 index 00000000..b36814b1 --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDict/MyFunctor.purs @@ -0,0 +1,4 @@ +module MyFunctor where + +class MyFunctor f where + myMap :: forall a b. (a -> b) -> f a -> f b diff --git a/tests/fixtures/codegen/SuperclassDict/TestSuperclass.purs b/tests/fixtures/codegen/SuperclassDict/TestSuperclass.purs new file mode 100644 index 00000000..44437866 --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDict/TestSuperclass.purs @@ -0,0 +1,9 @@ +module TestSuperclass where + +import MyFunctor (class MyFunctor, myMap) +import MyApply (class MyApply, myApply) + +-- Uses myMap (MyFunctor method) through MyApply constraint. +-- Should extract the MyFunctor dict from dictMyApply via superclass accessor. +useBoth :: forall f a b. MyApply f => (a -> b) -> f a -> f b +useBoth f fa = myMap f fa diff --git a/tests/fixtures/codegen/SuperclassDictDeep/MyAlternative.purs b/tests/fixtures/codegen/SuperclassDictDeep/MyAlternative.purs new file mode 100644 index 00000000..075150c7 --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDictDeep/MyAlternative.purs @@ -0,0 +1,6 @@ +module MyAlternative where + +import MyApply (class MyApply) + +class MyApply f <= MyAlternative f where + myEmpty :: forall a. f a diff --git a/tests/fixtures/codegen/SuperclassDictDeep/MyApply.purs b/tests/fixtures/codegen/SuperclassDictDeep/MyApply.purs new file mode 100644 index 00000000..287456cf --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDictDeep/MyApply.purs @@ -0,0 +1,8 @@ +module MyApply where + +import MyFunctor (class MyFunctor) + +class MyFunctor f <= MyApply f where + myApply :: forall a b. f (a -> b) -> f a -> f b + +infixl 4 myApply as <*> diff --git a/tests/fixtures/codegen/SuperclassDictDeep/MyFunctor.purs b/tests/fixtures/codegen/SuperclassDictDeep/MyFunctor.purs new file mode 100644 index 00000000..8736fea0 --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDictDeep/MyFunctor.purs @@ -0,0 +1,6 @@ +module MyFunctor where + +class MyFunctor f where + myMap :: forall a b. (a -> b) -> f a -> f b + +infixl 4 myMap as <$> diff --git a/tests/fixtures/codegen/SuperclassDictDeep/MyPrelude.purs b/tests/fixtures/codegen/SuperclassDictDeep/MyPrelude.purs new file mode 100644 index 00000000..0c7f7f2f --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDictDeep/MyPrelude.purs @@ -0,0 +1,5 @@ +module MyPrelude (module MyFunctor, module MyApply, module MyAlternative) where + +import MyFunctor (class MyFunctor, myMap, (<$>)) +import MyApply (class MyApply, myApply, (<*>)) +import MyAlternative (class MyAlternative, myEmpty) diff --git a/tests/fixtures/codegen/SuperclassDictDeep/TestDeep.purs b/tests/fixtures/codegen/SuperclassDictDeep/TestDeep.purs new file mode 100644 index 00000000..8b626a52 --- /dev/null +++ b/tests/fixtures/codegen/SuperclassDictDeep/TestDeep.purs @@ -0,0 +1,8 @@ +module TestDeep where + +import MyPrelude + +-- Uses <$> (myMap from MyFunctor) through MyAlternative constraint, +-- imported via MyPrelude re-export. 2-level superclass chain. +combine :: forall f a b. MyAlternative f => (a -> b) -> f a -> f (a -> b) -> f b +combine f fa fab = f <$> fa diff --git a/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName.purs b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName.purs new file mode 100644 index 00000000..5989037c --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName.purs @@ -0,0 +1,16 @@ +-- Regression test: a foreign type and a type synonym with the same name +-- should not cause a false "partially applied synonym" error. +-- +-- When ForeignNode.Node (foreign, kind Type, arity 0) and +-- SynonymNode.Node (type synonym with 3 params) are both in scope, +-- uses of the foreign Node should not be flagged as a partially applied +-- synonym. +module ForeignTypeSynonymSameName where + +import ForeignTypeSynonymSameName.ForeignNode (Node) as DOM +import ForeignTypeSynonymSameName.Reexporter as R + +-- Use the foreign Node in a type signature — this should not +-- trigger "Type synonym Node is partially applied" +identity :: DOM.Node -> DOM.Node +identity x = x diff --git a/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/ForeignNode.purs b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/ForeignNode.purs new file mode 100644 index 00000000..54668145 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/ForeignNode.purs @@ -0,0 +1,4 @@ +module ForeignTypeSynonymSameName.ForeignNode where + +-- A foreign type named Node (like Web.DOM.Node.Node) +foreign import data Node :: Type diff --git a/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/Reexporter.purs b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/Reexporter.purs new file mode 100644 index 00000000..81dc7517 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/Reexporter.purs @@ -0,0 +1,5 @@ +module ForeignTypeSynonymSameName.Reexporter + ( module ForeignTypeSynonymSameName.SynonymNode + ) where + +import ForeignTypeSynonymSameName.SynonymNode diff --git a/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/SynonymNode.purs b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/SynonymNode.purs new file mode 100644 index 00000000..c71b1e1a --- /dev/null +++ b/tests/fixtures/original-compiler/passing/ForeignTypeSynonymSameName/SynonymNode.purs @@ -0,0 +1,4 @@ +module ForeignTypeSynonymSameName.SynonymNode where + +-- A type synonym also named Node (like Halogen.HTML.Elements.Node) +type Node r w i = Array r -> Array w -> i diff --git a/tests/fixtures/original-compiler/passing/NestedForallKindGeneralization.purs b/tests/fixtures/original-compiler/passing/NestedForallKindGeneralization.purs new file mode 100644 index 00000000..c559c0d3 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/NestedForallKindGeneralization.purs @@ -0,0 +1,17 @@ +-- Regression test: data types with higher-rank constructor fields containing +-- forall-bound type variables whose kinds depend on the outer data type's +-- type parameters should not trigger "Cannot unambiguously generalize kinds". +-- +-- The inner forall's type variables (slot, m) have kinds that are determined +-- by unification with the outer parameters (g, f). The kind checker must +-- exclude the outer type parameter kind variables when checking the inner +-- forall's quantification. +module NestedForallKindGeneralization where + +import Prelude + +data Wrapper g f a = + Wrapper + (forall slot m. Applicative m => (slot g -> m a) -> m (f a)) + (g a) + (f a -> a) diff --git a/tests/fixtures/original-compiler/passing/QualifiedImportReexport.purs b/tests/fixtures/original-compiler/passing/QualifiedImportReexport.purs new file mode 100644 index 00000000..d8c25996 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/QualifiedImportReexport.purs @@ -0,0 +1,16 @@ +-- Regression test: module re-exports should not include names from +-- qualified-only imports (import M as Q with no explicit import list). +-- +-- Here, Lib exports both `foo` and `bar`. We import `bar` explicitly and +-- also do a qualified-only import `as L`. The `module Lib` re-export should +-- only include `bar` (from the explicit import), not `foo` (from the +-- qualified-only import). Wrapper also exports `foo`, so if the qualified +-- import leaked `foo` from Lib, it would cause a false ExportConflict. +module QualifiedImportReexport + ( module QualifiedImportReexport.Lib + , module QualifiedImportReexport.Wrapper + ) where + +import QualifiedImportReexport.Lib (bar) +import QualifiedImportReexport.Lib as L +import QualifiedImportReexport.Wrapper (foo) diff --git a/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Lib.purs b/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Lib.purs new file mode 100644 index 00000000..d176a30f --- /dev/null +++ b/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Lib.purs @@ -0,0 +1,11 @@ +module QualifiedImportReexport.Lib where + +-- Both foo and bar are exported. When this module is re-exported via +-- `module QualifiedImportReexport.Lib` in the main module, only names +-- from the *explicit* import should be included, not those from the +-- qualified-only import. +foo :: Int -> Int +foo x = x + +bar :: Int -> Int +bar x = x diff --git a/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Wrapper.purs b/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Wrapper.purs new file mode 100644 index 00000000..5390b8a1 --- /dev/null +++ b/tests/fixtures/original-compiler/passing/QualifiedImportReexport/Wrapper.purs @@ -0,0 +1,5 @@ +module QualifiedImportReexport.Wrapper where + +-- Defines its own `foo`, different from Lib.foo +foo :: String -> String +foo _ = "wrapped" diff --git a/tests/node_integration.rs b/tests/node_integration.rs new file mode 100644 index 00000000..6b5f873a --- /dev/null +++ b/tests/node_integration.rs @@ -0,0 +1,160 @@ +//! Node.js integration tests for the full compiler pipeline. +//! +//! These tests compile PureScript modules using the test-compiler-fun project +//! (with real Prelude, Effect, etc.) and run the output with Node.js to verify +//! correctness of the generated JavaScript. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Get the project root directory (where Cargo.toml lives). +fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +/// Get the test-compiler-fun project directory. +fn test_project_dir() -> PathBuf { + project_root().join("..").join("test-compiler-fun") +} + +/// Mutex to serialize integration tests that share the filesystem. +use std::sync::Mutex; +static TEST_MUTEX: Mutex<()> = Mutex::new(()); + +/// Write a Main.purs to the test project, compile all modules, and return +/// the output directory path. Panics on compilation failure. +fn compile_with_main(main_source: &str) -> PathBuf { + let test_dir = test_project_dir(); + let main_path = test_dir.join("src").join("Main.purs"); + let output_dir = project_root().join("output"); + + // Write the Main.purs source + std::fs::write(&main_path, main_source).expect("Failed to write Main.purs"); + + // Clean output + let _ = std::fs::remove_dir_all(&output_dir); + + // Compile using cargo run + let status = Command::new("cargo") + .args([ + "run", + "--", + "compile", + "../test-compiler-fun/src/**/*.purs", + "../test-compiler-fun/.spago/*/src/**/*.purs", + ]) + .current_dir(&project_root()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .status() + .expect("Failed to run cargo"); + + assert!(status.success(), "Compilation failed"); + output_dir +} + +/// Run a Node.js expression that imports and executes Main, returning stdout. +fn run_main(output_dir: &Path) -> String { + let main_js = output_dir.join("Main").join("index.js"); + assert!( + main_js.exists(), + "Main/index.js not found at {}", + main_js.display() + ); + + let script = format!( + "import('{}').then(m => {{ if (typeof m.main === 'function') m.main(); }})", + main_js.display() + ); + + let output = Command::new("node") + .args(["--input-type=module", "-e", &script]) + .output() + .expect("Failed to run node"); + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + if !output.status.success() { + panic!( + "Node.js execution failed:\n--- stdout ---\n{}\n--- stderr ---\n{}", + stdout, stderr + ); + } + + stdout +} + +/// Compile and run a Main.purs, returning stdout. +fn compile_and_run(main_source: &str) -> String { + let output_dir = compile_with_main(main_source); + run_main(&output_dir) +} + +// ===== Integration tests ===== + +#[test] +fn node_simple_map_show_log() { + let _lock = TEST_MUTEX.lock().unwrap(); + let output = compile_and_run( + r#"module Main where + +import Prelude + +import Effect (Effect) +import Effect.Console (log) + +main :: Effect Unit +main = + [ 1, 2, 3 ] <#> (\x -> x + 1) + # show >>> log +"#, + ); + assert_eq!(output.trim(), "[2,3,4]"); +} + +#[test] +fn node_do_notation_bind_pure() { + let _lock = TEST_MUTEX.lock().unwrap(); + let output = compile_and_run( + r#"module Main where + +import Prelude + +import Effect (Effect) +import Effect.Console (log) + +main :: Effect Unit +main = do + twoThreeFour <- pure $ [ 1, 2, 3 ] <#> (\x -> x + 1) + log $ show twoThreeFour + log $ "HI!" + pure unit +"#, + ); + assert_eq!(output.trim(), "[2,3,4]\nHI!"); +} + +#[test] +fn node_show_at_multiple_types() { + let _lock = TEST_MUTEX.lock().unwrap(); + let output = compile_and_run( + r#"module Main where + +import Prelude + +import Effect (Effect) +import Effect.Console (log) +import Data.Array (length) + +main :: Effect Unit +main = do + twoThreeFour <- pure $ [ 1, 2, 3 ] <#> (\x -> x + 1) + log $ show twoThreeFour + log $ "HI!" + log $ show $ length twoThreeFour + pure unit +"#, + ); + assert_eq!(output.trim(), "[2,3,4]\nHI!\n3"); +} diff --git a/tests/snapshots/codegen__codegen_CallSiteDictPassing.snap b/tests/snapshots/codegen__codegen_CallSiteDictPassing.snap new file mode 100644 index 00000000..a76778cb --- /dev/null +++ b/tests/snapshots/codegen__codegen_CallSiteDictPassing.snap @@ -0,0 +1,22 @@ +--- +source: tests/codegen.rs +expression: js +--- +var myShowThing = function(dictMyShow) { + return function(x) { + return dictMyShow.myShow(x); + }; +}; + +var wrapper = function(dictMyShow) { + return function(x) { + return myShowThing(dictMyShow)(x); + }; +}; + +var myShow = function(dict) { + return dict.myShow; +}; + + +export { myShow, myShowThing, wrapper }; diff --git a/tests/snapshots/codegen__codegen_CaseExpressions.snap b/tests/snapshots/codegen__codegen_CaseExpressions.snap index a8d80b8d..d9e3f52e 100644 --- a/tests/snapshots/codegen__codegen_CaseExpressions.snap +++ b/tests/snapshots/codegen__codegen_CaseExpressions.snap @@ -2,21 +2,6 @@ source: tests/codegen.rs expression: js --- -var fromEither = function(e) { - return (function() { - var $case0_0 = e; - if ($case0_0 instanceof Left) { - var x = $case0_0.value0; - return x; - } - if ($case0_0 instanceof Right) { - var x = $case0_0.value0; - return x; - } - throw Error("Failed pattern match"); - })(); -}; - var multiCase = function(a) { return function(b) { return (function() { @@ -54,5 +39,20 @@ var Right = (function() { return Right; })(); +var fromEither = function(e) { + return (function() { + var $case0_0 = e; + if ($case0_0 instanceof Left) { + var x = $case0_0.value0; + return x; + } + if ($case0_0 instanceof Right) { + var x = $case0_0.value0; + return x; + } + throw Error("Failed pattern match"); + })(); +}; + export { Left, Right, fromEither, multiCase }; diff --git a/tests/snapshots/codegen__codegen_ConstrainedFunctions.snap b/tests/snapshots/codegen__codegen_ConstrainedFunctions.snap new file mode 100644 index 00000000..e81497f6 --- /dev/null +++ b/tests/snapshots/codegen__codegen_ConstrainedFunctions.snap @@ -0,0 +1,56 @@ +--- +source: tests/codegen.rs +expression: js +--- +var showDesc = function(dictDescribable) { + return function(x) { + return dictDescribable.describe(x); + }; +}; + +var describeTwo = function(dictDescribable) { + return function(dictMyEq) { + return function(x) { + return function($_0) { + return dictDescribable.describe(x); + }; + }; + }; +}; + +var describe = function(dict) { + return dict.describe; +}; + +var myEq = function(dict) { + return dict.myEq; +}; + +var describableBool = { + describe: function($arg0_1) { + if ($arg0_1 === true) { + return "true"; + } + if ($arg0_1 === false) { + return "false"; + } + throw Error("Failed pattern match in describe"); + } +}; + +var describableInt = { + describe: function($_2) { + return "int"; + } +}; + +var myEqInt = { + myEq: function($_4) { + return function($_3) { + return true; + }; + } +}; + + +export { describableBool, describableInt, describe, describeTwo, myEq, myEqInt, showDesc }; diff --git a/tests/snapshots/codegen__codegen_DataConstructors.snap b/tests/snapshots/codegen__codegen_DataConstructors.snap index cd2cfd3a..baa7836c 100644 --- a/tests/snapshots/codegen__codegen_DataConstructors.snap +++ b/tests/snapshots/codegen__codegen_DataConstructors.snap @@ -2,14 +2,6 @@ source: tests/codegen.rs expression: js --- -var nullaryUse = Red.value; - -var unaryUse = Just.create(42); - -var nothingUse = Nothing.value; - -var pairUse = Pair.create(1)("hello"); - var Red = (function() { function Red() { }; @@ -17,6 +9,8 @@ var Red = (function() { return Red; })(); +var nullaryUse = Red.value; + var Green = (function() { function Green() { }; @@ -38,6 +32,8 @@ var Nothing = (function() { return Nothing; })(); +var nothingUse = Nothing.value; + var Just = (function() { function Just(value0) { this.value0 = value0; @@ -48,6 +44,8 @@ var Just = (function() { return Just; })(); +var unaryUse = Just.create(42); + var Pair = (function() { function Pair(value0, value1) { this.value0 = value0; @@ -61,6 +59,8 @@ var Pair = (function() { return Pair; })(); +var pairUse = Pair.create(1)("hello"); + var Leaf = (function() { function Leaf(value0) { this.value0 = value0; diff --git a/tests/snapshots/codegen__codegen_InstanceConstraints.snap b/tests/snapshots/codegen__codegen_InstanceConstraints.snap new file mode 100644 index 00000000..96e9d9c8 --- /dev/null +++ b/tests/snapshots/codegen__codegen_InstanceConstraints.snap @@ -0,0 +1,54 @@ +--- +source: tests/codegen.rs +expression: js +--- +var showMaybeValue = function(dictMyShow) { + return function(x) { + return dictMyShow.myShow(x); + }; +}; + +var Nothing = (function() { + function Nothing() { + }; + Nothing.value = new Nothing(); + return Nothing; +})(); + +var Just = (function() { + function Just(value0) { + this.value0 = value0; + }; + Just.create = function(value0) { + return new Just(value0); + }; + return Just; +})(); + +var myShow = function(dict) { + return dict.myShow; +}; + +var myShowInt = { + myShow: function($_0) { + return "int"; + } +}; + +var myShowMaybe = function(dictMyShow) { + return { + myShow: function($arg0_1) { + if ($arg0_1 instanceof Nothing) { + return "nothing"; + } + if ($arg0_1 instanceof Just) { + var x = $arg0_1.value0; + return dictMyShow.myShow(x); + } + throw Error("Failed pattern match in myShow"); + } + }; +}; + + +export { Just, Nothing, myShow, myShowInt, myShowMaybe, showMaybeValue }; diff --git a/tests/snapshots/codegen__codegen_InstanceDictionaries.snap b/tests/snapshots/codegen__codegen_InstanceDictionaries.snap index c7b4a3f3..d79f6334 100644 --- a/tests/snapshots/codegen__codegen_InstanceDictionaries.snap +++ b/tests/snapshots/codegen__codegen_InstanceDictionaries.snap @@ -2,8 +2,14 @@ source: tests/codegen.rs expression: js --- -var showValue = function(x) { - return myShow(x); +var showValue = function(dictMyShow) { + return function(x) { + return dictMyShow.myShow(x); + }; +}; + +var myShow = function(dict) { + return dict.myShow; }; var myShowInt = { @@ -19,4 +25,4 @@ var myShowString = { }; -export { myShowInt, myShowString, showValue }; +export { myShow, myShowInt, myShowString, showValue }; diff --git a/tests/snapshots/codegen__codegen_MethodConstraints.snap b/tests/snapshots/codegen__codegen_MethodConstraints.snap new file mode 100644 index 00000000..ad3a8e54 --- /dev/null +++ b/tests/snapshots/codegen__codegen_MethodConstraints.snap @@ -0,0 +1,19 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyEq from "../MyEq/index.js"; + + +var myEq1 = function(dict) { + return dict.myEq1; +}; + +var myEq1Array = { + myEq1: function(dictMyEq) { + return (MyEq.myEqArray(dictMyEq)).myEq; + } +}; + + +export { myEq1, myEq1Array }; diff --git a/tests/snapshots/codegen__codegen_NewtypeErasure.snap b/tests/snapshots/codegen__codegen_NewtypeErasure.snap index 87e0c327..6e38108c 100644 --- a/tests/snapshots/codegen__codegen_NewtypeErasure.snap +++ b/tests/snapshots/codegen__codegen_NewtypeErasure.snap @@ -2,19 +2,11 @@ source: tests/codegen.rs expression: js --- -var mkName = function(s) { - return Name.create(s); -}; - var unwrapName = function($v0) { var s = $v0; return s; }; -var wrapInt = function(n) { - return Wrapper.create(n); -}; - var unwrapWrapper = function($v1) { var x = $v1; return x; @@ -29,6 +21,10 @@ var Name = (function() { return Name; })(); +var mkName = function(s) { + return Name.create(s); +}; + var Wrapper = (function() { function Wrapper() { }; @@ -38,5 +34,9 @@ var Wrapper = (function() { return Wrapper; })(); +var wrapInt = function(n) { + return Wrapper.create(n); +}; + export { Name, Wrapper, mkName, unwrapName, unwrapWrapper, wrapInt }; diff --git a/tests/snapshots/codegen__codegen_OperatorExplicitImport.snap b/tests/snapshots/codegen__codegen_OperatorExplicitImport.snap new file mode 100644 index 00000000..bb83742c --- /dev/null +++ b/tests/snapshots/codegen__codegen_OperatorExplicitImport.snap @@ -0,0 +1,17 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyLib from "../MyLib/index.js"; + + +var joined = function(dictMyAppend) { + return function(x) { + return function(y) { + return dictMyAppend.myAppend(x)(y); + }; + }; +}; + + +export { joined }; diff --git a/tests/snapshots/codegen__codegen_OperatorHidingImport.snap b/tests/snapshots/codegen__codegen_OperatorHidingImport.snap new file mode 100644 index 00000000..bb83742c --- /dev/null +++ b/tests/snapshots/codegen__codegen_OperatorHidingImport.snap @@ -0,0 +1,17 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyLib from "../MyLib/index.js"; + + +var joined = function(dictMyAppend) { + return function(x) { + return function(y) { + return dictMyAppend.myAppend(x)(y); + }; + }; +}; + + +export { joined }; diff --git a/tests/snapshots/codegen__codegen_OperatorModuleReexport.snap b/tests/snapshots/codegen__codegen_OperatorModuleReexport.snap new file mode 100644 index 00000000..09d65952 --- /dev/null +++ b/tests/snapshots/codegen__codegen_OperatorModuleReexport.snap @@ -0,0 +1,17 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyPrelude from "../MyPrelude/index.js"; + + +var joined = function(dictMyAppend) { + return function(x) { + return function(y) { + return dictMyAppend.myAppend(x)(y); + }; + }; +}; + + +export { joined }; diff --git a/tests/snapshots/codegen__codegen_OperatorResolutionLocal.snap b/tests/snapshots/codegen__codegen_OperatorResolutionLocal.snap new file mode 100644 index 00000000..d2cf2a65 --- /dev/null +++ b/tests/snapshots/codegen__codegen_OperatorResolutionLocal.snap @@ -0,0 +1,26 @@ +--- +source: tests/codegen.rs +expression: js +--- +var joined = function(dictMyAppend) { + return function(x) { + return function(y) { + return dictMyAppend.myAppend(x)(y); + }; + }; +}; + +var myAppend = function(dict) { + return dict.myAppend; +}; + +var myAppendInt = { + myAppend: function(x) { + return function($_0) { + return x; + }; + } +}; + + +export { joined, myAppend, myAppendInt }; diff --git a/tests/snapshots/codegen__codegen_PatternMatching.snap b/tests/snapshots/codegen__codegen_PatternMatching.snap index 0feab445..fd0cfb37 100644 --- a/tests/snapshots/codegen__codegen_PatternMatching.snap +++ b/tests/snapshots/codegen__codegen_PatternMatching.snap @@ -37,6 +37,23 @@ var boolMatch = function(b) { })(); }; +var Nothing = (function() { + function Nothing() { + }; + Nothing.value = new Nothing(); + return Nothing; +})(); + +var Just = (function() { + function Just(value0) { + this.value0 = value0; + }; + Just.create = function(value0) { + return new Just(value0); + }; + return Just; +})(); + var constructorMatch = function(m) { return (function() { var $case0_3 = m; @@ -68,22 +85,6 @@ var nestedMatch = function(m) { })(); }; -var colorToInt = function(c) { - return (function() { - var $case0_5 = c; - if ($case0_5 instanceof Red) { - return 0; - } - if ($case0_5 instanceof Green) { - return 1; - } - if ($case0_5 instanceof Blue) { - return 2; - } - throw Error("Failed pattern match"); - })(); -}; - var asPattern = function(m) { return (function() { var $case0_6 = m; @@ -98,23 +99,6 @@ var asPattern = function(m) { })(); }; -var Nothing = (function() { - function Nothing() { - }; - Nothing.value = new Nothing(); - return Nothing; -})(); - -var Just = (function() { - function Just(value0) { - this.value0 = value0; - }; - Just.create = function(value0) { - return new Just(value0); - }; - return Just; -})(); - var Red = (function() { function Red() { }; @@ -136,5 +120,21 @@ var Blue = (function() { return Blue; })(); +var colorToInt = function(c) { + return (function() { + var $case0_5 = c; + if ($case0_5 instanceof Red) { + return 0; + } + if ($case0_5 instanceof Green) { + return 1; + } + if ($case0_5 instanceof Blue) { + return 2; + } + throw Error("Failed pattern match"); + })(); +}; + export { Blue, Green, Just, Nothing, Red, asPattern, boolMatch, colorToInt, constructorMatch, literalMatch, nestedMatch, varMatch, wildcardMatch }; diff --git a/tests/snapshots/codegen__codegen_SuperclassDict.snap b/tests/snapshots/codegen__codegen_SuperclassDict.snap new file mode 100644 index 00000000..c30a7a6c --- /dev/null +++ b/tests/snapshots/codegen__codegen_SuperclassDict.snap @@ -0,0 +1,19 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyFunctor from "../MyFunctor/index.js"; + +import * as MyApply from "../MyApply/index.js"; + + +var useBoth = function(dictMyApply) { + return function(f) { + return function(fa) { + return (dictMyApply.MyFunctor0()).myMap(f)(fa); + }; + }; +}; + + +export { useBoth }; diff --git a/tests/snapshots/codegen__codegen_SuperclassDictDeep.snap b/tests/snapshots/codegen__codegen_SuperclassDictDeep.snap new file mode 100644 index 00000000..44b25a28 --- /dev/null +++ b/tests/snapshots/codegen__codegen_SuperclassDictDeep.snap @@ -0,0 +1,19 @@ +--- +source: tests/codegen.rs +expression: js +--- +import * as MyPrelude from "../MyPrelude/index.js"; + + +var combine = function(dictMyAlternative) { + return function(f) { + return function(fa) { + return function(fab) { + return ((dictMyAlternative.MyApply0()).MyFunctor0()).myMap(f)(fa); + }; + }; + }; +}; + + +export { combine };