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."); +} 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")