From f302fc89d75a3f7c208e7fa464844417433eb7a6 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 27 Sep 2026 22:16:11 +0100 Subject: [PATCH] Comparing a union's field with null or undefined no longer crashes The discriminated-union narrowing in checkSafeCastPropertyAccessLogic treated every const value as a ConstantOp. isConstValue is also true for null and undefined, so `s.r !== null` on a union-typed `s` asserted in debug builds and dereferenced a null op in release. null and undefined cannot be discriminant literals, so the narrowing now skips them. A field missing from one member of the union no longer indexes the tuple with -1 or dereferences a null FieldInfo. Found in the BrowserLib sources attached to #231. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 16 ++++-- tslang/test/tester/CMakeLists.txt | 3 ++ .../tester/tests/00safe_cast_null_field.ts | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tslang/test/tester/tests/00safe_cast_null_field.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 139d174a8..9785ef562 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3619,10 +3619,10 @@ class MLIRGenImpl auto isConst = false; mlir::Attribute value; isConst = isConstValue(constVal); - if (isConst) + // null and undefined are const values too, but not literals a discriminant field can hold + auto constantOp = isConst ? constVal.getDefiningOp() : mlir_ts::ConstantOp(); + if (constantOp) { - auto constantOp = constVal.getDefiningOp(); - assert(constantOp); auto valueAttr = constantOp.getValueAttr(); MLIRCodeLogic mcl(builder, compileOptions); @@ -3633,6 +3633,11 @@ class MLIRGenImpl if (auto tupleType = dyn_cast(unionSubType)) { auto fieldIndex = tupleType.getIndex(fieldNameAttr); + if (fieldIndex < 0) + { + continue; + } + auto fieldType = tupleType.getType(fieldIndex); if (auto literalType = dyn_cast(fieldType)) { @@ -3657,6 +3662,11 @@ class MLIRGenImpl if (auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue())) { auto fieldInfo = interfaceInfo->findField(fieldNameAttr); + if (!fieldInfo) + { + continue; + } + if (auto literalType = dyn_cast(fieldInfo->type)) { if (literalType.getValue() == valueAttr) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a90056cb0..9b9f4a18a 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -528,6 +528,7 @@ tslang_add_test(NAME test-compile-00-safe-cast-typeof COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-safe-cast-while COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_while.ts") tslang_add_test(NAME test-compile-01-safe-cast-while COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01safe_cast_while.ts") tslang_add_test(NAME test-compile-00-safe-cast-field-access COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_field_access.ts") +tslang_add_test(NAME test-compile-00-safe-cast-null-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_null_field.ts") tslang_add_test(NAME test-compile-00-safe-cast-bug COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_bug.ts") tslang_add_test(NAME test-compile-00-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional.ts") tslang_add_test(NAME test-compile-01-optional COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01optional.ts") @@ -968,6 +969,7 @@ tslang_add_test(NAME test-jit-00-safe-cast-typeof COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-safe-cast-while COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_while.ts") tslang_add_test(NAME test-jit-01-safe-cast-while COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01safe_cast_while.ts") tslang_add_test(NAME test-jit-00-safe-cast-field-access COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_field_access.ts") +tslang_add_test(NAME test-jit-00-safe-cast-null-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_null_field.ts") tslang_add_test(NAME test-jit-00-safe-cast-bug COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00safe_cast_bug.ts") tslang_add_test(NAME test-jit-00-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional.ts") tslang_add_test(NAME test-jit-01-optional COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01optional.ts") @@ -1749,6 +1751,7 @@ set(TSLANG_CORPUS 00reference_ref_deref.ts 00safe_cast_bug.ts 00safe_cast_field_access.ts + 00safe_cast_null_field.ts 00safe_cast_typeof.ts 00safe_cast_while.ts 00safe_cast.ts diff --git a/tslang/test/tester/tests/00safe_cast_null_field.ts b/tslang/test/tester/tests/00safe_cast_null_field.ts new file mode 100644 index 000000000..7d3858a15 --- /dev/null +++ b/tslang/test/tester/tests/00safe_cast_null_field.ts @@ -0,0 +1,51 @@ +// A field of a union-typed object compared with null or undefined. The discriminated-union +// narrowing took null and undefined for literal constants and crashed the compiler (#231). + +interface Circle { + kind: "circle"; + r: number | null; +} + +interface Square { + kind: "square"; + r: number | null; +} + +type Shape = Circle | Square; + +function hasR(s: Shape) { + if (s.r !== null) { + return true; + } + + return false; +} + +function hasRUndef(s: Shape) { + if (s.r === undefined) { + return false; + } + + return true; +} + +function kind(s: Shape) { + // a real literal must still narrow + if (s.kind === "circle") { + return 1; + } + + return 2; +} + +function main() { + const c: Circle = { kind: "circle", r: 1 }; + const q: Square = { kind: "square", r: null }; + assert(hasR(c)); + assert(!hasR(q)); + assert(hasRUndef(c)); + assert(kind(c) == 1); + assert(kind(q) == 2); + + print("done."); +}