Skip to content

A union of null and several types can be tested for truth - #540

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-union-null-truthiness
Oct 11, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-union-null-truthiness

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

boolean && (Class | null) used as a condition failed to compile:

function use(enabled: boolean, handle: Handle | null): number {
    if (enabled && handle) {        // error: can't instantiate function. '___cast'
        return handle.value;        //        not all generic types can be identified
    }
    return 0;
}

Reported in docs/bugs/native-ts-graphics-findings.md, issue 6 (docs/bugs/ntg_and_nullable_class.ts). The problem isn't && itself: any truth test of a union of null and two or more other types failed the same way, e.g. if (x) with x: boolean | string | null. enabled && handle simply has the type boolean | Handle | null.

Cause

Casting such a union to boolean goes through castFromUnion, which generates a __cast<T, U> function with one if (typeof t == '...') return t; per member. For the null member, return t casts null to boolean, which strict-null mode refuses ("can't cast from 'null' to 'boolean'"), so the instantiation fails. (That first error is also reported at a wrong location; that's a separate PR.)

Once that compiled, a second bug showed up: when a Handle | null holding null is converted into boolean | Handle | null, the value keeps the class tag. The class member's if (t instanceof Handle) then read the vtable through the null pointer and crashed.

Fix

In the generated cast, when the target is boolean:

  • a null or undefined member returns false (the same thing castFromAny already does for an any);
  • with a single class member, the class member returns t. A class tests as non-null, so a null instance is false and nothing is dereferenced.

Not covered: a union with two or more classes plus null still dereferences a null instance in its instanceof chain. That was already the case before this PR, and it needs a different shape of generated code.

Testing

  • New 00union_null_truthiness.ts (compile + JIT + rc/none corpus): the issue's function (including handle narrowed inside the block), the reversed handle && enabled, boolean | string | null, !x on boolean | number | null, and e && s ? 1 : 0. Under the current release it doesn't compile.
  • docs/bugs/ntg_and_nullable_class.ts now prints 7, done.
  • Full ctest: 3919/3919 pass.

🤖 Generated with Claude Code

`if (x)` with `x: boolean | string | null` failed to compile ("can't
instantiate function. '___cast' not all generic types can be
identified"), and so did `if (enabled && handle)` for a boolean and a
`Handle | null`, whose `&&` makes such a union. The generated union cast
answered `return t` for the null member, which strict null mode refuses
to cast to boolean. Cast to boolean, a null or undefined member is now
false, as castFromAny already does for an `any`.

A null `Handle` carries the class's tag in that union, and the class
member's `instanceof` test read its vtable through the null pointer; for a
truth test of a union with one class, the class member is now just tested
for null.

Reported in docs/bugs/native-ts-graphics-findings.md (issue 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 4e57a5a into main Oct 11, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-union-null-truthiness branch October 11, 2026 12:47
ASDAlexander77 added a commit that referenced this pull request Oct 11, 2026
…547)

* A null class, string or array widened to a union becomes its null member

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 union of pointer types stores null in its pointer, not as a member

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>

* CastLogicHelper: drop isPointerNullableType again, the merge brought it back

MLIRTypeHelper::isNullAbsorbingType replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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