Let LLVM merge the comparisons of a three-way string comparator

This commit is contained in:
Chris Tate
2026-10-23 03:20:04 -05:00
parent 4511be6307
commit e7bced94fd
3 changed files with 53 additions and 4 deletions
@@ -1601,8 +1601,8 @@ export function helperDefs(emitter: LlEmitter): string[] {
// order; other differing words decide by their first differing byte,
// and strings shorter than a word by their first byte. Otherwise a
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
// strings, so LLVM can merge repeated comparisons of one pair. Callers
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
// so LLVM can merge repeated comparisons of one pair. Callers
// only test the sign of the result.
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
const fn = "scr_str_cmp_u16";
@@ -1732,7 +1732,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
` %anyascii = icmp slt i32 %cws, 0`,
` br i1 %anyascii, label %memorder, label %units`,
`memorder:`,
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
` ret i32 %m`,
`units:`,
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
@@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r
expect(units).toContain("%below = icmp ult i8 %bx, %by");
expect(units).toContain("%tailable = and i1 %samelen, %short");
expect(units).toContain("%anyascii = icmp slt i32 %cws, 0");
expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
expect(units).toContain(
"call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
);
expect(units).toContain(
"call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
);
@@ -0,0 +1,47 @@
/* String comparison emission: the runtime calls behind a relational string
* operator only read the two strings, so LLVM merges the comparisons of one
* pair. A three-way comparator (`a < b ? -1 : a > b ? 1 : 0`) then compares
* its strings once, as a comparator that tests one operator does. */
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
async function emittedAsm(name: string, source: string): Promise<string> {
const key = createHash("sha256").update(source).digest("hex").slice(0, 16);
const outDir = join(cacheDir, `string-compare-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.ts`);
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, name),
outDir,
backend: "llvm",
outputKind: "asm",
});
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
return readFileSync(result.artifact.path, "utf8");
}
const program = (body: string): string => `function order(a: string, b: string): number {
return ${body};
}
const words = process.argv.slice(2);
words.sort(order);
console.log(words.join(","));
`;
const compareCalls = (asm: string): number =>
asm.match(/\bcall[a-z]*\s+(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g)?.length ?? 0;
test("a three-way string comparator compares its pair once", async () => {
const once = compareCalls(await emittedAsm("once", program("a < b ? -1 : 1")));
const both = compareCalls(await emittedAsm("both", program("a < b ? -1 : a > b ? 1 : 0")));
expect(once).toBeGreaterThan(0);
expect(both).toBe(once);
});