Make variant runtime representation canonical across compiler IRs - #8579
Conversation
Constructors no longer carry positional integer tags anywhere in the compiler. Types.constructor_tag (Cstr_constant/Cstr_block of int) is replaced by a semantic identity (declaring type path + name for ordinary constructors, path for extensions), and every consumer now answers its actual question directly: - Parmatch compares constructors by identity and derives completeness from the type declaration instead of forging tags from counts; column coherence compares declared head types instead of count equality. - Matching keys constructor switches by canonical constructor cases and takes case counts from the variant layout in scope; the layout_from_construct_pattern mutable callback is inlined as a plain function and its polyfill removed. - Construction Lambda carries canonical runtime descriptors only: Const_pointer loses its ordinal, Lam_constant gains a first-class Const_constructor, and the tag ints are removed from Blk_constructor, Blk_record_inlined, Record_inlined, Lam.Pmakeblock, Const_block, and J.Caml_block. Constructors represented as numbers (@as(Int)) convert to genuine int constants so folding is preserved; JS block equality compares runtime descriptors instead of ordinals. - cstr_consts/cstr_nonconsts are removed; transparency is minted once in datarepr as cstr_transparent; Blk_constructor.num_nonconst is sourced from the declaration via the constructor identity. - unboxed_status collapses to type_representation = Boxed | Transparent; the internal -unboxed-types flag is removed and the runtime's Primitive_js_extern.res declares its unboxed records explicitly. Emitted JavaScript is unchanged across the test suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw5GW8t4UDEWAzoqcDMkE
Introduce Variant_runtime, a leaf module below Types holding the plain data that describes how variants are represented in JavaScript; Ast_untagged_variants re-exports the definitions and keeps deriving them. Type_variant now carries the canonical layout, mirroring how Type_record carries record_representation: typedecl computes it once the recursive group is in the environment, at the same point the untagged invariants were already being validated by computing this exact layout and discarding it. Predefined declarations mint their layouts by hand, and the untagged helper refs are installed from Typedecl so every binary that types code has them. The stored layout is not consumed yet; matching still derives its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw5GW8t4UDEWAzoqcDMkE
Constructor descriptions now carry their declaring variant's layout, minted in datarepr from the declaration, and every consumer reads it instead of re-deriving representation facts: - Matching takes the layout straight from the constructor description; the per-switch type resolution and the sw_layout plumbing are gone, and sw_dispatch is the layout's precomputed dispatch. - Translcore counts payload constructors from the stored layout instead of looking the declaration up in the environment. - Parmatch reads a constructor's untagged block type from the stored layout instead of re-resolving the declaration. With typedecl the only remaining layout computer, the derivation (get_block_type and friends) moves to a new Variant_layout module above Ctype, and the Obj.magic forward references in Ast_untagged_variants are deleted: typing a declaration now determines its representation once, and it is never revisited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw5GW8t4UDEWAzoqcDMkE
The Ordinary_constructor payload duplicated information already on the description: its name field mirrored cstr_name (read only by same_constructor, which has the whole description in hand), and its type_path had no remaining reader once the stored layout replaced the declaration lookups. constructor_identity becomes constructor_kind — Ordinary_constructor | Extension_constructor of Path.t — and identity is the pair of cstr_kind and cstr_name, or the extension's path. This also removes the unenforced invariant that the identity's name matched cstr_name, and makes the wrong comparison (ordinary constructors by path, which re-exports would break) inexpressible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw5GW8t4UDEWAzoqcDMkE
Review of the representation changes found derived data stored beside its source and duplicate derivations: - variant_layout collapses to the constructors array. The by-name map duplicated every case (and the names inside them), and the dispatch field was derivable; both are now computed by accessors, with the dispatch derived at its single consumer in Matching. - cstr_transparent was derivable from cstr_layout plus the unboxed attribute once descriptions carried their layout; it is a Datarepr predicate again, now environment-free. - Construction in Translcore reads the constructor's layout entry (Datarepr.constructor_case) instead of re-deriving the tag and block runtime from attributes, closing the last spot where construction and matching could derive representation independently. - Parmatch's full_match compares against the layout's length instead of looking the declaration up in the environment; the block-count folds in Translcore, Matching, and Datarepr use one Variant_runtime helper; js_dump drops a tautological num_nonconst test. - The type-equation re-exports in Ast_untagged_variants are gone: consumers reference Variant_runtime directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YWw5GW8t4UDEWAzoqcDMkE
The runtime representation of a variant is declaration-level data, while the decision for a particular match depends on its arms, actions, default, and exhaustiveness. Represent the two separately: - Variant_runtime.layout stores immutable constructor representations and declaration-level matching facts computed once. - Type_variant stores an abstract one-shot layout_ref. Recursive declarations allocate it while provisional, then complete the same identity after the recursive group is available. - Ordinary constructor descriptions address their representation by layout reference and source position, removing repeated name lookup and duplicated layout storage. - Matching builds one occurrence-specific constructor_matching_plan in combine_constructor and immediately lowers it to existing Lambda forms. No Lam or Lambda expression form is added. - Construction, matching, and type-based optimization consume the canonical representation instead of reinterpreting runtime attributes. Rename Transparent to Unboxed and variant_dispatch to matching_facts so the remaining terms describe the represented facts rather than an implementation strategy. Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Variant representation annotations are syntax-level inputs, but their meaning is needed throughout type inclusion, coercion, printing, diagnostics, and GenType. Previously those consumers independently re-read attributes from typed declarations and constructors, leaving both the annotations and their interpreted representation live in later compiler phases. Build the canonical declaration configuration together with the variant layout after the recursive declaration group has entered the environment. Retain the declared unboxing bit and custom tag field even for shapes, such as nullary-only variants, from which those choices cannot be recovered by inspecting constructor blocks. Constructor tags and unboxed payload facts remain indexed by source position in the same completed layout. Migrate Ctype coercions, inclusion checks, Printtyp, error suggestions, and GenType to consume the typed layout and the existing type/record representations instead of parsing attributes. This also makes GenType distinguish nominal variants from polymorphic variants explicitly: nominal cases use canonical constructor tags, while polymorphic variants continue to interpret their own row-field annotations. Keep attribute interpretation only at the typing boundary that creates the layout and in the pre-typing variant-spread compatibility check, where no completed target layout exists yet. Preserve legacy single-payload unboxing through Types.type_representation and combine it with declaration-level layout configuration when an effective runtime configuration is required. Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
ed49110 to
af49658
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed491104b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ~type_representation:decl.type_representation ~layout | ||
| in | ||
| configuration.tag_name | ||
| | Type_abstract | Type_record _ | Type_open -> None |
There was a problem hiding this comment.
Preserve tag metadata for abstract signature types
When a .resi intentionally hides a tagged variant as @tag("kind") type t and its .res defines the same tagged variant, this branch returns None for the abstract signature declaration while returning Some "kind" for the implementation layout. The comparison below consequently emits Tag_name despite matching annotations; the previous implementation read type_attributes for abstract declarations, so this valid abstraction no longer compiles. Preserve the source tag configuration for abstract types and cover this .res/.resi case with a multi-file fixture.
AGENTS.md reference: AGENTS.md:L181-L183
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This does identify a behavior change relative to master, but I do not think we should preserve it by attaching variant representation metadata to abstract types.
An abstract type exposes no constructors, so @tag does not change anything clients can construct or pattern-match. It also cannot be used in a variant spread: Variant_type_spread rejects Type_abstract (covered by the existing variant_spread_abstract_type fixture). The unsafe case is a concrete signature that exposes constructors while hiding or changing its tag field; that must remain rejected, and concrete variants retain their canonical layout here.
There is already an asymmetry in the old inclusion code: unboxing comparison is skipped when the expected declaration is abstract, while tag-name comparison is unconditional. The accepted example therefore looks like an accidental representation constraint on an otherwise abstract type, rather than an intentional abstraction feature.
The principled rule should be that concrete variants expose and compare their runtime layout, while abstract types carry no variant layout and hide representation. We should either ignore @tag when including into an abstract declaration, consistently with unboxing, or reject @tag on an abstract declaration as meaningless. Storing a tag configuration solely to preserve this non-operational constraint would reintroduce representation information in the wrong place.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8579 +/- ##
==========================================
- Coverage 75.95% 75.92% -0.03%
==========================================
Files 474 476 +2
Lines 62907 63007 +100
==========================================
+ Hits 47781 47839 +58
- Misses 15126 15168 +42
🚀 New features to boost your workflow:
|
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
Developer playground preview: https://rescript-lang.github.io/rescript/dev-playground/?version=pr-8579 |
cknitt
left a comment
There was a problem hiding this comment.
Great cleanup! Tested against a large project, no output diffs.
Summary
This PR gives nominal variants one canonical runtime representation and makes compiler phases consume that representation instead of independently reconstructing it from constructor ordinals, type lookups, and attributes.
The resulting design separates three concepts:
Generated JavaScript and language behavior remain unchanged.
Rationale
Variant representation was previously distributed across several mechanisms:
Constructor ordinals describe source ordering, not JavaScript values. That distinction matters for constructors customized with
@as, unboxed payloads, booleans, options, lists, and other special representations.Repeatedly interpreting attributes also permits compiler phases to disagree about the representation of the same declaration. Some representation facts require type expansion, so deriving them again later is both unnecessary and sensitive to which environment is available.
The representation is fundamentally a property of the variant declaration. It should therefore be computed once, after the recursive declaration group is available, and referenced directly by every consumer.
Design
Canonical declaration layout
Variant_runtimeis introduced as a low-level module containing the plain data that describes the JavaScript representation of a nominal variant.Type_variantstores aVariant_runtime.layout_ref. Recursive declarations allocate a pending reference while the group is provisional and complete that same reference exactly once after the group has entered the environment.The completed immutable layout contains:
Configuration that cannot be recovered from constructor shapes alone is retained explicitly. This matters, for example, for
@unboxedor@tagon variants with no payload constructor. Predefined variants construct equivalent layouts directly.Stable constructor references
Ordinary constructor descriptions reference their declaration's layout and their source position. This provides constant-time access to the canonical constructor representation without copying the layout or performing name-based declaration lookups.
Extension constructors remain identified by their extension path because they have different rebinding and runtime semantics.
Semantic runtime values instead of ordinals
Positional constructor integers are removed from the compiler IRs.
Lambda and Lam constants and blocks now carry their actual runtime descriptors. Constructors represented by numeric
@asvalues become genuine integer constants, preserving constant folding without pretending that a source ordinal is the emitted value. JS block comparison and lowering use these runtime descriptors directly.Declaration layout versus matching plan
A variant layout is declaration-level data. A matching plan depends on a particular match occurrence: its arms, actions, default case, exhaustiveness, and special-case opportunities.
Pattern matching therefore builds a local
constructor_matching_planwith documented cases for:The plan is immediately lowered to the existing Lambda control-flow forms. No new long-lived Lambda or Lam expression form is introduced, avoiding further divergence between those IRs. The general switch carries only the declaration-level
matching_factsneeded by later lowering.Representation annotations become a typing-boundary concern
The canonical layout is built where the declaration's annotations are interpreted and validated.
Downstream consumers now read typed representation data:
Legacy single-payload unboxing remains represented by
Types.type_representation. Multi-constructor unboxing and constructor-specific representation are supplied by the variant layout. Record representation continues to useTypes.record_representation.GenType now distinguishes nominal variants from polymorphic variants explicitly: nominal variants use the canonical layout, while polymorphic variants continue to interpret their row-field annotations because they have no nominal declaration layout.
The pre-typing variant-spread compatibility check remains an annotation consumer because the destination declaration does not yet have a completed layout.
Explicit runtime declarations
The internal global
-unboxed-typesmode is removed. Runtime callback wrapper records now declare@unboxedexplicitly, making their representation local and visible rather than dependent on compiler-wide state.Additional cleanup
This representation makes several older mechanisms unnecessary:
Obj.magicforward references used by the previous derivation path;Transparentterminology, replaced byUnboxed.Compatibility
Parsetree0remains unchanged for legacy PPX compatibility.-unboxed-typesoption was internal; its runtime uses are now explicit annotations.Testing
opam exec -- dune buildmake testmake test-gentypeThe existing compiler, integration, runtime, formatting, error-output, and GenType suites pass without expected-output changes.