mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
feat(compiler): run LLVM layout and helper emission natively
- 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
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { commentText, octalByte } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Type-directed dispatch tables of the C emitter: the C spelling of every IR
|
||||
* type and the per-type runtime entry points (retain/release, box kinds,
|
||||
@@ -431,12 +432,7 @@ export function mapValKindC(value: IrType): string {
|
||||
* ordinary output byte-for-byte, but split comment delimiters and encode
|
||||
* source-control characters so the text cannot alter the translation unit. */
|
||||
export function cCommentText(text: string): string {
|
||||
return text
|
||||
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, (char) =>
|
||||
`\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
|
||||
)
|
||||
.replace(/\*\//g, "* /")
|
||||
.replace(/\/\*/g, "/ *");
|
||||
return commentText(text).replace(/\*\//g, "* /").replace(/\/\*/g, "/ *");
|
||||
}
|
||||
|
||||
/** UTF-8 bytes as an unambiguous C string literal (octal escapes are always
|
||||
@@ -453,7 +449,7 @@ export function cStringLiteral(bytes: Buffer): string {
|
||||
// standard C, exactly for this.
|
||||
else if (b === 0x3f) out += "\\?";
|
||||
else if (b >= 0x20 && b < 0x7f) out += String.fromCharCode(b);
|
||||
else out += "\\" + b.toString(8).padStart(3, "0");
|
||||
else out += "\\" + octalByte(b);
|
||||
}
|
||||
return out + '"';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { commentText, llvmBytes, llvmQuoted, octalByte, unsignedHex } from "./literals.js";
|
||||
import { cCommentText, cNumberLiteral, cStringLiteral } from "./c/types.js";
|
||||
import { f64Lit, llvmCommentText } from "./llvm/common.js";
|
||||
import { mangleFunction, mangleLocal } from "./mangle.js";
|
||||
|
||||
test("integer escaping agrees with JavaScript across all UTF-16 code units", () => {
|
||||
for (let value = 0; value <= 0xffff; value++) expect(unsignedHex(value)).toBe(value.toString(16));
|
||||
for (const value of [0x10000, 0x10ffff, 2 ** 32, 2 ** 48 - 1, Number.MAX_SAFE_INTEGER]) {
|
||||
expect(unsignedHex(value)).toBe(value.toString(16));
|
||||
}
|
||||
for (const value of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
|
||||
expect(() => unsignedHex(value)).toThrow(RangeError);
|
||||
}
|
||||
});
|
||||
|
||||
test("octal and LLVM escapes cover every byte without ambiguous suffixes", () => {
|
||||
const bytes = Uint8Array.from({ length: 256 }, (_, i) => i);
|
||||
for (const byte of bytes) expect(octalByte(byte)).toBe(byte.toString(8).padStart(3, "0"));
|
||||
const expected = [...bytes].map((b) => b >= 32 && b < 127 && b !== 34 && b !== 92
|
||||
? String.fromCharCode(b) : "\\" + b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
||||
expect(llvmBytes(bytes)).toBe(expected + "\\00");
|
||||
expect(llvmBytes(bytes, false)).toBe(expected);
|
||||
expect(llvmBytes(new Uint8Array())).toBe("\\00");
|
||||
expect(cStringLiteral(Buffer.from([0, 49, 255, 55, 63, 34, 92]))).toBe('"\\0001\\3777\\?\\"\\\\"');
|
||||
});
|
||||
|
||||
test("metadata strings preserve UTF-8 replacement, embedded NUL and quotes", () => {
|
||||
for (const text of ["", "alpha.ts", "é/日本/😀.ts", 'quote"slash\\\0', "\ud800", "\udfff", "\ud83d\ude00"]) {
|
||||
const bytes = Buffer.from(text, "utf8");
|
||||
expect(llvmQuoted(text)).toBe('"' + llvmBytes(bytes, false) + '"');
|
||||
}
|
||||
expect(llvmQuoted("é")).toBe('"\\C3\\A9"');
|
||||
expect(llvmQuoted("\ud800")).toBe('"\\EF\\BF\\BD"');
|
||||
});
|
||||
|
||||
test("comments preserve ordinary text and encode all source-control units", () => {
|
||||
const text = Array.from({ length: 0x100 }, (_, i) => String.fromCharCode(i)).join("") + "\u2028\u2029é😀\ud800/*end*/";
|
||||
const expected = text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, (ch) =>
|
||||
"\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
|
||||
expect(commentText(text)).toBe(expected);
|
||||
expect(llvmCommentText(text)).toBe(expected);
|
||||
expect(cCommentText(text)).toBe(expected.replace(/\*\//g, "* /").replace(/\/\*/g, "/ *"));
|
||||
});
|
||||
|
||||
test("name spelling remains byte-for-byte compatible, including surrogate units", () => {
|
||||
for (const name of ["abc_X9", "x.12", "$x%y", "é日本", "😀", "\ud800\0", "dash-here"]) {
|
||||
const expected = name.replace(/[^A-Za-z0-9_]/g, (ch) => ch === "." ? "_" : `_x${ch.codePointAt(0)!.toString(16)}_`);
|
||||
expect(mangleFunction(name)).toBe("sc_f_" + expected);
|
||||
expect(mangleLocal(name)).toBe("sc_l_" + expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("double spelling preserves every bit, including signed zero and subnormals", () => {
|
||||
for (const number of [0, -0, 1, -1, 0.1, Number.MIN_VALUE, Number.MAX_VALUE, Infinity, -Infinity, NaN]) {
|
||||
const bytes = Buffer.alloc(8);
|
||||
bytes.writeDoubleBE(number);
|
||||
expect(f64Lit(number)).toBe("0x" + bytes.toString("hex").toUpperCase());
|
||||
}
|
||||
expect(cNumberLiteral(-0)).toBe("-0.0");
|
||||
expect(cNumberLiteral(2 ** 64)).toBe("18446744073709552000.0");
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/** Integer spelling for compiler-generated names and escapes. These
|
||||
* callers need exact nonnegative integers, not Number.toString's general
|
||||
* fractional-radix conversion. Division preserves all safe integer bits. */
|
||||
export function unsignedHex(value: number): string {
|
||||
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("hexadecimal value must be a nonnegative safe integer");
|
||||
const digits = "0123456789abcdef";
|
||||
let result = "";
|
||||
do {
|
||||
result = digits.charAt(value % 16) + result;
|
||||
value = Math.floor(value / 16);
|
||||
} while (value !== 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Three octal digits prevent a following source digit extending an escape. */
|
||||
export function octalByte(value: number): string {
|
||||
const digits = "01234567";
|
||||
return digits.charAt((value >>> 6) & 3) + digits.charAt((value >>> 3) & 7) + digits.charAt(value & 7);
|
||||
}
|
||||
|
||||
/** LLVM byte-string contents, without quotes or the c prefix. */
|
||||
export function llvmBytes(bytes: Uint8Array, nulTerminated = true): string {
|
||||
let out = "";
|
||||
for (const byte of bytes) {
|
||||
out += byte >= 0x20 && byte < 0x7f && byte !== 0x22 && byte !== 0x5c
|
||||
? String.fromCharCode(byte)
|
||||
: "\\" + unsignedHex(byte).padStart(2, "0").toUpperCase();
|
||||
}
|
||||
return nulTerminated ? out + "\\00" : out;
|
||||
}
|
||||
|
||||
/** Metadata operands use the same UTF-8 escapes, without a trailing NUL. */
|
||||
export function llvmQuoted(text: string): string {
|
||||
return '"' + llvmBytes(Buffer.from(text, "utf8"), false) + '"';
|
||||
}
|
||||
|
||||
/** Escape UTF-16 source-control units without combining surrogate pairs. */
|
||||
export function commentText(text: string): string {
|
||||
let out = "";
|
||||
let start = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
if (code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029) {
|
||||
out += text.slice(start, i) + "\\u" + unsignedHex(code).padStart(4, "0");
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
return out + text.slice(start);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
test("entry allocations precede instructions and temporaries stay unique", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
const first = blocks.tmp();
|
||||
const second = blocks.slot();
|
||||
expect(first).not.toBe(second);
|
||||
blocks.line(`${first} = fadd double 0.0, 1.0`);
|
||||
blocks.entryAllocas.push(`${second} = alloca double`);
|
||||
blocks.terminate("ret void");
|
||||
expect(blocks.render()).toBe(`entry:\n ${second} = alloca double\n ${first} = fadd double 0.0, 1.0\n ret void`);
|
||||
});
|
||||
|
||||
test("terminators suppress dead instructions and keep their first target", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.br("done");
|
||||
blocks.line("invalid dead instruction");
|
||||
blocks.terminate("ret i32 99");
|
||||
expect(blocks.isTerminated()).toBe(true);
|
||||
blocks.startBlock("done");
|
||||
expect(blocks.isTerminated()).toBe(false);
|
||||
blocks.terminate("ret void");
|
||||
expect(blocks.render()).toBe("entry:\n br label %done\ndone:\n ret void");
|
||||
});
|
||||
|
||||
test("unterminated unreachable joins receive an explicit terminator", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.terminate("ret void");
|
||||
blocks.startBlock("join");
|
||||
expect(blocks.render()).toContain("join:\n unreachable");
|
||||
});
|
||||
|
||||
test("debug attachments precede instruction comments and skip comment-only lines", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.debugLocation = "!7";
|
||||
blocks.line('; comment "quotes" ; remain text');
|
||||
blocks.line('call void @f(ptr @"name;part") ; instruction comment');
|
||||
blocks.terminate("ret void ; tail");
|
||||
expect(blocks.render()).toBe([
|
||||
"entry:", ' ; comment "quotes" ; remain text',
|
||||
' call void @f(ptr @"name;part"), !dbg !7 ; instruction comment',
|
||||
" ret void, !dbg !7 ; tail",
|
||||
].join("\n"));
|
||||
});
|
||||
|
||||
test("changing a debug location affects only subsequent instructions", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.line("call void @first()");
|
||||
blocks.debugLocation = "!1";
|
||||
blocks.line("call void @second()");
|
||||
blocks.debugLocation = "!2";
|
||||
blocks.line("call void @third()");
|
||||
blocks.debugLocation = null;
|
||||
blocks.terminate("ret void");
|
||||
expect(blocks.render()).toContain("call void @first()\n");
|
||||
expect(blocks.render()).toContain("call void @second(), !dbg !1");
|
||||
expect(blocks.render()).toContain("call void @third(), !dbg !2");
|
||||
expect(blocks.render()).toContain("ret void");
|
||||
});
|
||||
|
||||
test("counted loops expose one increment path and preserve nested block endings", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
const visited: string[] = [];
|
||||
blocks.countedLoop(f64Lit(4), (index, next) => {
|
||||
visited.push(index, next);
|
||||
const skip = blocks.newLabel("skip");
|
||||
const emit = blocks.newLabel("emit");
|
||||
blocks.condBr("%condition", skip, emit);
|
||||
blocks.startBlock(skip);
|
||||
blocks.br(next);
|
||||
blocks.startBlock(emit);
|
||||
blocks.line(`call void @consume(double ${index})`);
|
||||
});
|
||||
blocks.terminate("ret void");
|
||||
const text = blocks.render();
|
||||
expect(visited).toHaveLength(2);
|
||||
expect(text).toContain("fcmp olt double");
|
||||
expect(text).toContain(`br label %${visited[1]}`);
|
||||
expect(text).toContain("fadd double");
|
||||
expect(text).not.toContain("unreachable");
|
||||
});
|
||||
|
||||
test("inclusive loops and nested loops keep labels separate", () => {
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.countedLoop(f64Lit(2), (outer) => {
|
||||
blocks.countedLoop(outer, (inner) => { blocks.line(`call void @consume(double ${inner})`); }, "ole");
|
||||
});
|
||||
blocks.terminate("ret void");
|
||||
const text = blocks.render();
|
||||
const labels = text.split("\n").filter((line) => line.endsWith(":"));
|
||||
expect(new Set(labels).size).toBe(labels.length);
|
||||
expect(text).toContain("fcmp olt double");
|
||||
expect(text).toContain("fcmp ole double");
|
||||
expect(text.match(/alloca double/g)).toHaveLength(2);
|
||||
});
|
||||
@@ -45,8 +45,8 @@ export class BlockBuilder {
|
||||
// can themselves contain semicolons; those are not comment delimiters.
|
||||
let quoted = false;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] === '"') quoted = !quoted;
|
||||
if (s[i] === ";" && !quoted) {
|
||||
if (s.charAt(i) === '"') quoted = !quoted;
|
||||
if (s.charAt(i) === ";" && !quoted) {
|
||||
return `${s.slice(0, i).trimEnd()}, !dbg ${this.debugLocation} ${s.slice(i)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,16 +50,19 @@ export interface LlVtSlot {
|
||||
}
|
||||
|
||||
/** Per-class node of the class graph — the CEmitter ClassMeta shape. */
|
||||
export interface LlClassMeta {
|
||||
def: IrClassDef;
|
||||
base: LlClassMeta | null;
|
||||
children: LlClassMeta[];
|
||||
export class LlClassMeta {
|
||||
base: LlClassMeta | null = null;
|
||||
children: LlClassMeta[] = [];
|
||||
root: LlClassMeta;
|
||||
pre: number;
|
||||
post: number;
|
||||
hierarchy: boolean;
|
||||
pre = 0;
|
||||
post = 0;
|
||||
hierarchy = false;
|
||||
/** Root classes: the hierarchy's slots in DFS-declaration order. */
|
||||
slots: LlVtSlot[];
|
||||
slots: LlVtSlot[] = [];
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
}
|
||||
}
|
||||
|
||||
/** The class graph: link base/children, number the forest in preorder
|
||||
@@ -72,16 +75,7 @@ export interface LlClassMeta {
|
||||
export function buildClassGraph(mod: IrModule, fnByName: Map<string, IrFunction>): Map<string, LlClassMeta> {
|
||||
const metaMap = new Map<string, LlClassMeta>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
metaMap.set(cls.name, {
|
||||
def: cls,
|
||||
base: null,
|
||||
children: [],
|
||||
root: undefined as unknown as LlClassMeta,
|
||||
pre: 0,
|
||||
post: 0,
|
||||
hierarchy: false,
|
||||
slots: [],
|
||||
});
|
||||
metaMap.set(cls.name, new LlClassMeta(cls));
|
||||
}
|
||||
for (const meta of metaMap.values()) {
|
||||
if (meta.def.base === undefined) continue;
|
||||
@@ -107,7 +101,7 @@ export function buildClassGraph(mod: IrModule, fnByName: Map<string, IrFunction>
|
||||
const collectSlots = (m: LlClassMeta, root: LlClassMeta): void => {
|
||||
for (const method of m.def.methods ?? []) {
|
||||
let inherited = false;
|
||||
for (let a = m.base; a; a = a.base) inherited ||= declares(a, method);
|
||||
for (let a = m.base; a; a = a.base) inherited = inherited || declares(a, method);
|
||||
if (!inherited && declaredBelow(m, method)) {
|
||||
let fn = fnByName.get(`%${m.def.name}.${method}`);
|
||||
if (!fn && m.def.abstractMethods?.includes(method)) {
|
||||
@@ -143,7 +137,7 @@ export function buildClassGraph(mod: IrModule, fnByName: Map<string, IrFunction>
|
||||
* dispatches to, or null outside the slot's declaring subtree / on a
|
||||
* fully-abstract chain (vtEntriesFor, ported). */
|
||||
function vtEntriesFor(meta: LlClassMeta): { slot: LlVtSlot; impl: LlClassMeta | null }[] {
|
||||
return meta.root.slots.map((slot) => {
|
||||
return meta.root.slots.map((slot): { slot: LlVtSlot; impl: LlClassMeta | null } => {
|
||||
if (!(slot.declarer.pre <= meta.pre && meta.pre <= slot.declarer.post)) {
|
||||
return { slot, impl: null };
|
||||
}
|
||||
@@ -271,8 +265,13 @@ export function emitClassShapes(
|
||||
// runtime root — %Error — counts exactly when an emitted subclass needs
|
||||
// its type): a ScrVt head plus one ptr per slot. All slots are `ptr`, so
|
||||
// the type is layout-only.
|
||||
const roots = [...new Set(emitted.map((c) => metaMap.get(c.name)!).filter((m) => m.hierarchy).map((m) => m.root))];
|
||||
for (const root of roots) {
|
||||
const roots = new Set<string>();
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (meta.hierarchy) roots.add(meta.root.def.name);
|
||||
}
|
||||
for (const name of roots) {
|
||||
const root = metaMap.get(name)!;
|
||||
const slotPtrs = root.slots.map(() => "ptr").join(", ");
|
||||
typeDefs.push(
|
||||
`%${mangleVtStruct(root.def.name)} = type { %ScrVt${root.slots.length ? ", " + slotPtrs : ""} } ` +
|
||||
@@ -307,7 +306,7 @@ export function emitClassShapes(
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => fieldBase(meta) + i;
|
||||
const refFields = cls.fields
|
||||
.map((f, i) => ({ ...f, index: fieldIndex(i) }))
|
||||
.map((f, i) => ({ name: f.name, type: f.type, index: fieldIndex(i) }))
|
||||
.filter((f) => isRefCounted(f.type));
|
||||
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
|
||||
// An embedded prefix slot (the emitter registry at 2, the stream
|
||||
@@ -468,7 +467,7 @@ export function emitClassShapes(
|
||||
// trace: visit exactly the cycle-capable fields; gcFree: release
|
||||
// exactly the complement, then free (the trace/teardown complement
|
||||
// contract in scr_runtime.h).
|
||||
const indexed = cls.fields.map((f, i) => ({ ...f, index: fieldIndex(i) }));
|
||||
const indexed = cls.fields.map((f, i) => ({ name: f.name, type: f.type, index: fieldIndex(i) }));
|
||||
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
|
||||
const untracedRefFields = indexed.filter(
|
||||
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { commentText, unsignedHex } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { IrFfiCallbackParamClass, IrFfiReturnClass, IrFfiValueParamClass } from "../../ir/ir.js";
|
||||
|
||||
@@ -5,9 +6,7 @@ import type { IrFfiCallbackParamClass, IrFfiReturnClass, IrFfiValueParamClass }
|
||||
* ordinary output byte-for-byte, but encode control and line-separator code
|
||||
* units so a property name can never inject a line or invalid source byte. */
|
||||
export function llvmCommentText(text: string): string {
|
||||
return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, (char) =>
|
||||
`\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
|
||||
);
|
||||
return commentText(text);
|
||||
}
|
||||
|
||||
export function ffiNativeTypeLl(
|
||||
@@ -33,9 +32,9 @@ export function ffiNativeTypeLl(
|
||||
}
|
||||
|
||||
export function f64Lit(n: number): string {
|
||||
const buf = new ArrayBuffer(8);
|
||||
new DataView(buf).setFloat64(0, n);
|
||||
return `0x${[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
||||
const bytes = new Uint8Array(8);
|
||||
new DataView(bytes.buffer).setFloat64(0, n);
|
||||
return `0x${[...bytes].map((b) => unsignedHex(b).padStart(2, "0")).join("").toUpperCase()}`;
|
||||
}
|
||||
|
||||
export const F64_INF = f64Lit(Infinity);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, STRING, VOID, type IrFunction, type IrLocal, type SrcLoc } from "../../ir/ir.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
|
||||
const file = "/source/日本/main.ts";
|
||||
const text = "function example(value: number) {\r\n const outer = 1;\n { const inner = 2; }\n}\n";
|
||||
const loc: SrcLoc = { file, start: 0, end: text.length };
|
||||
const at = (word: string): SrcLoc => ({ file, start: text.indexOf(word), end: text.indexOf(word) + word.length });
|
||||
function local(name: string, scope = loc): IrLocal {
|
||||
return { id: name + ".0", name, type: F64, mutable: true, source: { loc: at(name), scope } };
|
||||
}
|
||||
function fn(locals: IrLocal[] = []): IrFunction {
|
||||
return { name: "example", params: [], locals, returnType: VOID, body: [], loc };
|
||||
}
|
||||
|
||||
test("missing sources leave functions and bindings without invented positions", () => {
|
||||
const debug = new LlvmDebugInfo(file, new Map());
|
||||
expect(debug.function(fn())).toBeNull();
|
||||
expect(debug.local(local("outer"), null)).toBeNull();
|
||||
expect(debug.location(loc, null)).toBeNull();
|
||||
expect(debug.global({ id: "g", name: "g", type: F64, mutable: false })).toBeNull();
|
||||
expect(debug.render()).not.toContain("DILocation");
|
||||
});
|
||||
|
||||
test("parameters retain argument numbers and source positions across CRLF", () => {
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text]]));
|
||||
const value = local("value");
|
||||
const scope = debug.function(fn([value]));
|
||||
const binding = debug.local(value, scope, 1);
|
||||
expect(binding).not.toBeNull();
|
||||
expect(debug.render()).toContain('name: "value", arg: 1');
|
||||
expect(debug.render()).toContain("line: 1, column: 18");
|
||||
expect(debug.location(at("outer"), scope)).not.toBeNull();
|
||||
expect(debug.render()).toContain("line: 2, column: 9");
|
||||
expect(debug.render()).toContain('filename: "main.ts"');
|
||||
expect(debug.render()).toContain('directory: "/source/\\E6\\97\\A5\\E6\\9C\\AC"');
|
||||
});
|
||||
|
||||
test("nested lexical scopes bind inner locals to the smallest containing block", () => {
|
||||
const nested: SrcLoc = { file, start: text.indexOf("{ const"), end: text.indexOf("; }") + 3 };
|
||||
const outer = local("outer");
|
||||
const inner = local("inner", nested);
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text]]));
|
||||
const scope = debug.function(fn([outer, inner]));
|
||||
debug.local(outer, scope);
|
||||
debug.local(inner, scope);
|
||||
const rendered = debug.render();
|
||||
const block = /(!\d+) = distinct !DILexicalBlock\(/.exec(rendered)?.[1];
|
||||
expect(block).toBeDefined();
|
||||
expect(rendered).toContain(`name: "outer", scope: ${scope}`);
|
||||
expect(rendered).toContain(`name: "inner", scope: ${block}`);
|
||||
});
|
||||
|
||||
test("captured locals use function scope while ordinary duplicate scopes intern", () => {
|
||||
const nested: SrcLoc = { file, start: text.indexOf("{ const"), end: text.indexOf("; }") + 3 };
|
||||
const inner = local("inner", nested);
|
||||
inner.boxed = true;
|
||||
const second = { ...inner, id: "other.0", name: "other" };
|
||||
const captured = { ...inner, id: "captured.0", name: "captured" };
|
||||
const func = fn([inner, second, captured]);
|
||||
func.captures = [{ localId: captured.id, name: captured.name, type: F64 }];
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text]]));
|
||||
const scope = debug.function(func);
|
||||
debug.local(captured, scope, 0, true);
|
||||
expect(debug.render().match(/distinct !DILexicalBlock\(/g)).toHaveLength(1);
|
||||
expect(debug.render()).toContain(`name: "captured", scope: ${scope}`);
|
||||
});
|
||||
|
||||
test.each([32, 64])("boxed and TDZ locals use honest storage descriptions (%i-bit)", (bits) => {
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text]]), bits);
|
||||
const boxed: IrLocal = { ...local("outer"), boxed: true };
|
||||
const tdz: IrLocal = { ...local("inner"), boxed: true, tdz: true };
|
||||
const scope = debug.function(fn([boxed, tdz]));
|
||||
const real = debug.local(boxed, scope);
|
||||
const opaque = debug.local(tdz, scope);
|
||||
expect(real?.expression).toBe(`!DIExpression(DW_OP_deref, DW_OP_plus_uconst, ${bits === 32 ? 24 : 40})`);
|
||||
expect(opaque?.expression).toBe("!DIExpression()");
|
||||
expect(debug.render()).toContain('name: "ScrBox", flags: DIFlagFwdDecl');
|
||||
});
|
||||
|
||||
test("globals are attached to the correct compilation units and render is stable", () => {
|
||||
const other = "/source/other.ts";
|
||||
const otherLoc = { file: other, start: 0, end: 5 };
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text], [other, "const other = 1;"]]));
|
||||
const first = debug.global({ id: "first", name: "first", type: STRING, mutable: false, source: { loc, scope: loc } });
|
||||
const second = debug.global({ id: "second", name: "second", type: BOOL, mutable: true, source: { loc: otherLoc, scope: otherLoc } });
|
||||
const rendered = debug.render();
|
||||
expect(rendered).toContain(`!{${first}}`);
|
||||
expect(rendered).toContain(`!{${second}}`);
|
||||
expect(rendered.match(/distinct !DICompileUnit\(/g)).toHaveLength(2);
|
||||
expect(debug.render()).toBe(rendered);
|
||||
});
|
||||
|
||||
test("locations intern per file and scope and clamp columns to DWARF limits", () => {
|
||||
const long = " ".repeat(70000) + "value";
|
||||
const other = "/source/other.ts";
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text], [other, long]]));
|
||||
const scope = debug.function(fn());
|
||||
const pos = { file: other, start: 70000, end: 70005 };
|
||||
const first = debug.location(pos, scope);
|
||||
expect(debug.location(pos, scope)).toBe(first);
|
||||
expect(debug.location({ file: "missing.ts", start: 0, end: 0 }, scope)).toBeNull();
|
||||
expect(debug.render()).toContain("column: 65535");
|
||||
expect(debug.render()).toContain("DILexicalBlockFile");
|
||||
});
|
||||
|
||||
test.each([32, 64])("nullable reference types describe tags and ABI payload offsets (%i-bit)", (bits) => {
|
||||
const debug = new LlvmDebugInfo(file, new Map([[file, text]]), bits, [
|
||||
{ id: "value", arms: [STRING, { kind: "undefinedT" }] },
|
||||
]);
|
||||
const value: IrLocal = { ...local("value"), type: { kind: "union", unionId: "value" } };
|
||||
const scope = debug.function(fn([value]));
|
||||
debug.local(value, scope);
|
||||
const rendered = debug.render();
|
||||
expect(rendered).toContain('!DIEnumerator(name: "undefined", value: 1');
|
||||
expect(rendered).toContain(`offset: ${(bits === 32 ? 24 : 40) * 8}`);
|
||||
expect(rendered).toContain('name: "ScrUnion_value"');
|
||||
});
|
||||
@@ -1,17 +1,9 @@
|
||||
import { llvmQuoted as quoted } from "../literals.js";
|
||||
import { basename, dirname } from "node:path";
|
||||
import { isUnitType, typeKey, type IrFunction, type IrGlobal, type IrLocal, type IrType, type IrUnionDef, type SrcLoc } from "../../ir/ir.js";
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** LLVM metadata strings use UTF-8 byte escapes, including for paths. */
|
||||
function quoted(text: string): string {
|
||||
return '"' + [...Buffer.from(text)].map((byte) =>
|
||||
byte >= 0x20 && byte < 0x7f && byte !== 0x22 && byte !== 0x5c
|
||||
? String.fromCharCode(byte)
|
||||
: `\\${byte.toString(16).padStart(2, "0").toUpperCase()}`,
|
||||
).join("") + '"';
|
||||
}
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
@@ -69,7 +61,7 @@ export class LlvmDebugInfo {
|
||||
const id = this.add(`distinct !DISubprogram(name: ${quoted(fn.name)}, linkageName: ${quoted(mangleFunction(fn.name))}, scope: ${file}, file: ${file}, line: ${pos.line}, type: ${signature}, scopeLine: ${pos.line}, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: ${this.unit(pos.file).id})`);
|
||||
const scopes: { loc: SrcLoc; id: string }[] = [];
|
||||
this.lexicalScopes.set(id, scopes);
|
||||
const captures = new Set(fn.captures?.map((c) => c.localId));
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
const spans = fn.locals.filter((local) => !captures.has(local.id)).flatMap((local) => local.source ? [local.source.scope] : []);
|
||||
// Outer scopes first; declarations in flattened IR still retain their
|
||||
// source block, loop, catch, or function scope (including hoisted var).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* The dyn (ScrDyn dyn) helper EMITTERS for the LLVM backend — the .ll
|
||||
* mirror of walkers.ts's dyn slice: per-type match predicates
|
||||
@@ -69,13 +70,6 @@ export interface DynHost extends WalkerHost {
|
||||
liveDynRefAdapter(t: IrType): { snapshot: string; commit: string };
|
||||
}
|
||||
|
||||
/** Exact double literal (the emitter's f64Lit — the walkers' copy). */
|
||||
function f64Lit(n: number): string {
|
||||
const buf = new ArrayBuffer(8);
|
||||
new DataView(buf).setFloat64(0, n);
|
||||
return `0x${[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
|
||||
export class LlDyn {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { llvmBytes as llBytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* IR → LLVM IR text (.ll). The LLVM backend consumes the SAME in-memory
|
||||
* IrModule the C backend does (never the JSON dump — see the -0 lesson in
|
||||
@@ -113,8 +115,6 @@ import {
|
||||
buildClassGraph,
|
||||
classFieldIndex,
|
||||
classStructSym,
|
||||
emitClassObjDefs,
|
||||
emitClassShapes,
|
||||
type LlClassMeta,
|
||||
} from "./classes.js";
|
||||
import { LlDyn } from "./dyn.js";
|
||||
@@ -126,7 +126,6 @@ import {
|
||||
boxNewCall,
|
||||
computeTraced,
|
||||
elemAccess,
|
||||
emitRecordShapes,
|
||||
FN_ATTRS,
|
||||
llFieldType,
|
||||
releaseSym,
|
||||
@@ -190,19 +189,6 @@ export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}):
|
||||
return new LlEmitter(options.debugSources === undefined ? scalarizeNumericRecords(mod) : mod, options).emit();
|
||||
}
|
||||
|
||||
/** LLVM c"..." payload for a UTF-8 literal, NUL-terminated like the C
|
||||
* emitter's flexible-array-member initializer. */
|
||||
function llBytes(bytes: Uint8Array): string {
|
||||
let s = "";
|
||||
for (const b of bytes) {
|
||||
s +=
|
||||
b >= 0x20 && b < 0x7f && b !== 0x22 && b !== 0x5c
|
||||
? String.fromCharCode(b)
|
||||
: `\\${b.toString(16).padStart(2, "0").toUpperCase()}`;
|
||||
}
|
||||
return `${s}\\00`;
|
||||
}
|
||||
|
||||
function llStrBytes(text: string): string {
|
||||
return llBytes(Buffer.from(text, "utf8"));
|
||||
}
|
||||
@@ -775,9 +761,10 @@ class LlEmitter {
|
||||
// emit), then the file assembles around them — the C emitter's order.
|
||||
const fnDefs: string[] = [];
|
||||
for (const fn of this.mod.functions) fnDefs.push(this.emitFunction(fn));
|
||||
const shapes = emitRecordShapes(this, this.mod);
|
||||
const classShapes = emitClassShapes(this, this.mod, this.classMeta);
|
||||
const classObjDefs = emitClassObjDefs(this, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t));
|
||||
const layouts = emitLlvmLayouts(this, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t));
|
||||
const shapes = layouts.records;
|
||||
const classShapes = layouts.classes;
|
||||
const classObjDefs = layouts.classObjects;
|
||||
const wrappers = this.emitFnValueDefs();
|
||||
const asyncDefs = this.emitAsyncScaffolding();
|
||||
const ffiCallbacks = this.emitFfiCallbackDefs();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IrFunction, IrModule, IrType } from "../../ir/ir.js";
|
||||
import { emitClassObjDefs, emitClassShapes, type ClassHost, type LlClassMeta } from "./classes.js";
|
||||
import { emitRecordShapes } from "./shapes.js";
|
||||
|
||||
export interface LlvmLayouts {
|
||||
records: { typeDefs: string[]; defs: string[] };
|
||||
classes: { typeDefs: string[]; defs: string[] };
|
||||
classObjects: string[];
|
||||
}
|
||||
|
||||
/** Generate the module's layouts, ownership helpers, vtables and class
|
||||
* constructors together. Hosts supply symbol interning and declarations;
|
||||
* all layout and lifetime decisions stay in this production stage. */
|
||||
export function emitLlvmLayouts(
|
||||
host: ClassHost,
|
||||
mod: IrModule,
|
||||
classes: Map<string, LlClassMeta>,
|
||||
classObjects: Map<string, { nameSym: string }>,
|
||||
functions: Map<string, IrFunction>,
|
||||
llType: (type: IrType) => string,
|
||||
): LlvmLayouts {
|
||||
const records = emitRecordShapes(host, mod);
|
||||
const classShapes = emitClassShapes(host, mod, classes);
|
||||
const objects = emitClassObjDefs(host, classes, classObjects, functions, llType);
|
||||
return { records, classes: classShapes, classObjects: objects };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
|
||||
* of walkers.ts's phase-3 slice: type-directed JSON serializers over
|
||||
@@ -37,14 +38,6 @@ export interface WalkerHost extends ShapeHost {
|
||||
needBadTag(): void;
|
||||
}
|
||||
|
||||
/** Exact double literal (the emitter's f64Lit — duplicated to avoid a
|
||||
* cyclic import; both spell every bit pattern identically). */
|
||||
function f64Lit(n: number): string {
|
||||
const buf = new ArrayBuffer(8);
|
||||
new DataView(buf).setFloat64(0, n);
|
||||
return `0x${[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
|
||||
export class LlWalkers {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { unsignedHex } from "./literals.js";
|
||||
|
||||
/* Name mangling. Distinct prefixes partition the C namespace:
|
||||
* sc_f_ user functions sc_l_ locals
|
||||
* sc_t emitter temporaries sc_lit_ interned string literals
|
||||
@@ -12,9 +14,15 @@
|
||||
*/
|
||||
|
||||
function sanitize(name: string): string {
|
||||
return name.replace(/[^A-Za-z0-9_]/g, (ch) =>
|
||||
ch === "." ? "_" : `_x${ch.codePointAt(0)!.toString(16)}_`,
|
||||
);
|
||||
let result = "";
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const code = name.charCodeAt(i);
|
||||
const ch = name.charAt(i);
|
||||
result += code === 46 ? "_"
|
||||
: (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 95
|
||||
? ch : `_x${unsignedHex(code)}_`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mangleFunction(name: string): string {
|
||||
|
||||
@@ -437,15 +437,14 @@ export interface GenericInstance {
|
||||
|
||||
/** The fences on a defaulted parameter's body type: it becomes the value
|
||||
* arm of the synthesized `T | undefined` ABI union, so it must be a valid
|
||||
* single arm. func and Set ARE valid here: the ABI union's only test is
|
||||
* single arm. Functions, Maps and Sets are valid: the ABI union's test is
|
||||
* the prologue's own undefined-tag check (never a user narrowing, which
|
||||
* is what keeps map/set out of general unions), so `runner: Runner =
|
||||
* is what keeps containers out of unions with data siblings), so `runner: Runner =
|
||||
* defaultRunner` and `skip: Set<string> = new Set()` arm like any ref
|
||||
* kind — the nullable-callback union shape, built by the compiler. */
|
||||
export function checkDefaultParamBodyType(lowerer: Lowerer, param: ts.ParameterDeclaration, bodyType: IrType): void {
|
||||
if (
|
||||
bodyType.kind === "void" ||
|
||||
bodyType.kind === "map" ||
|
||||
bodyType.kind === "regex" ||
|
||||
bodyType.kind === "date" ||
|
||||
bodyType.kind === "dyn" ||
|
||||
|
||||
@@ -100,7 +100,13 @@ function lowerArraySpreadItems(
|
||||
];
|
||||
for (const node of nodes) {
|
||||
if (ts.isSpreadElement(node)) {
|
||||
const source = lowerer.lowerExpr(node.expression);
|
||||
let source = lowerer.lowerExpr(node.expression);
|
||||
if (source.type.kind === "set" && typeEquals(source.type.elem, elem)) {
|
||||
source = { kind: "setIntrinsic", method: "toArray", receiver: source, args: [], type: arrType, loc };
|
||||
}
|
||||
if (source.type.kind === "record" && lowerer.shapes.get(source.type.shapeId)?.tuple) {
|
||||
source = lowerer.widthCoerce(source, arrType) ?? source;
|
||||
}
|
||||
if (!typeEquals(source.type, arrType)) {
|
||||
lowerer.noLowering(`Array insertion spread from '${lowerer.fmt(source.type)}'`, node);
|
||||
}
|
||||
@@ -513,6 +519,17 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
// Finish argument evaluation before mutating the receiver. Each
|
||||
// spread snapshots its elements immediately, so a later argument
|
||||
// can mutate that source without changing already-collected values.
|
||||
const items = lowerArraySpreadItems(lowerer, call.arguments, elem, receiverIr, loc);
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
method: name === "push" ? "pushSpread" : "unshiftSpread",
|
||||
receiver, args: [items], type: F64, loc,
|
||||
};
|
||||
}
|
||||
const valueProbes = call.arguments.map((arg) => tryLowerExpression(lowerer, arg));
|
||||
if (valueProbes.some((probe) =>
|
||||
probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null)) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { dirname, posix } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { wasiGuestPath } from "../../wasi-paths.js";
|
||||
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/ir.js";
|
||||
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionContainerArmsOk } from "../../ir/ir.js";
|
||||
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
|
||||
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, COMPOUND_ASSIGN_OPS, CompoundOp, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STRING_INDEX_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
||||
@@ -9143,14 +9143,12 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
}
|
||||
if (joinArms.length === 0) return null;
|
||||
// The join must be a BUILDABLE union: arm kinds the union invariants
|
||||
// admit (no map/dyn/jsval/generator arms; func arms only beside
|
||||
// func/unit siblings — unionFuncSetArmsOk, the validator's rule). A
|
||||
// join mixing, say, a func answer with data answers has no union to
|
||||
// surface as; the caller owns the fence message.
|
||||
// admit (no dyn/jsval/generator arms; containers only beside unit
|
||||
// siblings). The caller owns the diagnostic for a refused join.
|
||||
if (
|
||||
joinArms.length > 1 &&
|
||||
(!unionFuncSetArmsOk(joinArms) ||
|
||||
joinArms.some((a) => a.kind === "map" || a.kind === "dyn" || a.kind === "jsval" || a.kind === "generator" || a.kind === "caught"))
|
||||
(!unionContainerArmsOk(joinArms) ||
|
||||
joinArms.some((a) => a.kind === "dyn" || a.kind === "jsval" || a.kind === "generator" || a.kind === "caught"))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { F64, STRING, VOID, type IrType } from "../ir/ir.js";
|
||||
import { formatIrType, ShapeRegistry, UnionRegistry } from "./type-mapper.js";
|
||||
import { F64, STRING, UNDEFINED_T, VOID, mapOf, setOf, type IrType } from "../ir/ir.js";
|
||||
import { formatIrType, genResultRecord, ShapeRegistry, UnionRegistry, withUndefinedArm } from "./type-mapper.js";
|
||||
|
||||
describe("nullable collection union builders", () => {
|
||||
test.each([mapOf(STRING, F64), setOf(STRING)])("optional %j fields and generator results share the same union", (type) => {
|
||||
const shapes = new ShapeRegistry();
|
||||
const unions = new UnionRegistry();
|
||||
const optional = withUndefinedArm(type, unions);
|
||||
expect(optional?.kind).toBe("union");
|
||||
if (optional?.kind !== "union") throw new Error("missing union");
|
||||
expect(unions.get(optional.unionId)?.arms).toContainEqual(type);
|
||||
expect(unions.get(optional.unionId)?.arms).toContainEqual(UNDEFINED_T);
|
||||
expect(withUndefinedArm(optional, unions)).toEqual(optional);
|
||||
const result = genResultRecord(type, VOID, shapes, unions);
|
||||
expect(result).not.toBeNull();
|
||||
expect(shapes.get(result!.shapeId)?.fields.find((field) => field.name === "value")?.type).toEqual(optional);
|
||||
});
|
||||
|
||||
test("collection generators retain the data-sibling refusal", () => {
|
||||
expect(genResultRecord(mapOf(STRING, F64), STRING, new ShapeRegistry(), new UnionRegistry())).toBeNull();
|
||||
expect(genResultRecord(setOf(STRING), F64, new ShapeRegistry(), new UnionRegistry())).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("IR type diagnostics", () => {
|
||||
test("preserves small types, repeated sibling shapes, and array precedence", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { InternalCompilerError } from "../errors.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "./arguments-usage.js";
|
||||
import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../ir/ir.js";
|
||||
import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, UNDEFINED_T, VOID } from "../ir/ir.js";
|
||||
import { arrayOf, BOOL, bytesOf, canConvertToDyn, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DYN, F64, funcOf, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, JSVAL, mapOf, NULL_T, PROCSTREAM_T, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, setOf, STRING, SYMBOL_T, typeEquals, typeKey, unionContainerArmsOk, UNDEFINED_T, VOID } from "../ir/ir.js";
|
||||
import { BIGINT_T } from "../ir/ir.js";
|
||||
|
||||
import { isJsSourceFile, isNodeTypesPath } from "./program.js";
|
||||
@@ -2884,27 +2884,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
arms.some(
|
||||
(a) =>
|
||||
a.kind === "void" || a.kind === "union" ||
|
||||
// Map/Set arms stay out (like func against data arms: no
|
||||
// narrowing test — no discriminant fields on them). REGEX
|
||||
// arms map: `x instanceof RegExp` is their narrowing test
|
||||
// (the skip-utility `string | RegExp` shape), and the arm
|
||||
// rides the ref machinery like array regex elements.
|
||||
a.kind === "map" || a.kind === "set" || a.kind === "date" || a.kind === "dyn" ||
|
||||
// Generator arms follow the map/set rule: no narrowing test.
|
||||
a.kind === "generator" ||
|
||||
// Func arms map beside ANY sibling: `typeof x === "function"`
|
||||
// is the narrowing against data arms (typeofAnswer knows every
|
||||
// arm kind), unit TAG tests cover the nullable-callback shape
|
||||
// (cb !== null, cb ?? f, cb?.()), and against FUNC siblings
|
||||
// (`StringConstructor | NumberConstructor` — the option-table
|
||||
// field) closures compare by pointer identity per tag
|
||||
// (unionEq), so `x === String` narrows. No restriction left.
|
||||
// Promise arms follow the func rule (typeof gives no test
|
||||
// against sibling data arms): only the promise-or-absent shape
|
||||
// maps — `Promise<T> | undefined`, and `Promise<T> | void`
|
||||
// return types whose void part became the undefined arm above.
|
||||
(a.kind === "promise" && !arms.every((b) => b === a || isUnitType(b))),
|
||||
)
|
||||
a.kind === "date" || a.kind === "dyn" ||
|
||||
a.kind === "generator",
|
||||
) || !unionContainerArmsOk(arms)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -3328,8 +3310,8 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType {
|
||||
* answers undefined). ONE shape per channel pair — `g.next()`'s lowering,
|
||||
* `.return()`, `.throw()`, the for-of desugar, and mapType's
|
||||
* IteratorResult alias mapping all intern through here, so reads agree.
|
||||
* Null when the combined union would be illegal (a func/set arm beside
|
||||
* data arms, a map/regex/Date arm — kinds with no narrowing test): such
|
||||
* Null when the combined union would be illegal (a container arm beside
|
||||
* data arms, a regex/Date arm — kinds with no narrowing test here): such
|
||||
* generators stay unmapped. */
|
||||
export function genResultRecord(
|
||||
yieldT: IrType,
|
||||
@@ -3349,7 +3331,7 @@ export function genResultRecord(
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
t.kind === "map" || t.kind === "regex" || t.kind === "date" ||
|
||||
t.kind === "regex" || t.kind === "date" ||
|
||||
t.kind === "jsval" || t.kind === "generator"
|
||||
) {
|
||||
return false; // no legal union arm exists for these kinds
|
||||
@@ -3360,11 +3342,11 @@ export function genResultRecord(
|
||||
if (!add(yieldT) || !add(retT)) return null;
|
||||
byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T);
|
||||
const arms = [...byKey.values()];
|
||||
// func/set arms are legal only beside unit arms (no narrowing test
|
||||
// against data siblings — the union rule).
|
||||
// Containers keep the shared nullable-only rule. Generator function
|
||||
// payloads retain their existing unit-only boundary too.
|
||||
if (
|
||||
arms.some(
|
||||
(a) => (a.kind === "func" || a.kind === "set") && !arms.every((b) => b === a || isUnitType(b)),
|
||||
!unionContainerArmsOk(arms) || arms.some(
|
||||
(a) => a.kind === "func" && !arms.every((b) => b === a || isUnitType(b)),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
@@ -3400,8 +3382,8 @@ export function isUnitOnlyTsType(t: ts.Type): boolean {
|
||||
}
|
||||
|
||||
/** IR-level `t | undefined`, canonicalized and fenced exactly like the
|
||||
* ts-union branch of mapType (typeKey-sorted arms, deduplicated; map/
|
||||
* regex/Date/dyn/void arms unrepresentable; a func arm IS representable — the
|
||||
* ts-union branch of mapType (typeKey-sorted arms, deduplicated;
|
||||
* Date/dyn/void arms unrepresentable; a func arm IS representable — the
|
||||
* result is exactly the nullable-callback shape mapType's union branch
|
||||
* admits, `(() => void) | undefined`) so the interned union is IDENTICAL
|
||||
* to what mapping the checker's own `T | undefined` produces. */
|
||||
@@ -3416,7 +3398,7 @@ export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | nul
|
||||
return { kind: "union", unionId: unions.intern(arms) };
|
||||
}
|
||||
if (
|
||||
t.kind === "void" || t.kind === "map" || t.kind === "date" || t.kind === "dyn" ||
|
||||
t.kind === "void" || t.kind === "date" || t.kind === "dyn" ||
|
||||
// A bare unit field type cannot occur (units live only inside unions),
|
||||
// but guard against constructing a single-arm union from one.
|
||||
isUnitType(t)
|
||||
@@ -4030,15 +4012,15 @@ function armHasUnionHome(arm: IrType, siblingCount: number): boolean {
|
||||
switch (arm.kind) {
|
||||
case "void":
|
||||
case "union":
|
||||
case "map":
|
||||
case "set":
|
||||
case "regex":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "dyn":
|
||||
return false;
|
||||
// Promise arms map only beside unit siblings (the promise-or-absent
|
||||
// shape); a data sibling has no narrowing test against them.
|
||||
// Containers map only beside unit siblings; data siblings have no
|
||||
// supported runtime narrowing test against them.
|
||||
case "map":
|
||||
case "set":
|
||||
case "promise":
|
||||
return siblingCount === 0;
|
||||
default:
|
||||
|
||||
@@ -173,8 +173,15 @@ export function streamTypedRefEligible(t: IrType): boolean {
|
||||
return t.kind === "record" || t.kind === "array" || t.kind === "bytes";
|
||||
}
|
||||
|
||||
/** Only ancestry is needed here. Descendant links would turn a native
|
||||
* structural projection into a copy of the entire cyclic class graph. */
|
||||
export interface IrClassAncestry {
|
||||
def: { name: string };
|
||||
base: IrClassAncestry | null;
|
||||
}
|
||||
|
||||
/** True when a class descends from a runtime stream class. */
|
||||
export function streamRooted(meta: IrClassGraphNode): boolean {
|
||||
export function streamRooted(meta: IrClassAncestry): boolean {
|
||||
for (let current = meta.base; current; current = current.base) {
|
||||
if (RUNTIME_STREAM_CLASSES.has(current.def.name)) return true;
|
||||
}
|
||||
|
||||
@@ -685,18 +685,13 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
return { kind: "func", params, ret };
|
||||
}
|
||||
|
||||
/** The union FUNC/SET-arm sibling rule, shared by the frontend's union
|
||||
* builders and the validator: FUNC arms are valid beside ANY sibling —
|
||||
* `typeof x === "function"` narrows against data arms, unit tag tests
|
||||
* cover the nullable-callback shape, and between func arms closures
|
||||
* compare by pointer identity per tag (unionEq), so `x === String` is the
|
||||
* narrowing (the primitive-constructor tables' `StringConstructor |
|
||||
* NumberConstructor` field, and LinkOptions' `false | ((s: string) =>
|
||||
* string)`). A SET arm keeps the unit-only rule (no narrowing test
|
||||
* against data arms). */
|
||||
export function unionFuncSetArmsOk(arms: IrType[]): boolean {
|
||||
/** Maps, sets and promises may share a union with null/undefined only.
|
||||
* Unit tag tests can narrow nullable containers without losing identity;
|
||||
* arbitrary data siblings require a separate runtime narrowing operation.
|
||||
* Share this rule across checker mapping, synthesized unions and validation. */
|
||||
export function unionContainerArmsOk(arms: IrType[]): boolean {
|
||||
return arms.every(
|
||||
(a, i) => a.kind !== "set" || arms.every((b, j) => j === i || isUnitType(b)),
|
||||
(a, i) => (a.kind !== "map" && a.kind !== "set" && a.kind !== "promise") || arms.every((b, j) => j === i || isUnitType(b)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, VOID, arrayOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
|
||||
import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
|
||||
import { deserializeModule, serializeModule } from "./serialize.js";
|
||||
import { validateModule } from "./validate.js";
|
||||
|
||||
const loc = { file: "numeric-read.ts", start: 0, end: 0 };
|
||||
|
||||
test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as IrType])("nullable %j payloads preserve an explicit absence tag", (type) => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "nullable", arms: [type, NULL_T, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
});
|
||||
|
||||
test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as IrType])("%j payloads still refuse unrelated data siblings", (type) => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "mixed", arms: [type, STRING, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod).map((error) => error.message)).toContain(`union mixed: ${type.kind} arm 0 beside non-unit arms`);
|
||||
});
|
||||
|
||||
test("two differently typed Map payloads cannot silently share one tag test", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "maps", arms: [mapOf(STRING, F64), mapOf(STRING, STRING), UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod).filter((error) => error.message.includes("beside non-unit arms"))).toHaveLength(2);
|
||||
});
|
||||
|
||||
function numericReadModule(overrides: Partial<IrExpr & { kind: "arrIntrinsic" }> = {}): IrModule {
|
||||
const read: IrExpr = {
|
||||
kind: "arrIntrinsic", method: "getNumber",
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
IrUnionDef,
|
||||
SrcLoc,
|
||||
} from "./ir.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canDynCheckTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./ir.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canDynCheckTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionContainerArmsOk, URL_T, VOID } from "./ir.js";
|
||||
import { BIGINT_T } from "./ir.js";
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
@@ -1780,19 +1780,12 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
}
|
||||
u.arms.forEach((arm, i) => {
|
||||
// The unit kinds (undefinedT/nullT) are valid arms — union membership
|
||||
// is the ONLY place they may appear; void/union/map/dyn/jsval/date
|
||||
// stay out (maps and scalar Date values have no supported union
|
||||
// representation/discriminant to narrow on). Func/set arm
|
||||
// sibling rules live in unionFuncSetArmsOk (shared with the frontend's
|
||||
// union builders): a func arm allows unit and FUNC siblings (the
|
||||
// nullable-callback shape, and the primitive-constructor tables where
|
||||
// closure pointer identity per tag is the narrowing); a set arm is
|
||||
// valid exactly when every other arm is a unit (the defaulted-Set-
|
||||
// param ABI); func/set-beside-data stays out.
|
||||
// is the ONLY place they may appear. Containers are valid beside
|
||||
// unit arms: tag tests distinguish absence while the reference
|
||||
// payload preserves identity. The shared rule refuses data siblings.
|
||||
if (
|
||||
arm.kind === "void" ||
|
||||
arm.kind === "union" ||
|
||||
arm.kind === "map" ||
|
||||
arm.kind === "dyn" ||
|
||||
arm.kind === "jsval" ||
|
||||
arm.kind === "date" ||
|
||||
@@ -1801,8 +1794,8 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
|
||||
}
|
||||
if (
|
||||
(arm.kind === "func" || arm.kind === "set") &&
|
||||
!unionFuncSetArmsOk(u.arms)
|
||||
(arm.kind === "map" || arm.kind === "set" || arm.kind === "promise") &&
|
||||
!unionContainerArmsOk(u.arms)
|
||||
) {
|
||||
errors.push({ message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`, loc: noLoc });
|
||||
}
|
||||
|
||||
@@ -7481,6 +7481,24 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3105-nullable-collections.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3105-nullable-collections.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3106-mixed-insertion-spreads.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3106-mixed-insertion-spreads.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3107-nullable-collection-lifetimes.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3107-nullable-collection-lifetimes.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/400-fib.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/400-fib.ts"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
interface Options {
|
||||
sources?: ReadonlyMap<string, string>;
|
||||
names?: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
function lookup(options: Options, key: string): string {
|
||||
const sources = options.sources;
|
||||
if (sources === undefined) return "absent";
|
||||
return sources.get(key) ?? "missing";
|
||||
}
|
||||
|
||||
function count(options: Options): number {
|
||||
return options.names?.size ?? -1;
|
||||
}
|
||||
|
||||
function chooseMap(mode: number, value: Map<string, number>): Map<string, number> | null | undefined {
|
||||
if (mode === 0) return undefined;
|
||||
if (mode === 1) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
function chooseSet(mode: number, value: Set<string>): Set<string> | null | undefined {
|
||||
if (mode === 0) return undefined;
|
||||
if (mode === 1) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
const sources = new Map<string, string>([["first.ts", "hello"], ["second.ts", "world"]]);
|
||||
const names = new Set<string>(["first.ts", "second.ts"]);
|
||||
console.log(lookup({}, "first.ts"), count({}));
|
||||
console.log(lookup({ sources, names }, "first.ts"), count({ sources, names }));
|
||||
console.log(lookup({ sources }, "other.ts"));
|
||||
|
||||
const values = new Map<string, number>([["value", 7]]);
|
||||
for (let mode = 0; mode < 3; mode++) {
|
||||
const map = chooseMap(mode, values);
|
||||
const set = chooseSet(mode, names);
|
||||
console.log(mode, map === undefined, map === null, Boolean(map), typeof map);
|
||||
console.log(mode, set === undefined, set === null, Boolean(set), typeof set);
|
||||
console.log(map?.get("value"), set?.has("first.ts"));
|
||||
const present = map ?? values;
|
||||
present.set("value", (present.get("value") ?? 0) + 1);
|
||||
console.log(present === values, present.size, values.get("value"));
|
||||
if (set) {
|
||||
set.add("third.ts");
|
||||
console.log(set === names, [...set].join(","));
|
||||
}
|
||||
}
|
||||
|
||||
function defaultMap(map: Map<string, number> = new Map<string, number>()): Map<string, number> {
|
||||
map.set("default", 12);
|
||||
return map;
|
||||
}
|
||||
|
||||
function defaultSet(set: Set<string> = new Set<string>()): Set<string> {
|
||||
set.add("default");
|
||||
return set;
|
||||
}
|
||||
|
||||
console.log(defaultMap().get("default"), defaultMap(values) === values);
|
||||
console.log(defaultSet().has("default"), defaultSet(names) === names);
|
||||
|
||||
const optional: (Map<string, number> | undefined)[] = [undefined, values];
|
||||
for (const map of optional) {
|
||||
if (map !== undefined) console.log([...map.keys()].join(","));
|
||||
else console.log("no map");
|
||||
}
|
||||
|
||||
interface Holder {
|
||||
map?: Map<string, number>;
|
||||
set?: Set<string>;
|
||||
}
|
||||
const holder: Holder = {};
|
||||
holder.map = values;
|
||||
holder.set = names;
|
||||
console.log(holder.map === values, holder.set === names);
|
||||
holder.map = undefined;
|
||||
holder.set = undefined;
|
||||
console.log(holder.map, holder.set);
|
||||
@@ -0,0 +1,92 @@
|
||||
const events: string[] = [];
|
||||
function mark(value: string): string {
|
||||
events.push(value);
|
||||
return value;
|
||||
}
|
||||
const text = ["head"];
|
||||
const middle = ["a", "b"];
|
||||
console.log(text.push(mark("before"), ...middle, mark("after"), ...["tail"]));
|
||||
console.log(text.join(","), events.join(","));
|
||||
console.log(text.unshift("start", ...new Set(["x", "x", "y"]), "end"));
|
||||
console.log(text.join(","));
|
||||
|
||||
const source = [1, 2];
|
||||
const target = [0];
|
||||
function mutateSource(): number {
|
||||
source[0] = 10;
|
||||
source.push(3);
|
||||
return 7;
|
||||
}
|
||||
console.log(target.push(...source, mutateSource(), ...source));
|
||||
console.log(target.join(","), source.join(","));
|
||||
|
||||
const self = [1, 2];
|
||||
console.log(self.push(0, ...self, 3, ...self));
|
||||
console.log(self.join(","));
|
||||
const front = [1, 2];
|
||||
console.log(front.unshift(0, ...front, 3, ...front));
|
||||
console.log(front.join(","));
|
||||
|
||||
let current = [4, 5];
|
||||
const original = current;
|
||||
function swap(): number {
|
||||
current = [8, 9];
|
||||
return 6;
|
||||
}
|
||||
console.log(current.push(...current, swap(), ...current));
|
||||
console.log(original.join(","), current.join(","));
|
||||
|
||||
const failed = ["original"];
|
||||
function fail(): string {
|
||||
throw new Error("argument failed");
|
||||
}
|
||||
try {
|
||||
failed.push("pending", ...["spread"], fail());
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(error.message);
|
||||
}
|
||||
console.log(failed.join(","));
|
||||
try {
|
||||
failed.unshift(...["spread"], fail(), "pending");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(error.message);
|
||||
}
|
||||
console.log(failed.join(","));
|
||||
|
||||
const sparse = new Array<number>(3);
|
||||
sparse[1] = 4;
|
||||
const copied: number[] = [];
|
||||
console.log(copied.push(1, ...sparse, 2));
|
||||
console.log(copied.length, copied[1], copied[2], copied[3], 1 in copied, 3 in copied);
|
||||
console.log(copied.unshift(0, ...sparse, -1));
|
||||
console.log(copied.length, copied[1], copied[2], copied[3], 1 in copied, 3 in copied);
|
||||
|
||||
interface Item { label: string }
|
||||
const one: Item = { label: "one" };
|
||||
const two: Item = { label: "two" };
|
||||
const refs: Item[] = [one];
|
||||
const refsSource: Item[] = [two, one];
|
||||
console.log(refs.push(two, ...refsSource, one));
|
||||
two.label = "changed";
|
||||
console.log(refs.map((item) => item.label).join(","), refs[1] === two, refs[3] === one);
|
||||
|
||||
const optional: (string | undefined)[] = ["one", undefined];
|
||||
const optionalTarget: (string | undefined)[] = [];
|
||||
console.log(optionalTarget.push(undefined, ...optional, "last"));
|
||||
console.log(optionalTarget.map((value) => value ?? "absent").join(","));
|
||||
|
||||
const empty: number[] = [];
|
||||
const numbers: number[] = [7];
|
||||
console.log(numbers.push(...empty, ...empty, 8), numbers.join(","));
|
||||
console.log(numbers.unshift(6, ...empty, 5), numbers.join(","));
|
||||
|
||||
const booleans = [true];
|
||||
console.log(booleans.unshift(false, ...booleans, true), booleans.join(","));
|
||||
|
||||
function receiver(): string[] {
|
||||
events.push("receiver");
|
||||
return text;
|
||||
}
|
||||
events.length = 0;
|
||||
console.log(receiver().push(mark("left"), ...middle, mark("right")));
|
||||
console.log(events.join(","));
|
||||
@@ -0,0 +1,90 @@
|
||||
interface State {
|
||||
label: string;
|
||||
cache?: Map<string, State>;
|
||||
}
|
||||
|
||||
function cycle(label: string): string {
|
||||
const state: State = { label };
|
||||
const cache = new Map<string, State>();
|
||||
state.cache = cache;
|
||||
cache.set("self", state);
|
||||
return state.cache.get("self")!.label;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 200; i++) cycle("temporary");
|
||||
console.log(cycle("live"));
|
||||
|
||||
class Config {
|
||||
sources?: Map<string, string>;
|
||||
flags?: Set<string>;
|
||||
constructor(enabled: boolean) {
|
||||
if (enabled) {
|
||||
this.sources = new Map<string, string>([["entry", "text"]]);
|
||||
this.flags = new Set<string>(["debug"]);
|
||||
}
|
||||
}
|
||||
read(): string {
|
||||
return this.sources?.get("entry") ?? "none";
|
||||
}
|
||||
clear(): void {
|
||||
this.sources = undefined;
|
||||
this.flags = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const active = new Config(true);
|
||||
const empty = new Config(false);
|
||||
console.log(active.read(), empty.read(), active.flags?.has("debug"));
|
||||
const saved = active.sources;
|
||||
active.clear();
|
||||
console.log(active.read(), saved?.get("entry"), active.flags);
|
||||
|
||||
function captured(): () => string {
|
||||
let cache: Map<string, string> | undefined = new Map<string, string>([["key", "value"]]);
|
||||
return () => {
|
||||
const result = cache?.get("key") ?? "cleared";
|
||||
cache = undefined;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
const read = captured();
|
||||
console.log(read(), read());
|
||||
|
||||
function* maps(): Generator<Map<string, number>, void, unknown> {
|
||||
yield new Map<string, number>([["value", 3]]);
|
||||
yield new Map<string, number>([["value", 9]]);
|
||||
}
|
||||
const iterator = maps();
|
||||
const first = iterator.next();
|
||||
const second = iterator.next();
|
||||
const last = iterator.next();
|
||||
console.log(first.done, first.value?.get("value"));
|
||||
console.log(second.done, second.value?.get("value"));
|
||||
console.log(last.done, last.value);
|
||||
|
||||
interface Options {
|
||||
values?: Map<string, number>;
|
||||
}
|
||||
function readOptions(options: Options): number {
|
||||
const selected = options.values ?? new Map<string, number>([["fallback", 11]]);
|
||||
return selected.get("fallback") ?? 0;
|
||||
}
|
||||
const supplied = new Map<string, number>([["fallback", 23]]);
|
||||
const merged: Options = { values: supplied };
|
||||
console.log(readOptions({}), readOptions(merged));
|
||||
console.log(merged.values === supplied);
|
||||
supplied.set("fallback", 42);
|
||||
console.log(readOptions(merged));
|
||||
|
||||
function maybe(mode: number): ReadonlyMap<string, number> | null {
|
||||
return mode ? supplied : null;
|
||||
}
|
||||
for (let mode = 0; mode < 2; mode++) {
|
||||
const candidate = maybe(mode);
|
||||
console.log(candidate !== null, candidate?.get("fallback"));
|
||||
if (candidate !== null) {
|
||||
let total = 0;
|
||||
for (const value of candidate.values()) total += value;
|
||||
console.log(total);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { commentText, llvmBytes, llvmQuoted, octalByte, unsignedHex } from "../../../packages/compiler/src/backend/literals.js";
|
||||
import { cCommentText, cDecl, cNumberLiteral, cStringLiteral, cType } from "../../../packages/compiler/src/backend/c/types.js";
|
||||
import { f64Lit, ffiNativeTypeLl, llvmCommentText } from "../../../packages/compiler/src/backend/llvm/common.js";
|
||||
import { mangleClassNew, mangleClassObj, mangleClassRelease, mangleClassRetain, mangleClassStruct, mangleField, mangleFunction, mangleGlobal, mangleLocal, mangleRecordStruct, mangleVtInstance } from "../../../packages/compiler/src/backend/mangle.js";
|
||||
import { BOOL, F64, STRING, VOID, arrayOf, funcOf, mapOf, type IrType } from "../../../packages/compiler/src/ir/ir.js";
|
||||
|
||||
const bytes = new Uint8Array(256);
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = i;
|
||||
console.log(llvmBytes(bytes));
|
||||
console.log(llvmBytes(bytes, false));
|
||||
console.log(cStringLiteral(Buffer.from(bytes)));
|
||||
console.log(llvmBytes(new Uint8Array()));
|
||||
|
||||
for (let i = 0; i < 256; i++) console.log(unsignedHex(i), octalByte(i));
|
||||
for (const value of [0x100, 0xffff, 0x10000, 0x10ffff, 2 ** 32, 2 ** 48 - 1, Number.MAX_SAFE_INTEGER]) {
|
||||
console.log(unsignedHex(value));
|
||||
}
|
||||
for (const value of [-1, 0.5, NaN, Infinity]) {
|
||||
try { console.log(unsignedHex(value)); }
|
||||
catch (error) { if (error instanceof Error) console.log(error.name + ": " + error.message); }
|
||||
}
|
||||
|
||||
for (const value of [0, -0, 1, -1, 0.1, Number.MIN_VALUE, Number.MAX_VALUE, Infinity, -Infinity, NaN]) {
|
||||
console.log(f64Lit(value), cNumberLiteral(value));
|
||||
}
|
||||
|
||||
for (const text of ["", "ordinary", "é日本😀", 'quote"slash\\\0', "line\nnext\u2028last", "/* comment */"]) {
|
||||
console.log(llvmQuoted(text));
|
||||
console.log(cStringLiteral(Buffer.from(text, "utf8")));
|
||||
console.log(JSON.stringify(commentText(text)));
|
||||
console.log(JSON.stringify(cCommentText(text)));
|
||||
console.log(JSON.stringify(llvmCommentText(text)));
|
||||
}
|
||||
|
||||
for (const name of ["ordinary", "x.12", "$x%y", "é日本", "😀", "nul\0", "dash-here", "a\nb"]) {
|
||||
console.log(mangleFunction(name), mangleLocal(name), mangleGlobal(name), mangleField(name));
|
||||
console.log(mangleClassStruct(name), mangleClassObj(name), mangleClassNew(name));
|
||||
console.log(mangleClassRetain(name), mangleClassRelease(name), mangleVtInstance(name), mangleRecordStruct(name));
|
||||
}
|
||||
|
||||
const types: IrType[] = [
|
||||
F64, BOOL, STRING, VOID, arrayOf(F64), mapOf(STRING, F64),
|
||||
{ kind: "set", elem: STRING }, { kind: "record", shapeId: "row" },
|
||||
{ kind: "object", className: "User" }, { kind: "union", unionId: "choice" },
|
||||
{ kind: "classval", className: "User" }, { kind: "bytes", elem: "u8" },
|
||||
{ kind: "dyn" }, { kind: "jsval" }, { kind: "caught" },
|
||||
{ kind: "promise", inner: STRING }, funcOf([STRING], F64),
|
||||
{ kind: "generator", yieldT: F64, retT: STRING, nextT: VOID },
|
||||
{ kind: "date" }, { kind: "bigint" }, { kind: "regex" },
|
||||
{ kind: "url" }, { kind: "searchParams" }, { kind: "symbol" },
|
||||
{ kind: "stats" }, { kind: "fileHandle" }, { kind: "spawnRes" },
|
||||
{ kind: "child" }, { kind: "netServer" }, { kind: "netSocket" },
|
||||
{ kind: "http2Session" }, { kind: "http2Stream" },
|
||||
];
|
||||
for (const type of types) console.log(cType(type), cDecl(type, "slot"));
|
||||
console.log(ffiNativeTypeLl("f64"), ffiNativeTypeLl("bool"), ffiNativeTypeLl("u8"));
|
||||
console.log(ffiNativeTypeLl("u32"), ffiNativeTypeLl("i32"), ffiNativeTypeLl("cstring"), ffiNativeTypeLl("void"));
|
||||
try { console.log(ffiNativeTypeLl("bytes")); }
|
||||
catch (error) { if (error instanceof Error) console.log(error.name + ": " + error.message); }
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js";
|
||||
import { computeTraced } from "../../../packages/compiler/src/backend/cycle-analysis.js";
|
||||
import { buildClassGraph, classFieldIndex, type ClassHost } from "../../../packages/compiler/src/backend/llvm/classes.js";
|
||||
import { emitLlvmLayouts } from "../../../packages/compiler/src/backend/llvm/layouts.js";
|
||||
import { LlvmDebugInfo } from "../../../packages/compiler/src/backend/llvm/debug-info.js";
|
||||
import { LlWalkers, type WalkerHost } from "../../../packages/compiler/src/backend/llvm/walkers.js";
|
||||
import { BlockBuilder } from "../../../packages/compiler/src/backend/llvm/blocks.js";
|
||||
import { f64Lit } from "../../../packages/compiler/src/backend/llvm/common.js";
|
||||
import { mangleClassObj } from "../../../packages/compiler/src/backend/mangle.js";
|
||||
import { type IrType } from "../../../packages/compiler/src/ir/ir.js";
|
||||
|
||||
interface Request {
|
||||
bits: 32 | 64;
|
||||
clones: string[];
|
||||
classObjects: string[];
|
||||
sources: { file: string; text: string }[];
|
||||
writers?: IrType[];
|
||||
textUnions?: string[];
|
||||
joinUnions?: string[];
|
||||
indent?: boolean;
|
||||
}
|
||||
|
||||
try {
|
||||
const mod = deserializeModule(readFileSync(process.argv[2]!, "utf8"));
|
||||
const request = JSON.parse(readFileSync(process.argv[3]!, "utf8")) as Request;
|
||||
const declarations = new Set<string>();
|
||||
const strings: string[] = [];
|
||||
const literals: string[] = [];
|
||||
const units: { union: string; tag: number }[] = [];
|
||||
let needsOom = false;
|
||||
let needsBadTag = false;
|
||||
const traced = computeTraced(mod);
|
||||
const host: ClassHost & WalkerHost = {
|
||||
declare: (decl) => { declarations.add(decl); },
|
||||
needOom: () => { needsOom = true; },
|
||||
needBadTag: () => { needsBadTag = true; },
|
||||
internLiteral: (text) => {
|
||||
let index = literals.indexOf(text);
|
||||
if (index === -1) { index = literals.length; literals.push(text); }
|
||||
return `@native_literal_${index}`;
|
||||
},
|
||||
sizeType: request.bits === 32 ? "i32" : "i64",
|
||||
cycleColorOffset: request.bits === 32 ? 12 : 16,
|
||||
tracedShapes: traced.shapes,
|
||||
tracedUnions: traced.unions,
|
||||
recordsById: new Map((mod.records ?? []).map((record) => [record.id, record])),
|
||||
unionsById: new Map((mod.unions ?? []).map((union) => [union.id, union])),
|
||||
recordCloneShapes: new Set(request.clones),
|
||||
unitInstanceRef: (union, tag) => {
|
||||
let index = units.findIndex((unit) => unit.union === union && unit.tag === tag);
|
||||
if (index === -1) {
|
||||
index = units.length;
|
||||
units.push({ union, tag });
|
||||
}
|
||||
return `@native_unit_${index}`;
|
||||
},
|
||||
cstr: (text) => {
|
||||
let index = strings.indexOf(text);
|
||||
if (index === -1) { index = strings.length; strings.push(text); }
|
||||
return `@native_string_${index}`;
|
||||
},
|
||||
};
|
||||
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const graph = buildClassGraph(mod, functions);
|
||||
const classObjects = new Map(request.classObjects.map((name) => [name, { nameSym: `@name_${mangleClassObj(name)}` }]));
|
||||
const typeName = (type: IrType): string => type.kind === "void" ? "void" : type.kind === "f64" || type.kind === "date" ? "double" : type.kind === "bool" ? "i1" : "ptr";
|
||||
const layouts = emitLlvmLayouts(host, mod, graph, classObjects, functions, typeName);
|
||||
const walkers = new LlWalkers(host);
|
||||
const helpers: string[] = [];
|
||||
for (const type of request.writers ?? []) helpers.push(walkers.jsonWriteHelper(type));
|
||||
for (const union of request.textUnions ?? []) helpers.push(walkers.unionToStrHelper(union));
|
||||
for (const union of request.joinUnions ?? []) helpers.push(walkers.unionJoinHelper(union));
|
||||
if (request.indent) helpers.push(walkers.jsonIndentHelper());
|
||||
const classes = [...graph.values()].map((meta) => ({
|
||||
name: meta.def.name, root: meta.root.def.name, base: meta.base?.def.name ?? null,
|
||||
children: meta.children.map((child) => child.def.name), pre: meta.pre, post: meta.post,
|
||||
hierarchy: meta.hierarchy,
|
||||
fields: meta.def.fields.map((field) => ({ name: field.name, index: classFieldIndex(meta, field.name).index })),
|
||||
slots: meta.slots.map((slot) => ({ method: slot.method, declarer: slot.declarer.def.name, function: slot.fn.name })),
|
||||
}));
|
||||
const debug = new LlvmDebugInfo(mod.sourceFile, new Map(request.sources.map((source) => [source.file, source.text])), request.bits, mod.unions ?? []);
|
||||
const bindings: string[] = [];
|
||||
for (const global of mod.globals ?? []) bindings.push(debug.global(global) ?? "");
|
||||
for (const fn of mod.functions) {
|
||||
const scope = debug.function(fn);
|
||||
bindings.push(scope ?? "");
|
||||
for (const local of fn.locals) {
|
||||
const param = fn.params.findIndex((param) => param.localId === local.id);
|
||||
const entry = debug.local(local, scope, param + 1, fn.captures?.some((capture) => capture.localId === local.id) ?? false);
|
||||
bindings.push(entry === null ? "" : JSON.stringify(entry));
|
||||
}
|
||||
for (const stmt of fn.body) bindings.push(debug.location(stmt.loc, scope) ?? "");
|
||||
}
|
||||
const blocks = new BlockBuilder();
|
||||
blocks.countedLoop(f64Lit(3), (index, next) => {
|
||||
blocks.line(`call void @consume(double ${index})`);
|
||||
blocks.br(next);
|
||||
blocks.line("unreachable text must be dropped");
|
||||
});
|
||||
blocks.terminate("ret void");
|
||||
console.log(JSON.stringify({ layouts, classes, declarations: [...declarations], strings, literals, units, needsOom, needsBadTag, helpers, walkers: walkers.defs, bindings, debug: debug.render(), block: blocks.render() }));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(error.name + ": " + error.message);
|
||||
else console.log("unexpected thrown value");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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")),
|
||||
}]);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze, compile, compileC, deserializeModule, serializeModule } from "@scriptc/compiler";
|
||||
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
|
||||
import { scalarizeNumericRecords } from "../../packages/compiler/src/ir/scalar-records.js";
|
||||
import { everyStmtList } from "../../packages/compiler/src/ir/traverse.js";
|
||||
import { moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect, moduleUsesRegex, type IrModule } from "../../packages/compiler/src/ir/ir.js";
|
||||
import { emissionCases, emissionModule, emissionRequest, type EmissionRequest } from "./self-hosting-emission-cases.js";
|
||||
|
||||
const root = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const entry = join(root, "tests/fixtures/self-hosting/llvm-layouts.ts");
|
||||
const options = { cwd: root, timeout: 30_000, maxBuffer: 32 * 1024 * 1024 };
|
||||
|
||||
interface Emission {
|
||||
layouts: {
|
||||
records: { typeDefs: string[]; defs: string[] };
|
||||
classes: { typeDefs: string[]; defs: string[] };
|
||||
classObjects: string[];
|
||||
};
|
||||
classes: { name: string; root: string; base: string | null; children: string[]; pre: number; post: number; hierarchy: boolean; fields: { name: string; index: number }[]; slots: { method: string; declarer: string; function: string }[] }[];
|
||||
declarations: string[];
|
||||
walkers: string[];
|
||||
strings: string[];
|
||||
units: { union: string; tag: number }[];
|
||||
needsOom: boolean;
|
||||
bindings: string[];
|
||||
debug: string;
|
||||
block: string;
|
||||
}
|
||||
|
||||
test("the production layout and metadata stage lowers statically", () => {
|
||||
const { coverage } = analyze(entry, { dynamic: false });
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect(coverage.stats.statementsTotal).toBeGreaterThan(1200);
|
||||
expect(coverage.stats.statementsFailed).toBe(0);
|
||||
expect(coverage.stats.statementsIsland).toBe(0);
|
||||
expect(coverage.stats.functionsSkipped).toBe(0);
|
||||
});
|
||||
|
||||
for (const backend of ["c", "llvm"] as const) {
|
||||
test(`native LLVM layout and metadata emission (${backend})`, async () => {
|
||||
const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-emission-"));
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
const executable = (name: string): string => join(dir, name + (process.platform === "win32" ? ".exe" : ""));
|
||||
try {
|
||||
const built = await compile(entry, { outDir: dir, outPath: executable("stage"), backend, dynamic: false, optimization: "dev", sanitize });
|
||||
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
expect(built.backend).toBe(backend);
|
||||
const run = (mod: IrModule, request: EmissionRequest, name: string, status = 0): string => {
|
||||
const input = join(dir, "input.json");
|
||||
const config = join(dir, "config.json");
|
||||
writeFileSync(input, serializeModule(mod));
|
||||
writeFileSync(config, JSON.stringify(request));
|
||||
const args = [input, config];
|
||||
const oracle = spawnSync(process.execPath, ["--import", "tsx", entry, ...args], options);
|
||||
const native = spawnSync(built.binaryPath, args, options);
|
||||
for (const result of [oracle, native]) {
|
||||
expect(result.error, name).toBeUndefined();
|
||||
expect(result.signal, `${name}: ${result.stderr}`).toBeNull();
|
||||
expect(result.status, `${name}: ${result.stdout}\n${result.stderr}`).toBe(status);
|
||||
expect(result.stderr.toString(), name).toBe("");
|
||||
}
|
||||
if (status === 0) expect(JSON.parse(native.stdout.toString()), name).toEqual(JSON.parse(oracle.stdout.toString()));
|
||||
else expect(native.stdout, name).toEqual(oracle.stdout);
|
||||
return native.stdout.toString();
|
||||
};
|
||||
|
||||
for (const item of emissionCases()) {
|
||||
const text = run(item.module, item.request, item.name);
|
||||
const emitted = JSON.parse(text) as Emission;
|
||||
const definitions = [...emitted.declarations, ...emitted.layouts.records.typeDefs, ...emitted.layouts.records.defs, ...emitted.layouts.classes.typeDefs, ...emitted.layouts.classes.defs, ...emitted.layouts.classObjects, ...emitted.walkers, emitted.debug].join("\n");
|
||||
for (const expected of item.contains) expect(definitions, item.name).toContain(expected);
|
||||
if (item.name.startsWith("class forest")) {
|
||||
expect(emitted.classes.map((meta) => [meta.name, meta.root, meta.pre, meta.post])).toEqual([
|
||||
["Base", "Base", 0, 2], ["Child", "Base", 1, 2], ["Unrelated", "Unrelated", 3, 3], ["Leaf", "Base", 2, 2],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const invalid = emissionModule();
|
||||
invalid.classes = [{ name: "Child", base: "Missing", fields: [], loc: { file: invalid.sourceFile, start: 0, end: 0 } }];
|
||||
expect(run(invalid, emissionRequest(), "missing base", 1)).toContain("undeclared base class Missing");
|
||||
const absent = emissionRequest(); absent.classObjects = ["Missing"];
|
||||
expect(run(emissionModule(), absent, "missing class object", 1)).toContain("class object for unknown class Missing");
|
||||
|
||||
// A native bootstrap must preserve emitter refusals too: unsupported
|
||||
// requests must not quietly return partial helper definitions.
|
||||
const invalidRequests: { request: EmissionRequest; message: string }[] = [
|
||||
{ request: { ...emissionRequest(), writers: [{ kind: "map", key: { kind: "string" }, value: { kind: "f64" } }] }, message: "jsonStringify:map" },
|
||||
{ request: { ...emissionRequest(), writers: [{ kind: "record", shapeId: "Missing" }] }, message: "jsonStringify of unknown shape Missing" },
|
||||
{ request: { ...emissionRequest(), writers: [{ kind: "union", unionId: "Missing" }] }, message: "jsonStringify of unknown union Missing" },
|
||||
{ request: { ...emissionRequest(), textUnions: ["Missing"] }, message: "ToString of unknown union Missing" },
|
||||
{ request: { ...emissionRequest(), joinUnions: ["Missing"] }, message: "join of unknown union Missing" },
|
||||
];
|
||||
for (const { request, message } of invalidRequests) {
|
||||
expect(run(emissionModule(), request, message, 1)).toContain(message);
|
||||
}
|
||||
const unsupported = emissionModule();
|
||||
unsupported.unions = [{ id: "containers", arms: [{ kind: "array", elem: { kind: "string" } }, { kind: "undefinedT" }] }];
|
||||
expect(run(unsupported, { ...emissionRequest(), textUnions: ["containers"] }, "unsupported union stringification", 1)).toContain("unionToStr:array");
|
||||
|
||||
for (const source of ["711-inheritance-dispatch.ts", "756-cycle-inheritance.ts", "3103-scalar-record-nested-control-flow.ts"]) {
|
||||
const sourcePath = join(root, "tests/corpus", source);
|
||||
const irPath = join(dir, "frontend.json");
|
||||
const frontend = await compile(sourcePath, { outDir: dir, outPath: irPath, outputKind: "ir", dynamic: false });
|
||||
if (!frontend.ok) throw new Error(frontend.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = scalarizeNumericRecords(deserializeModule(readFileSync(irPath, "utf8")));
|
||||
const request = emissionRequest();
|
||||
request.sources = [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }];
|
||||
for (const fn of mod.functions) everyStmtList(fn.body, { stmt: () => true, expr: (expr) => {
|
||||
if (expr.kind === "recordClone" && expr.type.kind === "record" && !request.clones.includes(expr.type.shapeId)) request.clones.push(expr.type.shapeId);
|
||||
return true;
|
||||
} });
|
||||
const native = JSON.parse(run(mod, request, source)) as Emission;
|
||||
// Splice the native-generated sections into the real program. The
|
||||
// full emitter still runs on Node; these type and helper definitions
|
||||
// are supplied by the compiled production stage under test.
|
||||
let llvm = emitLlvmModule(mod);
|
||||
let supplied = 0;
|
||||
for (const lines of [native.layouts.records.typeDefs, native.layouts.records.defs, native.layouts.classes.typeDefs, native.layouts.classes.defs]) {
|
||||
const section = lines.join("\n");
|
||||
if (section.length === 0) continue;
|
||||
expect(llvm, source).toContain(section);
|
||||
llvm = llvm.replace(section, `; native layout stage\n${section}`);
|
||||
supplied++;
|
||||
}
|
||||
expect(supplied, source).toBeGreaterThan(0);
|
||||
const path = join(dir, "generated.ll");
|
||||
writeFileSync(path, llvm);
|
||||
const outPath = executable("generated");
|
||||
await compileC({ cPath: path, outPath, sanitize, inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod), regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod) });
|
||||
const oracle = spawnSync(process.execPath, [sourcePath], options);
|
||||
const program = spawnSync(outPath, [], options);
|
||||
for (const result of [oracle, program]) {
|
||||
expect(result.error, source).toBeUndefined();
|
||||
expect(result.signal, `${source}: ${result.stderr}`).toBeNull();
|
||||
expect(result.status, `${source}: ${result.stderr}`).toBe(0);
|
||||
}
|
||||
expect(program.stdout, source).toEqual(oracle.stdout);
|
||||
expect(program.stderr, source).toEqual(oracle.stderr);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -21,7 +21,7 @@ test("the complete IR validator lowers statically without skipped functions", ()
|
||||
// Keep these outside the ordinary corpus: they import implementation files
|
||||
// beyond the fixture directory, which the corpus oracle cache does not hash.
|
||||
// Node executes the actual TS modules through tsx's .js → .ts resolution.
|
||||
for (const component of ["source-locations", "ir-collections", "ir-types", "ir-control-flow"]) {
|
||||
for (const component of ["source-locations", "ir-collections", "ir-types", "ir-control-flow", "emitter-literals"]) {
|
||||
for (const backend of ["c", "llvm"] as const) {
|
||||
test(`self-hosting ${component}: ${backend} matches Node`, async () => {
|
||||
const entry = join(root, "tests/fixtures/self-hosting", `${component}.ts`);
|
||||
|
||||
Reference in New Issue
Block a user