Skip to content

Commit cd01720

Browse files
Emit mustprogress and add --fast-math for loop optimizations/vectorization (#198)
* Emit mustprogress on lowered functions to unlock loop optimizations Every ts function lowered to LLVM carried no function attributes at all, including mustprogress. Without it, LLVM's -O2/-O3 loop passes (LICM, unrolling, vectorization) must stay conservative since they can't assume a loop terminates or has an observable side effect. Verified with an isolated integer-bounded reduction loop that -O3 neither unrolled nor vectorized it despite a clean, SCEV-friendly shape; after this change the attribute is correctly emitted and the full 681-test suite (compile + JIT) still passes unchanged. Fast-math flags (needed for FP-reduction vectorization) are intentionally left out of this change since they'd change IEEE-754 semantics and should be an explicit opt-in, not bundled here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fast math attribute support --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b0a71a1 commit cd01720

9 files changed

Lines changed: 220 additions & 20 deletions

File tree

tslang/include/TypeScript/DataStructs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct CompileOptions
2626
std::string outputFolder;
2727
bool appendGCtorsToMethod;
2828
bool strictNullChecks;
29+
bool enableFastMath;
2930
};
3031

3132
#endif // TYPESCRIPT_DATASTRUCT_H_

tslang/lib/TypeScript/LowerToLLVM.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,9 @@ class IsNaNOpLowering : public TsLlvmPattern<mlir_ts::IsNaNOp>
337337

338338
TypeHelper th(rewriter.getContext());
339339

340-
// icmp
341-
auto cmpValue = rewriter.create<LLVM::FCmpOp>(loc, th.getLLVMBoolType(), LLVM::FCmpPredicate::one,
340+
// uno (unordered) self-compare is true iff the value is NaN; `one` here would
341+
// be constant false (ordered && x != x can never hold for a self-compare).
342+
auto cmpValue = rewriter.create<LLVM::FCmpOp>(loc, th.getLLVMBoolType(), LLVM::FCmpPredicate::uno,
342343
transformed.getArg(), transformed.getArg());
343344
rewriter.replaceOp(op, ValueRange{cmpValue});
344345
return success();
@@ -1132,6 +1133,14 @@ struct FuncOpLowering : public TsLlvmPattern<mlir_ts::FuncOp>
11321133

11331134
SmallVector<mlir::Attribute> funcAttrs;
11341135

1136+
// mustprogress licenses standard -O2/-O3 loop transforms (LICM, unrolling,
1137+
// vectorization of provably-finite loops) that otherwise stay conservative
1138+
// without proof the loop can't spin forever with no observable effect. Every
1139+
// ts loop with a call/store in its body already satisfies the (weaker)
1140+
// mustprogress contract; a body-less `while(true){}` is the only case this
1141+
// attribute changes the meaning of, same as in C/C++/Rust.
1142+
funcAttrs.push_back(ATTR("mustprogress"));
1143+
11351144
if (funcOp.getPersonality().has_value() && funcOp.getPersonality().value())
11361145
{
11371146
LLVMRTTIHelperVC rttih(funcOp, rewriter, typeConverter, tsLlvmContext->compileOptions);
@@ -3035,7 +3044,10 @@ struct LogicalBinaryOpLowering : public TsLlvmPattern<mlir_ts::LogicalBinaryOp>
30353044
break;
30363045
case SyntaxKind::ExclamationEqualsToken:
30373046
case SyntaxKind::ExclamationEqualsEqualsToken:
3038-
value = logicOp<arith::CmpIPredicate::ne, arith::CmpFPredicate::ONE>(logicalBinaryOp, op, op1, opType1, op2, opType2,
3047+
// UNE, not ONE: JS `!=`/`!==` is the negation of `==`, so NaN != x must be
3048+
// true. ONE (ordered) yields false when either side is NaN, and `x != x`
3049+
// (the idiomatic NaN test) folds to constant false.
3050+
value = logicOp<arith::CmpIPredicate::ne, arith::CmpFPredicate::UNE>(logicalBinaryOp, op, op1, opType1, op2, opType2,
30393051
rewriter);
30403052
break;
30413053
case SyntaxKind::GreaterThanToken:

tslang/test/tester/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ file(REMOVE
105105
"${CMAKE_CURRENT_BINARY_DIR}/compile.bat" "${CMAKE_CURRENT_BINARY_DIR}/jit.bat"
106106
"${CMAKE_CURRENT_BINARY_DIR}/compiled.bat" "${CMAKE_CURRENT_BINARY_DIR}/jitd.bat"
107107
"${CMAKE_CURRENT_BINARY_DIR}/compile.sh" "${CMAKE_CURRENT_BINARY_DIR}/jit.sh"
108-
"${CMAKE_CURRENT_BINARY_DIR}/compiled.sh" "${CMAKE_CURRENT_BINARY_DIR}/jitd.sh")
108+
"${CMAKE_CURRENT_BINARY_DIR}/compiled.sh" "${CMAKE_CURRENT_BINARY_DIR}/jitd.sh"
109+
"${CMAKE_CURRENT_BINARY_DIR}/compilefm.bat" "${CMAKE_CURRENT_BINARY_DIR}/jitfm.bat"
110+
"${CMAKE_CURRENT_BINARY_DIR}/compiledfm.bat" "${CMAKE_CURRENT_BINARY_DIR}/jitdfm.bat"
111+
"${CMAKE_CURRENT_BINARY_DIR}/compilefm.sh" "${CMAKE_CURRENT_BINARY_DIR}/jitfm.sh"
112+
"${CMAKE_CURRENT_BINARY_DIR}/compiledfm.sh" "${CMAKE_CURRENT_BINARY_DIR}/jitdfm.sh")
109113

110114
######## enable testing ############
111115
# open __build\tslang
@@ -443,6 +447,7 @@ add_test(NAME test-compile-reference-null-bug COMMAND test-runner "${PROJECT_SOU
443447
add_test(NAME test-compile-reference-index-bug COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00reference_index_bug.ts")
444448
add_test(NAME test-compile-uint-compare-bug COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00uint_compare_bug.ts")
445449
add_test(NAME test-compile-gc-malloc-cse-o3 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/gc_malloc_cse_o3.ts")
450+
add_test(NAME test-compile-fast-math COMMAND test-runner -fast-math "${PROJECT_SOURCE_DIR}/test/tester/tests/fast_math.ts")
446451

447452
add_test(NAME test-jit-00-print COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00print.ts")
448453
add_test(NAME test-jit-00-assert COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00assert.ts")
@@ -774,6 +779,7 @@ add_test(NAME test-jit-reference-null-bug COMMAND test-runner -jit "${PROJECT_SO
774779
add_test(NAME test-jit-reference-index-bug COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00reference_index_bug.ts")
775780
add_test(NAME test-jit-uint-compare-bug COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00uint_compare_bug.ts")
776781
add_test(NAME test-jit-gc-malloc-cse-o3 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/gc_malloc_cse_o3.ts")
782+
add_test(NAME test-jit-fast-math COMMAND test-runner -jit -fast-math "${PROJECT_SOURCE_DIR}/test/tester/tests/fast_math.ts")
777783

778784
# tesing include
779785
add_test(NAME test-compile-include-global-var COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/include_global_var.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/define_global_var.ts")

tslang/test/tester/test-runner.cpp

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,32 @@ auto tslang_opt = "--di --opt_level=0 --no-default-lib";
9999
#define COMPILE_NAME "compiled"
100100
#endif
101101

102+
auto fastMath = false;
102103
auto tslang_opt_ext = std::string("");
103104

105+
// -fast-math tests get their own cached script (jitfm/compilefm) because the
106+
// plain jit/compile scripts are shared across all parallel single-file tests
107+
// and embed tslang_opt_ext at creation time - reusing the same file name would
108+
// let whichever runner created it first decide the flags for everyone.
109+
std::string jitBatName()
110+
{
111+
return std::string(JIT_NAME) + (fastMath ? "fm" : "") + BAT_NAME;
112+
}
113+
114+
std::string compileBatName()
115+
{
116+
return std::string(COMPILE_NAME) + (fastMath ? "fm" : "") + BAT_NAME;
117+
}
118+
104119
void createJitBatchFile()
105120
{
106-
#ifdef WIN32
107-
if (exists(JIT_NAME BAT_NAME))
121+
#ifdef WIN32
122+
if (exists(jitBatName()))
108123
{
109124
return;
110125
}
111126

112-
std::ofstream batFile(JIT_NAME BAT_NAME);
127+
std::ofstream batFile(jitBatName());
113128
batFile << "echo off" << std::endl;
114129
batFile << "set FILENAME=%1" << std::endl;
115130
batFile << "set FILEPATH=%2" << std::endl;
@@ -119,12 +134,12 @@ void createJitBatchFile()
119134
<< std::endl;
120135
batFile.close();
121136
#else
122-
if (exists(JIT_NAME BAT_NAME))
137+
if (exists(jitBatName()))
123138
{
124139
return;
125140
}
126141

127-
std::ofstream batFile(JIT_NAME BAT_NAME);
142+
std::ofstream batFile(jitBatName());
128143
batFile << "FILENAME=$1" << std::endl;
129144
batFile << "FILEPATH=$2" << std::endl;
130145
batFile << "TSLANGEXEPATH=" << TEST_TSLANG_EXEPATH << std::endl;
@@ -136,13 +151,13 @@ void createJitBatchFile()
136151

137152
void createCompileBatchFile()
138153
{
139-
#ifdef WIN32
140-
if (exists(COMPILE_NAME BAT_NAME))
154+
#ifdef WIN32
155+
if (exists(compileBatName()))
141156
{
142157
return;
143158
}
144159

145-
std::ofstream batFile(COMPILE_NAME BAT_NAME);
160+
std::ofstream batFile(compileBatName());
146161
batFile << "echo off" << std::endl;
147162
batFile << "set FILENAME=%1" << std::endl;
148163
batFile << "set FILEPATH=%2" << std::endl;
@@ -169,12 +184,12 @@ void createCompileBatchFile()
169184
batFile << "echo on" << std::endl;
170185
batFile.close();
171186
#else
172-
if (exists(COMPILE_NAME BAT_NAME))
187+
if (exists(compileBatName()))
173188
{
174189
return;
175190
}
176191

177-
std::ofstream batFile(COMPILE_NAME BAT_NAME);
192+
std::ofstream batFile(compileBatName());
178193
batFile << "FILENAME=$1" << std::endl;
179194
batFile << "FILEPATH=$2" << std::endl;
180195
batFile << "LINKER_OPTS=$3" << std::endl;
@@ -207,12 +222,12 @@ void createBatchFile()
207222

208223
void buildJitExecCommand(std::stringstream &ss, std::string fileNameNoExt, std::string file)
209224
{
210-
ss << RUN_CMD << JIT_NAME BAT_NAME << " " << fileNameNoExt << " " << file;
225+
ss << RUN_CMD << jitBatName() << " " << fileNameNoExt << " " << file;
211226
}
212227

213228
void buildCompileExecCommand(std::stringstream &ss, std::string fileNameNoExt, std::string file)
214229
{
215-
ss << RUN_CMD << COMPILE_NAME BAT_NAME << " " << fileNameNoExt << " " << file;
230+
ss << RUN_CMD << compileBatName() << " " << fileNameNoExt << " " << file;
216231
if (sharedLib)
217232
{
218233
ss << SHARED_LIB_OPT;
@@ -664,6 +679,11 @@ void readParams(int argc, char **argv, std::vector<std::string> &files)
664679
{
665680
opt = false;
666681
}
682+
else if (std::string(argv[index]) == "-fast-math")
683+
{
684+
fastMath = true;
685+
tslang_opt_ext += " --fast-math";
686+
}
667687
else if (exists(argv[index]))
668688
{
669689
files.push_back(argv[index]);

tslang/test/tester/tests/02numbers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,12 @@ function testComma() {
202202
assert(y.length == 4, "y");
203203
}
204204

205+
// NaN is the only value not strictly equal to itself: `NaN !== NaN` is true and
206+
// `NaN === NaN` is false per ECMAScript. The previous form
207+
// `(x !== x) === (x === x)` is always false in conforming JS and only passed
208+
// while `!==` was mis-lowered to an ordered (ONE) float compare.
205209
function isnan(x: number) {
206-
return (x !== x) === (x === x);
210+
return x !== x;
207211
}
208212

209213
function mydiv(x: number, y: number) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Run by test-compile-fast-math / test-jit-fast-math with the extra --fast-math
2+
// compiler flag (see test-runner -fast-math). Verifies that the relaxed
3+
// floating-point mode keeps the language semantics it promises to keep:
4+
//
5+
// 1. FP loop reductions still compute correct results. All values below are
6+
// exact multiples of 0.125, so every partial sum is exactly representable
7+
// and reassociation (the transform --fast-math licenses, and what the -O3
8+
// vectorizer uses to turn the loop into SIMD lanes) cannot change the
9+
// result - exact equality asserts stay valid.
10+
// 2. NaN and Infinity semantics survive: --fast-math deliberately omits
11+
// nnan/ninf, so `x !== x` NaN checks and Infinity arithmetic keep working.
12+
// 3. Plain counted integer loops are unaffected.
13+
14+
function isnan(x: number) {
15+
return x !== x;
16+
}
17+
18+
function fpReduction(n: int): number {
19+
let arr: number[] = [];
20+
for (let i = 0; i < n; i++) {
21+
arr.push((i % 10) * 0.125);
22+
}
23+
24+
let sum: number = 0.0;
25+
for (let i = 0; i < n; i++) {
26+
sum = sum + arr[i];
27+
}
28+
29+
return sum;
30+
}
31+
32+
function dotProduct(n: int): number {
33+
let a: number[] = [];
34+
let b: number[] = [];
35+
for (let i = 0; i < n; i++) {
36+
a.push((i % 8) * 0.25);
37+
b.push(((i + 1) % 4) * 0.5);
38+
}
39+
40+
let sum: number = 0.0;
41+
for (let i = 0; i < n; i++) {
42+
sum = sum + a[i] * b[i];
43+
}
44+
45+
return sum;
46+
}
47+
48+
function intTriangle(n: int): int {
49+
let s: int = 0;
50+
for (let i: int = 1; i <= n; i++) {
51+
s = s + i;
52+
}
53+
54+
return s;
55+
}
56+
57+
function testNaNAndInfinity() {
58+
const zero: number = fpReduction(10) - 5.625; // 0.0, computed at runtime
59+
const nan = zero / zero;
60+
assert(nan !== nan, "NaN !== NaN must stay true under --fast-math");
61+
assert(!(nan === nan), "NaN === NaN must stay false under --fast-math");
62+
assert(isnan(nan), "isnan(0/0)");
63+
assert(!isnan(1.5), "!isnan(1.5)");
64+
assert(!isnan(zero), "!isnan(0)");
65+
66+
const inf = 1.0 / zero;
67+
assert(!isnan(inf), "Infinity is not NaN");
68+
assert(inf > 1.0e308, "Infinity compares greater than any finite double");
69+
assert(1.0 / inf === 0.0, "1/Infinity === 0");
70+
assert(-inf < -1.0e308, "-Infinity stays representable");
71+
}
72+
73+
function main() {
74+
// per 10 elements: (0+1+...+9)*0.125 = 5.625; 100 groups
75+
assert(fpReduction(1000) === 562.5, "fp sum reduction");
76+
77+
// per 8 elements the products sum to exactly 5.0; 100 cycles
78+
assert(dotProduct(800) === 500.0, "fp dot product");
79+
80+
assert(intTriangle(1000) === 500500, "integer counted loop");
81+
82+
testNaNAndInfinity();
83+
84+
print("done.");
85+
}

tslang/tslang/opts.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ extern cl::opt<bool> enableBuiltins;
2525
extern cl::opt<bool> noDefaultLib;
2626
extern cl::opt<std::string> outputFilename;
2727
extern cl::opt<bool> appendGCtorsToMethod;
28-
extern cl::opt<bool> strictNullChecks;
28+
extern cl::opt<bool> strictNullChecks;
2929
extern cl::opt<bool> embedExportDeclarationsAction;
30+
extern cl::opt<bool> enableFastMath;
3031

3132
// obj
3233
extern cl::opt<std::string> TargetTriple;
@@ -57,6 +58,7 @@ CompileOptions prepareOptions()
5758
compileOptions.isDLL = emitAction == Action::BuildDll;
5859
compileOptions.appendGCtorsToMethod = appendGCtorsToMethod.getValue();
5960
compileOptions.strictNullChecks = strictNullChecks.getValue();
61+
compileOptions.enableFastMath = enableFastMath.getValue();
6062
if (
6163
TheTriple.getArch() == llvm::Triple::UnknownArch
6264
|| TheTriple.getArch() == llvm::Triple::aarch64 // AArch64 (little endian): aarch64

0 commit comments

Comments
 (0)