diff --git a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h index 8e9963f52..237b74810 100644 --- a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h @@ -44,16 +44,39 @@ class AssertLogic typeOfValueType = th.getPtrType(); } - mlir::LogicalResult logic(mlir::Value condValue, std::string msg) + // `message`, when given, is the text known only at run time and is shown instead of `msg` + mlir::LogicalResult logic(mlir::Value condValue, std::string msg, mlir::Value message = mlir::Value()) { #ifdef WIN32 - return logicWin32(condValue, msg); + return logicWin32(condValue, msg, message); #else - return logicUnix(condValue, msg); + return logicUnix(condValue, msg, message); #endif } - mlir::LogicalResult logicWin32(mlir::Value condValue, std::string msg) + // `_assert` and `__assert_fail` abort, which flushes no stream: what the program printed before + // the failure was lost whenever stdout was not a console (a pipe, a file, the test runner) + void flushOutput() + { + auto fflushFuncOp = ch.getOrInsertFunction("fflush", th.getFunctionType(rewriter.getI32Type(), {th.getPtrType()})); + mlir::Value allStreams = rewriter.create(loc, th.getPtrType()); + rewriter.create(loc, fflushFuncOp, ValueRange{allStreams}); + } + + // a null string shows the constant message instead + mlir::Value messageOrConstant(mlir::Value message, mlir::Value msgCst) + { + if (!message) + { + return msgCst; + } + + auto nullPtr = rewriter.create(loc, message.getType()); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, message, nullPtr); + return rewriter.create(loc, isNull, msgCst, message); + } + + mlir::LogicalResult logicWin32(mlir::Value condValue, std::string msg, mlir::Value message) { auto unreachable = clh.FindUnreachableBlockOrCreate(); @@ -95,7 +118,8 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); - rewriter.create(loc, assertFuncOp, ValueRange{msgCst, fileCst, lineNumberRes}); + flushOutput(); + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); // rewriter.create(loc); rewriter.create(loc, unreachable); @@ -106,7 +130,7 @@ class AssertLogic return success(); } - mlir::LogicalResult logicUnix(mlir::Value condValue, std::string msg) + mlir::LogicalResult logicUnix(mlir::Value condValue, std::string msg, mlir::Value message) { auto unreachable = clh.FindUnreachableBlockOrCreate(); @@ -149,7 +173,8 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); mlir::Value funcName = rewriter.create(loc, i8PtrTy); - rewriter.create(loc, assertFuncOp, ValueRange{msgCst, fileCst, lineNumberRes, funcName}); + flushOutput(); + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes, funcName}); // rewriter.create(loc); rewriter.create(loc, unreachable); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRCodeLogic.h b/tslang/include/TypeScript/MLIRLogic/MLIRCodeLogic.h index 9362dfaac..4e68b5d88 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRCodeLogic.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRCodeLogic.h @@ -580,28 +580,36 @@ class MLIRCustomMethods return mlir::failure(); } + // The message: a constant string is kept as text, anything else - `what + " failed"` - is + // shown as it is at run time. Such a message used to be erased as if it were a constant, + // with its other uses still there ("operation's operand is unlinked"). auto msg = StringRef("assert"); + mlir::Value message; if (operands.size() > 1) { - for (auto opIndex = 1; opIndex < operands.size(); opIndex++) + auto param2 = operands[1]; + auto constantOp = param2.getDefiningOp(); + auto type = constantOp ? constantOp.getType() : mlir::Type(); + if (auto literalType = dyn_cast_or_null(type)) { - auto param2 = operands[opIndex]; - auto constantOp = dyn_cast(param2.getDefiningOp()); - if (constantOp) - { - auto type = constantOp.getType(); - if (auto literalType = dyn_cast(type)) - { - type = literalType.getElementType(); - } + type = literalType.getElementType(); + } - if (isa(type)) + if (constantOp && isa(type)) + { + msg = cast(constantOp.getValue()).getValue(); + } + else + { + message = param2; + if (!isa(message.getType())) + { + message = castFn(location, mlir_ts::StringType::get(builder.getContext()), message, genContext, false); + if (!message) { - msg = cast(constantOp.getValue()).getValue(); + return mlir::failure(); } } - - param2.getDefiningOp()->erase(); } } @@ -612,7 +620,7 @@ class MLIRCustomMethods } auto assertOp = - builder.create(location, op, mlir::StringAttr::get(builder.getContext(), msg)); + builder.create(location, op, message, mlir::StringAttr::get(builder.getContext(), msg)); return mlir::success(); } diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 1133f6c04..1da2e29f2 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -424,9 +424,9 @@ def TypeScript_AssertOp : TypeScript_Op<"Assert"> { ``` }]; - let arguments = (ins TypeScript_Boolean:$arg, StrAttr:$msg); - - + // `message`, when present, is a message known only at run time (`assert(ok, what + " failed")`) + // and is shown instead of `msg` + let arguments = (ins TypeScript_Boolean:$arg, Optional:$message, StrAttr:$msg); let hasCanonicalizer = 1; } diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 89bd928e7..f43c85003 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -135,7 +135,7 @@ class AssertOpLowering : public TsLlvmPattern { TypeConverterHelper tch(getTypeConverter()); AssertLogic al(op, rewriter, tch, op->getLoc(), tsLlvmContext->compileOptions); - return al.logic(transformed.getArg(), op.getMsg().str()); + return al.logic(transformed.getArg(), op.getMsg().str(), transformed.getMessage()); } }; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index dabe5728a..caa56ee1c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -277,6 +277,7 @@ tslang_add_test(NAME test-compile-00-funcs-vararg COMMAND test-runner "${PROJECT tslang_add_test(NAME test-compile-00-declare-function-twice COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00declare_function_twice.ts") tslang_add_test(NAME test-compile-00-function-overloads COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00function_overloads.ts") tslang_add_test(NAME test-compile-00-gc-allocations-distinct COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00gc_allocations_distinct.ts") +tslang_add_test(NAME test-compile-00-assert-message COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00assert_message.ts") tslang_add_test(NAME test-compile-01-funcs-vararg COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01funcs_vararg.ts") # printf is avaliable in JIT only, C has snprintf #tslang_add_test(NAME test-compile-02-funcs-vararg COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/02funcs_vararg.ts") @@ -730,6 +731,7 @@ tslang_add_test(NAME test-jit-00-funcs-vararg COMMAND test-runner -jit "${PROJEC tslang_add_test(NAME test-jit-00-declare-function-twice COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00declare_function_twice.ts") tslang_add_test(NAME test-jit-00-function-overloads COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00function_overloads.ts") tslang_add_test(NAME test-jit-00-gc-allocations-distinct COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00gc_allocations_distinct.ts") +tslang_add_test(NAME test-jit-00-assert-message COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00assert_message.ts") tslang_add_test(NAME test-jit-01-funcs-vararg COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01funcs_vararg.ts") tslang_add_test(NAME test-jit-02-funcs-vararg COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/02funcs_vararg.ts") tslang_add_test(NAME test-jit-00-funcs-bindings COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_bindings.ts") @@ -1659,6 +1661,7 @@ set(TSLANG_CORPUS 00funcs_vararg.ts 00function_overloads.ts 00gc_allocations_distinct.ts + 00assert_message.ts 00funcs.ts 00generator_manual_next.ts 00generator_manual_next2.ts @@ -2535,6 +2538,19 @@ set_tests_properties(test-compile-specialization-cast-pair-error PROPERTIES PASS_REGULAR_EXPRESSION "is not assignable to type Pair" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +# A failing assert shows a message known only at run time, after the output printed before it. +# Windows only: that is where the JIT reports a failed assert; on Linux abort() reaches LLVM's +# crash handler. +if (WIN32) + add_test(NAME test-jit-assert-message-failing + COMMAND $ --emit=jit --no-default-lib + --shared-libs=$ + "${PROJECT_SOURCE_DIR}/test/tester/assert-message/failing.ts") + set_tests_properties(test-jit-assert-message-failing + PROPERTIES PASS_REGULAR_EXPRESSION "printed before.*assertion failed: n is 7" + FAIL_REGULAR_EXPRESSION "Stack dump|unlinked") +endif() + # The shared-component tier under the other two models. A shared library records the model # it was built under, so both halves of a pair are built with the same flag - which is what # these run. The file pairs are the default model's, verbatim. diff --git a/tslang/test/tester/assert-message/failing.ts b/tslang/test/tester/assert-message/failing.ts new file mode 100644 index 000000000..5ea537deb --- /dev/null +++ b/tslang/test/tester/assert-message/failing.ts @@ -0,0 +1,7 @@ +// A failing assert shows its message as it is at run time, after what the program printed +// before it: abort flushes nothing, and that output was lost whenever stdout was a pipe. +function main() { + print("printed before"); + const n = 7; + assert(n == 3, "n is " + n); +} diff --git a/tslang/test/tester/tests/00assert_message.ts b/tslang/test/tester/tests/00assert_message.ts new file mode 100644 index 000000000..58733a94a --- /dev/null +++ b/tslang/test/tester/tests/00assert_message.ts @@ -0,0 +1,19 @@ +// An assert message known only at run time used to fail to compile ("operation's operand is +// unlinked"): the message was erased as if it were a constant. +function check(ok: boolean, what: string) { + assert(ok, what + ": failed"); +} + +function main() { + check(true, "check"); + + const n = 3; + assert(n == 3, `n is ${n}`); + + let none: string | null = null; + assert(true, none); + + assert(n > 0, n); + + print("done."); +} diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 8b9c2bebf..4785307a8 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -312,6 +312,9 @@ static void *jitSearchForAddressOfSymbol(const char *symbolName) // instead. The lowering passes the source position along, so this says more than the box did. static void jitAssertFailed(const char *message, const char *file, unsigned line) { + // what the program printed before failing comes first, and _exit below flushes nothing + fflush(stdout); + if (file != nullptr && *file != '\0') { fprintf(stderr, "%s:%u: ", file, line); @@ -641,6 +644,7 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile addSym("calloc", (void*)&calloc); addSym("memset", (void*)&memset); addSym("memcpy", (void*)&memcpy); + addSym("fflush", (void*)&fflush); // see jitAssertFailed above: bound to ucrtbase this is a modal message box, which an // unattended run cannot answer addSym("_assert", (void*)&jitAssertFailed); @@ -874,6 +878,7 @@ int runJit(int argc, char **argv, mlir::ModuleOp module, CompileOptions &compile addOverride("calloc", (void *)&calloc); addOverride("memset", (void *)&memset); addOverride("memcpy", (void *)&memcpy); + addOverride("fflush", (void *)&fflush); // see jitAssertFailed above: bound to ucrtbase this is a modal message box, which an // unattended run cannot answer addOverride("_assert", (void *)&jitAssertFailed);