Skip to content

Commit d2c96d8

Browse files
dillonkearnsclaude
andcommitted
Fix wire encoder+decoder for type aliases with extensible records through alias chains
When a type alias like Color = ColorValue { red, green, blue, alpha } references an extensible record alias (ColorValue compatible = { compatible | value : String, color : Compatible }), the encoder and decoder's TAlias Holey branch only checked for a direct TRecord inner type to resolve extensible records. When the inner type is a TAlias chain (not a direct TRecord), both encoder and decoder fell through to normalEncoder/normalDecoder, which passed only the extension fields to the extensible record codec — losing the base fields (value, color). This caused a type mismatch when the codec produced a 4-field record instead of the full 6-field record. Fix: in both Decoder.hs and Encoder.hs, when the Holey inner type is not a direct TRecord, use resolveTvar to resolve through alias chains. If this produces a Filled TRecord, inline the fully merged record encoder/decoder. Also set decoder Filled branch to normalDecoder to match encoder. Test: Added Wire_Union_ForeignRecordAlias.elm test fixture with ExternalExtensibleBase/ExternalRecordViaExtensible types in External.elm. Verified red→green with lamdera make on a project using rtfeldman/elm-css. Runtime roundtrip verified for Css.Color and Css.Px across 12 test cases (DirectWrap, RecordWrap, ListWrap, Mixed unions, Complex nesting, etc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 75ab4b1 commit d2c96d8

5 files changed

Lines changed: 137 additions & 2 deletions

File tree

extra/Lamdera/Wire3/Decoder.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,14 @@ decoderForType ifaces cname tipe =
416416
let extendedRecord = TRecord resolved Nothing & resolveTvar tvars_
417417
in decoderForType ifaces cname extendedRecord
418418
Nothing -> normalDecoder
419-
otherTypes -> normalDecoder
419+
_ ->
420+
-- Resolve extensible records through TAlias chains,
421+
-- e.g. Color = ColorValue { red, green, blue, alpha }
422+
case resolveTvar tvars_ tipe of
423+
TAlias _ _ _ (Filled (TRecord fieldMap Nothing)) ->
424+
let fields = fieldMap & fieldsToList & List.sortOn (\(name, field) -> name)
425+
in decodeRecord ifaces cname fields
426+
_ -> normalDecoder
420427
Filled tipe ->
421428
case tipe of
422429
TRecord fieldMap extensibleName ->

extra/Lamdera/Wire3/Encoder.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,14 @@ inlineIfRecordOrCall depth ifaces cname tipe tvars aType =
365365
in deepEncoderForType depth ifaces cname extendedRecord
366366
Nothing -> normalEncoder
367367

368-
otherTypes -> normalEncoder
368+
_ ->
369+
-- Resolve extensible records through TAlias chains,
370+
-- e.g. Color = ColorValue { red, green, blue, alpha }
371+
case resolveTvar tvars tipe of
372+
TAlias _ _ _ (Filled (TRecord fieldMap Nothing)) ->
373+
let extendedRecord = TRecord fieldMap Nothing
374+
in deepEncoderForType depth ifaces cname extendedRecord
375+
_ -> normalEncoder
369376
Filled _ -> normalEncoder
370377

371378
{-| Called for encoding tvar type values, i.e.

test/Test/Wire.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ wire = do
130130
, "src/Test/Wire_Tvar_Recursive_Reference.elm"
131131
, "src/Test/Wire_Unsupported.elm"
132132
, "src/Test/Wire_Unconstructable.elm"
133+
, "src/Test/Wire_Union_ForeignRecordAlias.elm"
133134
]
134135

135136
let

test/scenario-alltypes/src/Test/External.elm

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ type alias SubSubRecordAlias threadedTvar =
3030
}
3131

3232

33+
type alias ExternalExtensibleBase compatible =
34+
{ compatible | base : String }
35+
36+
37+
type alias ExternalRecordViaExtensible =
38+
ExternalExtensibleBase { red : Int, green : Int }
39+
40+
3341
expected_w3_encode_ExternalRecordBasic : ExternalRecordBasic -> Lamdera.Wire3.Encoder
3442
expected_w3_encode_ExternalRecordBasic =
3543
\w3_rec_var0 -> Lamdera.Wire3.encodeSequenceWithoutLength [ Lamdera.Wire3.encodeInt w3_rec_var0.int ]
@@ -113,3 +121,30 @@ expected_w3_decode_ExternalCustomThreaded w3_x_c_threadedTvar w3_x_c_threadedTva
113121
_ ->
114122
Lamdera.Wire3.failDecode
115123
)
124+
125+
126+
expected_w3_encode_ExternalExtensibleBase : ({ compatible | base : String.String } -> Lamdera.Wire3.Encoder) -> ExternalExtensibleBase compatible -> Lamdera.Wire3.Encoder
127+
expected_w3_encode_ExternalExtensibleBase w3_x_c_compatible =
128+
w3_x_c_compatible
129+
130+
131+
expected_w3_decode_ExternalExtensibleBase w3_x_c_compatible =
132+
w3_x_c_compatible
133+
134+
135+
expected_w3_encode_ExternalRecordViaExtensible : ExternalRecordViaExtensible -> Lamdera.Wire3.Encoder
136+
expected_w3_encode_ExternalRecordViaExtensible =
137+
\w3_rec_var0 ->
138+
Lamdera.Wire3.encodeSequenceWithoutLength
139+
[ Lamdera.Wire3.encodeString w3_rec_var0.base
140+
, Lamdera.Wire3.encodeInt w3_rec_var0.green
141+
, Lamdera.Wire3.encodeInt w3_rec_var0.red
142+
]
143+
144+
145+
expected_w3_decode_ExternalRecordViaExtensible =
146+
Lamdera.Wire3.succeedDecode
147+
(\base0 green0 red0 -> { base = base0, green = green0, red = red0 })
148+
|> Lamdera.Wire3.andMapDecode Lamdera.Wire3.decodeString
149+
|> Lamdera.Wire3.andMapDecode Lamdera.Wire3.decodeInt
150+
|> Lamdera.Wire3.andMapDecode Lamdera.Wire3.decodeInt
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
module Test.Wire_Union_ForeignRecordAlias exposing (..)
2+
3+
import Bytes.Decode
4+
import Bytes.Encode
5+
import Lamdera.Wire3
6+
import Test.External exposing (ExternalRecordBasic, ExternalRecordViaExtensible)
7+
8+
9+
{-| Regression test: extensible record aliases through alias chains.
10+
See: <https://github.com/elm-explorations/test/pull/249#issuecomment-4076937757>
11+
-}
12+
type WrapsBasicRecord
13+
= WrapsBasicRecord ExternalRecordBasic
14+
15+
16+
type WrapsExtensibleRecord
17+
= WrapsExtensibleRecord ExternalRecordViaExtensible
18+
19+
20+
type WrapsInRecord
21+
= WrapsInRecord { field : ExternalRecordViaExtensible }
22+
23+
24+
expected_w3_encode_WrapsBasicRecord : WrapsBasicRecord -> Lamdera.Wire3.Encoder
25+
expected_w3_encode_WrapsBasicRecord w3v =
26+
case w3v of
27+
WrapsBasicRecord v0 ->
28+
Lamdera.Wire3.encodeSequenceWithoutLength [ Bytes.Encode.unsignedInt8 0, Test.External.w3_encode_ExternalRecordBasic v0 ]
29+
30+
31+
expected_w3_decode_WrapsBasicRecord =
32+
Bytes.Decode.unsignedInt8
33+
|> Lamdera.Wire3.andThenDecode
34+
(\w3v ->
35+
case w3v of
36+
0 ->
37+
Lamdera.Wire3.succeedDecode WrapsBasicRecord |> Lamdera.Wire3.andMapDecode Test.External.w3_decode_ExternalRecordBasic
38+
39+
_ ->
40+
Lamdera.Wire3.failDecode
41+
)
42+
43+
44+
expected_w3_encode_WrapsExtensibleRecord : WrapsExtensibleRecord -> Lamdera.Wire3.Encoder
45+
expected_w3_encode_WrapsExtensibleRecord w3v =
46+
case w3v of
47+
WrapsExtensibleRecord v0 ->
48+
Lamdera.Wire3.encodeSequenceWithoutLength [ Bytes.Encode.unsignedInt8 0, Test.External.w3_encode_ExternalRecordViaExtensible v0 ]
49+
50+
51+
expected_w3_decode_WrapsExtensibleRecord =
52+
Bytes.Decode.unsignedInt8
53+
|> Lamdera.Wire3.andThenDecode
54+
(\w3v ->
55+
case w3v of
56+
0 ->
57+
Lamdera.Wire3.succeedDecode WrapsExtensibleRecord |> Lamdera.Wire3.andMapDecode Test.External.w3_decode_ExternalRecordViaExtensible
58+
59+
_ ->
60+
Lamdera.Wire3.failDecode
61+
)
62+
63+
64+
expected_w3_encode_WrapsInRecord : WrapsInRecord -> Lamdera.Wire3.Encoder
65+
expected_w3_encode_WrapsInRecord w3v =
66+
case w3v of
67+
WrapsInRecord v0 ->
68+
Lamdera.Wire3.encodeSequenceWithoutLength [ Bytes.Encode.unsignedInt8 0, Test.External.w3_encode_ExternalRecordViaExtensible v0.field ]
69+
70+
71+
expected_w3_decode_WrapsInRecord =
72+
Bytes.Decode.unsignedInt8
73+
|> Lamdera.Wire3.andThenDecode
74+
(\w3v ->
75+
case w3v of
76+
0 ->
77+
Lamdera.Wire3.succeedDecode WrapsInRecord
78+
|> Lamdera.Wire3.andMapDecode
79+
(Lamdera.Wire3.succeedDecode (\field0 -> { field = field0 })
80+
|> Lamdera.Wire3.andMapDecode Test.External.w3_decode_ExternalRecordViaExtensible
81+
)
82+
83+
_ ->
84+
Lamdera.Wire3.failDecode
85+
)

0 commit comments

Comments
 (0)