Semantic Diff

#9023 Fix and simplify the untagged variant stack after review

8 commits

c49c11ed1288a9e20998fa23416d8f3567ef3952..pr-9023

59739f7a2 · cknitt + Claude · Oct 24, 2026, 13:27 UTC

Say in rule 6 that variants with some @unboxed constructors are checked

#9021 checks a variant with some @unboxed constructors like an untagged one: tagged blocks count as one object case, and an untagged case's @as is not a tag. Rule 6 now says so and names its tests.

bb944d85a · cknitt + Claude · Oct 24, 2026, 16:18 UTC

Require a private abbreviation for every subclass

Why

A public abbreviation could declare a subclass when its arguments were not all type variables, meant for phantom type hierarchies. The test was syntactic, so @instanceOf("Dog") type dog = animal<int> passed, and every animal<int> was then a dog without a coercion.

A subclass is always a private abbreviation, phantom hierarchies included; upcasts use :>.

addedDiagnostic public subclass of an instantiated class

@instanceOf("Animal") type animal<'a>@instanceOf("Dog") type dog = animal<int>
diagnostic
Before
After
noneA subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion.
ab924c652 · cknitt + Claude · Oct 24, 2026, 16:18 UTC

Say precisely when literals use an if and what folds

Rule 7 counts the literals that get a case, as the code does, and rule 11 names the values constant folding sees: literals and blocks of constants.

ad2cc3320 · cknitt + Claude · Oct 24, 2026, 16:21 UTC

Emit untagged checks directly instead of through Dynamic_checks

The check AST in Ast_untagged_variants only encoded the int-tag test of tagged variants and the instance checks. Js_exp_make now builds both directly (is_int_tag from the literal list, is_instance_of), and matching_facts no longer carries has_null, has_undefined and has_other_literal, which that test derived. A class is never a switch key, so block_type_name no longer pretends to emit one. No output changes.

7139b2784 · cknitt + Claude · Oct 24, 2026, 16:23 UTC

Share which literal passes which payload check

Code generation and constant folding each had a table of the literals a payload case accepts, and they disagreed on null and an object case. Variant_runtime.literal_passes is now the one table; folding follows the generated code, where typeof null is "object", so null without a null case folds into the object case as it is matched at run time.

18bb26716 · cknitt + Claude · Oct 24, 2026, 16:23 UTC

Move the variant invariant check next to the layout

check_invariant checks a layout, and Variant_layout, which derives the layout, is its only caller; Ast_untagged_variants keeps the attributes and errors. Its doc comment, which described another function, now says what it checks.

19cd7e926 · cknitt + Claude · Oct 24, 2026, 16:25 UTC

Compare every runtime shape claim of an interface in one place

Interface inclusion compared classes and kinds in Includecore and @notUndefined in Includemod, the two levels of one declared shape. Includecore.compare_runtime_annotations now checks both; the messages are unchanged.

be31af558 · cknitt + Claude · Oct 25, 2026, 05:11 UTC

Merge the @notUndefined rule into the rule on declared shapes

@notUndefined, @typeOf and @instanceOf declare levels of one stored shape, checked on abbreviations by one entry point and across interfaces by one comparer. Rule 5 now describes the three together, with their errors and interface checks; rule 13 is gone and the rule on @tag and interfaces becomes rule 13.