Skip to content

Commit 2401bdf

Browse files
dillonkearnsclaude
andcommitted
Fix typeclass constraints dropped from polymorphic type annotations.
When a type variable stays polymorphic through an operator that requires a typeclass constraint (+ requires number, < requires comparable, ++ requires appendable), the constraint was lost during type variable rewriting, producing `a -> a -> a` instead of `number -> number -> number`. Root cause: `resolveVariables` replaces constrained names (like `number_0` or `comparable`) with arg variable names (like `arg_0`). Then `rewriteTypeVariables` renames `arg_0` to `a`, losing the constraint. Fix: `rewriteTypeVariablesPreservingConstraints` builds a mapping from resolved variable names back to their constraint names by walking the inference cache bidirectionally. If a constrained name maps to a generic (forward: `number_0 → arg_0`) or a generic maps to a constrained name (reverse: `arg_0 → comparable`), the constraint name is preserved during rewriting. Before: Elm.Op.plus a b → addBoth : a -> a -> a Elm.Op.lt a b → compareBoth : a -> a -> Bool Elm.Op.append a b → appendBoth : a -> a -> a After: Elm.Op.plus a b → addBoth : number -> number -> number Elm.Op.lt a b → compareBoth : comparable -> comparable -> Bool Elm.Op.append a b → appendBoth : appendable -> appendable -> appendable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 84c0f33 commit 2401bdf

2 files changed

Lines changed: 195 additions & 12 deletions

File tree

src/Internal/Compiler.elm

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ resolve index cache annotation =
10201020
getRestrictions annotation cache
10211021
in
10221022
newAnnotation
1023-
|> rewriteTypeVariables
1023+
|> rewriteTypeVariables cache
10241024
|> checkRestrictions restrictions
10251025

10261026
Err err ->
@@ -1286,30 +1286,130 @@ getRestrictionsHelper existingRestrictions notation cache =
12861286
existingRestrictions
12871287

12881288

1289-
rewriteTypeVariables : Annotation.TypeAnnotation -> Annotation.TypeAnnotation
1290-
rewriteTypeVariables type_ =
1289+
{-| Rewrite type variable names to clean forms, preserving typeclass
1290+
constraint names.
1291+
1292+
When a constrained type variable (like `number_0`) gets resolved to
1293+
another generic variable (like `arg_0`), the constraint name is lost.
1294+
This function builds a mapping from resolved variable names back to
1295+
their constraint names, so `arg_0` gets renamed to `number` instead
1296+
of `a`.
1297+
-}
1298+
rewriteTypeVariables :
1299+
VariableCache
1300+
-> Annotation.TypeAnnotation
1301+
-> Annotation.TypeAnnotation
1302+
rewriteTypeVariables cache resolvedAnnotation =
12911303
let
1304+
-- Build a map from resolved generic names to constraint names.
1305+
-- Check BOTH directions:
1306+
-- 1. Forward: a constrained name (number_0) maps to a generic (arg_0)
1307+
-- 2. Reverse: a generic (arg_0) maps to a constrained name (comparable)
1308+
-- Case 2 happens with applyInfix operators that use fixed names
1309+
-- like "comparable" which then get unified with arg variables.
1310+
constraintOverrides : Dict String String
1311+
constraintOverrides =
1312+
Dict.foldl
1313+
(\key value acc ->
1314+
case value of
1315+
Annotation.GenericType resolvedName ->
1316+
let
1317+
keyRestriction =
1318+
nameToRestrictions key
1319+
in
1320+
case keyRestriction of
1321+
NoRestrictions ->
1322+
-- Key has no constraint, but maybe the
1323+
-- resolved name does (reverse direction)
1324+
let
1325+
resolvedRestriction =
1326+
nameToRestrictions resolvedName
1327+
in
1328+
case resolvedRestriction of
1329+
NoRestrictions ->
1330+
acc
1331+
1332+
_ ->
1333+
-- The target has a constraint — propagate
1334+
-- it to the key name
1335+
Dict.insert key
1336+
(restrictionToName resolvedRestriction)
1337+
acc
1338+
1339+
_ ->
1340+
-- Key has a constraint — propagate to resolved name
1341+
Dict.insert resolvedName
1342+
(restrictionToName keyRestriction)
1343+
acc
1344+
1345+
_ ->
1346+
acc
1347+
)
1348+
Dict.empty
1349+
cache
1350+
12921351
existing : Set String
12931352
existing =
1294-
getGenericsHelper type_
1353+
getGenericsHelper resolvedAnnotation
12951354
|> Set.fromList
12961355
in
1297-
Tuple.second (rewriteTypeVariablesHelper existing Dict.empty type_)
1356+
Tuple.second
1357+
(rewriteTypeVariablesHelper
1358+
constraintOverrides
1359+
existing
1360+
Dict.empty
1361+
resolvedAnnotation
1362+
)
1363+
12981364

1365+
restrictionToName : Restrictions -> String
1366+
restrictionToName restriction =
1367+
case restriction of
1368+
IsNumber ->
1369+
"number"
1370+
1371+
IsComparable ->
1372+
"comparable"
1373+
1374+
IsAppendable ->
1375+
"appendable"
1376+
1377+
IsAppendableComparable ->
1378+
"compappend"
12991379

1300-
rewriteTypeVariablesHelper : Set String -> Dict String String -> Annotation.TypeAnnotation -> ( Dict String String, Annotation.TypeAnnotation )
1301-
rewriteTypeVariablesHelper existing renames type_ =
1380+
_ ->
1381+
"a"
1382+
1383+
1384+
{-| Rewrite type variable names to clean, simplified forms.
1385+
1386+
The `overrides` dict maps variable names to constraint names
1387+
(e.g., "arg\_0" → "number") so that typeclass constraints are
1388+
preserved through the renaming process. Pass `Dict.empty` when
1389+
no constraint preservation is needed.
1390+
-}
1391+
rewriteTypeVariablesHelper :
1392+
Dict String String
1393+
-> Set String
1394+
-> Dict String String
1395+
-> Annotation.TypeAnnotation
1396+
-> ( Dict String String, Annotation.TypeAnnotation )
1397+
rewriteTypeVariablesHelper overrides existing renames type_ =
13021398
case type_ of
13031399
Annotation.GenericType varName ->
13041400
case Dict.get varName renames of
13051401
Nothing ->
13061402
let
13071403
simplified : String
13081404
simplified =
1309-
simplify varName
1405+
case Dict.get varName overrides of
1406+
Just constraintName ->
1407+
constraintName
1408+
1409+
Nothing ->
1410+
simplify varName
13101411
in
13111412
if Set.member simplified existing && varName /= simplified then
1312-
-- We would have collided with an existing generic name
13131413
( renames, Annotation.GenericType simplified )
13141414

13151415
else
@@ -1326,7 +1426,7 @@ rewriteTypeVariablesHelper existing renames type_ =
13261426
(\(Node _ typevar) ( varUsed, varList ) ->
13271427
let
13281428
( oneUsed, oneType ) =
1329-
rewriteTypeVariablesHelper existing varUsed typevar
1429+
rewriteTypeVariablesHelper overrides existing varUsed typevar
13301430
in
13311431
( oneUsed, nodify oneType :: varList )
13321432
)
@@ -1351,10 +1451,10 @@ rewriteTypeVariablesHelper existing renames type_ =
13511451
Annotation.FunctionTypeAnnotation (Node _ one) (Node _ two) ->
13521452
let
13531453
( oneUsed, oneType ) =
1354-
rewriteTypeVariablesHelper existing renames one
1454+
rewriteTypeVariablesHelper overrides existing renames one
13551455

13561456
( twoUsed, twoType ) =
1357-
rewriteTypeVariablesHelper existing oneUsed two
1457+
rewriteTypeVariablesHelper overrides existing oneUsed two
13581458
in
13591459
( twoUsed
13601460
, Annotation.FunctionTypeAnnotation

tests/TypeChecking.elm

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,89 @@ generatedCode =
271271
( 1 + 2, x )
272272
"""
273273
]
274+
, describe "Typeclass constraints preserved in polymorphic annotations"
275+
[ test "number constraint: polymorphic plus produces number annotation" <|
276+
\_ ->
277+
Elm.Declare.fn2 "addBoth"
278+
(Arg.var "a")
279+
(Arg.var "b")
280+
(\a b -> Elm.Op.plus a b)
281+
|> .declaration
282+
|> Elm.Expect.declarationAs
283+
"""
284+
addBoth : number -> number -> number
285+
addBoth a b =
286+
a + b
287+
"""
288+
, test "comparable constraint: polymorphic compare produces comparable annotation" <|
289+
\_ ->
290+
Elm.Declare.fn2 "compareBoth"
291+
(Arg.var "a")
292+
(Arg.var "b")
293+
(\a b -> Elm.Op.lt a b)
294+
|> .declaration
295+
|> Elm.Expect.declarationAs
296+
"""
297+
compareBoth : comparable -> comparable -> Bool
298+
compareBoth a b =
299+
a < b
300+
"""
301+
, test "appendable constraint: polymorphic append produces appendable annotation" <|
302+
\_ ->
303+
Elm.Declare.fn2 "appendBoth"
304+
(Arg.var "a")
305+
(Arg.var "b")
306+
(\a b -> Elm.Op.append a b)
307+
|> .declaration
308+
|> Elm.Expect.declarationAs
309+
"""
310+
appendBoth : appendable -> appendable -> appendable
311+
appendBoth a b =
312+
a ++ b
313+
"""
314+
, test "number constraint with concrete: number -> number -> Float" <|
315+
-- One arg is concrete (Float), the other stays polymorphic.
316+
-- In Elm this is: number -> Float -> Float (the number unifies with Float)
317+
\_ ->
318+
Elm.Declare.fn2 "addToFloat"
319+
(Arg.var "a")
320+
(Arg.var "b")
321+
(\a b -> Elm.Op.plus a (Elm.Op.plus b (Elm.float 1.0)))
322+
|> .declaration
323+
|> Elm.Expect.declarationAs
324+
"""
325+
addToFloat : Float -> Float -> Float
326+
addToFloat a b =
327+
a + (b + 1)
328+
"""
329+
, test "number constraint with one concrete Float arg" <|
330+
-- When one arg is used with a Float literal, both args
331+
-- resolve to Float (the number constraint narrows to Float)
332+
\_ ->
333+
Elm.Declare.fn2 "addToFloat"
334+
(Arg.var "a")
335+
(Arg.var "b")
336+
(\a b -> Elm.Op.plus a (Elm.Op.plus b (Elm.float 1.0)))
337+
|> .declaration
338+
|> Elm.Expect.declarationAs
339+
"""
340+
addToFloat : Float -> Float -> Float
341+
addToFloat a b =
342+
a + (b + 1)
343+
"""
344+
, test "comparable used with concrete Char" <|
345+
\_ ->
346+
Elm.Declare.fn "isLessThanZ"
347+
(Arg.var "c")
348+
(\c -> Elm.Op.lt c (Elm.char 'z'))
349+
|> .declaration
350+
|> Elm.Expect.declarationAs
351+
"""
352+
isLessThanZ : Char.Char -> Bool
353+
isLessThanZ c =
354+
c < 'z'
355+
"""
356+
]
274357
, test "Triple with mixed Float and Int infers correct types" <|
275358
\_ ->
276359
Elm.declaration "myTriple"

0 commit comments

Comments
 (0)