Repository navigation
@value T | null without a tag when T holds an interface or a class; null tested by descriptor kind - #760
Merged
Conversation
…n its name is "null" A tagged union compared with null compared its tag's text with "null" (StringCompareOp, a strcmp) - every `Intersection | null` result of the ray tracer's Thing.intersect, and every traceRay and testRay. The tag points into its type's descriptor (TypeOfOpHelper::typeDescriptorValue), whose kind says null: one load and one compare. A tag never set reads undefined's descriptor, so it is not null, as before. Ray tracer, median ms: gc 79 -> 75, none 79 -> 74, rc 120 -> 115, own 86 -> 82. The hot path has no strcmp left; removing the union altogether (README variant B) now saves about 6 ms under none. ctest 6534/6534; fuzz seeds 81000-81599 clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`T | null` of a @value type was a tagged union inside an optional, 88 bytes for the ray tracer's Intersection. An interface is null when the object in it is (`{vtable, this}`, `this` null), and a class when its pointer is, so a T holding either holds its own null: MLIRTypeHelper::getValueNullNiche finds the first such field (looking into @value fields too), and isUnionTypeNeedsTag answers no, base type T. - null into it is T zeroed (CastLogicHelper); `== null` reads that pointer (UnaryBinLogicalOrHelper valueNullCompare); `typeof` and boxing into `any` ask the same (TypeOfOpHelper, castToAny). Undefined is still the empty optional, so null and undefined stay apart. - -mm=own: a @value value widened to its own `T | null`, or read back, is a view (isView) - the cast stays a cast where a tagged one became CreateUnionInstanceOp. - A T with only numbers keeps its tag. Under `// @strict-null false` a T built with that field null now reads as null (spec "Missing values"). Ray tracer, median ms: gc 76 -> 72, none 76 -> 70, rc 117 -> 111, own 83 -> 77; the same as the variant with no union at all. Test value/value_missing_niche (gc, rc, none). ctest 6540/6540; fuzz seeds 82000-82599 clean; DefaultLib compiles under all four models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 31, 2026
…n imported .d.ts (#761) * @value T | null: ?: of a literal holding a new object (rc), ??, and print Three failures of a @value type that may be missing, all on main before #760: - rc: `d < 0 ? null : { at: d, owner: new Owner(d) }` returned as `Mark | null`. The branch released the new object before the result's retain after the join, which then retained freed memory. consumeConditionalResults' producerOf looks through a tuple cast to the @value type it is laid out as, so yieldsLiteralOfBranchTemporary sees the literal and the branch retains first. - `h ?? find(3)` was typed `Hit | (Hit | null)` and did not compile: an optional member of a union gave its element as one member. processUnionTypeItem now processes the element like any other member, so a union in it gives its members. - print / string conversion: an undefined `T | undefined` printed as null or as a zeroed T (the optional went through ValueOrDefault); it is "undefined" now. A null `T | null` stored without a tag (#760) printed T's fields; castNullableToString takes a @value base too and prints "null". Test value/value_missing_ops (gc, rc, none; own cannot yet tell which branch of a `?:` owns a literal it merges). ctest 6546/6546; fuzz seeds 83000-83599 clean; DefaultLib compiles under own and rc. Printing a @value value holding an interface still crashes, as printing any interface does - not changed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * An interface printed shows its fields, as an object literal does Printing an interface value, or putting it into a string, crashed at run time, whatever object was behind it - a class, an object literal, or one a @value value holds. USER RULING: print its fields. castInterfaceToString: - its `toString` when the interface has one, as a class's is; - else its fields, read through the interface, those of the interfaces it extends first, in an object literal's format (`{ n: 3, name: 'b' }`, fieldsToString, which castTupleToString now shares); - null as "null"; - a field of an interface type already being printed is `[Object]`: the text is made inline, and `interface Chain { next: Chain }` nests without end. Test tests/00interface_print (every model). ctest 6552/6552; fuzz seeds 84000-84599 clean; DefaultLib compiles under all four models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A C string result is copied when an imported module loaded its .d.ts first TSLANG_ISSUES 21. The string a C function returns is copied into a string of the program's own (#575), but not when the `.d.ts` declaring it was first reached through a module this file imports: `declaredFunctions` kept the module of that earlier pass, and isForeignCall wanted the one being generated. The raw `char*` was taken as it was - stored strerror() results showed the buffer's last text, and under -mm=rc the count was written into C's memory and the release freed it (native_ts_graphics on Android: "Scudo ERROR: misaligned pointer when deallocating"). A top-level body-less `declare function` is C whichever module's pass loaded its `.d.ts`: isForeignCall now asks that, and that this module's function has no body. Test c-string-import/ (gc, rc, none, own; compiled and JIT), whose `.d.ts` is outside tests/, where *.d.ts are build output and ignored. ctest 6560/6560; fuzz seeds 85000-85599 clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- 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.
The last ray tracer cost:
Intersection | null. Two commits:strcmpof its tag with"null". The tag points into its type's descriptor, whose kind (TYPE_KIND_NULL) answers with one load and one compare.@valuetype holding an interface or a class needs no tag for its null (63be294). An interface is{vtable, this}and is null whenthisis; a class is null when its pointer is. So a@value Twith such a field holds its own null.MLIRTypeHelper::getValueNullNichefinds the first such field, looking into nested@valuefields too, andisUnionTypeNeedsTaganswers no, with base typeT.Tzeroed, and== nullreads that pointer.typeofand boxing intoanycheck the same pointer. Undefined is still the empty optional, so null and undefined stay apart.-mm=own: a cast betweenTand its ownT | nullis a view. A cast into a tagged union becomesCreateUnionInstanceOp, but this one stays a cast.@valuetype with only numeric fields keeps its tag.Ray tracer
Median of 7 runs, ms; every build prints the same checksum.
Under none the benchmark now runs as fast as the variant that removes the union by hand (69.9 ms).
Testing
lib.tscompiles under own, rc, gc and none.value/value_missing_niche.ts, which isvalue_missing.tswith an interface and a class in the@valuetypes, runs under gc, rc and none. It covers every spelling and state,typeof, and boxing intoany. It doesn't run under own, which rejects that program's records holding a borrowed global;own_raytracecovers own.Behavior change
Under
// @strict-null false, a@valuevalue built with its interface or class field null ({ thing: null, dist: 1 }) now reads as null. This is documented in the spec's "Missing values" section.Found along the way, already on main
function mark(d): Mark | null { return d < 0 ? null : { at: d, owner: new Owner(d) }; }(a@valueholding a new class instance, returned from a?:with null) crashes at run time.(find(-1) ?? find(3)).distwithfind(): Hit | nullfails to compile: the??result type is a union nested in a union.print(find(-1))of a@valueT | nullcrashes the compiler.🤖 Generated with Claude Code