Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
665 changes: 614 additions & 51 deletions compiler/backend/emit.ts

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions compiler/backend/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Errors on a map of our own (TrigScript 4, step 11 of the way off eudplib). The front end has done most of it (B4):
* before a `throw` it sets the error in flight — its class's code into `Program.errors.thrown`, its message into
* `message` — `finally` is copied before every way out, and a program's body is a `try` whose `catch` is `uncaught`.
* What is left is where the walk goes, which is the emitter's (`Emitter.handler`): a `throw` goes to the `catch` of the
* nearest `try` around it, or out of the function it is in, and the call goes on from there.
*
* `ownErrors` rewrites what the emitter would need texts for, before the texts are lowered (`texts.ts`): the line each
* error is thrown at kept in a variable of the program's, `uncaught` as the print the Python side makes of it, and —
* when a function calls itself — the RangeError a stack that runs out throws, a function the emitter calls there.
*/
import { ERROR_CODES, STACK_OVERFLOW_MESSAGE, type At, type FuncDecl, type Program, type ProgramErrors, type Stmt, type VarDecl } from "../ir";

/** The variable the line an error was thrown at is kept in: what an uncaught one is said with. */
export const errorLine = (errors: ProgramErrors): string => `${errors.thrown}@line`;
/** The function that sets the error in flight to the RangeError of a stack that ran out (`Emitter.keep`). */
export const OVERFLOW_FN = "@error.overflow";
/** The function that says an error nothing caught, where more than one place may: each a call of it. */
const UNCAUGHT_FN = "@error.uncaught";
/**
* The functions a task's error goes through: the front end's `catch` around a task's body — the error noted in the
* task's slot for an `await` of it, or said — which reads the task's number (`taskHandle`) and nothing of the task's
* own, laid out once for every task whose `catch` is the same, a call each (a start of sixteen slots had sixteen).
*/
const TASK_FAILED_FN = "@error.task";

export function ownErrors(program: Program): Program {
const errors = program.errors;
if (!errors) return program;
const line = errorLine(errors), at0 = program.at;
const set = (target: string, value: number, at: At): Stmt => ({ kind: "assign", target, value: { kind: "const", value }, at, label: "" });
// (`throw` and `uncaught` are statements only: wherever a list holds one, it is a list of statements.)
const walk = (list: Stmt[]): Stmt[] => deep(list) as Stmt[];
const deep = (x: unknown): unknown => {
if (Array.isArray(x)) return x.flatMap((y) => (y && typeof y === "object" && (y.kind === "throw" || y.kind === "uncaught") ? statement(y as Stmt) : [deep(y)]));
if (!x || typeof x !== "object") return x;
const o = (x as { kind?: string }).kind === "start" ? outline(x as Extract<Stmt, { kind: "start" }>) : (x as object);
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(o)) out[k] = v && typeof v === "object" ? deep(v) : v;
return out;
};
// (Said where it is when only one place says it; else once, in a function each place calls — a task's body has one
// in every slot.)
let sites = 0;
const count = (x: unknown): void => {
if (Array.isArray(x)) x.forEach(count);
else if (x && typeof x === "object") {
if ((x as { kind?: string }).kind === "uncaught") sites++;
Object.values(x).forEach(count);
}
};
count([program.body, program.functions ?? []]);
// (A task's `catch` first: it says an uncaught error too.)
const failed: { shape: string; f: FuncDecl }[] = [];
const outline = (st: Extract<Stmt, { kind: "start" }>): Extract<Stmt, { kind: "start" }> => {
const top = st.call.body.length === 1 ? st.call.body[0] : undefined;
if (top?.kind !== "try" || !taskCatch(top.catch, st.task)) return st;
const shape = canonical(top.catch);
let f = failed.find((x) => x.shape === shape)?.f;
if (!f) {
const h: VarDecl = { id: `${TASK_FAILED_FN}${failed.length}.h`, name: "(task)", kind: "number", shared: false, at: st.at };
const body = replaced(top.catch, (o) => (o.kind === "taskHandle" ? { kind: "var", id: h.id } : undefined)) as Stmt[];
f = { id: `${TASK_FAILED_FN}${failed.length}`, name: "(a task's error)", params: [h], at: st.at, body };
failed.push({ shape, f });
}
const call: Stmt = { kind: "call", call: { fn: f.id, name: f.name, params: [{ decl: f.params[0], init: { kind: "taskHandle", at: top.at }, label: "" }], body: [], at: top.at, label: "" }, at: top.at, label: "" };
return { ...st, call: { ...st.call, body: [{ ...top, catch: [call] }] } };
};
const said = (at: At, label: string): Stmt[] => [
{ kind: "print", to: CURRENT_PLAYER, position: "chat", at, label, parts: [
{ kind: "text", text: "\x06TrigScript: uncaught " },
{ kind: "value", text: { kind: "textOf", array: errors.names, index: { kind: "var", id: errors.thrown }, at } },
{ kind: "text", text: ": " },
{ kind: "value", text: { kind: "textVar", id: errors.message } },
{ kind: "text", text: ", line " },
{ kind: "number", expr: { kind: "var", id: line } },
{ kind: "text", text: "." },
] },
set(errors.thrown, 0, at),
];
const statement = (s: Stmt): Stmt[] => {
switch (s.kind) {
// Where it was thrown: a caught error thrown again keeps where it came from.
case "throw": return s.rethrow ? [s] : [set(line, s.at.line, s.at), s];
// Said in red to the player the program runs as, as a fault of the game's is, and given up.
case "uncaught": return sites > 1 ? [{ kind: "call", call: { fn: UNCAUGHT_FN, name: "(uncaught)", params: [], body: [], at: s.at, label: s.label }, at: s.at, label: s.label }] : said(s.at, s.label);
default: return [s];
}
};
const decl: VarDecl = { id: line, name: "(error line)", kind: "number", shared: true, at: at0 };
const functions: FuncDecl[] = (program.functions ?? []).map((f) => ({ ...f, body: walk(f.body) }));
if (sites > 1) functions.push({ id: UNCAUGHT_FN, name: "(uncaught)", params: [], at: at0, body: said(at0, "") });
const body = walk(program.body);
for (const { f } of failed) functions.push({ ...f, body: walk(f.body) });
if (functions.some((f) => f.recursive)) {
functions.push({ id: OVERFLOW_FN, name: "(stack overflow)", params: [], at: at0, body: [
{ kind: "assignText", target: errors.message, value: { kind: "text", text: STACK_OVERFLOW_MESSAGE }, at: at0, label: "" },
set(errors.thrown, ERROR_CODES.RangeError, at0),
] });
}
return {
...program,
body: [{ kind: "declare", decl, init: { kind: "const", value: 0 }, at: at0, label: "" }, ...body],
...(functions.length ? { functions } : {}),
};
}

/** Player 13 in a print: the current player. */
const CURRENT_PLAYER = 13;

/**
* Whether a `catch` is the front end's around a task's body: it starts by taking the task's number, reads it nowhere
* else, and reads no variable of the task's — what lets it be laid out once for every slot and every task.
*/
function taskCatch(list: Stmt[], task: number): boolean {
const first = list[0];
if (first?.kind !== "declare" || (first.init as { kind?: string }).kind !== "taskHandle") return false;
let handles = 0, own = false;
const look = (x: unknown): void => {
if (Array.isArray(x)) { x.forEach(look); return; }
if (!x || typeof x !== "object") return;
const o = x as Record<string, unknown>;
if (o.kind === "taskHandle") handles++;
if (o.decl && typeof o.decl === "object" && (o.decl as VarDecl).task === task) own = true;
if (o.kind === "start" || o.kind === "sleep" || o.kind === "parallel" || o.kind === "return" || o.kind === "break" || o.kind === "continue") own = true;
Object.values(o).forEach(look);
};
look(list);
// (What it reads of the task's would be declared in the task: a variable it reads and does not declare is the program's.)
return handles === 1 && !own;
}

/** A piece of the IR as text, the variables it declares numbered in the order they come, `at` and labels left out. */
function canonical(x: unknown): string {
const names = new Map<string, string>();
const look = (y: unknown): void => {
if (Array.isArray(y)) { y.forEach(look); return; }
if (!y || typeof y !== "object") return;
const o = y as Record<string, unknown>;
if (o.decl && typeof o.decl === "object" && !names.has((o.decl as VarDecl).id)) names.set((o.decl as VarDecl).id, `#${names.size}`);
Object.values(o).forEach(look);
};
look(x);
return JSON.stringify(x, (k, v) => (k === "at" || k === "label" || k === "range" ? undefined : typeof v === "string" && names.has(v) ? names.get(v) : v));
}

/** A copy of a piece of the IR with each object `f` gives something for replaced by it. */
function replaced(x: unknown, f: (o: Record<string, unknown>) => unknown): unknown {
if (Array.isArray(x)) return x.map((y) => replaced(y, f));
if (!x || typeof x !== "object") return x;
const o = x as Record<string, unknown>;
const r = f(o);
if (r !== undefined) return r;
return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, replaced(v, f)]));
}
23 changes: 19 additions & 4 deletions compiler/backend/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ export interface FixedFn { fn: "fmul_vars" | "fdiv_vars" | "fmul_by" | "fdiv_by"
/**
* `current`: the dword whose top byte is the trigger's current action (where a trigger held by a Wait or a
* Transmission goes on from); an action's `flags` dword has its flags in the low byte (bit 0: a Wait or a Transmission
* started, which the game steps over the next time it is reached).
* started, which the game steps over the next time it is reached). `flags`: the trigger's own (4, preserved; 8 more,
* done: the walk passes it by before its conditions, at about 40% of what an empty trigger costs it).
*/
export type Field = { cond: number; part?: "value" | "player" | "mask" } | { act: number; part: ActPart } | "next" | "current";
export type Field = { cond: number; part?: "value" | "player" | "mask" } | { act: number; part: ActPart } | "next" | "current" | "flags";
/** A trigger's flags: preserved, and preserved and done (passed by: `Trigger.off`). */
export const TRIGGER_ON = 4, TRIGGER_OFF = 12;
export type ActPart = "value" | "dest" | "modifier" | "mask" | "text" | "wav" | "time" | "flags";

export type Compare = "atLeast" | "atMost" | "exactly";
Expand Down Expand Up @@ -129,7 +132,8 @@ export const SWITCHES = 0x58dc40;
* A trigger: its conditions all hold, its actions run, in order. Then the walk goes to `next` — a label of the graph's,
* or `END` for what follows the graph — or, unset, to the trigger after it in the list.
*/
export interface Trigger { label: string; conds: Cond[]; acts: Act[]; next?: string; at?: At; copy?: Copy; island?: Stmt; call?: Call }
/** `off`: laid out done (`TRIGGER_OFF`), passed by until an action of the graph's switches it on: a task's slot with nothing in it. */
export interface Trigger { label: string; conds: Cond[]; acts: Act[]; next?: string; at?: At; copy?: Copy; island?: Stmt; call?: Call; off?: boolean }

/**
* A call of a fixed-point function, in place of a trigger as a copy is: its parameters' cells were written by the
Expand Down Expand Up @@ -159,6 +163,13 @@ export interface Copy { from: string; into: { dword: Dword; add: Num; scale?: nu
/** Where the walk goes when it leaves the graph: the statement after it. */
export const END = "@end";

/**
* A map of our own's count of frames (TrigScript 4, step 11): a variable of the map's, one more each pass before the
* runs (`layout.ts`). A coroutine's wake — the program's, a task's, a branch of a `Promise.all`'s — asks whether it has
* reached the frame its sleep wrote: one copy of it plus the frames, where the game's own tick was read a bit at a time.
*/
export const NOW = "@now";

/**
* A copy into the graph on the way in: the variable's value times `scale` (1 when unset; wrapping) plus `add`, written
* into the dword. The variables a graph reads that something else writes come in this way, once each time the walk
Expand All @@ -184,7 +195,11 @@ export interface Entry { from: string; into: Dword; add: Num; scale?: number }
* order, `TRIGGER_BYTES` apart. Every other trigger the layout puts where it fits (`layout.ts`).
*/
/** (A template with `at` is the game's own memory there — the line a print at the centre of the screen is written into — and its bytes are written by the program, not laid out.) */
export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; cells?: Region[]; templates?: { id: string; bytes: number[]; at?: number; data?: boolean }[]; rows?: string[][] }
/**
* `tasks`: where a run's tasks have their turn (a map of our own): the walk goes there after the program's turn, and when
* the program's wake does not hold; it ends at `END`.
*/
export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; cells?: Region[]; templates?: { id: string; bytes: number[]; at?: number; data?: boolean }[]; rows?: string[][]; tasks?: string }
export interface Region { id: string; cells: number; values?: number[] }

/**
Expand Down
30 changes: 16 additions & 14 deletions compiler/backend/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
* went on — a trigger of a few conditions and actions in a little over 200 bytes, not 2,408. A row a doubling trigger
* walks (`Graph.rows`) is stacked as one, its triggers 2,408 bytes apart.
*/
import { END, TRIGGER_BYTES, type Act, type Cond, type Dword, type Graph, type Value } from "./graph";
import { GAME_TICK, TRIGGER_LISTS, WAKE_AT, WAKE_NEXT, WAKE_TICK } from "./emit";
import { END, NOW, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, type Act, type Cond, type Dword, type Graph, type Value } from "./graph";
import { TRIGGER_LISTS, WAKE_AT, WAKE_NEXT, WAKE_TICK } from "./emit";
import type { ScriptString } from "../runtime";

export const STRINGS = 0x191943c8;
Expand Down Expand Up @@ -274,10 +274,11 @@ export function layout(input: LayoutInput): OwnMap {
const routed = new Set(runs.flatMap((r) => [...r.routed]));
const pad = -input.strings & 3;
const base = STRINGS + input.strings + pad;
// The payload's triggers. Each run's `@wake` first: when the game's tick has reached the one the run sleeps until
// and its player is in the game (their list not empty), it sends the walk where the run goes on (its `@init`, the
// first time) with the current player theirs; otherwise on to the next run. A sleep writes `@wake`'s tick and where
// to go on; the run's end, a tick never reached. `@init`: `@wake` back to going on to the next run once woken.
// The payload's triggers. Each run's `@wake` first: when the map's count of frames (`NOW`) has reached the one the run
// sleeps until and its player is in the game (their list not empty), it sends the walk where the run goes on (its
// `@init`, the first time) with the current player theirs; otherwise on to the run's tasks, or the next run. A sleep
// writes `@wake`'s frame and where to go on; the run's end, a frame never reached. `@init`: `@wake` back to going on
// past the program once woken.
const wake = (i: number) => (i ? `@wake#${i}` : "@wake"), init = (i: number) => (i ? `@init#${i}` : "@init");
const stop = "@stop", run = "@run", entry = (p: number) => `@entry${p}`, gate = (p: number) => `@gate${p}`;
const after = (i: number) => (i + 1 < runs.length ? wake(i + 1) : stop);
Expand All @@ -286,16 +287,16 @@ export function layout(input: LayoutInput): OwnMap {
// (Where each run's graph ends: the next run.)
const ends = new Map<string, string>(runs.map((_, i) => [i ? `${END}#${i}` : END, after(i)]));
const endOf = (l: string) => ends.get(l) ?? l;
type T = { label: string; conds: Cond[]; acts: Act[]; next?: string | number; prev?: number };
type T = { label: string; conds: Cond[]; acts: Act[]; next?: string | number; prev?: number; off?: boolean };
const triggers: T[] = runs.flatMap(({ graph, owner }, i) => [
{
label: wake(i),
conds: [{ dword: { game: GAME_TICK }, compare: "atLeast", value: 0 }, { dword: { game: list(owner) + 8 }, compare: "atMost", value: 0x7fffffff }],
conds: [{ dword: { var: NOW }, compare: "atLeast", value: 0 }, { dword: { game: list(owner) + 8 }, compare: "atMost", value: 0x7fffffff }],
acts: [{ dword: { at: wake(i), field: "next" }, modifier: "set", value: { label: init(i) } }, { dword: { game: CURRENT_PLAYER }, modifier: "set", value: owner }],
next: after(i),
next: graph.tasks ?? after(i),
} satisfies T,
{ label: init(i), conds: [], acts: [{ dword: { at: wake(i), field: "next" }, modifier: "set", value: { label: after(i) } }], next: graph.triggers[0]?.label ?? after(i) } satisfies T,
...graph.triggers.map((t, k) => ({ label: t.label, conds: t.conds, acts: t.acts, next: t.next !== undefined ? endOf(t.next) : graph.triggers[k + 1]?.label ?? after(i) })),
{ label: init(i), conds: [], acts: [{ dword: { at: wake(i), field: "next" }, modifier: "set", value: { label: graph.tasks ?? after(i) } }], next: graph.triggers[0]?.label ?? after(i) } satisfies T,
...graph.triggers.map((t, k) => ({ label: t.label, conds: t.conds, acts: t.acts, next: t.next !== undefined ? endOf(t.next) : graph.triggers[k + 1]?.label ?? after(i), ...(t.off ? { off: true } : {}) })),
]);
triggers.push(
// (Every pass ends here: the game's wait before the next pass set to none, as eudplib's eudTurbo does, so the
Expand All @@ -321,8 +322,8 @@ export function layout(input: LayoutInput): OwnMap {
acts: [{ dword: { at: gate(p), field: "next" }, modifier: "set", value: { label: run } }],
next: listEnd(p),
})),
// Every entry and gate back to ending at its list's end, then the first run.
{ label: run, conds: [], acts: players.flatMap((p): Act[] => [{ dword: { at: entry(p), field: "next" }, modifier: "set", value: listEnd(p) }, { dword: { at: gate(p), field: "next" }, modifier: "set", value: listEnd(p) }]), next: wake(0) },
// Every entry and gate back to ending at its list's end, the count of frames one more, then the first run.
{ label: run, conds: [], acts: [...players.flatMap((p): Act[] => [{ dword: { at: entry(p), field: "next" }, modifier: "set", value: listEnd(p) }, { dword: { at: gate(p), field: "next" }, modifier: "set", value: listEnd(p) }]), { dword: { var: NOW }, modifier: "add", value: 1 }], next: wake(0) },
);
for (const t of runs.flatMap((r) => r.graph.triggers)) if (t.copy || t.island || t.call) throw new Error(`${t.label}: a copy, an island or a call has no place in a map of our own`);
// The triggers stacked, a row as one.
Expand Down Expand Up @@ -385,6 +386,7 @@ export function layout(input: LayoutInput): OwnMap {
const f = d.field;
if (f === "next") return t + 4;
if (f === "current") return t + 2404;
if (f === "flags") return t + 2376;
if ("cond" in f) return t + 8 + 20 * f.cond + (f.part === "player" ? 4 : f.part === "mask" ? 0 : 8);
return t + 328 + 32 * f.act + { value: 20, dest: 16, modifier: 24, mask: 0, text: 4, wav: 8, time: 12, flags: 28 }[f.part];
};
Expand Down Expand Up @@ -521,7 +523,7 @@ export function layout(input: LayoutInput): OwnMap {
dv.setUint8(pad + o + 28 - base, 20);
if (a.mask !== undefined) dv.setUint16(pad + o + 30 - base, MASKED, true);
});
w32(n + 2376, 4); // preserved
w32(n + 2376, t.off ? TRIGGER_OFF : TRIGGER_ON); // preserved (and done, one passed by)
});
for (const l of listData) l.values.forEach((v, i) => w32(l.at + 4 * l.stride * i, v));
for (const t of graphs.flatMap((g) => g.templates ?? [])) if (!input.templateAt?.has(t.id) && t.at === undefined) t.bytes.forEach((b, i) => dv.setUint8(pad + templates.get(t.id)! + i - base, b));
Expand Down
Loading
Loading