Skip to content

A ? field of a type literal is T | undefined: absent, it reads as undefined - #427

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-optional-fields
Oct 1, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-optional-fields

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes the open item from #424: { v?: string } lowered to plain string, so an absent field read as null (or 0) and === undefined was false.

type A = { v?: string, n?: number };
const a: A = {};
print(a.v === undefined, a.n === undefined, a.v, a.n);   // before: false false null 0   after: true true undefined undefined

Changes

  • Type literals: a ? member is T | undefined (optional<T>); v?: any stays any. A cast fills a field the source lacks with undefined (absentFieldValue).
  • {} given an interface or type-literal type built an undef tuple of every field. An interface's ? members are now left out (absent, so undefined). A type literal with such a field is built by the cast.
  • Tuple with ? fields cast to an interface declaring them. Interface fields go through a vtable whose slot holds the field's offset, and the access site reads the interface's T. optional<T> lowers to {T, i1} with the value first, so the matcher accepts an optional<T> field for a ? member T, and the slot points at the value. Whether the value is there is only known at run time, so the cast copies the object into a block with one extra trailing field, .ivtbl. The vtable is patched there: the slot is set to -1 (absent) when the field has no value. The vtable is freed with the block the interface holds; 2,000,000 casts under rc peak at 4 MB.
  • Spreading a ? field ({ ...desc.data }) spreads its value, or the type's defaults when absent, which is what it did while the field was a plain T. This fixes 00type_aliases_in_generics.ts, which this change broke at first.

Limitations

  • Classes are not changed. class C { v?: string } still reads an unassigned field as null/0. Storing optional<string> there would stop class C implements I { v?: string } from compiling: class-to-interface matching has no patch step, and the vtable reads the storage as T. class C { v: string | undefined } cast to a v?: string interface already fails on main.
  • Copy semantics. An object whose ? fields are held as optional<T> is copied when cast to such an interface, and the slot is decided at the cast. For a boxed object literal (one with methods), writes through the interface don't reach the original and later writes to the original aren't seen. On main this case (string | undefined field against a ? member) didn't compile.
  • {} against a type literal with a required field is now a compile error ("field ... can't be found"), as in TypeScript; it used to build a silent undef tuple.
  • any leniency: a tuple type can't mark a field as ?, so any any-typed field may be absent. type T = { a: any }; const t: T = {} compiles, where TypeScript errors.

Found, not fixed (exists on main)

  • let n: number | undefined = undefined; print(n ?? -1) warns invalid cast from 'f64' to '!llvm.ptr'; the result is right.

Tests

  • New 00optional_fields.ts (compile + jit): type literal, interface, {}, tuple to interface (present, absent, write, array of interfaces), extends, a literal with a method, spread.
  • export_type_alias.ts / import_type_alias.ts get a ? alias across the module boundary (all 7 variants, incl. -shared, rc, none).
  • Release suite: 3339/3339 passed.
  • DefaultLib (main b67a213) rebuilt release/gc with this compiler: 158/158 compile, 158/158 jit.
  • 00optional_fields.ts passes under gc, rc and none. -mm=own stops on own's existing "takes a second reference" limits, not on this change.
  • Not run: Debug, x86.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 1, 2026 18:29
… undefined

`type A = { v?: string, n?: number }` ignored the `?`: an absent field read as
null / 0 and `a.v === undefined` was false. The field is now T | undefined
(optional<T>; `v?: any` stays any), and a cast fills a field the source does
not have with undefined.

`{}` given an interface or a type literal as its type built an undef tuple of
every field. An interface's `?` members are now left out (absent, so
undefined), and a type literal with such a field is built by the cast.

A tuple with `?` fields still casts to an interface declaring them: the member
is read as T and optional<T> holds its T first, so the vtable slot points at
the value, and a copy of the vtable made for the object sets the slot to
absent when the field has no value.

Spreading a `?` field spreads its value, or its type's defaults when it has
none, as it did while the field was a plain T.

Class fields are unchanged: an interface reads a field through the object's
own storage, so `class C { v?: string }` storing optional<string> would no
longer implement `interface I { v?: string }`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-object vtable copy was a block of its own that nothing freed: under rc,
none and own every cast of a tuple with `?` fields to an interface leaked one.
castTupleWithOwnVTableToInterface now copies the object into a block with one
more field after its own, `.ivtbl`, and patches the vtable there, so it is
freed with the block the interface holds. The object's fields keep their
offsets. A boxed object literal is copied the same way, as its block is not
made by the cast.

2,000,000 casts under rc peak at 4 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 32a8984 into main Oct 1, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-optional-fields branch October 1, 2026 18:11
ASDAlexander77 added a commit that referenced this pull request Oct 1, 2026
…ce (#433)

`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>
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