Skip to content

Fix wire encoder+decoder for type aliases with extensible records thr…#85

Open
dillonkearns wants to merge 1 commit intolamdera:lamdera-nextfrom
dillonkearns:fix/wire-decoder-filled-alias-inlining
Open

Fix wire encoder+decoder for type aliases with extensible records thr…#85
dillonkearns wants to merge 1 commit intolamdera:lamdera-nextfrom
dillonkearns:fix/wire-decoder-filled-alias-inlining

Conversation

@dillonkearns
Copy link
Collaborator

Hey @supermario! I ran into an issue, reported in elm-explorations/test#249 (comment) by @micahhahn, who hit this using elm-snapshot (via elm-pages runlamdera make) with a codebase using rtfeldman/elm-css. The salient point being that elm-pages scripts use lamdera make under the hood, and having a type definition with this particular shape caused a compiler error because of incorrect wire codegen.

The Problem

When a type alias references an extensible record alias through a chain of aliases (e.g., Css.Color = ColorValue { red, green, blue, alpha } where ColorValue compatible = { compatible | value : String, color : Compatible }), the wire encoder and decoder only handle the extension fields (4 fields), losing the base fields from the extensible record (should be 6 fields).

This causes a compiler error when any custom type wraps such a type alias:

type Theme = Theme { primary : Css.Color }
-- TYPE MISMATCH: Css.Color vs { alpha : Float, blue : Int, green : Int, red : Int }

Root cause

In both decoderForType (Decoder.hs) and inlineIfRecordOrCall (Encoder.hs), the TAlias case's Holey branch checks for a direct TRecord inner type to resolve extensible records via resolvedRecordFieldMapM. But when the inner type is another TAlias layer (common with packages like elm-css that define aliases through multiple modules), the check doesn't fire and falls to normalDecoder/normalEncoder. This generates w3_decode_Color = w3_decode_ColorValue (decodeRecord {alpha, blue, green, red}), but since w3_decode_ColorValue just delegates to its argument, the result only handles 4 of 6 fields. Same for the encoder.

Fix

In the Holey branch's fallthrough case for both encoder and decoder, use resolveTvar to resolve through alias chains.

Reproducing In Tests

Here's what I was able to test:

  • Added a test fixture in test/scenario-alltypes/src/Test/Wire_Union_ForeignRecordAlias.elm with custom types wrapping foreign extensible record aliases (mirrors the Css.Color pattern)
  • Reproduced the error with lamdera make on a project using rtfeldman/elm-css, and confirmed red→green by building a patched lamdera from source with stack build
  • Beyond just compilation, I also verified runtime encode -> decode roundtrips across 12 test cases (Css.Color, Css.Px, Maybe Css.Color, List Css.Color, custom types with multiple variants, nested records, etc.) and confirmed that field values roundtrip correctly. That test case isn't checked in, I couldn't find a good place for that to live within the codebase, could be a good thing to explore in the future to have automated encoder/decoder roundtrip runs

The one thing I wasn't able to run locally was the full wire test suite Test.Wire.all, maybe you could try running that in your environment as a sanity check?

Let me know if you have any feedback on this, happy to make any changes or discuss!

…ough 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant