From 568ea29ec1b55f3840732ad22ae245403f724459 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 3 Oct 2026 21:04:05 +0100 Subject: [PATCH] A global initialised with a constant string concatenation runs as a constructor (#469) `let S = "text" + 4;` at module level, read by a function, failed to compile in every model: "ops with side effects not allowed in global initializers". GlobalOpLowering puts an initializer that looks constant into the llvm.mlir.global's own region and anything that runs code into a per-global constructor. Its list knew ts.StringConcat, but at that point the concatenation is still a ts.ArithmeticBinary with a string result, and the number's conversion (`ts.Cast` si32 -> string, a sprintf into a new block) was not on the list either, so both landed in the region, where allocating is not allowed. With a variable or a call among the operands it was already a constructor, and a global nothing reads was dropped before it mattered. Now a string-typed ts.ArithmeticBinary, and a cast to string from anything that is not already a string, make the global a constructor. A plain string constant stays in place. Test: 00global_constant_concat (compile and JIT, and in the rc/none corpus): `"text" + 4`, `"text" + "four"`, a `string | null` and `"" + 6.5` globals read in main, a plain string constant, and a reassignment. It failed to compile before the change. Fixes #469. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 18 +++++++++++++++ tslang/test/tester/CMakeLists.txt | 3 +++ .../tester/tests/00global_constant_concat.ts | 22 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tslang/test/tester/tests/00global_constant_concat.ts diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index a01c87f55..43f4f4f25 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4418,13 +4418,31 @@ struct GlobalOpLowering : public TsLlvmPattern { createAsGlobalConstructor = true; } + else if (auto binaryOp = dyn_cast(op); + binaryOp && isa(binaryOp.getResult().getType())) + { + // `"text" + 4` is a string concatenation, which allocates, even when every operand + // is a constant (#469) + createAsGlobalConstructor = true; + } else if (auto castOp = dyn_cast(op)) { auto castType = castOp.getRes().getType(); + auto inType = castOp.getIn().getType(); + if (auto literalType = dyn_cast(inType)) + { + inType = literalType.getElementType(); + } + if (isa(castType) || isa(castType)) { createAsGlobalConstructor = true; } + else if (isa(castType) && !isa(inType)) + { + // a number or a boolean made into a string allocates it (#469) + createAsGlobalConstructor = true; + } else { // see CreateUnionInstanceOp above: a cast to or from a tagged union lowers to diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 0f53f727a..bf29b37f8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -467,6 +467,7 @@ tslang_add_test(NAME test-compile-00-void COMMAND test-runner "${PROJECT_SOURCE_ tslang_add_test(NAME test-compile-00-in COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") tslang_add_test(NAME test-compile-00-in-method-names COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") tslang_add_test(NAME test-compile-00-class-structural-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") +tslang_add_test(NAME test-compile-00-global-constant-concat COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_constant_concat.ts") tslang_add_test(NAME test-compile-00-global-object-literal COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_object_literal.ts") tslang_add_test(NAME test-compile-00-instanceof COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") tslang_add_test(NAME test-compile-00-const-of-let COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_of_let.ts") @@ -955,6 +956,7 @@ tslang_add_test(NAME test-jit-00-void COMMAND test-runner -jit "${PROJECT_SOURCE tslang_add_test(NAME test-jit-00-in COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in.ts") tslang_add_test(NAME test-jit-00-in-method-names COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00in_method_names.ts") tslang_add_test(NAME test-jit-00-class-structural-extends COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_structural_extends.ts") +tslang_add_test(NAME test-jit-00-global-constant-concat COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_constant_concat.ts") tslang_add_test(NAME test-jit-00-global-object-literal COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_object_literal.ts") tslang_add_test(NAME test-jit-00-instanceof COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00instanceof.ts") tslang_add_test(NAME test-jit-00-const-of-let COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_of_let.ts") @@ -1777,6 +1779,7 @@ set(TSLANG_CORPUS 00infer.ts 00global_union_from_call.ts 00instanceof.ts + 00global_constant_concat.ts 00const_of_let.ts 00instanceof_namespace_narrowing.ts 00instanceof_union.ts diff --git a/tslang/test/tester/tests/00global_constant_concat.ts b/tslang/test/tester/tests/00global_constant_concat.ts new file mode 100644 index 000000000..b750862a4 --- /dev/null +++ b/tslang/test/tester/tests/00global_constant_concat.ts @@ -0,0 +1,22 @@ +// A module-level variable initialised with a string concatenation of constants, read by a function +// (#469). The initializer looked constant, so it was placed where nothing may run, and the +// concatenation (and the number's conversion to a string) failed to compile there: "ops with side +// effects not allowed in global initializers". +let S = "text" + 4; +const C = "text" + "four"; +let U: string | null = "nullable" + 5; +let N = "" + 6.5; +let P = "plain"; + +function main() { + assert(S == "text4", "a let from a constant concatenation"); + assert(C == "textfour", "a const from two string constants"); + assert(U == "nullable5", "a nullable string from a constant concatenation"); + assert(N == "6.5", "a number converted to a string"); + assert(P == "plain", "a plain string constant stays as it is"); + + S = S + "!"; + assert(S == "text4!"); + + print("done."); +}