Repository navigation
A union of pointer types stores null in its pointer, not as a member - #547
Merged
Merged
Conversation
Cast into a tagged union, a value got its static type as the tag. A class, string, array or object value holding null was therefore stored as that type holding a null pointer: a null `Handle | null` widened to `boolean | Handle | null` was tagged "class", so `=== null` missed it, `typeof` said "class", and a truth test read its vtable through the null pointer. Where the union has a null member of its own, such a value now takes the null tag when its pointer is null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class or a string holds null in its own pointer, which is why `Class | null` is stored as the class. A union of several such types (`A | B | null`, `string | A | null`) still got a null member of its own; now it doesn't. Its value is null when its pointer is null, whichever member's tag it carries. The declared type keeps `| null` under strict null checks, which is what allows or refuses assigning null. A union with a member that can't hold null (a number: 0 is not null) keeps its null member. - needsNullMember is false when every non-null member absorbs null, and the storage type (disableStrickNullCheck) no longer adds null in strict mode on the merged path either. - `=== null` on a tagged union is no longer cast to null through a generated cast first (syncUnionTypes): LogicOp compares the union, by its tag or, for such a union, by its value's pointer - judged from the actual type isUnionTypeNeedsTag returns. - The generated union cast asks `t === null` first for such a union, so no `instanceof` meets a null instance: false for a truth test, "null" as text, null where the target takes it, else the usual throw. #540's one-class special case is gone. - CastLogicHelper uses MLIRTypeHelper::isNullAbsorbingType. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y-pointers # Conflicts: # tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h # tslang/test/tester/CMakeLists.txt
…it back MLIRTypeHelper::isNullAbsorbingType replaces it. 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 rule
A class or a string holds null in its own pointer. That's why
Class | nullis stored as just the class, with| nullkept on the type only so that strict-null code can allow or refuse assigning null. This PR applies the same rule to unions of several such types:Class | nullA | B | null,string | A | nullA | B(wasnull | A | B)boolean | A | null,string | number | nullnullmember (unchanged)nullmember (0 can't stand for null)The declared type keeps
nullunder strict null checks, so assigning null is still allowed or refused as before.What this fixes
A null
Awidened toA | B | nullcrashed=== null, truth tests and??: each went through a generated cast whoseinstanceofread the vtable through the null pointer. Printing anA | B | null(${u}) didn't compile in strict mode ("can't cast from 'null' to 'string'"). This also closes the gap #540 left for unions with several classes.Changes
MLIRTypeHelper):needsNullMemberis false when every non-null member absorbs null (isNullAbsorbingType: class, string, array, object, opaque). The merged path ofgetUnionTypeMergeTypesno longer adds null to the storage type (disableStrickNullCheck) in strict mode; the non-merged path already worked that way. The storage layout doesn't change, because null and the absorbing members are all pointer-sized.=== null/!= null:syncUnionTypesno longer casts a tagged union tonullthrough a generated cast before comparing.LogicOpcompares the union directly: by its tag when the actual type (fromisUnionTypeNeedsTag) has anullmember, otherwise by the first word of its value field.castFromUnion): for such a union it first asksif (t === null), so no member test meets a null instance. The result isfalsefor a truth test,"null"for text,nullwhere the target accepts null, and otherwise the same throw the function ends with. A union of null and several types can be tested for truth #540's one-class special case is removed; between A null class, string or array widened to a union becomes its null member #546 and this check, a class member never sees a null pointer.CastLogicHelper(A null class, string or array widened to a union becomes its null member #546) uses the sharedMLIRTypeHelper::isNullAbsorbingType.Unchanged:
typeofof a null held this way saysclass, which is what it already says for aClass | nullholding null.Testing
00union_null_in_pointer.ts(compile + JIT + rc/none corpus): a widened null and a literal null inA | B | null(=== null,!= null, truth test,??, narrowing by!== null, as text), non-null values, andstring | A | null(null, empty string, text, class). It passes under gc, rc and none. On current main it fails to compile (can't cast from 'null' to 'string').typeGuardFunction.ts, which caught a first version of the null check that returned null to aB | C | Atarget.🤖 Generated with Claude Code