Files
scriptc/tests/harness/self-hosting-emission-cases.ts
Chris Tate 8f46e7ae8f feat(compiler): run LLVM layout and helper emission natively (#499)
- Compile production LLVM layouts, ownership helpers, debug metadata, and JSON emitters on both native backends
- Support nullable collections and mixed insertion spreads needed by compiler code
- Verify native emission parity and generated executables with focused sanitizer coverage
2026-09-27 18:49:36 -05:00

227 lines
12 KiB
TypeScript

import { BOOL, F64, STRING, UNDEFINED_T, VOID, arrayOf, funcOf, mapOf, type IrClassDef, type IrFunction, type IrModule, type IrType } from "../../packages/compiler/src/ir/ir.js";
import { IR_VERSION } from "../../packages/compiler/src/ir/serialize.js";
export interface EmissionRequest {
bits: 32 | 64;
clones: string[];
classObjects: string[];
sources: { file: string; text: string }[];
writers?: IrType[];
textUnions?: string[];
joinUnions?: string[];
indent?: boolean;
}
export interface EmissionCase {
name: string;
module: IrModule;
request: EmissionRequest;
contains: string[];
}
const loc = { file: "native layouts/日本.ts", start: 0, end: 12 };
const obj = (name: string): IrType => ({ kind: "object", className: name });
const rec = (id: string): IrType => ({ kind: "record", shapeId: id });
const optional: IrType = { kind: "union", unionId: "optional" };
export function emissionRequest(bits: 32 | 64 = 64): EmissionRequest {
return { bits, clones: [], classObjects: [], sources: [] };
}
export function emissionModule(): IrModule {
return {
irVersion: IR_VERSION, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
};
}
function method(name: string, member: string, result: IrType = F64): IrFunction {
return {
name: `%${name}.${member}`,
params: [{ localId: "this.0", name: "this", type: obj(name) }],
locals: [{ id: "this.0", name: "this", type: obj(name), mutable: false }],
returnType: result, body: [], loc,
};
}
function cls(name: string, base?: string): IrClassDef {
return { name, ...(base ? { base } : {}), fields: [{ name: "value", type: F64 }], loc };
}
export function emissionCases(): EmissionCase[] {
const cases: EmissionCase[] = [];
const add = (name: string, module: IrModule, contains: string[], request = emissionRequest()): void => {
cases.push({ name, module, contains, request });
};
add("empty module", emissionModule(), []);
const scalar = emissionModule();
scalar.records = [{ id: "scalar", fields: [{ name: "enabled", type: BOOL }, { name: "value", type: F64 }] }];
add("scalar record allocation", scalar, ["type { i64, i8, double }", "@calloc", "@free"]);
const refs = emissionModule();
refs.records = [{ id: "references", fields: [
{ name: "text", type: STRING }, { name: "values", type: arrayOf(F64) },
{ name: "index", type: mapOf(STRING, F64) }, { name: "bytes", type: { kind: "bytes", elem: "u8" } },
] }];
add("reference fields", refs, ["@scr_str_release", "@scr_arr_release", "@scr_map_release", "@scr_bytes_release"]);
const recursive = emissionModule();
recursive.records = [{ id: "node", fields: [{ name: "children", type: arrayOf(rec("node")) }, { name: "text", type: STRING }] }];
add("recursive records", recursive, ["@scr_cyc_alloc", "@scr_cyc_on_dead", "@scr_arr_trace", "@scr_cyc_free"]);
const union = emissionModule();
union.records = [{ id: "linked", fields: [{ name: "next", type: optional }] }];
union.unions = [{ id: "optional", arms: [rec("linked"), UNDEFINED_T] }];
add("cycles through a nullable union", union, ["@scr_union_trace", "@scr_union_release"]);
const overflow = emissionModule();
overflow.records = [{ id: "dictionary", fields: [{ name: "label", type: STRING }], indexValue: rec("dictionary") }];
add("overflow-map cycles", overflow, ["[key: string]", "@scr_map_trace", "@scr_map_release"]);
const tuple = emissionModule();
tuple.records = [{ id: "tuple", tuple: true, fields: [{ name: "0", type: STRING }, { name: "1", type: F64 }] }];
add("tuple layout", tuple, ["type { i64, ptr, double }"]);
const wide = emissionModule();
wide.records = [{ id: "wide", fields: Array.from({ length: 18 }, (_, i) => ({ name: `f${i}`, type: i % 2 ? STRING : F64 })) }];
const clones = emissionRequest();
clones.clones = ["wide"];
add("wide record clone", wide, ["clone", "@scr_str_retain"], clones);
const small = emissionModule();
small.records = [{ id: "small", fields: [{ name: "value", type: F64 }] }];
const smallClones = emissionRequest();
smallClones.clones = ["small"];
add("inline clone threshold", small, ["type { i64, double }"], smallClones);
const exotic = emissionModule();
exotic.records = [{ id: "unicode😀", fields: [{ name: "break\nline\0*/é", type: STRING }] }];
add("source text escaping", exotic, ["break\\u000aline\\u0000*/é", "_xd83d__xde00_"]);
const standalone = emissionModule();
standalone.classes = [cls("Alone")];
add("standalone class", standalone, ["; class Alone { value }", "@sc_new_Alone"]);
const hierarchy = emissionModule();
const root = cls("Base"); root.methods = ["read"];
const child = cls("Child", "Base"); child.methods = ["read"];
child.fields.push({ name: "label", type: STRING });
hierarchy.classes = [root, child];
hierarchy.functions.push(method("Base", "read"), method("Child", "read"));
add("virtual dispatch and inherited fields", hierarchy, ["vtable: hierarchy rooted at Base [read]", "@sc_reld_Child", "dispatches"]);
const forest = structuredClone(hierarchy);
forest.classes!.push(cls("Unrelated"), cls("Leaf", "Child"));
forest.classes![3]!.fields = [...child.fields];
add("class forest numbering", forest, ["class Leaf (vt at 1)", "class Unrelated { value }"]);
const abstract = emissionModule();
const abstractRoot = cls("Abstract");
abstractRoot.abstract = true; abstractRoot.methods = ["read"]; abstractRoot.abstractMethods = ["read"];
const concrete = cls("Concrete", "Abstract"); concrete.methods = ["read"];
abstract.classes = [abstractRoot, concrete];
abstract.functions.push(method("Concrete", "read"));
add("abstract slot implementation", abstract, ["ptr null", "@sc_f__x25_Concrete_read"]);
const incomplete = emissionModule();
const abstractChild = cls("AbstractChild", "Abstract"); abstractChild.abstract = true;
incomplete.classes = [structuredClone(abstractRoot), abstractChild];
add("abstract hierarchy without implementation", incomplete, ["vtable: hierarchy rooted at Abstract"]);
const siblingSlots = emissionModule();
const left = cls("Left", "Root"); left.methods = ["read"];
const leftLeaf = cls("LeftLeaf", "Left"); leftLeaf.methods = ["read"];
const right = cls("Right", "Root"); right.methods = ["read"];
const rightLeaf = cls("RightLeaf", "Right"); rightLeaf.methods = ["read"];
siblingSlots.classes = [cls("Root"), left, leftLeaf, right, rightLeaf];
for (const name of ["Left", "LeftLeaf", "Right", "RightLeaf"]) siblingSlots.functions.push(method(name, "read"));
add("same method name in sibling subtrees", siblingSlots, ["[read, read]"]);
const classCycle = emissionModule();
classCycle.classes = [
{ name: "First", fields: [{ name: "other", type: obj("Second") }], loc },
{ name: "Second", fields: [{ name: "other", type: obj("First") }], loc },
];
add("mutually recursive classes", classCycle, ["@sc_trace_First", "@sc_trace_Second", "@scr_cyc_alloc"]);
const nullable = emissionModule();
nullable.unions = [{ id: "optional", arms: [mapOf(STRING, F64), UNDEFINED_T] }];
nullable.classes = [{ name: "Options", fields: [{ name: "map", type: optional }], loc }];
add("undefined field initialization", nullable, ["@native_unit_0", "starts undefined"]);
const dynField = emissionModule();
dynField.classes = [{ name: "Island", fields: [{ name: "value", type: { kind: "jsval" } }], loc }];
add("island field initialization", dynField, ["@scr_jsval_undefined", "@scr_jsval_release"]);
const emitter = emissionModule();
emitter.classes = [{ name: "%EventEmitter", runtime: true, fields: [], loc }, { name: "Emitter", base: "%EventEmitter", fields: [], loc }];
add("event emitter prefix", emitter, ["ScrEmitter prefix at 2", "@scr_emitter_reg_drop", "@native_string_0"]);
const stream = emissionModule();
stream.classes = [
{ name: "%EventEmitter", runtime: true, fields: [], loc },
{ name: "%Readable", runtime: true, base: "%EventEmitter", fields: [], loc },
{ name: "Reader", base: "%Readable", fields: [], loc },
];
add("stream prefix", stream, ["ScrStream prefix at 2", "@scr_stream_st_release", "@scr_stream_st_trace"]);
const construct = emissionModule();
construct.classes = [cls("Construct")];
const ctor = method("Construct", "constructor", VOID);
ctor.params.push({ localId: "n.0", name: "n", type: F64 }, { localId: "s.0", name: "s", type: STRING }, { localId: "b.0", name: "b", type: BOOL });
construct.functions.push(ctor);
const constructRequest = emissionRequest(); constructRequest.classObjects = ["Construct"];
add("constructor thunk ABI", construct, ["double %a0, ptr %a1, i1 %a2", "%ScrClassObj"], constructRequest);
const generic = structuredClone(construct);
generic.classes = [cls("Family"), { ...cls("Construct", "Family"), genericOf: "Family" }];
add("generic class object family interval", generic, ["construct thunk Construct", "vtable: hierarchy rooted at Family"], constructRequest);
const captured = emissionModule();
captured.records = [{ id: "closure", fields: [{ name: "fn", type: funcOf([], VOID) }] }];
add("closure cycles", captured, ["@scr_closure_trace_v", "@scr_closure_release"]);
const writer = structuredClone(recursive);
const writerRequest = emissionRequest();
writerRequest.writers = [rec("node"), arrayOf(rec("node")), F64, BOOL, STRING, rec("node")];
writerRequest.indent = true;
add("recursive JSON writers and indentation", writer, ["@scr_jb_enter", "@scr_jb_put_f64"], writerRequest);
const tupleRequest = emissionRequest(); tupleRequest.writers = [rec("tuple")];
add("tuple JSON writer", tuple, ["@scr_jb_putc"], tupleRequest);
const dictionaryRequest = emissionRequest(); dictionaryRequest.writers = [rec("dictionary")];
add("overflow JSON writer", overflow, ["@scr_map_keys_js_order"], dictionaryRequest);
const textUnion = emissionModule();
textUnion.unions = [{ id: "text", arms: [BOOL, F64, STRING, { kind: "nullT" }, UNDEFINED_T] }];
const textRequest = emissionRequest();
textRequest.writers = [{ kind: "union", unionId: "text" }];
textRequest.textUnions = ["text", "text"];
textRequest.joinUnions = ["text"];
add("union JSON, string and join helpers", textUnion, ["switch i32", "@scr_str_retain"], textRequest);
const debug = emissionModule();
const debugText = "function example(value: number) {\r\n const outer = 1;\n { const inner = 2; }\n}\n";
const scope = { file: loc.file, start: 0, end: debugText.length };
const innerScope = { file: loc.file, start: debugText.indexOf("{ const"), end: debugText.indexOf("; }") + 3 };
const position = (name: string) => ({ file: loc.file, start: debugText.indexOf(name), end: debugText.indexOf(name) + name.length });
debug.functions[0]!.loc = scope;
debug.functions[0]!.params = [{ localId: "value.0", name: "value", type: F64 }];
debug.functions[0]!.locals = [
{ id: "value.0", name: "value", type: F64, mutable: false, source: { loc: position("value"), scope } },
{ id: "outer.0", name: "outer", type: STRING, mutable: true, boxed: true, source: { loc: position("outer"), scope } },
{ id: "inner.0", name: "inner", type: F64, mutable: true, boxed: true, tdz: true, source: { loc: position("inner"), scope: innerScope } },
];
debug.functions[0]!.body = [{ kind: "return", value: null, loc: position("inner") }];
debug.globals = [{ id: "global", name: "global", type: BOOL, mutable: false, source: { loc: position("outer"), scope } }];
const debugRequest = emissionRequest(); debugRequest.sources = [{ file: loc.file, text: debugText }];
add("debug bindings and nested scopes", debug, ["DILexicalBlock", 'name: "ScrBox"', 'name: "value", arg: 1'], debugRequest);
// Every layout case also exercises the actual wasm32 size_t and cycle
// header offsets. The textual result must match the Node production stage.
return cases.flatMap((item) => [item, {
...item, name: item.name + " (32-bit)",
request: { ...item.request, bits: 32 as const },
contains: item.contains.map((text) => text.replaceAll("i64", "i32")),
}]);
}