Skip to content

An any compares with undefined, null and strings by what it holds - #417

Merged
ASDAlexander77 merged 1 commit into
mainfrom
any-equality-undefined
Sep 30, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
any-equality-undefined

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Summary

Comparing an any gave wrong answers in four ways, and if (a) on an any could throw. These are the two any bugs noted in #416, both of which turned out to be broader.

before after
const u: any = undefined; u == undefined false true
const n: any = null; n == null false true
let t: any = "text"; t == "text" false true
two anys holding equal text built at run time unequal equal
if (u) / !u with u: any = undefined threw "Can't cast from any type" false / true

Causes and fixes

  1. Compared with undefined, the answer was fixed at compile time. UndefLogicHelper returned == false and != true whatever the any held. It now hands an any to the run-time comparison (AnyCompareOp), as it already did for a class or an interface.

  2. The other operand wasn't boxed. LogicOp boxed the other operand only when its type differed from the any's. By then the operands are already lowered, and an any, a string and null are all just ptr, so no box was made:

    • the string itself went into the comparison as if it were a box;
    • its "tag" was read from whatever the string's bytes pointed at;
    • for null, the tag was read from address 8.

    The operand is now boxed by its TypeScript type, on the stack. The box only lives for the comparison. A heap box would never have been freed under rc: 96 MB for 2M comparisons at -O0. With the stack box it's 3.9 MB.

  3. undefined and null are answered before any box is opened. A null any pointer counts as undefined, which is also what a zeroed any slot is. Otherwise the tag's kind decides:

    • loosely (==, !=), undefined and null equal each other and nothing else;
    • strictly (===, !==), each equals only itself;
    • an ordering comparison stays false.
  4. Two string boxes compared their pointer bytes. Equal text built at run time came out unequal. They're compared with strcmp now.

Truthiness: ___unbox<boolean> threw for an any holding undefined or null, and it's what if (a) and !a use. It now returns false for both, as JavaScript does.

Tests

00any_equality_undefined.ts covers:

  • undefined and null under each operator;
  • truthiness;
  • a let that is reassigned;
  • equal text built at run time;
  • undefined against null;
  • a 100k-iteration comparison loop.

Without the change it fails in every model, compiled and under JIT.

Results

Release ctest 3311/3311
DefaultLib gc (compile + JIT) 158/158
DefaultLib rc (compile + JIT) 3 of 316 fail: the known sleep/weakref_basic link errors

Not in this PR

  • Under rc, a JIT run of anyValue == 7 fails with "Symbols not found: ??_7type_info@@6B@". The comparison unboxes through ___unbox<si32>, whose fallback throw needs MSVC's type_info vtable, and the rc JIT doesn't resolve it. This was already the case before this PR.

🤖 Generated with Claude Code

Four ways comparing an `any` gave the wrong answer:

- With undefined, UndefLogicHelper answered at compile time: `==` false,
  `!=` true, whatever the `any` held. It now hands an `any` to the run-time
  comparison (AnyCompareOp), as it already did for a class or an interface.
- LogicOp boxed the other operand only when its type differed from the
  `any`'s - but the operands are lowered already, and an `any`, a string
  and null are all a `ptr`. The string (or null) itself went in as the box,
  and its "tag" was read from whatever it pointed at (address 8 for null):
  `let t: any = "text"; t == "text"` was false. It is boxed now by its
  TypeScript type, on the stack - the box lives for the comparison only, and
  a heap one was never freed under reference counting (96 MB for 2M
  comparisons at -O0).
- AnyCompareOp answers undefined and null before opening any box: a null
  pointer is undefined (a zeroed `any` slot is one), otherwise the tag's
  kind says. Loosely undefined and null equal each other and nothing else;
  strictly each equals only itself; an ordering stays false.
- Two string boxes compared the bytes of their payload, a pointer: equal
  text built at run time was unequal. They compare with strcmp now.

And `if (a)` / `!a` of an `any` holding undefined or null threw "Can't cast
from any type" from the generated ___unbox<boolean>; it returns false for
both, as JavaScript does.

Test 00any_equality_undefined.ts. Without the change it fails in every
model, compile and JIT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit d8572ca into main Sep 30, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the any-equality-undefined branch September 30, 2026 16:09
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