From 40f664cf5befc3e3619873208a34bb407114d855 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 28 Sep 2026 11:52:06 +0100 Subject: [PATCH 1/3] An assert message can be known only at run time `assert(ok, what + ": failed")` failed to compile with "operation's operand is unlinked": mlirGenAssert erased the op behind every message argument, as if each were a constant string, while other ops still used it. A constant string is still kept as the op's text; anything else is now a string operand of ts.Assert, shown by _assert/__assert_fail when the assertion fails (a null string shows the constant text). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/LowerToLLVM/AssertLogic.h | 28 ++++++++++---- .../TypeScript/MLIRLogic/MLIRCodeLogic.h | 38 +++++++++++-------- tslang/include/TypeScript/TypeScriptOps.td | 6 +-- tslang/lib/TypeScript/LowerToLLVM.cpp | 2 +- tslang/test/tester/CMakeLists.txt | 11 ++++++ tslang/test/tester/assert-message/failing.ts | 5 +++ tslang/test/tester/tests/00assert_message.ts | 19 ++++++++++ 7 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 tslang/test/tester/assert-message/failing.ts create mode 100644 tslang/test/tester/tests/00assert_message.ts diff --git a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h index 8e9963f52..e3fdd87c7 100644 --- a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h @@ -44,16 +44,30 @@ 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) + // 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 +109,7 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); - rewriter.create(loc, assertFuncOp, ValueRange{msgCst, fileCst, lineNumberRes}); + rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); // rewriter.create(loc); rewriter.create(loc, unreachable); @@ -106,7 +120,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 +163,7 @@ 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}); + 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..6c2188753 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,14 @@ 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. +add_test(NAME test-jit-assert-message-failing + COMMAND $ --emit=jit --no-default-lib + "${PROJECT_SOURCE_DIR}/test/tester/assert-message/failing.ts") +set_tests_properties(test-jit-assert-message-failing + PROPERTIES PASS_REGULAR_EXPRESSION "assertion failed: n is 7" + FAIL_REGULAR_EXPRESSION "Stack dump|unlinked") + # 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..a4a24009a --- /dev/null +++ b/tslang/test/tester/assert-message/failing.ts @@ -0,0 +1,5 @@ +// A failing assert shows its message as it is at run time. +function main() { + 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."); +} From ffc75e4ed82849d8fe468619b1efa97c2620ad5d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 28 Sep 2026 11:55:26 +0100 Subject: [PATCH 2/3] Output printed before a failing assert is not lost _assert and __assert_fail abort, which flushes no stream, and the JIT's _assert handler ends with _exit: whenever stdout was not a console - a pipe, a file, the test runner - everything the program printed before the failure was lost. The lowered assert now calls fflush(NULL) first, the JIT handler flushes stdout, and the JIT binds fflush to its own CRT like puts. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/LowerToLLVM/AssertLogic.h | 11 +++++++++++ tslang/test/tester/CMakeLists.txt | 4 ++-- tslang/test/tester/assert-message/failing.ts | 4 +++- tslang/tslang/jit.cpp | 5 +++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h index e3fdd87c7..237b74810 100644 --- a/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/AssertLogic.h @@ -54,6 +54,15 @@ class AssertLogic #endif } + // `_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) { @@ -109,6 +118,7 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); + flushOutput(); rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes}); // rewriter.create(loc); rewriter.create(loc, unreachable); @@ -163,6 +173,7 @@ class AssertLogic mlir::Value lineNumberRes = rewriter.create(loc, rewriter.getI32Type(), rewriter.getI32IntegerAttr(line)); mlir::Value funcName = rewriter.create(loc, i8PtrTy); + flushOutput(); rewriter.create(loc, assertFuncOp, ValueRange{messageOrConstant(message, msgCst), fileCst, lineNumberRes, funcName}); // rewriter.create(loc); rewriter.create(loc, unreachable); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6c2188753..2f40e4b09 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2538,12 +2538,12 @@ 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. +# A failing assert shows a message known only at run time, after the output printed before it. add_test(NAME test-jit-assert-message-failing COMMAND $ --emit=jit --no-default-lib "${PROJECT_SOURCE_DIR}/test/tester/assert-message/failing.ts") set_tests_properties(test-jit-assert-message-failing - PROPERTIES PASS_REGULAR_EXPRESSION "assertion failed: n is 7" + PROPERTIES PASS_REGULAR_EXPRESSION "printed before.*assertion failed: n is 7" FAIL_REGULAR_EXPRESSION "Stack dump|unlinked") # The shared-component tier under the other two models. A shared library records the model diff --git a/tslang/test/tester/assert-message/failing.ts b/tslang/test/tester/assert-message/failing.ts index a4a24009a..5ea537deb 100644 --- a/tslang/test/tester/assert-message/failing.ts +++ b/tslang/test/tester/assert-message/failing.ts @@ -1,5 +1,7 @@ -// A failing assert shows its message as it is at run time. +// 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/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); From 4b756133dd32e68adc0a4979d22743dea795ca19 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 28 Sep 2026 15:49:33 +0100 Subject: [PATCH 3/3] The failing-assert JIT test loads the runtime library; Windows only Run on its own, tslang --emit=jit finds the GC only through the GC_LIB_PATH/TSLANG_LIB_PATH environment, which CI does not set, so the test failed with "Symbols not found: GC_init, GC_malloc". It now passes --shared-libs=TypeScriptRuntime, as the other direct JIT tests do. It is Windows only, like the uncaught-exception JIT tests: on Linux the message and the earlier output are printed, but abort() then reaches LLVM's crash handler ("Stack dump"), and ctest fails the signal. Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/CMakeLists.txt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2f40e4b09..caa56ee1c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2539,12 +2539,17 @@ set_tests_properties(test-compile-specialization-cast-pair-error FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") # A failing assert shows a message known only at run time, after the output printed before it. -add_test(NAME test-jit-assert-message-failing - COMMAND $ --emit=jit --no-default-lib - "${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") +# 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