From a320b80b78b591fb95d7da052810f790e8445ded Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Fri, 10 Apr 2026 10:56:22 -0700 Subject: [PATCH] Fix Elm.Let.fn/fn2/fn3 missing type annotations in declarations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a let-bound function (from Elm.Let.fn, fn2, or fn3) was called from the body of the let expression, the resulting expression had no proper function type annotation. This caused the enclosing declaration to be generated without a type annotation at all. Root cause: the `return` function in each of fn/fn2/fn3 built the call expression using `innerFnDetails` directly, which contains the body's return type as the annotation — not a function type. When Elm.apply then tried to type-check the call, it couldn't derive a proper type, so the declaration got no annotation. Fix: extract a `letFnAnnotation` helper that builds a proper function type annotation `arg1 -> arg2 -> ... -> body` from the arg annotations and the body's annotation. Use it from fn, fn2, and fn3. Before: useLetFn = -- No annotation! let myFn x = x + 1 in myFn 5 After: useLetFn : Int useLetFn = let myFn x = x + 1 in myFn 5 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Elm/Let.elm | 78 +++++++++++++++++++++++++++++++++++------- tests/TypeChecking.elm | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/Elm/Let.elm b/src/Elm/Let.elm index 0df1431e..b4c93b8e 100644 --- a/src/Elm/Let.elm +++ b/src/Elm/Let.elm @@ -142,6 +142,7 @@ import Elm exposing (Expression) import Elm.Syntax.Expression as Exp import Elm.Syntax.Node as Node import Elm.Syntax.Pattern as Pattern +import Elm.Syntax.TypeAnnotation as Annotation import Internal.Arg import Internal.Compiler as Compiler exposing (Module) import Internal.Index as Index @@ -335,10 +336,13 @@ fn desiredName arg toInnerFn sourceLet = Elm.apply (Compiler.Expression (\_ -> - { innerFnDetails - | expression = - Exp.FunctionOrValue [] - name + { expression = + Exp.FunctionOrValue [] name + , annotation = + letFnAnnotation + [ argDetails.details.annotation ] + innerFnDetails.annotation + , imports = innerFnDetails.imports } ) ) @@ -349,6 +353,45 @@ fn desiredName arg toInnerFn sourceLet = ) +{-| Build the function type annotation for a let-bound function's +reference expression. Takes the arg annotations (in order) and the +body's annotation, and produces `arg1 -> arg2 -> ... -> body`. + +This is needed because `Elm.apply` needs a proper function type +annotation to derive the return type when calling the let-bound +function. Without this, the call would use the body's annotation +directly, which is the return type rather than a function type. +-} +letFnAnnotation : + List (Result (List Compiler.InferenceError) Compiler.Inference) + -> Result (List Compiler.InferenceError) Compiler.Inference + -> Result (List Compiler.InferenceError) Compiler.Inference +letFnAnnotation argAnnotations bodyAnnotation = + List.foldr + (\argResult resultSoFar -> + Result.map2 + (\argAnn soFar -> + { type_ = + Annotation.FunctionTypeAnnotation + (Compiler.nodify argAnn.type_) + (Compiler.nodify soFar.type_) + , inferences = + Compiler.mergeInferences + argAnn.inferences + soFar.inferences + , aliases = + Compiler.mergeAliases + argAnn.aliases + soFar.aliases + } + ) + argResult + resultSoFar + ) + bodyAnnotation + argAnnotations + + {-| -} fn2 : String @@ -399,10 +442,15 @@ fn2 desiredName argOne argTwo toInnerFn sourceLet = Elm.apply (Compiler.Expression (\_ -> - { innerFnDetails - | expression = - Exp.FunctionOrValue [] - name + { expression = + Exp.FunctionOrValue [] name + , annotation = + letFnAnnotation + [ argOneDetails.details.annotation + , argTwoDetails.details.annotation + ] + innerFnDetails.annotation + , imports = innerFnDetails.imports } ) ) @@ -473,10 +521,16 @@ fn3 desiredName argOne argTwo argThree toInnerFn sourceLet = Elm.apply (Compiler.Expression (\_ -> - { innerFnDetails - | expression = - Exp.FunctionOrValue [] - name + { expression = + Exp.FunctionOrValue [] name + , annotation = + letFnAnnotation + [ argOneDetails.details.annotation + , argTwoDetails.details.annotation + , argThreeDetails.details.annotation + ] + innerFnDetails.annotation + , imports = innerFnDetails.imports } ) ) diff --git a/tests/TypeChecking.elm b/tests/TypeChecking.elm index 25a9ab38..862df035 100644 --- a/tests/TypeChecking.elm +++ b/tests/TypeChecking.elm @@ -6,6 +6,7 @@ import Elm.Arg as Arg import Elm.Case import Elm.Declare import Elm.Expect +import Elm.Let import Elm.Op import Elm.ToString import Expect @@ -271,6 +272,71 @@ generatedCode = ( 1 + 2, x ) """ ] + , test "Elm.Let.fn declaration has a type annotation" <| + \_ -> + Elm.declaration "useLetFn" + (Elm.Let.letIn + (\myFn -> myFn (Elm.int 5)) + |> Elm.Let.fn "myFn" + (Arg.var "x") + (\x -> Elm.Op.plus x (Elm.int 1)) + |> Elm.Let.toExpression + ) + |> Elm.Expect.declarationAs + """ + useLetFn : Int + useLetFn = + let + myFn x = + x + 1 + in + myFn 5 + """ + , test "Elm.Let.fn2 declaration has a type annotation" <| + \_ -> + Elm.declaration "useLetFn2" + (Elm.Let.letIn + (\myFn -> myFn (Elm.int 1) (Elm.int 2)) + |> Elm.Let.fn2 "myFn" + (Arg.var "x") + (Arg.var "y") + (\x y -> Elm.Op.plus x y) + |> Elm.Let.toExpression + ) + |> Elm.Expect.declarationAs + """ + useLetFn2 : Int + useLetFn2 = + let + myFn x y = + x + y + in + myFn 1 2 + """ + , test "Elm.Let.fn3 declaration has a type annotation" <| + \_ -> + Elm.declaration "useLetFn3" + (Elm.Let.letIn + (\myFn -> myFn (Elm.int 1) (Elm.int 2) (Elm.int 3)) + |> Elm.Let.fn3 "myFn" + (Arg.var "x") + (Arg.var "y") + (Arg.var "z") + (\x y z -> + Elm.Op.plus x (Elm.Op.plus y z) + ) + |> Elm.Let.toExpression + ) + |> Elm.Expect.declarationAs + """ + useLetFn3 : Int + useLetFn3 = + let + myFn x y z = + x + (y + z) + in + myFn 1 2 3 + """ , test "Triple with mixed Float and Int infers correct types" <| \_ -> Elm.declaration "myTriple"