Skip to content

Skip statically dead typeof branches at MLIR generation - #324

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fold-static-typeof
Sep 14, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fold-static-typeof

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

In a generic specialised for a concrete type, a typeof check whose answer is known still generated its branch. When that branch uses the value as the narrowed type, it emits a cast the value cannot take:

class Array1 {
    static [Symbol.hasInstance](instance) {
        if (typeof instance === "string")
            print("str: ", instance);   // specialised for array<any>: array -> string cast
        return false;
    }
}
print([] instanceof Array1);

In debug builds the cast reached llvm_unreachable("review usage") at CastLogicHelper.h:815; in release builds that is undefined behaviour and the test only passes because the branch never runs. This is why 01symbol and 00funcs_generic_with_typeof fail on a local debug build but never on CI (all workflows build Release).

Two separate causes:

  1. typeof x === "name" was not folded. For a statically typed x, typeof emits a TypeDescriptor, whose run-time name (typeOfAsString of its type) is compared with the literal by content, so the result is known during MLIR generation.
  2. if added narrowing before deciding to skip a branch. Even with a constant condition, checkSafeCast emitted the narrowing cast into the skipped branch. Unused, it was removed as dead code, but under --di (which the test runner always passes) the narrowed variable's ts.DebugVariable kept it alive into LLVM lowering.

Fix

  • foldStaticTypeOfCompare (MLIRGenImpl.h): ==, ===, != and !== between a TypeDescriptor and a string literal, in either order, produce a boolean literal, which if already uses to skip a branch. any, union tags and stored typeof values keep their run-time check. Names are compared exactly as at run time (e.g. an integer literal is still "s32"), so no existing result changes.
  • mlirGen(IfStatement): narrowing is added only to a branch that is generated, for both then and else.

Not covered: ?:, &&/|| and while also narrow but always generate both branches, so a typeof-guarded ?: in an array-specialised generic can still hit the same cast. That was already the case before this change.

Tests

  • New 00typeof_static_fold.ts (compile, jit, rc/none corpus): a generic describe<T> called with a string, an empty array, a string array, a number and a boolean, plus typeof on an any. It crashes the compiler without the fix.
  • 01symbol and 00funcs_generic_with_typeof produce the same output as the release build.
  • Full Windows debug suite: 2722/2727. The 5 remaining failures are pre-existing and unrelated (00owned_array_ops under rc, gc-shared-auto, gc-defaultlib-collector); 12 tests that failed before now pass.

🤖 Generated with Claude Code

`typeof x === "name"` on a value whose type is known at compile time emits a
TypeDescriptor, whose run-time name (typeOfAsString of its type) is compared
with the literal by content, so the result is known while generating MLIR. It
was not folded, so a branch that can never run was generated anyway. In a
generic specialised for an array, such a branch can hold casts an array cannot
take: `print("s: ", x)` under `typeof x === "string"` emitted an array-to-string
cast that reached llvm_unreachable at CastLogicHelper.h:815 in debug builds
(01symbol, 00funcs_generic_with_typeof) and undefined behaviour in release.

Two changes:

- The ==, ===, != and !== comparison between a TypeDescriptor and a string
  literal (either order) now produces a boolean literal, which `if` already
  uses to skip a branch. `any`, union tags and stored typeof values keep their
  run-time check; names compare exactly as at run time, so no result changes.

- `if` added its narrowing (safe-cast) before deciding whether to generate a
  branch, so a skipped branch still got a cast of the tested value. Unused, it
  was removed as dead code, but under --di the narrowed variable's debug
  record kept it alive into LLVM lowering. Narrowing is now added only to a
  branch that is generated, for both `then` and `else`.

Adds 00typeof_static_fold.ts (compile, jit, rc/none corpus).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 9e54b86 into main Sep 14, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fold-static-typeof branch September 14, 2026 20:12
ASDAlexander77 added a commit that referenced this pull request Sep 14, 2026
#324 stopped `if` from generating the branch of a `typeof x === "name"` test
that folds to a constant. `?:`, `&&` and `while` still generated it, with its
narrowing: in a generic specialised for an array, `typeof x === "string" ?
x.length : -1` cast the array to string, and under --di the narrowed
variable's debug record kept that cast alive until LLVM lowering, which hit
llvm_unreachable in CastLogicHelper.

- getStaticBoolean() recognises a constant condition, also through casts to
  boolean (a folded typeof, or an `&&` whose left side folded to false);
  `if` uses it too.
- ?: and `false && right` evaluate the dead branch, narrowing included, in
  the temporary module to keep its type, and yield an undefined value of
  that type; the expression's type is unchanged. A boolean `false && right`
  is just the constant condition, so an enclosing if/while sees it too.
- `while` with a false constant condition does not generate its body.

`||` does not narrow and was not affected.

Co-authored-by: Claude Opus 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