From 9dddf6a8ddee3a6862cb1b3643a589d463d3696a Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Fri, 10 Apr 2026 11:22:02 -0700 Subject: [PATCH 1/4] Fix Elm.unwrapper generating an invalid type annotation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elm.unwrapper generates a lambda that pattern matches on a single-variant custom type: \(Wrapper val) -> val The previous annotation was `val -> unwrapped` — two generic type variables. This annotation doesn't compile in Elm: the lambda body extracts a concrete value (whatever `Wrapper` wraps), but the annotation says ANY type, so Elm rejects it with a TYPE MISMATCH. Any annotation we could generate would have the same problem, because unwrapper doesn't know the inner type of the custom type from just the name. The fix is to omit the annotation entirely, letting Elm infer the type from the custom type definition. Before: extract : val -> unwrapped extract (Wrapper val) = val -- TYPE MISMATCH: `val` is `Int`, but the annotation says `unwrapped` After: extract (Wrapper val) = val -- Elm infers: extract : Wrapper -> Int Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Elm.elm | 20 ++++++++++++-------- tests/TypeChecking.elm | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Elm.elm b/src/Elm.elm index 0240686..dff622c 100644 --- a/src/Elm.elm +++ b/src/Elm.elm @@ -583,14 +583,18 @@ unwrapper modName typename = ) } , annotation = - Ok - { type_ = - Annotation.FunctionTypeAnnotation - (Compiler.nodify (Annotation.GenericType argVal.typename)) - (Compiler.nodify (Annotation.GenericType return.typename)) - , inferences = Dict.empty - , aliases = Compiler.emptyAliases - } + -- We can't generate a valid type annotation for an + -- unwrapper lambda: the arg type is the named custom + -- type (e.g. Wrapper), but the return type is its + -- unknown inner type. Generating `Wrapper -> a` would + -- be wrong because the body extracts a concrete value, + -- not a polymorphic one, so Elm would reject it with + -- a TYPE MISMATCH. + -- + -- Returning `Err []` causes the declaration to be + -- generated without a type annotation, letting Elm + -- infer it from the custom type definition. + Err [] , imports = case modName of [] -> diff --git a/tests/TypeChecking.elm b/tests/TypeChecking.elm index 25a9ab3..fbbcd3b 100644 --- a/tests/TypeChecking.elm +++ b/tests/TypeChecking.elm @@ -271,6 +271,22 @@ generatedCode = ( 1 + 2, x ) """ ] + , test "Elm.unwrapper omits annotation to let Elm infer it" <| + -- unwrapper creates `\(Wrapper val) -> val` but can't + -- derive a valid type annotation because it doesn't know + -- the inner type of Wrapper. Any annotation we could + -- generate (like `Wrapper -> a`) would be rejected by Elm + -- because the extracted value has a concrete type, not a + -- polymorphic one. So we omit the annotation and let Elm + -- infer it from the custom type definition. + \_ -> + Elm.declaration "extract" + (Elm.unwrapper [] "Wrapper") + |> Elm.Expect.declarationAs + """ + extract (Wrapper val) = + val + """ , test "Triple with mixed Float and Int infers correct types" <| \_ -> Elm.declaration "myTriple" From 1ce0a08c0b5c3598ddc6b9561bd12d88a15194fa Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 21 Apr 2026 18:27:00 -0700 Subject: [PATCH 2/4] Let downstream inference flow through unknown function types. When a function's annotation is `Err []` (intentionally unknown, e.g. Elm.unwrapper's lambda), `applyType` now synthesizes a fresh generic return type instead of short-circuiting to `Err`. This restores inference in expressions like `1 + Elm.unwrap "Wrapper" wrapped`, which previously lost their signature when the unwrap bug was fixed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Internal/Compiler.elm | 24 +++++++++++++++++++++++- tests/TypeChecking.elm | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Internal/Compiler.elm b/src/Internal/Compiler.elm index 796e46a..0809aae 100644 --- a/src/Internal/Compiler.elm +++ b/src/Internal/Compiler.elm @@ -1473,9 +1473,31 @@ applyType : -> Result (List InferenceError) Inference applyType index annotation args = case annotation of - Err err -> + Err ((_ :: _) as err) -> Err err + Err [] -> + -- The function's type is intentionally unknown (e.g. + -- Elm.unwrapper can't derive an annotation for its lambda). + -- Synthesize a fresh generic return type so downstream + -- inference can still flow through the application. + if Index.typecheck index then + case mergeArgInferences args [] Dict.empty of + Ok mergedArgs -> + Ok + { type_ = + Annotation.GenericType + (Index.protectTypeName "result" index) + , inferences = mergedArgs.inferences + , aliases = emptyAliases + } + + Err _ -> + Err [] + + else + Err [] + Ok fnAnnotation -> if Index.typecheck index then case mergeArgInferences args [] fnAnnotation.inferences of diff --git a/tests/TypeChecking.elm b/tests/TypeChecking.elm index fbbcd3b..61955e1 100644 --- a/tests/TypeChecking.elm +++ b/tests/TypeChecking.elm @@ -287,6 +287,34 @@ generatedCode = extract (Wrapper val) = val """ + , test "Elm.unwrapper respects withType when caller provides an annotation" <| + \_ -> + Elm.declaration "extract" + (Elm.unwrapper [] "Wrapper" + |> Elm.withType (Type.function [ Type.named [] "Wrapper" ] Type.string) + ) + |> Elm.Expect.declarationAs + """ + extract : Wrapper -> String + extract (Wrapper val) = + val + """ + , test "Elm.unwrap result propagates through downstream inference" <| + -- Even though unwrapper itself can't produce a type + -- annotation, `apply` synthesizes a fresh generic return + -- type when the function's type is unknown, so outer + -- expressions can still unify and infer correctly. + \_ -> + Elm.declaration "foo" + (Elm.Op.plus (Elm.int 1) + (Elm.unwrap [] "Wrapper" (Elm.val "wrapped")) + ) + |> Elm.Expect.declarationAs + """ + foo : Int + foo = + 1 + (\\(Wrapper val) -> val) wrapped + """ , test "Triple with mixed Float and Int infers correct types" <| \_ -> Elm.declaration "myTriple" From 535e9b6661dd24df86e7b890d48033e926a0c317 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 21 Apr 2026 18:30:48 -0700 Subject: [PATCH 3/4] Remove unused `return` binding flagged by elm-review. Became dead when unwrapper's annotation switched to `Err []`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Elm.elm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Elm.elm b/src/Elm.elm index dff622c..72f17c6 100644 --- a/src/Elm.elm +++ b/src/Elm.elm @@ -558,10 +558,6 @@ unwrapper modName typename = argVal : { name : String, typename : String, val : Compiler.Expression, index : Index.Index } argVal = Compiler.toVar index "val" - - return : { name : String, typename : String, val : Compiler.Expression, index : Index.Index } - return = - Compiler.toVar argVal.index "unwrapped" in { expression = Exp.LambdaExpression From 7af311a97638872f54a140f7caa797a8e915cbb5 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 22 Apr 2026 09:10:56 -0700 Subject: [PATCH 4/4] Skip generic-return synthesis when apply has zero args. `apply fn []` with an unknown fn type should still be unknown (the result is the fn itself, whose type we still don't know), rather than gaining a synthesized return type out of thin air. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Internal/Compiler.elm | 33 +++++++++++++++++++-------------- tests/TypeChecking.elm | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/Internal/Compiler.elm b/src/Internal/Compiler.elm index c7e7bf4..f5cbc5c 100644 --- a/src/Internal/Compiler.elm +++ b/src/Internal/Compiler.elm @@ -1581,22 +1581,27 @@ applyType index annotation args = -- Elm.unwrapper can't derive an annotation for its lambda). -- Synthesize a fresh generic return type so downstream -- inference can still flow through the application. - if Index.typecheck index then - case mergeArgInferences args [] Dict.empty of - Ok mergedArgs -> - Ok - { type_ = - Annotation.GenericType - (Index.protectTypeName "result" index) - , inferences = mergedArgs.inferences - , aliases = emptyAliases - } + -- + -- Only synthesize when args is non-empty; `apply fn []` + -- with an unknown fn is still "unknown" (the result is + -- the fn itself, whose type we still don't know). + case ( Index.typecheck index, args ) of + ( True, _ :: _ ) -> + case mergeArgInferences args [] Dict.empty of + Ok mergedArgs -> + Ok + { type_ = + Annotation.GenericType + (Index.protectTypeName "result" index) + , inferences = mergedArgs.inferences + , aliases = emptyAliases + } - Err _ -> - Err [] + Err _ -> + Err [] - else - Err [] + _ -> + Err [] Ok fnAnnotation -> if Index.typecheck index then diff --git a/tests/TypeChecking.elm b/tests/TypeChecking.elm index b70615d..3aac30a 100644 --- a/tests/TypeChecking.elm +++ b/tests/TypeChecking.elm @@ -316,6 +316,22 @@ generatedCode = foo = 1 + (\\(Wrapper val) -> val) wrapped """ + , test "Elm.apply with zero args on an unknown function stays unknown" <| + -- `apply fn []` with an unknown fn type should not fabricate + -- a return type out of thin air. Without this guard, the + -- outer Op.plus would unify the fabricated generic with + -- `number` and emit `foo : Int`, even though the body is + -- `1 + (\(Wrapper val) -> val)` (adding an int to a lambda). + \_ -> + Elm.declaration "foo" + (Elm.Op.plus (Elm.int 1) + (Elm.apply (Elm.unwrapper [] "Wrapper") []) + ) + |> Elm.Expect.declarationAs + """ + foo = + 1 + (\\(Wrapper val) -> val) + """ , describe "aliasAs pattern type" [ test "aliasAs on record pattern uses the underlying record type" <| \_ ->