Skip to content

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

Merged
supermario merged 2 commits intolamdera:lamdera-nextfrom
dillonkearns:fix/wire-decoder-filled-alias-inlining
Mar 25, 2026
Merged

Fix wire encoder+decoder for type aliases with extensible records thr…#85
supermario merged 2 commits intolamdera:lamdera-nextfrom
dillonkearns:fix/wire-decoder-filled-alias-inlining

Conversation

@dillonkearns
Copy link
Copy Markdown
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!

@dillonkearns dillonkearns force-pushed the fix/wire-decoder-filled-alias-inlining branch from 6d55195 to d2c96d8 Compare March 24, 2026 15:19
@miniBill
Copy link
Copy Markdown
Member

Related: #33
That PR has a test which we could add to this PR

…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.

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>
@dillonkearns dillonkearns force-pushed the fix/wire-decoder-filled-alias-inlining branch from d2c96d8 to 6447c47 Compare March 24, 2026 15:30
@miniBill
Copy link
Copy Markdown
Member

This seems to fix the issue with elm-review-simplify

- Use @-pattern to avoid reconstructing TRecord
- No sorting needed in encoder (accesses fields by name, consistent
  with existing Holey TRecord branch at line 364)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@supermario
Copy link
Copy Markdown
Member

Happy to roll with this for now #33 doesn't have an implementation yet, we can qualify that for further edge cases later. Thank you @dillonkearns

@supermario supermario merged commit a996ba0 into lamdera:lamdera-next Mar 25, 2026
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.

3 participants