Skip to content

A class's v?: T is T | undefined, directly and through an interface - #433

Merged
ASDAlexander77 merged 1 commit into
mainfrom
class-optional-fields
Oct 1, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
class-optional-fields

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

class C { v?: string } stored a plain string. An unassigned c.v read as null (0 for a number), and c.v === undefined was false. The same was true for a constructor parameter field, constructor(public m?: string), so new Error().message === undefined was false.

Type literals were fixed in #427, but classes were left out. An interface's v?: T reads the field at an offset stored in the vtable, using the interface's type. The type-literal fix patches a copy of the vtable for each object, which works because the object is copied. A class instance is shared, so that approach doesn't apply.

Fix

  • Storage. A class field v?: T, and a public m?: T constructor parameter field, is stored as optional<T>, i.e. {T, flag} with the value first.
  • Class → interface. A conditional interface member v?: T accepts optional<T> storage. The class's interface vtable marks that slot with INTERFACE_OPTIONAL_STORAGE_TAG added to the offset.
  • Lowering (InterfaceSymbolRefOpLowering):
    • a -1 slot is absent, as before;
    • a tagged slot reads the field's flag, and an unset flag reads as undefined;
    • a write through the interface sets the flag, and = undefined clears it. mlirGenSaveLogicOneItem marks the access a store goes to with INTERFACE_OPTIONAL_WRITE_ATTR.
  • InterfaceSymbolRefOp now reads memory (MemoryEffects<[MemRead]>, previously Pure). A flag read before a write must not stand in for one after it: CSE merged them, and LICM could hoist one out of a loop.

Needs DefaultLib #20

DefaultLib's declarations have to match its compiled library. Its RegExp classes declare ? fields that the implementation stores plain; DefaultLib #20 drops those ?. Error's message? now matches its public message? constructor fields. A DefaultLib built before this PR has the old layouts, so it needs a rebuild: I:\tslang, which your environment points DEFAULT_LIB_PATH at, will fail test-compile-gc-defaultlib-collector until it's rebuilt.

Tests

  • New 00class_optional_fields.ts, run as compile and jit. It covers:

    • unassigned fields, directly and through an interface;
    • writing a value and writing undefined through an interface;
    • an inherited field;
    • an initializer;
    • a generic class;
    • a loop through an interface under --opt;
    • a class passed as a type literal;
    • constructor parameter fields.

    It passes under gc, rc and none, with and without --opt.

  • Release suite: 3351/3351, against a DefaultLib built from DefaultLib [Question] Speed difference #20 with this compiler.

  • That DefaultLib: 159/159 compiled and 159/159 under JIT (release).

  • rc: a 2M-iteration loop that writes and clears ? fields directly and through an interface gives the right result. Its memory matches the same loop without ? fields (226 MB vs 235 MB). That loop grows under rc either way, and the growth isn't from this change.

  • Not run: Debug builds, x86, -mm=own.

🤖 Generated with Claude Code

`class C { v?: string }` stored a plain string, so an unassigned `c.v` read as null
(0 for a number) and `c.v === undefined` was false; the same for a constructor
parameter field `constructor(public m?: string)`. Both are now optional<T>, as a
type literal's `v?: T` has been since #427.

An interface's `v?: T` reads a field at an offset kept in the vtable, with the
interface's T. A class can't have its vtable patched per object as a type literal
does (an instance is shared, not copied), so its interface vtable marks such a slot
with INTERFACE_OPTIONAL_STORAGE_TAG on top of the offset. InterfaceSymbolRefOpLowering
then reads the field's flag: an unset one is undefined. A write through the interface
(`i.v = x`) sets the flag, and `i.v = undefined` clears it: mlirGenSaveLogicOneItem
marks the access it stores to with INTERFACE_OPTIONAL_WRITE_ATTR.

InterfaceSymbolRefOp reads memory now (MemoryEffects<[MemRead]>, was Pure): a read of
the flag before a write must not stand for one after it (CSE merged them, and LICM
could hoist one out of a loop).

DefaultLib's declarations must match its build: its RegExp classes declared fields
`?` that the implementation stores plain (DefaultLib PR), and Error's `message?` now
matches its `public message?` constructor fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 75f5aa9 into main Oct 1, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the class-optional-fields branch October 1, 2026 23:44
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