Skip to content

Commit 98232f4

Browse files
committed
Fix FinalTagless
1 parent b05705b commit 98232f4

4 files changed

Lines changed: 163 additions & 9 deletions

File tree

src/codegen/js.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11931,7 +11931,7 @@ fn resolve_op_ref(ctx: &CodegenCtx, op: &Spanned<QualifiedIdent>, expr_span: Opt
1193111931
Box::new(JsExpr::StringLit("create".to_string())),
1193211932
);
1193311933
}
11934-
if ctx.local_names.contains(target_name) || ctx.name_source.contains_key(target_name) {
11934+
if source_parts.is_none() && (ctx.local_names.contains(target_name) || ctx.name_source.contains_key(target_name)) {
1193511935
// Temporarily remove the operator target from local_bindings so that
1193611936
// a local let-shadow (e.g. `let f = (-) in a % b` where % aliases module-level f)
1193711937
// doesn't intercept the operator resolution.

src/typechecker/check.rs

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3249,6 +3249,20 @@ fn check_module_impl(module: &Module, registry: &ModuleRegistry, collect_span_ty
32493249
}
32503250
}
32513251

3252+
// Build map of type alias constraints: alias name → constraints from its definition.
3253+
// These are stripped during convert_type_expr and lost from the Type representation,
3254+
// but needed for codegen when a value's signature uses the alias (e.g. `three :: Expr Number`
3255+
// where `type Expr a = forall e. E e => e a`).
3256+
let mut type_alias_constraints: HashMap<Symbol, Vec<(QualifiedIdent, Vec<Type>)>> = HashMap::new();
3257+
for decl in &module.decls {
3258+
if let Decl::TypeAlias { name, ty, .. } = decl {
3259+
let constraints = extract_type_signature_constraints(ty, &type_ops);
3260+
if !constraints.is_empty() {
3261+
type_alias_constraints.insert(name.value, constraints);
3262+
}
3263+
}
3264+
}
3265+
32523266
// Pass 1: Collect type signatures and data constructors
32533267
for decl in &module.decls {
32543268
let decl_name = match decl.name() {
@@ -3324,7 +3338,28 @@ fn check_module_impl(module: &Module, registry: &ModuleRegistry, collect_span_ty
33243338
.insert(qi(name.value), sig_constraints);
33253339
}
33263340
// Extract return-type inner-forall constraints
3327-
let rt_constraints = extract_return_type_constraints(ty, &type_ops);
3341+
let mut rt_constraints = extract_return_type_constraints(ty, &type_ops);
3342+
// Fallback: if CST-level extraction found nothing, check if the
3343+
// return type is a type alias that hides a forall+constraints.
3344+
// E.g. `foo :: forall a. a -> Foo a` where `type Foo a = forall f. Monad f => f a`
3345+
if rt_constraints.is_empty() {
3346+
let sig_ty = &signatures[&name.value].1;
3347+
// Only check return-type alias expansion when the sig has
3348+
// function arrows. For 0-arity values like `three :: Expr Number`,
3349+
// the constraint is top-level and goes into signature_constraints.
3350+
let has_arrows = matches!(find_return_type_depth(sig_ty), d if d > 0);
3351+
if has_arrows {
3352+
let ret_type = find_return_type(sig_ty);
3353+
let expanded_ret = expand_type_aliases_limited(ret_type, &ctx.state.type_aliases, 0);
3354+
if matches!(&expanded_ret, Type::Forall(..)) {
3355+
if let Some(alias_name) = extract_type_head_name(ret_type) {
3356+
if let Some(alias_cs) = type_alias_constraints.get(&alias_name) {
3357+
rt_constraints = alias_cs.clone();
3358+
}
3359+
}
3360+
}
3361+
}
3362+
}
33283363
if !rt_constraints.is_empty() {
33293364
let depth = count_return_type_arrow_depth(ty);
33303365
ctx.return_type_constraints
@@ -6062,10 +6097,15 @@ fn check_module_impl(module: &Module, registry: &ModuleRegistry, collect_span_ty
60626097
// expands to `forall e. E e => e Number`, so the body is checked against
60636098
// the inner type with rigid Var(e), producing deferrable constraints.
60646099
let expanded_sig_storage;
6100+
let mut sig_alias_expanded_to_forall = false;
6101+
let mut sig_alias_name: Option<Symbol> = None;
60656102
let sig = if let Some(sig_ty) = sig {
60666103
if !matches!(sig_ty, Type::Forall(..)) {
60676104
let expanded = expand_type_aliases_limited(sig_ty, &ctx.state.type_aliases, 0);
60686105
if matches!(&expanded, Type::Forall(..)) {
6106+
sig_alias_expanded_to_forall = true;
6107+
// Extract alias name from head of original sig type
6108+
sig_alias_name = extract_type_head_name(sig_ty);
60696109
expanded_sig_storage = expanded;
60706110
Some(&expanded_sig_storage)
60716111
} else {
@@ -6550,15 +6590,22 @@ fn check_module_impl(module: &Module, registry: &ModuleRegistry, collect_span_ty
65506590
// `type Expr a = forall e. E e => e a`). Only alias-hidden
65516591
// constraints need body extraction; for regular explicit
65526592
// signatures, body constraints should NOT propagate.
6553-
let mut has_alias_hidden_forall = false;
6593+
let has_alias_hidden_forall = sig_alias_expanded_to_forall;
65546594
let scheme = if let Some(sig_ty) = sig {
6555-
// If sig_ty is NOT already a Forall, it might hide one inside
6556-
// a type alias. Expand aliases to expose the hidden Forall
6557-
// so the scheme has proper forall_vars.
6558-
if !matches!(sig_ty, Type::Forall(..)) {
6595+
if sig_alias_expanded_to_forall {
6596+
// The sig was alias-expanded earlier to reveal a hidden Forall.
6597+
// Extract forall vars properly for the scheme.
6598+
if let Type::Forall(vars, body) = sig_ty {
6599+
Scheme {
6600+
forall_vars: vars.iter().map(|&(v, _)| v).collect(),
6601+
ty: (**body).clone(),
6602+
}
6603+
} else {
6604+
Scheme::mono(ctx.state.zonk(sig_ty.clone()))
6605+
}
6606+
} else if !matches!(sig_ty, Type::Forall(..)) {
65596607
let expanded = expand_type_aliases_limited(sig_ty, &ctx.state.type_aliases, 0);
65606608
if let Type::Forall(vars, body) = expanded {
6561-
has_alias_hidden_forall = true;
65626609
Scheme {
65636610
forall_vars: vars.iter().map(|&(v, _)| v).collect(),
65646611
ty: *body,
@@ -6664,6 +6711,17 @@ fn check_module_impl(module: &Module, registry: &ModuleRegistry, collect_span_ty
66646711
// constraints (e.g., Union from calling `make` inside
66656712
// `makeStateless`) should not propagate to callers — the
66666713
// explicit signature defines the public contract.
6714+
// For alias-expanded sigs, look up constraints from the
6715+
// type alias definition (stored in type_alias_constraints).
6716+
// These constraints are stripped during convert_type_expr
6717+
// so they're not in the Type representation.
6718+
if has_alias_hidden_forall && !ctx.signature_constraints.contains_key(&qualified) {
6719+
if let Some(alias_name) = sig_alias_name {
6720+
if let Some(alias_constraints) = type_alias_constraints.get(&alias_name) {
6721+
ctx.signature_constraints.insert(qualified.clone(), alias_constraints.clone());
6722+
}
6723+
}
6724+
}
66676725
let skip_body_constraint_extraction =
66686726
sig.is_some() && !has_alias_hidden_forall;
66696727
if !skip_body_constraint_extraction && !ctx.signature_constraints.contains_key(&qualified) {
@@ -15908,6 +15966,36 @@ fn types_match_up_to_vars(pattern: &Type, target: &Type, subst: &mut HashMap<Sym
1590815966
}
1590915967
}
1591015968

15969+
/// Find the return type of a function type, stripping outer Forall.
15970+
/// E.g. `Forall(a, Fun(Var(a), App(Con("Foo"), Var(a))))` → `App(Con("Foo"), Var(a))`
15971+
fn find_return_type(ty: &Type) -> &Type {
15972+
match ty {
15973+
Type::Forall(_, body) => find_return_type(body),
15974+
Type::Fun(_, ret) => find_return_type(ret),
15975+
other => other,
15976+
}
15977+
}
15978+
15979+
/// Count function arrow depth, stripping Forall.
15980+
fn find_return_type_depth(ty: &Type) -> usize {
15981+
match ty {
15982+
Type::Forall(_, body) => find_return_type_depth(body),
15983+
Type::Fun(_, ret) => 1 + find_return_type_depth(ret),
15984+
_ => 0,
15985+
}
15986+
}
15987+
15988+
/// Extract the head type constructor name from a Type.
15989+
/// E.g. `App(Con("Expr"), Con("Number"))` → Some("Expr")
15990+
/// E.g. `Con("Foo")` → Some("Foo")
15991+
fn extract_type_head_name(ty: &Type) -> Option<Symbol> {
15992+
match ty {
15993+
Type::Con(qi) => Some(qi.name),
15994+
Type::App(head, _) => extract_type_head_name(head),
15995+
_ => None,
15996+
}
15997+
}
15998+
1591115999
/// Walks through Forall → Constrained patterns, converting constraint args to internal Types.
1591216000
/// Skips Partial and Warn (which are handled separately).
1591316001
pub(crate) fn extract_type_signature_constraints(

tests/build.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,6 @@ fn build_fixture_original_compiler_passing() {
755755
"DerivingFoldable",
756756
"DerivingFunctor",
757757
"DerivingTraversable",
758-
"FinalTagless",
759758
"Rank2TypeSynonym",
760759
"TCOMutRec",
761760
"VTAsClassHeads",

tests/integration.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,70 @@ wrapper component render = make component { initialState: Unit, render: \self ->
610610
assert!(!has_union,
611611
"wrapper should NOT have Union in signature_constraints (body constraint leak)");
612612
}
613+
614+
#[test]
615+
fn alias_hidden_forall_constraint_in_signature_constraints() {
616+
// Reproduces the FinalTagless bug: `type Expr a = forall e. E e => e a`
617+
// A value `three :: Expr Number` has constraints hidden inside the type alias.
618+
// The typechecker must extract `E` into signature_constraints so codegen
619+
// generates the dict parameter wrapper `function (dictE) { ... }`.
620+
let source = r#"
621+
module Main where
622+
623+
class E e where
624+
num :: Number -> e Number
625+
add :: e Number -> e Number -> e Number
626+
627+
type Expr a = forall e. E e => e a
628+
629+
three :: Expr Number
630+
three = add (num 1.0) (num 2.0)
631+
"#;
632+
let module = parse(source).expect("parse failed");
633+
let result = check_module(&module);
634+
assert!(result.errors.is_empty(), "typecheck errors: {:?}", result.errors.iter().map(|e| e.to_string()).collect::<Vec<_>>());
635+
636+
// Check that `three` has `E` in its signature_constraints
637+
let three_qi = purescript_fast_compiler::cst::unqualified_ident("three");
638+
let has_e = result.exports.signature_constraints.get(&three_qi)
639+
.map_or(false, |constraints| {
640+
constraints.iter().any(|(class_name, _)| {
641+
let name = purescript_fast_compiler::interner::resolve(class_name.name).unwrap_or_default();
642+
name == "E"
643+
})
644+
});
645+
assert!(has_e,
646+
"three should have E in signature_constraints (alias-hidden constraint)");
647+
}
648+
649+
#[test]
650+
fn operator_with_source_module_resolves_to_import_not_local() {
651+
// Reproduces the FinalTagless instance body bug: when class E defines method `add`,
652+
// and the `+` operator maps to Data.Semiring.add, using `+` inside the E instance
653+
// body should resolve to the imported Semiring add (inlined as JS +), not the
654+
// local E class accessor.
655+
// This is a codegen-level test — we verify the fixture passes via the build test.
656+
// Here we just verify the type-level constraints are correct.
657+
let source = r#"
658+
module Main where
659+
660+
class E e where
661+
num :: Number -> e Number
662+
add :: e Number -> e Number -> e Number
663+
664+
type Expr a = forall e. E e => e a
665+
666+
data Id a = Id a
667+
668+
instance exprId :: E Id where
669+
num = Id
670+
add (Id n) (Id m) = Id n
671+
672+
three :: Expr Number
673+
three = add (num 1.0) (num 2.0)
674+
"#;
675+
let module = parse(source).expect("parse failed");
676+
let result = check_module(&module);
677+
// Should have no errors (basic structure is valid)
678+
assert!(result.errors.is_empty(), "typecheck errors: {:?}", result.errors.iter().map(|e| e.to_string()).collect::<Vec<_>>());
679+
}

0 commit comments

Comments
 (0)