Files
scriptc/packages/compiler/test/string-accumulation-emission.test.ts
Chris Tate 3f66c8e20b Make LLVM the sole production backend
- Remove C emission, debugging options, and automatic backend fallback.
- Complete LLVM lowering and native bootstrap coverage across supported targets.
- Link executables and libraries with precompiled C runtime packs, including iOS and Android.
2026-09-29 20:54:09 -05:00

117 lines
5.0 KiB
TypeScript

import { expect, test } from "vitest";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
import { STRING, VOID, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrStmt } from "../src/ir/ir.js";
import { validateModule } from "../src/ir/validate.js";
const loc = { file: "string-accumulation.ts", start: 0, end: 0 };
const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: STRING, loc });
const selfConcat = (localId: string, right: IrExpr = str("+")): IrExpr => ({
kind: "strConcat", left: ref(localId), right, type: STRING, loc,
});
const assign = (localId: string, value: IrExpr): IrStmt => ({ kind: "assign", localId, value, loc });
function functionWithLocal(name: string, local: IrLocal, body: IrStmt[]): IrFunction {
return { name, params: [], returnType: VOID, locals: [local], body, loc };
}
function fixture(): IrModule {
const plain: IrLocal = { id: "acc", name: "acc", type: STRING, mutable: true };
const boxed: IrLocal = { id: "boxed", name: "boxed", type: STRING, mutable: true, boxed: true };
const functions: IrFunction[] = [
functionWithLocal("plain", plain, [
{ kind: "varDecl", localId: "acc", init: str("seed"), loc },
assign("acc", selfConcat("acc")),
]),
functionWithLocal("assignExpr", plain, [
{ kind: "varDecl", localId: "acc", init: str("seed"), loc },
{ kind: "exprStmt", expr: { kind: "assignExpr", localId: "acc", value: selfConcat("acc"), type: STRING, loc }, loc },
]),
functionWithLocal("suffixReassign", plain, [
{ kind: "varDecl", localId: "acc", init: str("seed"), loc },
assign("acc", selfConcat("acc", {
kind: "assignExpr", localId: "acc", value: str("replacement"), type: STRING, loc,
})),
]),
functionWithLocal("boxed", boxed, [
{ kind: "varDecl", localId: "boxed", init: str("seed"), loc },
assign("boxed", selfConcat("boxed")),
]),
functionWithLocal("negative", plain, [
{ kind: "varDecl", localId: "acc", init: str("seed"), loc },
assign("acc", { kind: "strConcat", left: ref("other"), right: str("+"), type: STRING, loc }),
]),
{
name: "__main", params: [], returnType: VOID, locals: [],
body: [
assign("%g.e.acc", str("global")),
assign("%g.e.acc", selfConcat("%g.e.acc")),
], loc,
},
];
// The negative function needs a real, distinct left binding.
functions.find((fn) => fn.name === "negative")!.locals.push(
{ id: "other", name: "other", type: STRING, mutable: false },
);
functions.find((fn) => fn.name === "negative")!.body.splice(1, 0,
{ kind: "varDecl", localId: "other", init: str("other"), loc },
);
return {
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
globals: [{ id: "%g.e.acc", name: "globalAccumulator", type: STRING, mutable: true }],
functions,
};
}
function expectInOrder(text: string, fragments: readonly string[]): void {
let offset = 0;
for (const fragment of fragments) {
const found = text.indexOf(fragment, offset);
expect(found, `missing or out-of-order fragment: ${fragment}`).toBeGreaterThanOrEqual(offset);
offset = found + fragment.length;
}
}
test("LLVM hands off canonical string self-concats after suffix evaluation", () => {
const mod = fixture();
expect(validateModule(mod)).toEqual([]);
const llvm = emitLlvmModule(mod);
// Plain local: retained snapshot, suffix, detach/release, concat, move.
expectInOrder(llvm, [
"call ptr @scr_str_retain_v(ptr %t",
"load ptr, ptr %sc_l_acc", "store ptr null, ptr %sc_l_acc",
"call void @scr_str_release", "call ptr @scr_str_concat", "store ptr %",
]);
// Module globals use the same plain-slot handoff; boxes use set_ref(NULL).
expect(llvm).toContain("store ptr null, ptr @sc_g_e_acc");
expect(llvm).toContain("call void @scr_box_set_ref(ptr %");
// The expression form leaves its own result live and gives the binding a
// retained sibling. A suffix assignment changes the binding before the
// final detach, so the detach must occur after its replacement store.
const suffixBody = llvm.match(/define internal void @sc_f_suffixReassign\([^]*?\n\}/)?.[0];
expect(suffixBody).toBeDefined();
expectInOrder(suffixBody!, [
"call ptr @scr_str_retain_v", "store ptr %", "ptr %sc_l_acc",
"load ptr, ptr %sc_l_acc", "store ptr null, ptr %sc_l_acc",
"call void @scr_str_release", "call ptr @scr_str_concat",
]);
// Concatenating another binding keeps the destination live until concat.
const negative = llvm.match(/define internal void @sc_f_negative\([^]*?\n\}/)?.[0];
expect(negative).toBeDefined();
expect(negative).toContain("call ptr @scr_str_concat");
const concat = negative!.indexOf("call ptr @scr_str_concat");
const initialized = negative!.match(/store ptr %\w+, ptr %sc_l_acc/);
expect(initialized).not.toBeNull();
expect(negative!.slice(initialized!.index!, concat)).not.toContain("store ptr null, ptr %sc_l_acc");
});