From d97e8d7fbc68155be8c3b5e4af6a900458130037 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 5 Oct 2026 15:53:53 +0100 Subject: [PATCH 1/2] An empty string is falsy `""` was true in every boolean test of a string: `if`, `while`, `!`, `?:`, `&&` and `||`, for a plain, optional, nullable or `any` string, under every memory model (#509). The string -> boolean cast had no case of its own and fell to the generic pointer -> boolean one, which only compares with null. castStringToBool reads the first character and is true when it is not the terminator. A null string reads the terminator of a static "", so the test needs no branch and is safe in any region. An optional string selects null when it has no value before reading, as the value of an undefined optional is no pointer at all. 00string_empty_falsy.ts: each of those forms with "" and with a non-empty string, a module-level `"" ? 1 : 2`, and empty strings made at run time (concatenation, template). On main the first assertion fails. Closes #509 Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/CastLogicHelper.h | 34 +++++++- tslang/test/tester/CMakeLists.txt | 3 + .../test/tester/tests/00string_empty_falsy.ts | 85 +++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00string_empty_falsy.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index 7fa678c56..8edb85217 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -407,6 +407,11 @@ class CastLogicHelper if (auto boolType = dyn_cast(resType)) { + if (isa(inType)) + { + return castStringToBool(in); + } + if (auto tupleType = dyn_cast(inType)) { auto llvmBoolType = tch.convertType(boolType); @@ -456,8 +461,19 @@ class CastLogicHelper { // TODO: use cond switch auto hasValue = rewriter.create(loc, boolType, in); - auto val = rewriter.create(loc, optType.getElementType(), in); + mlir::Value val = rewriter.create(loc, optType.getElementType(), in); auto llvmBoolType = tch.convertType(boolType); + if (isa(optType.getElementType())) + { + // a string is tested by reading its first character, and the value of an undefined + // optional is no pointer at all: test null instead + auto llvmPtrType = tch.convertType(optType.getElementType()); + mlir::Value hasValueAsLLVMType = rewriter.create(loc, llvmBoolType, hasValue); + mlir::Value valAsLLVMType = rewriter.create(loc, llvmPtrType, val); + mlir::Value nullPtr = rewriter.create(loc, llvmPtrType); + return castStringToBool(rewriter.create(loc, hasValueAsLLVMType, valAsLLVMType, nullPtr)); + } + auto valAsBool = cast(val, val.getType(), tch.convertType(val.getType()), boolType, llvmBoolType); if (valAsBool) { @@ -1001,6 +1017,22 @@ class CastLogicHelper ch.getOrCreateGlobalString("__false__", std::string("false"))); } + // `""` is falsy: a string is true when it is not null and its first character is not the terminator. + // A null string reads the terminator of a static "" instead, so there is no branch to make. + mlir::Value castStringToBool(mlir::Value in) + { + auto llvmPtrType = tch.convertType(mlir_ts::StringType::get(rewriter.getContext())); + mlir::Value ptr = in.getType() == llvmPtrType ? in : rewriter.create(loc, llvmPtrType, in); + + mlir::Value nullPtr = rewriter.create(loc, llvmPtrType); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, ptr, nullPtr); + auto readPtr = rewriter.create(loc, isNull, ch.getOrCreateGlobalString(std::string()), ptr); + + auto i8Type = rewriter.getIntegerType(8); + auto firstChar = rewriter.create(loc, i8Type, readPtr); + return rewriter.create(loc, LLVM::ICmpPredicate::ne, firstChar, clh.createIConstantOf(8, 0)); + } + mlir::Value castBoolToNumber(mlir::Value in) { mlir::Value valueAsLLVMType = rewriter.create(loc, tch.convertType(in.getType()), in); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 640c95710..3d3a25caf 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -356,6 +356,7 @@ tslang_add_test(NAME test-compile-00-await-void COMMAND test-runner "${PROJECT_S tslang_add_test(NAME test-compile-00-for-await-sequential COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_sequential.ts") tslang_add_test(NAME test-compile-00-for-await-control COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-compile-00-for-optional-class-condition COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") +tslang_add_test(NAME test-compile-00-string-empty-falsy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -869,6 +870,7 @@ tslang_add_test(NAME test-jit-00-await-void COMMAND test-runner -jit "${PROJECT_ tslang_add_test(NAME test-jit-00-for-await-sequential COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_sequential.ts") tslang_add_test(NAME test-jit-00-for-await-control COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-jit-00-for-optional-class-condition COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") +tslang_add_test(NAME test-jit-00-string-empty-falsy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -1711,6 +1713,7 @@ set(TSLANG_CORPUS 00for_await_sequential.ts 00for_await_control.ts 00for_optional_class_condition.ts + 00string_empty_falsy.ts 00const_record_owned_fields.ts 00array_length_valid.ts 00string_view_copy.ts diff --git a/tslang/test/tester/tests/00string_empty_falsy.ts b/tslang/test/tester/tests/00string_empty_falsy.ts new file mode 100644 index 000000000..5b95f1b14 --- /dev/null +++ b/tslang/test/tester/tests/00string_empty_falsy.ts @@ -0,0 +1,85 @@ +// an empty string is falsy (#509): a string tested as a boolean was a non-null pointer test, so "" was true +// in `if`, `while`, `!`, `?:`, `&&` and `||`, plain, optional, nullable and as `any` +let globalEmpty = ""; +const globalPick = "" ? 1 : 2; + +function truthy(s: string) { + return s ? true : false; +} + +function truthyIf(s: string) { + if (s) { + return true; + } + + return false; +} + +function truthyOptional(s: string | undefined) { + if (s) { + return true; + } + + return false; +} + +function truthyNullable(s: string | null) { + return !!s; +} + +function truthyAny(s: any) { + return !!s; +} + +function countWhile(s: string, steps: number) { + let count = 0; + while (s) { + count++; + s = count < steps ? s + "-" : ""; + } + + return count; +} + +function main() { + let e = ""; + let a = "a"; + + assert(!truthy(e), "ternary on an empty string"); + assert(truthy(a), "ternary on a string"); + assert(!truthyIf(e), "if on an empty string"); + assert(truthyIf(a), "if on a string"); + assert(!e, "! of an empty string"); + assert(!!a, "!! of a string"); + if ("") { + assert(false, "if on an empty literal"); + } + + assert((e || "x") == "x", "empty || x"); + assert((a || "x") == "a", "a || x"); + assert((e && "x") == "", "empty && x"); + assert((a && "x") == "x", "a && x"); + assert((e ?? "x") == "", "empty ?? x keeps the empty string"); + + assert(countWhile("abc", 3) == 3, "while until the string is empty"); + assert(countWhile(e, 3) == 0, "while on an empty string"); + + assert(!truthyOptional(e), "optional holding an empty string"); + assert(truthyOptional(a), "optional holding a string"); + assert(!truthyOptional(undefined), "undefined optional"); + + assert(!truthyNullable(e), "nullable holding an empty string"); + assert(truthyNullable(a), "nullable holding a string"); + assert(!truthyNullable(null), "null"); + + assert(!truthyAny(e), "any holding an empty string"); + assert(truthyAny(a), "any holding a string"); + + assert(!globalEmpty, "global empty string"); + assert(globalPick == 2, "module-level ternary on an empty literal"); + + assert(!(e + e), "empty concatenation"); + assert(!`${e}`, "empty template"); + + print("done."); +} From de74ce2b62f038b4d187295951eddcef7b139263 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 5 Oct 2026 16:00:10 +0100 Subject: [PATCH 2/2] Turn on the "" truthiness cases left as TODO in two tests 28boolcasts.ts had `if ("") { boolDie() }` commented out ("TODO: finish it"), and 29lazyreferences.ts had `"" || ...` and `"" && ...` commented out with "\"\" should be false for boolean". Both are right now (#509); on main each fails its assertion. Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/tests/28boolcasts.ts | 5 +---- tslang/test/tester/tests/29lazyreferences.ts | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tslang/test/tester/tests/28boolcasts.ts b/tslang/test/tester/tests/28boolcasts.ts index 71c35aed1..6b6418eae 100644 --- a/tslang/test/tester/tests/28boolcasts.ts +++ b/tslang/test/tester/tests/28boolcasts.ts @@ -12,12 +12,9 @@ function testBoolCasts() { boolDie(); } - // TODO: finish it - /* if ("") { - boolDie() + boolDie(); } -*/ let v = new BazClass(); if (v) { diff --git a/tslang/test/tester/tests/29lazyreferences.ts b/tslang/test/tester/tests/29lazyreferences.ts index 599e369f1..bcd665784 100644 --- a/tslang/test/tester/tests/29lazyreferences.ts +++ b/tslang/test/tester/tests/29lazyreferences.ts @@ -9,9 +9,7 @@ class Testrec { function testLazyRef() { print("testLazyRef") let x = ("x" + "Y") || "foo" - // "" should be false for boolean - //let y = "" || "bXr" + "2" - let y = "bXr" + "2" + let y = "" || "bXr" + "2" assert(x.length == 2, "two") assert(y.length == 4, "emp") @@ -21,9 +19,7 @@ function testLazyRef() { x = "x" + "12x" && "7" + "xx" assert(x.length == 3, "and") - // "" should be false for boolean - //x = "" && "blah" - x = "" + x = "" && "blah" assert(x == "", "andemp") x = "foo" && "x" + "Y" assert(x.length == 2, "twoand")