Repository navigation
A ? field of a type literal is T | undefined: absent, it reads as undefined - #427
Merged
Merged
Conversation
… 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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the open item from #424:
{ v?: string }lowered to plainstring, so an absent field read asnull(or0) and=== undefinedwas false.Changes
?member isT | undefined(optional<T>);v?: anystaysany. 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.?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'sT.optional<T>lowers to{T, i1}with the value first, so the matcher accepts anoptional<T>field for a?memberT, 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.?field ({ ...desc.data }) spreads its value, or the type's defaults when absent, which is what it did while the field was a plainT. This fixes00type_aliases_in_generics.ts, which this change broke at first.Limitations
class C { v?: string }still reads an unassigned field asnull/0. Storingoptional<string>there would stopclass C implements I { v?: string }from compiling: class-to-interface matching has no patch step, and the vtable reads the storage asT.class C { v: string | undefined }cast to av?: stringinterface already fails onmain.?fields are held asoptional<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. Onmainthis case (string | undefinedfield 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.anyleniency: a tuple type can't mark a field as?, so anyany-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)warnsinvalid cast from 'f64' to '!llvm.ptr'; the result is right.Tests
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.tsget a?alias across the module boundary (all 7 variants, incl.-shared, rc, none).00optional_fields.tspasses under gc, rc and none.-mm=ownstops on own's existing "takes a second reference" limits, not on this change.🤖 Generated with Claude Code