diff --git a/compiler/backend/emit.ts b/compiler/backend/emit.ts index cd006ff..a701302 100644 --- a/compiler/backend/emit.ts +++ b/compiler/backend/emit.ts @@ -18,12 +18,13 @@ * falls through one trigger whose conditions failed (13.4 ns); a branch is a trigger that sets its own next pointer to * where it goes, put back by an action at the place it went to. */ -import { STACK_DEPTH, type ArrayDecl, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type ReadSource, type Stmt, type TableCell, type TextPart, type VarDecl, type At } from "../ir"; +import { STACK_DEPTH, type ArrayDecl, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type ProgramErrors, type ReadSource, type Stmt, type TableCell, type TextPart, type VarDecl, type At } from "../ir"; import { PACK_FROM } from "../eud"; import type { Range } from "../ranges"; import { PlayerGroup, type ActionRecord, type ConditionRecord } from "../../vendor/triggers"; import { actionDef } from "../../vendor/triggerDefs"; -import { END, MOST_ACTS, MOST_CONDS, STACK, SWITCHES, TRIGGER_BYTES, checkGraph, nextOf, type Act, type ActPart, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; +import { OVERFLOW_FN, errorLine } from "./errors"; +import { END, MOST_ACTS, MOST_CONDS, NOW, STACK, SWITCHES, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, checkGraph, nextOf, type Act, type ActPart, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; /** The game's tick (frames since the game started), and the fields of the trigger a map of our own wakes by. */ export const GAME_TICK = 0x57f23c; @@ -166,7 +167,6 @@ function side(e: NumExpr): Side | null { function arrayWhy(a: ArrayDecl): string | undefined { if (a.texts) return "an array of texts"; if (a.dynamic || a.through) return "an array that grows"; - if (a.task !== undefined) return "an array of a task's"; if (a.kind !== "number" && a.kind !== "boolean") return `an array of ${a.kind}s`; return undefined; } @@ -582,7 +582,8 @@ class Scope { const d = this.decls.get(id); // (A boolean is a cell holding 1 or 0, as a number is.) // (A `u8` / `u16` in a map of our own: every store into it stopped at its top first, `Rewriter.top`.) - return !!d && (d.kind === "number" || d.kind === "boolean") && (!d.bits || this.native) && d.task === undefined && !d.held; + // (A task's variable in a map of our own: each slot's own, `planTasks`.) + return !!d && (d.kind === "number" || d.kind === "boolean") && (!d.bits || this.native) && (d.task === undefined || this.native) && !d.held; } stmts(list: Stmt[], inLoop: boolean): boolean { @@ -668,6 +669,11 @@ class Scope { returning: string[] = []; /** Whether a call that may come back into its own function is taken: the program then keeps a stack. */ recursion = false; + /** A map of our own whose program throws or catches: where the error in flight is kept (`ownErrors`). */ + errors?: ProgramErrors; + /** A map of our own: each `start`'s slots (`planTasks`), and the starts that run once — one slot, never busy. */ + slots = new Map(); + once = new Set(); /** * A call in a map of our own: its arguments written into the function's parameters, which are variables like any @@ -680,6 +686,11 @@ class Scope { if (c.saves.arrays.length) return this.no(`a growing array declared in ${c.saves.within}, which calls itself`); for (const id of c.saves.vars) if (!this.ok(id)) return this.no(`the variable ${this.decls.get(id)?.name ?? id} of ${c.saves.within}, which calls itself`); this.recursion = true; + // (A program that keeps errors throws a RangeError where the stack runs out: the function that sets it.) + if (this.errors) { + if (!this.function(OVERFLOW_FN)) return false; + this.written.add(errorLine(this.errors)); + } } for (const p of c.params) { const d = p.decl; @@ -711,6 +722,8 @@ class Scope { if (state !== undefined) return state || this.no(`a call of ${f?.name ?? id}`); const body = this.bodyOf?.(id); if (!f || !body) return this.no(`a call of ${id}`); + // (A called function is laid out once, outside every coroutine: one that waits could not say whose turn it gives back.) + if (this.slots.size && holdsKind(body, "sleep")) return this.no(`a call of ${f.name}, which waits, in a program with tasks`); this.taken.set(id, "walking"); let ok = true; if (f.result) { @@ -725,6 +738,25 @@ class Scope { return ok; } + /** A task's call (a map of our own): its parameters, the slot's own variables, which the start writes; its body. */ + taskCall(c: Call): boolean { + for (const p of c.params) { + const d = p.decl; + if (!this.ok(d.id)) return this.no(`the parameter ${d.name}, a ${d.kind}`); + if (d.kind === "boolean" ? !this.test(p.init as BoolExpr) : !this.lowers(p.init as NumExpr)) return this.no(`the value given to ${d.name}`); + if (d.kind !== "boolean") this.num(p.init as NumExpr); + this.declared.add(d.id); + this.written.add(d.id); + } + // (What a task gives back nothing reads: its `return` only ends it.) + this.returning.push("void"); + try { + return this.stmts(c.body, false); + } finally { + this.returning.pop(); + } + } + /** * A part of a printed text a map of our own takes: a text, a whole number (signed or a `u32`) the emitter lowers, a * byte from one (a colour code chosen as the map is played). Not yet a fixed number, a player's name or colour, a @@ -1079,6 +1111,16 @@ class Scope { return true; } case "return": return (this.native && this.returning.length > 0 && this.returned(s)) || this.island(s); + // (A map of our own: the slots `planTasks` made; listening — `on.*` — is part 3's.) + case "start": { + const slots = this.native && !s.listen ? this.slots.get(s) : undefined; + return (!!slots && slots.every((slot) => this.taskCall(slot.call))) || this.island(s); + } + case "cancel": return (this.native && this.lowers(s.task) && (this.num(s.task), true)) || this.island(s); + case "parallel": return (this.native && s.branches.every((b) => this.stmts(b, false))) || this.island(s); + // (Where a `throw` goes is the emitter's, `Emitter.handler`: the program keeps the error in flight.) + case "try": return (this.native && !!this.errors && this.stmts(s.body, inLoop) && this.stmts(s.catch, inLoop)) || this.island(s); + case "throw": return (this.native && !!this.errors) || this.island(s); default: return this.island(s); } } @@ -1111,7 +1153,8 @@ class Scope { // ── the layout ── /** A write of a variable, laid out where the statement was, expanded once every place of the variable is known. */ -interface Write { target: string; value: Linear; at?: At } +/** `cold`: written where only an error goes (`Emitter.cold`): what it reads is refreshed from its cell before it, never a place of it. */ +interface Write { target: string; value: Linear; at?: At; cold?: boolean } /** An action of an expanded write: it writes `into`; a variable it reads plus a constant is in its own number. */ interface WriteAct { into: Place; modifier: "set" | "add"; value: Num; reads?: Read } /** @@ -1156,13 +1199,30 @@ interface Store { array: ArrayDecl; index: Linear; reads: CellRead[]; value: Lin /** (`at` on a copy or a call: the statement it is for, when its trigger is another's.) */ /** (`label`: every place of `target` set to where `to` is — where a function that calls itself goes back to.) */ /** (`route`: a variable's cell into these dwords, and nothing else of its places.) */ -type Item = { act: Act } | { write: Write } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At } | { island: Stmt; writes: Set } | { bump: { target: string; add: number } } | { call: { fn: FixedFn; target: string }; at?: At } | { label: { target: string; to: string } } | { route: { from: string; into: Dword[] } }; +type Item = { act: Act } | { write: Write } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At } | { island: Stmt; writes: Set } | { bump: { target: string; add: number } } | { call: { fn: FixedFn; target: string }; at?: At } | { label: { target: string; to: string } } | { route: { from: string; into: Dword[]; add?: number } }; /** * `keep`: an empty trigger that stays (a jump whose next pointer a place sets). `guarded`: an `if` in one trigger (its * conditions, its statements' actions), which may be followed by the copies of what it writes — a copy whether or not * the conditions held, which writes back what the places hold when they did not. */ -interface Proto { label: string; conds: Cond[]; items: Item[]; next?: string; at?: At; depth?: number; keep?: boolean; guarded?: boolean; guardOf?: Stmt } +/** `off`: a trigger laid out done (`Trigger.off`): a task's slot or a branch's, switched on by what starts it. */ +interface Proto { label: string; conds: Cond[]; items: Item[]; next?: string; at?: At; depth?: number; keep?: boolean; guarded?: boolean; guardOf?: Stmt; cold?: boolean; off?: boolean } + +/** + * A coroutine (TrigScript 4, step 11): where its wake keeps the frame it sleeps until and where it goes on from — the + * fields a sleep writes — the action a resume puts its wake back with, where its turn goes when it sleeps, and what its + * end lays out. The program's wake is its run's `@wake` (`layout.ts`); a task's slot and a branch of a `Promise.all` or + * `race` have a trigger of their own, "the count of frames has reached W: go to R", passed by while done. + */ +interface Co { tick: Dword; at: Dword; reset: Act; yieldTo: string; end: () => void } + +/** + * One of a `start`'s slots, as `emitBody` plans them: the task's call as the slot runs it (variables of its own but in + * the first slot), its trigger in the tasks' turn, where its body starts, its generation and the number it is known by + * while it runs (`slot | task << 8 | generation << 16`; 0 when nothing is in it). + */ +interface TaskSlot { call: Call; check: string; entry: string; gen: string; handle: string } +type StartStmt = Extract; /** * A place of a variable: a dword that holds it times `scale` plus `add` (wrapping) — known (its cell, a home, a @@ -1219,7 +1279,8 @@ function inRegion(arrays: Map, id: string): boolean { * top first (`Rewriter.top`). */ function nativeArray(a: ArrayDecl): boolean { - return (a.kind === "number" || a.kind === "boolean") && !a.dynamic && !a.texts && !a.through && a.task === undefined; + // (A task's: in place, its start having one slot — `planTasks`.) + return (a.kind === "number" || a.kind === "boolean") && !a.dynamic && !a.texts && !a.through; } /** The fixed-point functions the emitter makes as its own triggers rather than calling the Python side's. */ @@ -1286,12 +1347,40 @@ class Emitter { * it starts, and the trigger it ends at, whose next pointer every call sets to where it goes on — and those still * to lay out. `depth`: the most loops around a call of it, which its triggers count as theirs. */ - fns = new Map(); + fns = new Map(); pending: string[] = []; /** The loops around the function being laid out, at its deepest call. */ depthBase = 0; /** The function or inlined call being laid out, innermost last: the variable its `return` writes, where it goes. */ frames: { result?: { id: string; kind: string }; end: string }[] = []; + /** Where an error thrown here goes: the `catch` of each `try` around, in the function being laid out, innermost last. */ + handlers: string[] = []; + /** + * Inside statements only an error goes through — those that end in a `throw`, and a `catch` (a map of our own): what + * they read of a variable is refreshed from its cell where it is read, as a routed variable's is, never a place that + * every write of the variable keeps — a throw's message made from a parameter cost every call of the function an + * action and a copy, for an error that seldom comes. + */ + cold = 0; + /** The coroutine being laid out: the program's (`emitBody`), a task's slot (`tasks()`), a branch (`parallel()`). */ + co!: Co; + /** The starts laid out, by task number, and those whose slots' bodies are still to lay out; inside a task's body, how deep. */ + pools = new Map(); + pendingTasks: number[] = []; + inTask = 0; + /** Whether a task starts one (`again`): the tasks' turn goes round again, for it to have its first turn in this frame too. */ + again = false; + /** Where the program's turn goes when it sleeps or ends: the tasks' turn (or the run's end, with no task). */ + get tasksLabel(): string { return `${this.key}:tasks`; } + get againVar(): string { return `${this.key}.again`; } + /** + * Of each function an error comes out of from a trigger of its own (`thrown`), the actions of the calls that point + * that trigger's next pointer at where the error goes from each: where every call names the same place, the + * trigger is given it when built, and the calls set nothing (`settleThrown`). + */ + thrownSets = new Map(); + thrownProtos = new Map(); + private throwing?: Set; /** * A map of our own whose functions call themselves: a stack each, by function — its id, the variable of the * emitter's own that holds the cells in use (`sp`: what addresses the stack — a store's destination, a cell's @@ -1357,7 +1446,7 @@ class Emitter { } push(p: Omit & { label?: string }): Proto { - const t: Proto = { label: p.label ?? this.label("t"), conds: p.conds, items: p.items, next: p.next, at: p.at ?? this.at, depth: this.depthBase + this.loops.length, ...(p.keep ? { keep: true } : {}), ...(p.guarded ? { guarded: true } : {}), ...(p.guardOf ? { guardOf: p.guardOf } : {}) }; + const t: Proto = { label: p.label ?? this.label("t"), conds: p.conds, items: p.items, next: p.next, at: p.at ?? this.at, depth: this.depthBase + this.loops.length, ...(p.keep ? { keep: true } : {}), ...(p.guarded ? { guarded: true } : {}), ...(p.guardOf ? { guardOf: p.guardOf } : {}), ...(this.cold ? { cold: true } : {}), ...(p.off ? { off: true } : {}) }; this.protos.push(t); return t; } @@ -1379,7 +1468,7 @@ class Emitter { } write(target: string, value: Linear, copy = true, into: Item[] = this.open().items, copies = into): void { - const w: Write = { target, value, ...(this.at ? { at: this.at } : {}) }; + const w: Write = { target, value, ...(this.at ? { at: this.at } : {}), ...(this.cold ? { cold: true } : {}) }; // (The copy is there for a variable that turns out to need one: the rest leave it out.) into.push({ write: w }); if (copy) copies.push({ copy: target, ...(this.at ? { at: this.at } : {}) }); @@ -1594,16 +1683,25 @@ class Emitter { // ── statements ── stmts(list: Stmt[]): void { - for (const s of list) this.stmt(s); + const cold = this.scope.native && !!this.scope.errors && throwsAtEnd(list); + if (cold) this.cold++; + try { + for (const s of list) this.stmt(s); + } finally { + if (cold) this.cold--; + } } stmt(s: Stmt): void { - const outer = this.at; + const outer = this.at, cold = this.cold; if (s.at) this.at = s.at; + // (A loop may turn many times on an error's way too: it, its test and its body, are not cold.) + if (s.kind === "while" || s.kind === "for" || s.kind === "do") this.cold = 0; try { this.statement(s); } finally { this.at = outer; + this.cold = cold; } } @@ -1723,7 +1821,9 @@ class Emitter { // What of the test nothing in the loop changes is tested once, on the way in (a u32 counter against a signed // bound is the bound's sign and the counter's place: the sign does not change). // (Everything the loop may write: a boolean set from a test, what a call in it writes, all the way down.) - const { once, every } = s.cond ? split(s.cond, writes({ kind: "block", body: [...s.body, ...(s.kind === "for" ? s.update : [])], at: s.at }, this.scope.functions)) : { once: null, every: null }; + // (Not in a loop that sleeps: while it sleeps, a task or a branch may write anything it reads.) + const sleeps = holdsKind(s.body, "sleep") || (s.kind === "for" && holdsKind(s.update, "sleep")); + const { once, every } = !s.cond ? { once: null, every: null } : sleeps ? { once: null, every: s.cond } : split(s.cond, writes({ kind: "block", body: [...s.body, ...(s.kind === "for" ? s.update : [])], at: s.at }, this.scope.functions)); if (once) this.jump(once, false, exit, s.at); this.land(head); if (every) this.jump(every, false, exit, s.at); @@ -1784,22 +1884,33 @@ class Emitter { } case "sleep": { if (!this.scope.native) return this.left(s); - // A map of our own (layout.ts): the tick the program wakes at (the game's tick now, a bit at a time, plus the - // frames) and where it goes on, written into the trigger that heads the owner's list every pass; then this - // pass ends. Where it goes on puts that trigger back to ending the pass. - const frames = s.cycles ?? Math.max(1, Math.round(((s.ms ?? 0) * FRAMES_PER_SECOND) / 1000)); - const resume = this.label("wake"); - this.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: frames } }, { act: { dword: { data: WAKE_AT }, modifier: "set", value: { label: resume } } }); - for (let bit = 0; bit < 32; bit++) this.push({ conds: [{ dword: { game: GAME_TICK }, compare: "atLeast", value: 1, mask: 2 ** bit }], items: [{ act: { dword: { data: WAKE_TICK }, modifier: "add", value: 2 ** bit } }] }); - this.goto(END); - this.push({ label: resume, conds: [], items: [{ act: { dword: { data: WAKE_NEXT }, modifier: "set", value: { label: END } } }] }); - return; + return this.sleepIn(this.co, s.cycles ?? Math.max(1, Math.round(((s.ms ?? 0) * FRAMES_PER_SECOND) / 1000))); } case "print": if (this.scope.native && !this.scope.islands.has(s)) return this.print(s); return this.left(s); case "break": return this.goto(this.loops[this.loops.length - 1].exit); case "continue": return this.goto(this.loops[this.loops.length - 1].cont); + case "start": return this.scope.islands.has(s) ? this.left(s) : this.start(s); + case "cancel": return this.scope.islands.has(s) ? this.left(s) : this.cancel(s); + case "parallel": return this.scope.islands.has(s) ? this.left(s) : this.parallel(s); + case "try": { + // The body, with its `catch` as where an error thrown in it goes; the `catch` after it, reached only that way. + // (Nothing of it is done while the game runs until an error is thrown.) + const caught = this.label("catch"), end = this.label("tend"); + this.handlers.push(caught); + this.stmts(s.body); + this.handlers.pop(); + this.goto(end); + this.land(caught); + this.cold++; + this.stmts(s.catch); + this.cold--; + this.land(end); + return; + } + // (The error in flight is set by the statements before it, the line it was thrown at among them: `ownErrors`.) + case "throw": return this.goto(this.handler()); default: return this.left(s); } } @@ -2107,6 +2218,12 @@ class Emitter { } fn.depth = Math.max(fn.depth, this.depthBase + this.loops.length); const back = this.label("back"); + // An error out of a function that never comes back into itself goes on from a trigger of the function's own, + // whose next pointer the call points at where the error goes from here: nothing is looked at on the way back. + // After a call of one that may come back, and after a call that keeps a frame — which takes it back first — the + // error in flight is looked at. + const throws = this.mayThrow(c.fn), routed = throws && !f.recursive; + if (routed && !fn.thrown) fn.thrown = this.label("fthrow"); // (A function that calls itself also keeps where it goes back to in a variable, which a frame keeps: the frame // of a call that may come back is the function's the call is in.) const ra = f.recursive ? this.returnOf(c.fn, fn.exit) : null; @@ -2114,10 +2231,19 @@ class Emitter { if (c.saves && !within) throw new Error(`emitter: a call of ${f.name} that keeps a frame, outside a function`); const kept = c.saves ? this.keep(c, this.returnOf(this.current!, within!.exit)) : null; if (kept) this.arguments(c); - this.open().items.push(ra ? { label: { target: ra, to: back } } : { act: { dword: nextOf(fn.exit), modifier: "set", value: { label: back } } }); + const items = this.open().items; + items.push(ra ? { label: { target: ra, to: back } } : { act: { dword: nextOf(fn.exit), modifier: "set", value: { label: back } } }); + if (routed) { + const to = kept ? back : this.handler(), item: Item = { act: { dword: nextOf(fn.thrown!), modifier: "set", value: { label: to } } }; + items.push(item); + let sets = this.thrownSets.get(c.fn); + if (!sets) this.thrownSets.set(c.fn, (sets = [])); + sets.push({ items, item, to }); + } this.goto(fn.entry); this.land(back); if (kept) this.bringBack(kept); + if (throws && (!routed || kept)) this.jump(this.inFlight(), true, this.handler(), this.at); if (c.result && f.result) { // What the function gives back goes into its cell alone, and each call reads it from there: as places, a // return would write every call's, and each of those actions would be a place of what the return reads — @@ -2185,11 +2311,16 @@ class Emitter { const room = this.label("room"); this.jump({ kind: "compare", op: "<", left: { kind: "var", id: this.depth }, right: { kind: "const", value: this.stackDepth }, unsigned: true, at: this.at!, label: "" }, true, room, this.at); const line = this.at?.line ?? "?"; - this.open().items.push( - { act: { dword: { game: 0 }, modifier: "set", value: 0, display: `\x06TrigScript: stack overflow in ${c.saves!.within}, line ${line} - ${this.stackDepth} calls deep. The program has stopped.` } }, - { act: { dword: { data: WAKE_TICK }, modifier: "set", value: U32 - 1 } }, - ); - this.goto(END); + if (this.scope.errors) { + // A program that keeps errors throws a RangeError, as JavaScript does: where the call is, before anything of it + // is kept. + this.call({ fn: OVERFLOW_FN, name: "(stack overflow)", params: [], body: [], at: this.at!, label: "" }); + this.write(errorLine(this.scope.errors), { c: this.at?.line ?? 0, terms: [] }); + this.goto(this.handler()); + } else { + this.open().items.push({ act: { dword: { game: 0 }, modifier: "set", value: 0, display: `\x06TrigScript: stack overflow in ${c.saves!.within}, line ${line} - ${this.stackDepth} calls deep. The program has stopped.` } }); + this.co.end(); + } this.land(room); // (Each variable through one of the emitter's own, written just before: a store's number is a place no refresh // reaches, and the variable itself may have to be read from its cell where it is used.) @@ -2528,6 +2659,271 @@ class Emitter { this.goto(frame.end); } + // ── coroutines and tasks (a map of our own: TrigScript 4, step 11) ── + + /** + * A sleep of a coroutine: the frame it wakes at — the map's count of frames now plus these, one copy — and where it + * goes on, written into its wake; then its turn is given back. Where it goes on puts its wake back as it was, and goes + * on to `then` when there is one. + */ + sleepIn(co: Co, frames: number, then?: string): void { + const resume = this.label("wake"); + this.open().items.push({ route: { from: NOW, into: [co.tick], add: frames } }, { act: { dword: co.at, modifier: "set", value: { label: resume } } }); + this.goto(co.yieldTo); + this.push({ label: resume, conds: [], items: [{ act: co.reset }], ...(then ? { next: then } : {}) }); + } + + /** A trigger of a slot's or a branch's that says whether nothing runs in it (done) or something does. */ + private flags(check: string, on: boolean): Item { + return { act: { dword: { at: check, field: "flags" }, modifier: "set", value: on ? TRIGGER_ON : TRIGGER_OFF } }; + } + + /** + * A task into a free slot of its start: the arguments as they are now into the slot's parameters, its generation one + * more, the number it is known by, and its trigger in the tasks' turn switched on, due now, at the body's start. With + * every slot running nothing starts: the game says so once, and the number is 0, as the Python side's build does. A + * start that runs once has one slot, which is free. + */ + start(s: StartStmt): void { + const slots = this.scope.slots.get(s)!; + if (!this.pools.has(s.task)) { + this.pools.set(s.task, { start: s, slots }); + this.pendingTasks.push(s.task); + } + if (this.inTask) this.again = true; + if (this.scope.once.has(s)) { + this.arguments(slots[0].call); + return this.startSlot(s, slots[0], 0); + } + // (The arguments into variables of the emitter's own first: the slot is chosen after.) + const temps = slots[0].call.params.map((p) => { + const t = `${this.key}.ta${this.n++}`; + this.own.add(t); + if (p.decl.kind === "boolean") this.setBool(t, p.init as BoolExpr); + else this.write(t, linear(p.init as NumExpr)!); + return t; + }); + const done = this.label("started"), into = slots.map(() => this.label("slot")); + slots.forEach((slot, k) => this.branch([{ dword: { at: slot.check, field: "flags" }, compare: "atLeast", value: 8, mask: 8 }], into[k], this.at)); + const said = `${this.key}.said${s.task}`, quiet = this.label("quiet"); + this.own.add(said); + this.jump({ kind: "compare", op: ">=", left: { kind: "var", id: said }, right: { kind: "const", value: 1 }, unsigned: true, at: this.at!, label: "" }, true, quiet, this.at); + this.open().items.push({ act: { dword: { game: 0 }, modifier: "set", value: 0, display: `\x06TrigScript: ${s.call.name ?? "a task"} did not start, line ${this.at?.line ?? "?"} - all ${slots.length} of this start's tasks are running.` } }); + this.write(said, { c: 1, terms: [] }); + this.land(quiet); + if (s.handle) this.write(s.handle, { c: 0, terms: [] }); + this.goto(done); + slots.forEach((slot, k) => { + this.land(into[k]); + slot.call.params.forEach((p, j) => (p.decl.kind === "boolean" ? this.setBool(p.decl.id, { kind: "var", id: temps[j] }) : this.write(p.decl.id, { c: 0, terms: [{ id: temps[j], k: 1 }] }))); + this.startSlot(s, slot, k); + this.goto(done); + }); + this.land(done); + } + + private startSlot(s: StartStmt, slot: TaskSlot, k: number): void { + // Its generation one more, from 1 again past 65 535; the number it is known by from it. + const past = this.label("gen"); + this.jump({ kind: "compare", op: "<", left: { kind: "var", id: slot.gen }, right: { kind: "const", value: 0xffff }, unsigned: true, at: this.at!, label: "" }, true, past, this.at); + this.write(slot.gen, { c: 0, terms: [] }); + this.land(past); + this.write(slot.gen, { c: 1, terms: [{ id: slot.gen, k: 1 }] }); + this.write(slot.handle, { c: k + 256 * s.task, terms: [{ id: slot.gen, k: 65536 }] }); + this.open().items.push( + { act: { dword: { at: slot.check, field: { cond: 0 } }, modifier: "set", value: 0 } }, + { act: { dword: { at: slot.check, field: { act: 0, part: "value" } }, modifier: "set", value: { label: slot.entry } } }, + this.flags(slot.check, true), + ); + if (this.inTask) this.write(this.againVar, { c: 1, terms: [] }); + if (s.handle) this.write(s.handle, { c: 0, terms: [{ id: slot.handle, k: 1 }] }); + } + + /** `t.cancel()`: the slot whose task the number names, while that task is in it, switched off — it is not resumed again. */ + cancel(s: Extract): void { + const h = s.task; + for (const slot of [...this.scope.slots.values()].flat()) { + const past = this.label("nc"); + this.jump({ kind: "and", items: [{ kind: "compare", op: ">=", left: h, right: { kind: "const", value: 1 }, unsigned: true, at: this.at!, label: "" }, { kind: "compare", op: "==", left: h, right: { kind: "var", id: slot.handle }, at: this.at!, label: "" }] }, false, past, this.at); + this.open().items.push(this.flags(slot.check, false)); + this.write(slot.handle, { c: 0, terms: [] }); + this.land(past); + } + } + + /** + * `Promise.all` and `Promise.race`: each branch a coroutine of its own, with a trigger that wakes it, all due as the + * statement is reached, each run in order until it sleeps; then, until all have ended (or one has, a race), the + * coroutine around sleeps a frame at a time and runs again those still running, in order — as the simulator does. A + * race goes on from the end of the first branch to end; the others are switched off where they are. + */ + parallel(s: Extract): void { + const outer = this.co, race = s.mode === "race"; + const checks = s.branches.map(() => this.label("br")), nexts = checks.map((c) => `${c}:after`), entries = checks.map(() => this.label("bre")); + const dispatch = this.label("disp"), test = this.label("ptest"), after = this.label("pafter"); + const items = this.open().items; + checks.forEach((c, j) => items.push( + { act: { dword: { at: c, field: { cond: 0 } }, modifier: "set", value: 0 } }, + { act: { dword: { at: c, field: { act: 0, part: "value" } }, modifier: "set", value: { label: entries[j] } } }, + this.flags(c, true), + )); + this.land(dispatch); + checks.forEach((c, j) => { + this.push({ label: c, conds: [{ dword: { var: NOW }, compare: "atLeast", value: 0 }], items: [{ act: { dword: nextOf(c), modifier: "set", value: { label: entries[j] } } }], next: nexts[j], off: true }); + this.push({ label: nexts[j], conds: [], items: [], next: j + 1 < checks.length ? checks[j + 1] : test }); + }); + this.land(test); + if (!race) { + // (All ended: on at once.) + const wait = this.label("pwait"); + for (const c of checks) this.branch([{ dword: { at: c, field: "flags" }, compare: "atMost", value: 0, mask: 8 }], wait, this.at); + this.goto(after); + this.land(wait); + } + this.sleepIn(outer, 1, dispatch); + s.branches.forEach((b, j) => { + const reset: Act = { dword: nextOf(checks[j]), modifier: "set", value: { label: nexts[j] } }; + this.push({ label: entries[j], conds: [], items: [{ act: reset }] }); + this.co = { tick: { at: checks[j], field: { cond: 0 } }, at: { at: checks[j], field: { act: 0, part: "value" } }, reset, yieldTo: nexts[j], end: outer.end }; + this.stmts(b); + this.co = outer; + this.open().items.push(...(race ? checks : [checks[j]]).map((c) => this.flags(c, false))); + this.goto(race ? after : nexts[j]); + }); + this.land(after); + } + + /** + * Each start's slots' bodies, laid out once each after the program, as a called function's are: the task's call, its + * coroutine the slot's — its wake the slot's trigger in the tasks' turn — and at its end the slot switched off and the + * number it was known by 0. + */ + tasks(): void { + for (let task = this.pendingTasks.shift(); task !== undefined; task = this.pendingTasks.shift()) { + for (const slot of this.pools.get(task)!.slots) { + const after = `${slot.check}:after`; + const reset: Act = { dword: nextOf(slot.check), modifier: "set", value: { label: after } }; + const end = () => { + this.open().items.push(this.flags(slot.check, false)); + this.write(slot.handle, { c: 0, terms: [] }); + this.goto(after); + }; + const outer = { co: this.co, handlers: this.handlers, cold: this.cold, loops: this.loops, frames: this.frames, current: this.current, depthBase: this.depthBase }; + this.co = { tick: { at: slot.check, field: { cond: 0 } }, at: { at: slot.check, field: { act: 0, part: "value" } }, reset, yieldTo: after, end }; + this.handlers = []; + this.cold = 0; + this.loops = []; + this.frames = []; + this.current = undefined; + this.depthBase = 0; + this.inTask++; + this.push({ label: slot.entry, conds: [], items: [{ act: reset }] }); + const done = this.label("tend"); + this.frames.push({ end: done }); + this.stmts(slot.call.body); + this.frames.pop(); + this.land(done); + end(); + this.inTask--; + Object.assign(this, outer); + } + } + } + + /** + * The tasks' turn, after the program's: each start's slots, in the order of the starts' numbers and then of their + * slots — a slot that is free passed by as done, one asleep a condition that fails, one awake sent into its body — and, + * when a task can start one, round again while one was started, for it to have its first turn in this frame too. + * Returns where it starts. + */ + turnOfTasks(): string | undefined { + const slots = [...this.pools.values()].sort((a, b) => a.start.task - b.start.task).flatMap((p) => p.slots); + const round = this.label("again"); + this.push({ label: this.tasksLabel, conds: [], items: [], next: slots.length ? slots[0].check : END }); + slots.forEach((slot, i) => { + this.push({ label: slot.check, conds: [{ dword: { var: NOW }, compare: "atLeast", value: 0 }], items: [{ act: { dword: nextOf(slot.check), modifier: "set", value: { label: slot.entry } } }], next: `${slot.check}:after`, off: true }); + this.push({ label: `${slot.check}:after`, conds: [], items: [], next: i + 1 < slots.length ? slots[i + 1].check : this.again ? round : END }); + }); + if (this.again) { + // (A jump is put back where it lands: not at the graph's end, which is no trigger of its own.) + const more = this.label("more"); + this.land(round); + this.own.add(this.againVar); + this.jump({ kind: "compare", op: ">=", left: { kind: "var", id: this.againVar }, right: { kind: "const", value: 1 }, unsigned: true, at: this.at!, label: "" }, true, more, this.at); + this.goto(END); + this.land(more); + this.write(this.againVar, { c: 0, terms: [] }); + this.goto(this.tasksLabel); + } + return slots[0]?.check; + } + + /** + * Where an error thrown here goes: the `catch` of the nearest `try` around it, or out of the function being laid out + * — from its trigger for errors (`thrown`), or, for one that may come back into itself, from its end, where the call + * looks at the error in flight. (A program's body is all inside a `try` of the front end's.) + */ + handler(): string { + const h = this.handlers[this.handlers.length - 1]; + if (h !== undefined) return h; + const fn = this.current !== undefined ? this.fns.get(this.current) : undefined; + if (!fn) throw new Error("emitter: an error with nothing to go to"); + return fn.thrown ?? fn.exit; + } + + /** Whether a variable has a cell its reads can be refreshed from where they are (a map of our own: all but a function's arguments). */ + hasCell(id: string): boolean { + const d = this.placesOf(id)[0].dword; + return !!d && "var" in d; + } + + /** Whether an error is in flight: its class's code is not 0. */ + inFlight(): BoolExpr { + return { kind: "compare", op: ">=", left: { kind: "var", id: this.scope.errors!.thrown }, right: { kind: "const", value: 1 }, unsigned: true, at: this.at!, label: "" }; + } + + /** + * Whether an error may come out of a called function: one with a `throw` outside every `try` of its own, or a call + * that may run out of stack, or a call of such a function — worked out for every function the program reaches at + * once, until nothing more comes in. + */ + mayThrow(fn: string): boolean { + if (!this.scope.errors) return false; + if (!this.throwing) { + const out = new Set(); + const escapes = (x: unknown): boolean => { + if (!x || typeof x !== "object") return false; + if (Array.isArray(x)) return x.some(escapes); + const o = x as Record; + if (o.kind === "throw" || o.saves) return true; + // (What the body of a `try` throws, its `catch` takes; what the `catch` throws goes on.) + if (o.kind === "try") return escapes(o.catch); + if (typeof o.fn === "string" && Array.isArray(o.params) && out.has(o.fn)) return true; + return Object.values(o).some((v) => v && typeof v === "object" && escapes(v)); + }; + const ids = [...this.scope.taken.keys()]; + for (let grew = true; grew; ) { + grew = false; + for (const id of ids) if (!out.has(id) && escapes(this.scope.bodyOf!(id))) { out.add(id); grew = true; } + } + this.throwing = out; + } + return this.throwing.has(fn); + } + + /** + * A function whose calls all point its trigger for errors at the same place: that trigger goes there as built, and + * the calls' actions go. + */ + settleThrown(): void { + for (const [id, sets] of this.thrownSets) { + const proto = this.thrownProtos.get(id); + if (!proto || new Set(sets.map((x) => x.to)).size !== 1) continue; + for (const x of sets) x.items.splice(x.items.indexOf(x.item), 1); + proto.next = sets[0].to; + } + } + /** * The functions the calls reached, each laid out once after the program (which never falls into them): its body, * what it gives back off its end (0), and the trigger it ends at — empty, kept, its next pointer the call's. A @@ -2543,11 +2939,19 @@ class Emitter { this.land(fn.entry); const result = f.result ? { id: f.result.decl.id, kind: f.result.kind } : undefined; this.frames.push({ ...(result ? { result } : {}), end: fn.exit }); + // (No `try` of the caller's is around the function's body: an error leaves it first. Nor is it cold for its call.) + const handlers = this.handlers, cold = this.cold; + this.handlers = []; + this.cold = 0; this.stmts(body); + this.handlers = handlers; + this.cold = cold; this.frames.pop(); const last = body.filter((s) => s.kind !== "remark").at(-1); if (result && last?.kind !== "return") this.write(result.id, { c: 0, terms: [] }); this.push({ label: fn.exit, conds: [], items: [], keep: true, at: f.at }); + // (Its trigger for errors, its next pointer the call's, as the end's is.) + if (fn.thrown) this.thrownProtos.set(id, this.push({ label: fn.thrown, conds: [], items: [], keep: true, at: f.at })); this.depthBase = outer; } } @@ -3713,7 +4117,8 @@ class Emitter { const self = w.value.terms.filter((t) => t.id === target); if (self.length && self[0].k !== 1) copied.add(target); for (const t of w.value.terms) { - if (t.id === target) continue; + // (A write only an error goes through reads its values from their cells: no place of them, `cold`.) + if (t.id === target || (w.cold && this.hasCell(t.id))) continue; if (!readers.has(t.id)) readers.set(t.id, new Set()); readers.get(t.id)!.add(target); } @@ -3854,9 +4259,14 @@ class Emitter { graph(entries: string[]): Graph | string { // A comparison's number is a place of what it reads: its trigger keeps its label and the condition its index. + // (In a trigger only an error goes through, refreshed from the variable's cell before it instead: `coldConds`.) + const coldConds = new Map>(); for (const p of this.protos) p.conds.forEach((c, k) => { const r = this.condReads.get(c); - if (r) this.placesOf(r.id).push({ dword: { at: p.label, field: { cond: k } }, scale: r.scale, add: r.add }); + if (r && p.cold && this.hasCell(r.id)) { + const of = coldConds.get(p.label) ?? new Map(); + coldConds.set(p.label, of.set(r.id, [...(of.get(r.id) ?? []), { dword: { at: p.label, field: { cond: k } }, scale: r.scale, add: r.add }])); + } else if (r) this.placesOf(r.id).push({ dword: { at: p.label, field: { cond: k } }, scale: r.scale, add: r.add }); const q = this.condPlayers.get(c); if (q) this.placesOf(q.id).push({ dword: { at: p.label, field: { cond: k, part: "player" } }, scale: q.scale, add: q.add, base: q.base }); }); @@ -3896,10 +4306,16 @@ class Emitter { // Each target's writes expanded once all its places are known — the writes that read it come first in the // order, and each action that reads a variable is a place of it. const expanded = new Map(); + /** The actions of writes only an error goes through that read a variable: refreshed from its cell before them. */ + const coldActs = new Set(); for (const v of order) for (const w of this.writes.get(v) ?? []) { const acts = this.expand(w, copied.has(v)); expanded.set(w, acts); - for (const a of acts) if (a.reads) this.placesOf(a.reads.id).push({ act: a, scale: a.reads.scale, add: a.reads.add, ...(a.reads.base ? { base: a.reads.base } : {}) }); + for (const a of acts) { + if (!a.reads) continue; + if (w.cold && this.hasCell(a.reads.id)) coldActs.add(a); + else this.placesOf(a.reads.id).push({ act: a, scale: a.reads.scale, add: a.reads.add, ...(a.reads.base ? { base: a.reads.base } : {}) }); + } } // Every fall-through said, so that dropping a trigger leaves the one before it going where it went. this.protos.forEach((p, i) => { if (p.next === undefined) p.next = this.protos[i + 1]?.label ?? END; }); @@ -3946,7 +4362,7 @@ class Emitter { } // What a trigger's conditions read of routed variables: the places refreshed before it. const condReads = (p: Proto): Map => { - const out = new Map(); + const out = new Map(coldConds.get(p.label) ?? []); for (const id of routed) for (const pl of this.places.get(id)!.slice(1)) { const d = pl.dword; const here = d && (("at" in d && d.at === p.label && typeof d.field === "object" && "cond" in d.field) || ("data" in d && p.conds.some((c) => "data" in c.dword && c.dword.data === d.data))); @@ -3977,7 +4393,7 @@ class Emitter { const fixed = new Set(); let n = 0; for (const p of kept) { - type Piece = { acts: (Act | WriteAct)[] } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At; island?: boolean; top?: number } | { island: Stmt } | { call: { fn: FixedFn; target: string }; at?: At } | { refresh: string; places: (Place | WriteAct)[] } | { route: { from: string; into: Dword[] } }; + type Piece = { acts: (Act | WriteAct)[] } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At; island?: boolean; top?: number } | { island: Stmt } | { call: { fn: FixedFn; target: string }; at?: At } | { refresh: string; places: (Place | WriteAct)[] } | { route: { from: string; into: Dword[]; add?: number } }; const pieces: Piece[] = [{ acts: [] }]; const acts = () => { const last = pieces[pieces.length - 1]; @@ -4033,6 +4449,7 @@ class Emitter { for (const piece of pieces) { if (!("acts" in piece) || p.conds.length) { cut.push(piece); continue; } let cur: (Act | WriteAct)[] = [], need = new Map(), dirty = new Set(); + const coldRead = new Set([...piece.acts].filter((a): a is WriteAct => coldActs.has(a as WriteAct)).map((a) => a.reads!.id)); const flush = () => { for (const [id, list] of need) cut.push({ refresh: id, places: list }); if (cur.length) cut.push({ acts: cur }); @@ -4050,7 +4467,7 @@ class Emitter { else groups.push([a]); } for (const group of groups) { - const reads = group.filter((a): a is WriteAct => "into" in a && !!a.reads && routed.has(a.reads.id)); + const reads = group.filter((a): a is WriteAct => "into" in a && !!a.reads && (routed.has(a.reads.id) || coldActs.has(a as WriteAct))); // (A refresh is a trigger before the actions it is for: not before an action whose fields are places, which // keeps the label.) if (reads.some((a) => dirty.has(a.reads!.id)) || (reads.length && cur.some((a) => fixed.has(a as Act)))) flush(); @@ -4058,7 +4475,7 @@ class Emitter { cur.push(...group); for (const a of group) { const owner = "into" in a ? cellOf.get(a.into) : undefined; - if (owner && routed.has(owner)) dirty.add(owner); + if (owner && (routed.has(owner) || coldRead.has(owner))) dirty.add(owner); } } flush(); @@ -4077,7 +4494,7 @@ class Emitter { continue; } if ("route" in piece) { - const t: Trigger = { label: label(), conds: [], acts: [], copy: { from: piece.route.from, into: piece.route.into.map((dword) => ({ dword, add: 0 })) }, ...(p.at ? { at: p.at } : {}) }; + const t: Trigger = { label: label(), conds: [], acts: [], copy: { from: piece.route.from, into: piece.route.into.map((dword) => ({ dword, add: piece.route.add ?? 0 })) }, ...(p.at ? { at: p.at } : {}) }; routes.add(t); made.push(t); continue; @@ -4107,7 +4524,7 @@ class Emitter { for (let i = 0; i === 0 || i < piece.acts.length; i += MOST_ACTS) { if (i === 0 && piece.acts.length === 0 && made.length) break; // (A trigger with conditions keeps its own label, entered through its refreshes when it has any.) - const t: Trigger = { label: p.conds.length && i === 0 ? p.label : label(), conds: p.conds.length && i === 0 ? p.conds.map((c) => ({ ...c })) : [], acts: [], ...(p.at ? { at: p.at } : {}) }; + const t: Trigger = { label: p.conds.length && i === 0 ? p.label : label(), conds: p.conds.length && i === 0 ? p.conds.map((c) => ({ ...c })) : [], acts: [], ...(p.at ? { at: p.at } : {}), ...(p.off && p.conds.length && i === 0 ? { off: true } : {}) }; piece.acts.slice(i, i + MOST_ACTS).forEach((a, k) => { if ("into" in a) { at.set(a, { at: t.label, field: { act: k, part: "value" } }); @@ -4322,8 +4739,8 @@ function joinRuns(triggers: Trigger[], keep: Set): void { export function liveAfterCalls(body: Stmt[]): Map> { const out = new Map>(); - /** What is live where a `break`, a `continue` and a `return` go. */ - type Flow = { brk: Set; cont: Set; ret: Set }; + /** What is live where a `break`, a `continue`, a `return` and an error go. */ + type Flow = { brk: Set; cont: Set; ret: Set; thr: Set }; const union = (...xs: Set[]) => new Set(xs.flatMap((x) => [...x])); const reads = (e: unknown) => (e && typeof e === "object" ? readIn(e as NumExpr) : new Set()); const seq = (list: Stmt[], after: Set, flow: Flow): Set => { @@ -4355,17 +4772,20 @@ export function liveAfterCalls(body: Stmt[]): Map> { case "call": { const c = s.call; // (Recorded on every pass of a loop around it: the last is the one that holds.) - if (c.saves) out.set(c, new Set(after)); + // (What the `catch` it may throw into reads, too: the frame is taken back before the error goes there.) + if (c.saves) out.set(c, union(after, flow.thr)); let live = new Set(after); if (c.result) live.delete(c.result.decl.id); // An inlined call's body where it is: its `return` goes on to what follows the call. - if (c.fn === undefined) live = seq(c.body, live, { brk: live, cont: live, ret: live }); + if (c.fn === undefined) live = seq(c.body, live, { brk: live, cont: live, ret: live, thr: flow.thr }); // (A call that may come back sets parameters that are this function's own, and takes back after it what it // keeps: what was live after it is so before it too.) if (!c.saves) for (const p of c.params) live.delete(p.decl.id); - return union(live, ...c.params.map((p) => reads(p.init))); + return union(live, flow.thr, ...c.params.map((p) => reads(p.init))); } case "return": return union(reads(s.value), flow.ret); + case "throw": return flow.thr; + case "try": return seq(s.body, after, { ...flow, thr: seq(s.catch, after, flow) }); case "if": return union(reads(s.cond), seq(s.then, after, flow), seq(s.else ?? [], after, flow)); case "block": return seq(s.body, after, flow); case "while": return loop(s.cond, s.body, [], after, true, flow); @@ -4391,7 +4811,7 @@ export function liveAfterCalls(body: Stmt[]): Map> { } }; const none = new Set(); - seq(body, none, { brk: none, cont: none, ret: none }); + seq(body, none, { brk: none, cont: none, ret: none, thr: none }); return out; } @@ -4440,6 +4860,115 @@ function writtenIn(list: Stmt[], into = new Set()): Set { return into; } +/** Whether a piece of the IR holds a statement or an expression of this kind. */ +function holdsKind(x: unknown, kind: string): boolean { + if (!x || typeof x !== "object") return false; + if (Array.isArray(x)) return x.some((y) => holdsKind(y, kind)); + const o = x as Record; + return o.kind === kind || Object.entries(o).some(([k, v]) => k !== "original" && v && typeof v === "object" && holdsKind(v, kind)); +} + +/** + * The tasks of a map of our own (TrigScript 4, step 11): each `start`'s slots — one when the start can run only once + * (in no loop, nowhere twice, in no task of more than one slot), else as many as its `max`, each slot after the first a + * copy of the task's call whose variables of the task's are its own (as the simulator swaps them) — and the program + * with `running` and `taskHandle` worked out against the slots: a number is a task's while one slot's number is it. + * Or why not. The new variables' declarations go into `known`. + */ +function planTasks(root: Stmt, key: string, known: Map, arrays: Map): { root: Stmt; slots: Map; once: Set } | string { + // Which starts run more than once, and which start each is inside. + const seen = new Map(), many = new Set(), parent = new Map(); + const visit = (x: unknown, looped: boolean, task: Stmt | null): void => { + if (!x || typeof x !== "object") return; + if (Array.isArray(x)) { x.forEach((y) => visit(y, looped, task)); return; } + const o = x as Record; + if (o.kind === "start" && typeof o.task === "number" && o.call) { + const st = o as unknown as StartStmt; + seen.set(st, (seen.get(st) ?? 0) + 1); + if (looped || seen.get(st)! > 1) many.add(st); + parent.set(st, task); + visit(st.call.params, looped, task); + visit(st.call.body, false, st); + return; + } + const loop = o.kind === "while" || o.kind === "for" || o.kind === "do" || o.kind === "unitLoop" || o.kind === "textLoop"; + for (const [k, v] of Object.entries(o)) if (k !== "original" && v && typeof v === "object") visit(v, looped || loop, task); + }; + visit(root, false, null); + for (const [st, p] of parent) if (p && many.has(p)) return `a task started inside a task of more than one slot (line ${st.at.line})`; + // (A slot's arrays would be its own, as its variables are: not yet.) + for (const st of many) { + const a = [...arrays.values()].find((x) => x.task === (st as StartStmt).task); + if (a) return `an array of a task that runs in more than one slot at a time (${a.name}, line ${st.at.line})`; + } + // The slots: their numbers' variables, and for each slot after the first, the task's variables renamed. + // (Declared as the program starts: what is worked out from them — a slot's number from its generation — is kept in + // places, which a write sets.) + const plans = new Map(), declares: Stmt[] = []; + for (const st of parent.keys()) { + const s = st as StartStmt; + const n = many.has(st) ? s.max : 1; + const handles = Array.from({ length: n }, (_, k) => `@slot${s.task}.${k}.id`); + plans.set(s.task, { n, handles }); + for (let k = 0; k < n; k++) for (const id of [`@slot${s.task}.${k}.gen`, handles[k]]) { + const decl: VarDecl = { id, name: "(task)", kind: "number", shared: false, at: s.at }; + known.set(id, decl); + declares.push({ kind: "declare", decl, init: { kind: "const", value: 0 }, at: s.at, label: "" }); + } + } + const every = [...plans.values()].flatMap((p) => p.handles); + const slots = new Map(), once = new Set(); + // (Copied where it is rewritten: the program's IR is the simulator's too, and each run's.) + const remake = (x: unknown, handle: NumExpr, rename: Map): unknown => { + if (Array.isArray(x)) return x.map((y) => remake(y, handle, rename)); + if (!x || typeof x !== "object") return typeof x === "string" && rename.has(x) ? rename.get(x) : x; + const o = x as Record; + if (o.kind === "taskHandle") return handle; + if (o.kind === "running") { + const h = remake(o.task, handle, rename) as NumExpr, at = (o.at ?? { file: "", line: 0, column: 0 }) as At; + if (!every.length) return { kind: "const", value: false }; + return { kind: "and", items: [{ kind: "compare", op: ">=", left: h, right: { kind: "const", value: 1 }, unsigned: true, at, label: "" }, { kind: "or", items: every.map((id) => ({ kind: "compare", op: "==", left: h, right: { kind: "var", id }, at, label: "" })) }] }; + } + if (o.kind === "start" && typeof o.task === "number" && o.call) { + const s = o as unknown as StartStmt, plan = plans.get(s.task)!; + const calls = plan.handles.map((h, k) => { + // (Slot k's own variables: the task's, `id~k`.) + const own = new Map(rename); + if (k) for (const d of declsIn(s.call)) if (d.task === s.task) own.set(d.id, `${d.id}~${k}`); + const call = remake(s.call, { kind: "var", id: h }, own) as Call; + for (const d of declsIn(call)) if (!known.has(d.id)) known.set(d.id, d); + return call; + }); + const made = { ...s, call: calls[0] } as StartStmt; + slots.set(made, calls.map((call, k) => ({ call, check: `${key}:task${s.task}.${k}`, entry: `${key}:task${s.task}.${k}:body`, gen: `@slot${s.task}.${k}.gen`, handle: plan.handles[k] }))); + if (!many.has(s)) once.add(made); + return made; + } + const out: Record = {}; + for (const [k, v] of Object.entries(o)) out[k] = remake(v, handle, rename); + return out; + }; + const made = remake(root, { kind: "const", value: 0 }, new Map()) as Stmt; + return { root: declares.length ? { kind: "block", body: [...declares, made], at: made.at } : made, slots, once }; +} + +/** Every declaration in a piece of the IR: a `declare`'s, a loop's, a call's parameters and result. */ +function declsIn(x: unknown, out: VarDecl[] = []): VarDecl[] { + if (Array.isArray(x)) { x.forEach((y) => declsIn(y, out)); return out; } + if (!x || typeof x !== "object") return out; + const o = x as Record; + const d = o.decl as VarDecl | undefined; + if (d && typeof d === "object" && typeof d.id === "string" && typeof d.kind === "string") out.push(d); + for (const [k, v] of Object.entries(o)) if (k !== "decl" && v && typeof v === "object") declsIn(v, out); + return out; +} + +/** Whether statements end in a `throw`: all they do is on an error's way. */ +function throwsAtEnd(list: Stmt[]): boolean { + const last = list.filter((s) => s.kind !== "remark").at(-1); + return last?.kind === "throw" || (last?.kind === "block" && throwsAtEnd(last.body)); +} + /** Whether a value or a test reads a cell of an array. */ function readsCell(x: unknown): boolean { if (!x || typeof x !== "object") return false; @@ -5470,6 +5999,16 @@ class Rewriter { return [{ ...s, body: this.stmts(s.body), ...(this.own && !pre.length ? { cond } : {}) }]; } case "unrolled": return [{ ...s, iterations: s.iterations.map((it) => this.stmts(it)) }]; + case "try": return [{ ...s, body: this.stmts(s.body), catch: this.stmts(s.catch) }]; + // (A task's arguments as a call's, worked out where it is started; its body rewritten as an inlined call's.) + case "start": { + if (!this.own) return [s]; + const pre: Stmt[] = []; + this.call(s.call, pre, where); + const made = pre.pop() as Extract; + return [...pre, { ...s, call: made.call }]; + } + case "parallel": return [{ ...s, branches: s.branches.map((b) => this.stmts(b)) }]; case "switch": { const pre: Stmt[] = []; // (Tested against each case: one variable plus a constant — bits under a mask computed first.) @@ -5554,7 +6093,7 @@ export const emitterCounts = { hoisted: 0, reciprocals: 0, jumps: 0, products: 0 * call of a function of the Python side's, and nothing read that the body did not write first (there is no one to * copy it in; a copy within the body is made of triggers by the layout). Returns the graph, or why it cannot be. */ -export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH, owner?: number, players: MapPlayers = ANY_PLAYERS): Graph | string { +export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH, owner?: number, players: MapPlayers = ANY_PLAYERS, errors?: ProgramErrors): Graph | string { // (Every statement must be the emitter's here, so a rewriting it would not take is refused whichever way it goes: // no trial is needed.) const take = () => true; @@ -5590,6 +6129,7 @@ export function emitBody(body: Stmt[], decls: Map, key: string, scope.functions = functions; scope.arrays = arrays; scope.bodyOf = bodyOf; + scope.errors = errors; }; const rw = new Rewriter(key, take, true, functions); rw.arrays = arrays; @@ -5597,9 +6137,14 @@ export function emitBody(body: Stmt[], decls: Map, key: string, rw.players = players; rw.bits = tops; const block = rw.stmt(plainFns(inlined({ kind: "block", body, at: body[0]?.at } as Stmt, functions)) as Stmt); - const s: Stmt = block.length === 1 ? block[0] : ({ kind: "block", body: block, at: body[0]?.at } as Stmt); + // The tasks' slots, each start's planned (and the variables of a slot after the first declared). + const planned = planTasks(block.length === 1 ? block[0] : ({ kind: "block", body: block, at: body[0]?.at } as Stmt), key, known, arrays); + if (typeof planned === "string") return planned; + const s = planned.root; const scope = new Scope(known); rules(scope); + scope.slots = planned.slots; + scope.once = planned.once; // (The map's own variables, which no program declares and no run's start sets: 0 as the map starts, one cell each.) for (const d of known.values()) if (d.global) scope.declared.add(d.id); if (!scope.stmt(s, false)) return scope.why || "?"; @@ -5622,11 +6167,28 @@ export function emitBody(body: Stmt[], decls: Map, key: string, // Stacks: each `sp` and `depth` set at the start, which sets every place of each `sp` — the fields of its stack it // addresses. (Which stacks there are is known once the functions are laid out: the writes go into this trigger then.) const start = scope.recursion ? e.push({ conds: [], items: [] }) : null; + // The program a coroutine whose wake is its run's `@wake`; when it sleeps or ends, the tasks have their turn. + e.co = { + tick: { data: WAKE_TICK }, + at: { data: WAKE_AT }, + reset: { dword: { data: WAKE_NEXT }, modifier: "set", value: { label: e.tasksLabel } }, + yieldTo: e.tasksLabel, + // At its end the program wakes no more. + end: () => { + e.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: U32 - 1 } }); + e.goto(e.tasksLabel); + }, + }; e.stmt(s); - // At its end the program wakes no more. - e.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: U32 - 1 } }); - e.goto(END); - e.functions(); + e.co.end(); + // The functions the program calls and the tasks it starts, each of which may call and start more. + while (true) { + e.functions(); + e.tasks(); + if (!e.pending.length && !e.pendingTasks.length) break; + } + const tasks = e.turnOfTasks(); + e.settleThrown(); e.emitDigits(); if (start) { const writes: Write[] = [...[...e.stacks.values()].map((st) => st.sp), ...(e.stacks.size ? [e.depth] : [])].map((target) => ({ target, value: { c: 0, terms: [] } })); @@ -5641,6 +6203,7 @@ export function emitBody(body: Stmt[], decls: Map, key: string, g.cells = [...(g.cells ?? []), ...[...e.stacks.values()].map((st) => ({ id: st.id, cells: st.frame * e.stackDepth }))]; } if (typeof g !== "string" && e.templates.length) g.templates = e.templates; + if (typeof g !== "string" && tasks) g.tasks = tasks; if (typeof g !== "string" && e.regions.size) g.cells = [...e.regions.values(), ...(g.cells ?? [])]; const cells = typeof g === "string" ? 0 : (g.cells ?? []).reduce((n, r) => n + r.cells, 0); if (cells > STACK_CELLS_MOST) return { e, g: `arrays and stacks of ${cells} cells, past the ${STACK_CELLS_MOST} a map of our own lays out (72 bytes a cell)` }; diff --git a/compiler/backend/errors.ts b/compiler/backend/errors.ts new file mode 100644 index 0000000..e47bb55 --- /dev/null +++ b/compiler/backend/errors.ts @@ -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) : (x as object); + const out: Record = {}; + 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): Extract => { + 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; + 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(); + const look = (y: unknown): void => { + if (Array.isArray(y)) { y.forEach(look); return; } + if (!y || typeof y !== "object") return; + const o = y as Record; + 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) => 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; + const r = f(o); + if (r !== undefined) return r; + return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, replaced(v, f)])); +} diff --git a/compiler/backend/graph.ts b/compiler/backend/graph.ts index a7650c5..3ee285a 100644 --- a/compiler/backend/graph.ts +++ b/compiler/backend/graph.ts @@ -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"; @@ -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 @@ -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 @@ -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[] } /** diff --git a/compiler/backend/layout.ts b/compiler/backend/layout.ts index 0331b1e..2cd8269 100644 --- a/compiler/backend/layout.ts +++ b/compiler/backend/layout.ts @@ -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; @@ -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); @@ -286,16 +287,16 @@ export function layout(input: LayoutInput): OwnMap { // (Where each run's graph ends: the next run.) const ends = new Map(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 @@ -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. @@ -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]; }; @@ -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)); diff --git a/compiler/backend/own.ts b/compiler/backend/own.ts index ddbda82..17fd69c 100644 --- a/compiler/backend/own.ts +++ b/compiler/backend/own.ts @@ -12,10 +12,11 @@ import type { CompileResult } from "../compiler"; import type { ArrayDecl, FuncDecl, Program, VarDecl } from "../ir"; import { ANY_PLAYERS, emitBody, groupSlots, type MapPlayers } from "./emit"; -import type { Act, Cond, Dword, Graph, Num, Trigger, Value } from "./graph"; +import { NOW, type Act, type Cond, type Dword, type Graph, type Num, type Trigger, type Value } from "./graph"; import { TextPool, knownTexts } from "./texts"; import { HEAP_GLOBALS, growingArrays, heldVariables, isHeapGlobal, loopScoped } from "./heap"; import { UNIT_GLOBALS, unitValues } from "./units"; +import { ownErrors } from "./errors"; export interface OwnRun { /** The program's index among the script's. */ @@ -59,8 +60,9 @@ export function emitOwn(r: Pick, p const runs: OwnRun[] = []; const values = new Map(); // Every program's texts known when built first: the pool is numbered once they are all in, before any is lowered. + // (What an error needs texts for — an uncaught one said, a stack that ran out — written out before: `errors.ts`.) const pool = new TextPool(); - const known = r.ir.map((p) => knownTexts(p, pool, textBase)); + const known = r.ir.map((p) => knownTexts(ownErrors(p), pool, textBase)); const texts = pool.finish(); // Units as two numbers each (`units.ts`), then arrays that grow on the heap (`heap.ts`), once the texts' numbers are // in: those passes copy what they rewrite. @@ -78,8 +80,8 @@ export function emitOwn(r: Pick, p } }; collect(program); - // (The heap's own variables, which no program declares: one cell each for the map.) - for (const d of [...HEAP_GLOBALS, ...UNIT_GLOBALS]) decls.set(d.id, d); + // (The heap's own variables, which no program declares, and the count of frames: one cell each for the map.) + for (const d of [...HEAP_GLOBALS, ...UNIT_GLOBALS, NOW_DECL]) decls.set(d.id, d); const functions = new Map((program.functions ?? []).map((f) => [f.id, f])); const arrays = new Map((program.arrays ?? []).map((a) => [a.id, a])); // The variables and arrays every run of the program has in common: one for every player of a per-player program. @@ -89,9 +91,9 @@ export function emitOwn(r: Pick, p for (const player of program.perPlayer ? [...slots].sort((a, b) => a - b) : slots.slice(0, 1)) { const n = runs.length; // (The first run keeps the emitter's names; each other one's are its own, but for what its program shares.) - const graph = emitBody(program.body, decls, n ? `own${n}` : "own", functions, arrays, undefined, player, players); + const graph = emitBody(program.body, decls, n ? `own${n}` : "own", functions, arrays, undefined, player, players, program.errors); if (typeof graph === "string") return graph; - const name = (id: string) => (!n || isHeapGlobal(id) || UNIT_GLOBALS.some((d) => d.id === id) ? id : shared.has(id) ? (k ? `${id}#p${k}` : id) : `${id}#${n}`); + const name = (id: string) => (!n || isHeapGlobal(id) || id === NOW || UNIT_GLOBALS.some((d) => d.id === id) ? id : shared.has(id) ? (k ? `${id}#p${k}` : id) : `${id}#${n}`); const g = n ? renamed(graph, `#${n}`, name) : graph; // An action of the game's names its text and its sound file by the script's strings: given here, for the layout. const of = (i: number) => (i ? r.strings[i - 1] : undefined); @@ -140,5 +142,9 @@ function renamed(g: Graph, suffix: string, name: (id: string) => string): Graph ...(g.cells ? { cells: g.cells.map((c) => ({ ...c, id: name(c.id) })) } : {}), ...(g.templates ? { templates: g.templates.map((t) => ({ ...t, id: t.id + suffix })) } : {}), ...(g.rows ? { rows: g.rows.map((r) => r.map(label)) } : {}), + ...(g.tasks ? { tasks: label(g.tasks) } : {}), }; } + +/** The map's count of frames (`NOW`): a variable of every run's, one more each pass. */ +const NOW_DECL: VarDecl = { id: NOW, name: "(frames)", kind: "number", shared: true, routed: true, global: true, at: { file: "", line: 0, column: 0 } }; diff --git a/compiler/backend/texts.ts b/compiler/backend/texts.ts index d960489..9797531 100644 --- a/compiler/backend/texts.ts +++ b/compiler/backend/texts.ts @@ -900,7 +900,8 @@ export function knownTexts(program: Program, pool: TextPool, base: number): Prog * of them the first time a text is stored there — its EPD then in the first array, 0 before — and gives it back * when the cell's text is let go of (popped, cut off, declared again). */ - interface TextArray { addr: string; block: string; chars: string; cells: Made[]; grows?: boolean } + /** `length`: of one that does not grow but is too long for a block a cell (`MOST_TEXT_CELLS`), which takes its blocks as one that grows does. */ + interface TextArray { addr: string; block: string; chars: string; cells: Made[]; grows?: boolean; length?: number } const textArrays = new Map(); const findTextArrays = (x: unknown): void => { if (Array.isArray(x)) { x.forEach(findTextArrays); return; } @@ -912,6 +913,9 @@ export function knownTexts(program: Program, pool: TextPool, base: number): Prog textArrays.set(o.addr!, { addr: o.addr!, block: o.block!, chars: o.chars!, cells: Array.from({ length: a.length }, (_, i) => ({ e: `${o.addr}.${i}@e`, n: `${o.addr}.${i}@n`, c: `${o.addr}.${i}@c`, block: `${o.addr}.${i}@block` })) }); } else if (a && a.dynamic && !a.slice && !a.through && a.task === undefined) textArrays.set(o.addr!, { addr: o.addr!, block: o.block!, chars: o.chars!, cells: [], grows: true }); + // (Longer: its cells' blocks from the pool, the first time a text is stored in each — the messages of the errors + // a program's tasks end on, a cell a slot, which few of them ever hold.) + else if (a && !a.dynamic && !a.slice && !a.through) textArrays.set(o.addr!, { addr: o.addr!, block: o.block!, chars: o.chars!, cells: [], grows: true, length: a.length }); } for (const v of Object.values(o)) if (v && typeof v === "object") findTextArrays(v); }; @@ -1239,7 +1243,7 @@ export function knownTexts(program: Program, pool: TextPool, base: number): Prog const reads = readsArray(s.value, a.addr); const made = reads ? [...blockOf(SCRATCH), ...w.pre, ...begin(V(SCRATCH.e)), ...w.body, ...finish(SCRATCH)] : w.pre; const fill = reads ? copyBlock(SCRATCH, into) : [...begin(V(into.e)), ...w.body, ...finish(into)]; - return [dec(at, num(s.index)), ...made, when(cmp("<", V(at), { kind: "length", array: a.addr, at: here } as NumExpr, true), [ + return [dec(at, num(s.index)), ...made, when(cmp("<", V(at), a.length !== undefined ? k(a.length) : ({ kind: "length", array: a.addr, at: here } as NumExpr), true), [ dec(into.e, element(a.addr, V(at))), dec(into.n, k(0)), dec(into.c, k(0)), when(cmp("==", V(into.e), k(0)), [...takeBlock(into.e), store(a.addr, V(at), V(into.e))]), when(cmp("!=", V(into.e), k(0)), fill), diff --git a/docs/v4-plan.md b/docs/v4-plan.md index e95ebbb..666daac 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3566,9 +3566,10 @@ found what they left out; where a step moved, its old number is given. 10. **Units** (was 5) ✓ — "Units on our own backend". Every unit value, unit loops, fields, flags, writes and verbs, picks, `select()` and the game command it sends (the events' rows are step 11's, `underMouse`'s step 12's); the queries scheduled (ideas 6, 7) still to come. -11. **Tasks, `async` and errors** (was 6; errors new). `start`, `cancel`, `running`, `Promise.all`/`race`/`any`, - `every`, `after`, `on.*` handlers, on the walk as the scheduler (idea 2). `try`, `catch`, `finally`, `throw`, - `uncaught`, a program's `errors`, the error classes. +11. **Tasks, `async` and errors** (was 6; errors new). Part 1 ✓: `try`, `catch`, `finally`, `throw`, `uncaught`, a + program's `errors`, the error classes — "Errors on our own backend". Part 2 ✓: `start`, `cancel`, `running`, + `Promise.all`/`race`/`any`, `every`, `after`, a task's error awaited — "Tasks on our own backend". Part 3: the + `on.*` handlers and the pass that finds what they are handed. 12. **Input** (was 7; idea 13). Keys, clicks, the mouse, typed lines, sync channels, the `local.*` reads and the user's player id. The test harness types and presses through chatEvent's and MSQC's addresses (`GameSim`): it moves with them, and the map stops needing what MSQC takes (a location, the Valkyrie's entry, Player 12's units). @@ -3898,7 +3899,7 @@ Bad Apple: 5, 6, 7, as the fog library. The bullets: 5 (a death count read), 9. ### The ledger (2026-10-19) -`ledger/` holds a script for each feature of the language and the library — 843 of them, in folders by area — whose +`ledger/` holds a script for each feature of the language and the library — 849 of them, in folders by area — whose first line says which step of the way off eudplib makes it a map of our own's, and whether a map of our own takes it today, in the words of its refusal: @@ -3926,7 +3927,7 @@ lists what no script reaches. A step is done when its scripts say so. | 8 Texts as values | 48 of 48 | | 9 Arrays that grow, the heap | 27 of 27 | | 10 Units | 99 of 99 | -| 11 Tasks, `async` and errors | 0 of 44 | +| 11 Tasks, `async` and errors | 35 of 50 | | 12 Input | 0 of 26 | | 13 The map's own triggers | 0 of 13 | @@ -4003,6 +4004,31 @@ the ledger is the list it can be written again from at 4.0. are, and a loop's body is written up to three times (place 0, two places a turn). `give()` leaves the game's counts as they were (both builds: eudplib's `cgive`), so `command()` after it reads the old owner's; the game's own Give Units does not. +- **Errors on a map of our own (step 11, part 1), what is left:** a call in a loop of a function that may throw is + about eudplib's (W1, 0.18 ms to 0.21 for 300): the error costs it one action, but the call itself does not keep step + 1's lead (1.5×) — the argument is copied into each place of the parameter (here three: its cell, the home a signed + comparison reads, the action that adds 1; about 170 ns), and the result is copied out (75 ns). Value ranges carried + through calls would let a parameter never below zero be compared without a home; the result could be read where it + is. A throw is 3.2× eudplib's (W2), and most of what is left is its message, made where it is thrown (7.3 µs of 8.7 + when the message has a number in it): the throw could keep where it was thrown and its values, and the message be + made only where it is read — a `catch` that reads `e.message`, an uncaught error said — about 1.3 µs a throw, 20× + eudplib's, for the many catches that read nothing. In code only an error goes through, a signed comparison still + reads its variable's home (the variable plus 2³¹), a place every write keeps. The map is bigger (1.52 MB to 1.05). +- **Tasks on a map of our own (step 11, part 2), what is left:** a start that can run while its tasks still run lays + the task's body out once a slot (`max`, 16 unless said): the fastest at its turn, and small since a task's `catch` + is laid out once (a 16-slot start's body is 9 triggers in the probe), but a large body grows the map sixteen times; + one body with the slot's variables swapped in and out at each turn, as the Python side does, would be the way for a + large one. Such a task's arrays, and a start inside it, are refused for now (they would be a slot's own). A called + function that waits is refused in a program with tasks (it is laid out once, outside every coroutine). A task asleep + is a condition that fails each frame (13 ns), one with nothing in it a trigger passed by as done: a timer wheel + (idea 2) would make a sleeper cost nothing until its frame. "did not start" is said in red by the game, as the Python + side's build says it, where the simulator notes a fault (both builds). +- A variable set from a constant less itself (`j = 1 - j`) is copied a bit at a time — 32 triggers, about 0.5 µs, where + eudplib's is two actions: the game's subtract stops at 0, so the emitter copies a variable read times −1 (found + weighing the errors probe, 2026-10-22). +- A call worked out when the script is built that throws is a compile error even inside a `try` (both builds): + `try { total += check(9); } catch { … }` says "too big: 9 — this expression is computed when the script is built"; + JavaScript would catch it. Found writing `tests/emitter.test.ts` ("a map of our own, errors"). - A map of our own: JSX inside a program was "made while the game runs" even when it held nothing of the program's — taken since step 8's part 3; (`Math.tan` and `Math.hypot`, refused as what they become, are taken since step 4's part 5; `a >> 0`, refused where @@ -4890,6 +4916,123 @@ game's (193 units, 3 Marines, 115 Zerglings), T about 4.2 s after the first grou one given to Player 2 is shot by Player 1's own, two Marines are in the panel after `select()`, the walked Marine ends at the right of the row; no hitch. +### Errors on our own backend (2026-10-22) + +Step 11, part 1 (`compiler/backend/errors.ts`, a pass before the texts'; the emitter's `try`, `throw` and `handler`). +The ledger's rows of errors are taken, 12 of 12 (six added for what they did not reach: an error up through a function +that calls itself, a stack that runs out caught, `finally` on a `return`, a class's own field and `name`, `e.stack`, an +uncaught error thrown in a called function). The two rows of a task's error awaited wait for the tasks (part 2). + +- **What the front end does already** (B4): before a `throw` it puts the error in flight — its class's code into + `Program.errors.thrown`, its message into `message` — copies `finally` before every way out, and makes a program's + body a `try` whose `catch` is `uncaught`. `errors.ts` writes out what the emitter would need texts for, before the + texts are lowered: the line each error is thrown at into a variable of the program's (one thrown again keeps it), + `uncaught` as the print the Python side makes ("TrigScript: uncaught RangeError: …, line N." in red, and the code + back to 0), and for a program with a function that calls itself, a function that sets the RangeError of a stack + that ran out. +- **A throw is a jump**: to the `catch` of the nearest `try` around it. In a called function with no `try` around it, + out of the function: one that never calls itself goes out through a trigger of its own for errors, whose next + pointer each call points at where an error goes from that call, in the trigger that sets where it goes back to — so + nothing is looked at on the way back, where eudplib looks at the error in flight after every call of a function + that may throw. When every call names the same place, that trigger is given it when the map is built, and the calls + set nothing. A function that calls itself goes out from its end, as a `return` does: each call takes its frame back + and then looks at the error in flight (a frame keeps what a `catch` after the call reads, too). A `try` costs + nothing while nothing is thrown. +- **A stack that runs out throws a RangeError** ("Maximum call stack size exceeded", at the line of the call), as + JavaScript, the simulator and the Python side's build do; a map of our own stopped the program there before, and + still does in a program that neither throws nor catches. +- **What only an error goes through is cold**: statements that end in a `throw`, and a `catch`. What they read of a + variable is refreshed from its cell just before it is read, as a routed variable's is, and is never a place of it + that every write keeps. Without it, a message made from a parameter cost every call of the function an action and a + copy (about 75 ns), and each throw wrote the error's code and its message's lengths into every place a `catch` read + them from (about 90 actions a throw in the probe). The payload went from 1.88 MB to 1.21; a throw from 11.4 µs to 8.7. + A loop is never cold, its test and its body: it may turn many times on an error's way too (`while (alive()) { … }` + and a `throw` after it, the whole body of a program, laid out its reads refreshed at every turn before). + +| Lifted game, Remastered's weights (`probes/own-errors.ts`) | Ours | eudplib | +| --- | ---: | ---: | +| A–B: a throw caught, classes told apart, messages made while the game runs | **0.211 ms** | 1.492 ms | +| C–D: thrown again, `finally`, out of called functions | **0.080 ms** | 0.125 ms | +| E–F: up through a function that calls itself; a stack run out at 1,024 calls, caught | **0.611 ms** | 2.412 ms | +| W1: 300 calls of a function that may throw, none thrown | **0.183 ms** | 0.208 ms | +| W2: 100 errors thrown in a called function, each caught (a number in the message) | **0.876 ms** | 2.846 ms | +| W3: an error up through 40 frames, four times | **0.096 ms** | 0.364 ms | +| W4: 200 turns through a `try` with a `finally`, one in two left by `continue` | **0.029 ms** | 0.058 ms | +| the uncaught error said | **0.023 ms** | 0.057 ms | +| the map | 1.52 MB | 1.05 MB | + +W1 is where the call is, not the error: the error costs one action a call. What is left is in the ledger's *Open* list. + +Checked: `tests/emitter.test.ts` ("a map of our own, errors": try and catch, the classes and a field, a rethrow and +`finally` on every way out, out of called functions and a function's own `try`, uncaught, through recursion and its +frames, the stack run out — and every one of the ledger's scripts of errors — the payload walked from its bytes against +the simulator); the 30 scripts of `tests/errors.test.ts` a map of our own takes, the same way (once, not kept as a +test); the probe both ways in the lifted game, every line the same at the same frame and "ok". + +Played in Remastered (2026-10-22, recording 20.07.15): all 10 lines word for word and in order, the uncaught error in +red and nothing after it; the groups 2, 2, 4, 18 and 2 game frames apart, as in the lifted game — the frame a stack runs +out 1,024 calls deep and the frame of 100 throws among them; no hitch. + +### Tasks on our own backend (2026-10-22) + +Step 11, part 2 (`compiler/backend/emit.ts`: `planTasks`, the coroutines, `start`, `cancel`, `parallel`, `tasks()`, the +tasks' turn). The ledger's rows of tasks and of `Promise.all`, `race` and `any` are taken, 23 of them — 35 of step 11's +50; the 15 left are the `on.*` events, part 3. + +- **A coroutine** is the program, a task's slot or a branch of a `Promise.all` or `race`: a trigger that wakes it — + "the map's count of frames has reached W: go to R" — whose W and R its sleep writes. The program's is its run's + `@wake`; a slot's and a branch's are triggers of their own, passed by as done while nothing runs in them (the walk + skips a trigger whose done flag is set before its conditions, at about 40% of an empty trigger's cost). +- **The map's count of frames** (`@now`) is one more each pass (`@run`). A sleep copies it, plus its frames, into its + wake: one copy, where the program's sleep read the game's tick a bit at a time (32 triggers, about 0.5 µs; now about + 70 ns, for every program). +- **The tasks' turn** follows the program's turn, and the program's wake when it does not hold: each start's slots in + the order of the starts' numbers, as the simulator orders them; when a task can start one, round again while one + was started, for it to have its first turn in this frame too. A task outlives its program. +- **A start** that can run only once — in no loop, nowhere twice, in no task of more than one slot — has one slot, + always free when it runs: its arguments go straight into the task's parameters. One that can run while its tasks do + has its `max` of slots, each with its own copy of the task's body and variables (the variables the simulator swaps): + it finds the first free slot, says once in red that none was when all run, and the task's number is + `slot | task << 8 | generation << 16`, as the Python side's. `running` is a number one slot's number is; `cancel` + switches that slot off where it waits. +- **`Promise.all` and `race`**: each branch a coroutine with a trigger of its own, all due as the statement is + reached and run in order; then the coroutine around sleeps a frame at a time and runs those still running, until + all have ended — or one has, a race, which goes on from that branch's end with the others switched off. +- **A task's error**: the front end's `catch` around every task's body (the error noted in the slot for an `await` of + it, or said) reads the task's number and nothing of the task's, and is laid out once, a function each task calls + with its number; an uncaught error is said through one function too. A probe with two starts of sixteen slots was + 19.8 MB before, 1.37 MB after (eudplib's 2.05). An array of texts longer than 64 cells — the errors' messages, a + cell a slot — takes a cell's block from the pool the first time it holds a text, as an array that grows does. +- **Found on the way:** a loop's test was split into what nothing in the loop writes, tested once on the way in — but + while the loop sleeps a task may write it (`await t` is `while (t.running) sleep(1)`): a loop that sleeps tests its + whole condition each turn. A slot's number is worked out from its generation into places, which only a write sets: + the slot's variables are declared as the program starts. + +| Lifted game, Remastered's weights (`probes/own-tasks.ts`), µs a frame | Ours | eudplib | +| --- | ---: | ---: | +| W1: sixteen tasks asleep, none waking | **0.7** | 40.2 | +| W2: sixteen tasks awake every frame, a little each | **3.9** | 90.4 | +| sixteen tasks started in one frame | **10.0** | 141.5 | +| W3: the program asleep a frame at a time (the sixteen asleep beside it) | **0.9** | 40.4 | +| W4: four branches of a `Promise.all`, each asleep a frame at a time | **1.0** | 41.0 | +| A: a task awaited, `running`, a task cancelled | **1.0** | 7.8 | +| F: a task's error caught where it is awaited | **20.7** | 55.2 | +| the first frame | **23.1** | 1,084.1 | +| the map | **1.37 MB** | 2.05 MB | + +A task's turn costs about 0.2 µs (W2, less the frame around it) against eudplib's 3.1: its variables are its slot's +own, no swapping; a slot asleep 13 ns a frame, a free one less. What is left is in the ledger's *Open* list. + +Checked: `tests/emitter.test.ts` ("a map of our own, tasks": every row of `ledger/tasks/`, `music.loop`, `ui.lines`, a +program for every player with tasks, and cancelling, slots and their numbers, a task started from a task earlier and +later in the turn, branches inside a task and a race inside a branch, errors in tasks — the payload walked from its +bytes frame after frame against the simulator, every text at its frame); the probe both ways in the lifted game, every +line the same at the same frame and "ok". + +Played in Remastered (2026-10-22, recording 20.57.34): all 9 lines word for word and in order, "did not start" in red; +each group at the lifted game's frame (2, 14, 22, 29, 40, 43, 47, 75), the frames where 32 tasks start among them; no +hitch. + ### Triggers stacked (2026-10-20) Brought forward from step 14 (`compiler/backend/layout.ts`). The game reads no more of a trigger's 2,408 bytes than diff --git a/ledger/language/error-class.ts b/ledger/language/error-class.ts index 7e58723..1d4b4d7 100644 --- a/ledger/language/error-class.ts +++ b/ledger/language/error-class.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // A class of error of the script's own, `class Late extends Error`, told apart in a `catch` with `instanceof`. class Late extends Error {} program(() => { diff --git a/ledger/language/error-fields.ts b/ledger/language/error-fields.ts new file mode 100644 index 0000000..0771b45 --- /dev/null +++ b/ledger/language/error-fields.ts @@ -0,0 +1,18 @@ +// ledger: step 11, takes +// A class of error with a field of its own, its `name`, and a message made while the game runs, read in the `catch`. +class OutOfGold extends Error { + need: number; + constructor(need: number) { + super(`need ${need} more`); + this.need = need; + } +} +program(() => { + let gold = 0; + while (gold < 20) gold += 7; + try { + if (gold < 50) throw new OutOfGold(50 - gold); + } catch (e) { + if (e instanceof OutOfGold) print(`${e.name}: ${e.message} (${e.need})`); + } +}); diff --git a/ledger/language/error-stack.ts b/ledger/language/error-stack.ts new file mode 100644 index 0000000..9434151 --- /dev/null +++ b/ledger/language/error-stack.ts @@ -0,0 +1,14 @@ +// ledger: step 11, takes +// `e.stack`: the error's class, message and the line it was thrown at, made where it is thrown because it is read. +function check(n: number) { + if (n > 1) throw new RangeError(`n is ${n}`); +} +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + check(k); + } catch (e) { + if (e instanceof Error) print(e.stack ?? ""); + } +}); diff --git a/ledger/language/finally-on-return.ts b/ledger/language/finally-on-return.ts new file mode 100644 index 0000000..e1138bb --- /dev/null +++ b/ledger/language/finally-on-return.ts @@ -0,0 +1,22 @@ +// ledger: step 11, takes +// `finally` in a function: it runs on a `return` out of the `try`, and on an error's way out to the caller's `catch`. +program(() => { + let k = 0; + while (k < 2) k += 1; + let cleaned = 0; + function guarded(n: number): number { + try { + if (n > 2) throw new Error("too big"); + return n * 2; + } finally { + cleaned += 1; + } + } + let total = guarded(k - 1); + try { + total += guarded(k + 5); + } catch { + total += 100; + } + print(`${total} ${cleaned}`); +}); diff --git a/ledger/language/rethrow.ts b/ledger/language/rethrow.ts index ce4ee1f..ddce6cd 100644 --- a/ledger/language/rethrow.ts +++ b/ledger/language/rethrow.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // An error caught and thrown again, `catch (e) { …; throw e; }`, to a `try` further out. program(() => { let k = 0; diff --git a/ledger/language/stack-overflow-caught.ts b/ledger/language/stack-overflow-caught.ts new file mode 100644 index 0000000..5ee8e70 --- /dev/null +++ b/ledger/language/stack-overflow-caught.ts @@ -0,0 +1,15 @@ +// ledger: step 11, takes +// A function that calls itself past the stack's depth throws a RangeError, as JavaScript's does, which a `catch` takes. +function deep(n: number): number { + if (n > 1000000) return 0; + return deep(n + 1) + 1; +} +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + print(`${deep(k)}`); + } catch (e) { + if (e instanceof RangeError) print(e.message); + } +}); diff --git a/ledger/language/throw-from-function.ts b/ledger/language/throw-from-function.ts index 6ad9ba8..a11835a 100644 --- a/ledger/language/throw-from-function.ts +++ b/ledger/language/throw-from-function.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // An error thrown in a called function, caught by the `try` around the call. function check(n: number) { if (n > 2) throw new Error("too big"); diff --git a/ledger/language/throw-through-recursion.ts b/ledger/language/throw-through-recursion.ts new file mode 100644 index 0000000..e075a38 --- /dev/null +++ b/ledger/language/throw-through-recursion.ts @@ -0,0 +1,15 @@ +// ledger: step 11, takes +// An error thrown at the bottom of a function that calls itself, caught around the first call: up through every frame. +function down(n: number): number { + if (n == 0) throw new Error("bottom"); + return down(n - 1) + 1; +} +program(() => { + let k = 0; + while (k < 2) k += 1; + try { + print(`${down(k + 3)}`); + } catch { + print("caught at the top"); + } +}); diff --git a/ledger/language/throw-uncaught.ts b/ledger/language/throw-uncaught.ts index b32bb97..5233102 100644 --- a/ledger/language/throw-uncaught.ts +++ b/ledger/language/throw-uncaught.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // A `throw` nothing catches: the game says the error, and the program stops there. program(() => { let k = 0; diff --git a/ledger/language/try-catch.ts b/ledger/language/try-catch.ts index 0473dc6..b133a51 100644 --- a/ledger/language/try-catch.ts +++ b/ledger/language/try-catch.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // `try { … } catch { … }`: an error thrown inside the `try` goes to its `catch`. program(() => { let k = 0; diff --git a/ledger/language/try-finally.ts b/ledger/language/try-finally.ts index 560f27a..ee5b4e2 100644 --- a/ledger/language/try-finally.ts +++ b/ledger/language/try-finally.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // `finally`: what runs however the `try` is left. program(() => { let k = 0; diff --git a/ledger/language/uncaught-in-function.ts b/ledger/language/uncaught-in-function.ts new file mode 100644 index 0000000..33fff6f --- /dev/null +++ b/ledger/language/uncaught-in-function.ts @@ -0,0 +1,12 @@ +// ledger: step 11, takes +// An error thrown in a called function that nothing catches: said in red with the line of the `throw`, and the program stops. +function explode(k: number) { + throw new RangeError(`k is ${k}`); +} +program(() => { + let k = 0; + while (k < 2) k += 1; + print("before"); + if (k > 1) explode(k); + print("after"); +}); diff --git a/ledger/programs/owner-all-players-tasks.ts b/ledger/programs/owner-all-players-tasks.ts index 48c0bba..f70801f 100644 --- a/ledger/programs/owner-all-players-tasks.ts +++ b/ledger/programs/owner-all-players-tasks.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // Each player's own tasks in a program for every player: `start(…)` with `{ owner: AllPlayers }`. async function hello(n: number) { await sleep(frames(n)); diff --git a/ledger/sounds/music-loop.ts b/ledger/sounds/music-loop.ts index 2317992..3c252ea 100644 --- a/ledger/sounds/music-loop.ts +++ b/ledger/sounds/music-loop.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A song played again and again beside the program: `start(music.loop(song))`. import { music } from "trigscript/music"; const lead = music.synth({ release: 0.05 }); diff --git a/ledger/tasks/cutscene-play.ts b/ledger/tasks/cutscene-play.ts index 571353d..0df4135 100644 --- a/ledger/tasks/cutscene-play.ts +++ b/ledger/tasks/cutscene-play.ts @@ -1,4 +1,4 @@ -// ledger: step 12, refused: a task started +// ledger: step 12, refused: the players' input (key) // trigscript/cutscene: a scene started as a task, which the player may skip with Escape: `await play(P1, start(scene()))`. import { play } from "trigscript/cutscene"; async function scene() { diff --git a/ledger/tasks/generator-as-a-task.ts b/ledger/tasks/generator-as-a-task.ts index 0d6d250..bbdb2a7 100644 --- a/ledger/tasks/generator-as-a-task.ts +++ b/ledger/tasks/generator-as-a-task.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A generator walked inside a task started beside the program. function* steps() { yield 1; diff --git a/ledger/tasks/promise-all-values.ts b/ledger/tasks/promise-all-values.ts index 0d3993c..bb5d1c1 100644 --- a/ledger/tasks/promise-all-values.ts +++ b/ledger/tasks/promise-all-values.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: Promise.all +// ledger: step 11, takes // Each side's value from a `Promise.all`: `const [a, b] = await Promise.all([…])`. async function slow(n: number): Promise { await sleep(frames(n)); diff --git a/ledger/tasks/promise-all.ts b/ledger/tasks/promise-all.ts index 1975fb4..f4fd4f2 100644 --- a/ledger/tasks/promise-all.ts +++ b/ledger/tasks/promise-all.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: Promise.all +// ledger: step 11, takes // Two waits side by side, the program going on when both have ended: `await Promise.all([…])`. program(async () => { await Promise.all([ diff --git a/ledger/tasks/promise-any.ts b/ledger/tasks/promise-any.ts index 1c629ce..7bbfcd4 100644 --- a/ledger/tasks/promise-any.ts +++ b/ledger/tasks/promise-any.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: Promise.race +// ledger: step 11, takes // `Promise.any`, the same as a race (nothing in a program fails): `await Promise.any([…])`. program(async () => { let n = 0; diff --git a/ledger/tasks/promise-race.ts b/ledger/tasks/promise-race.ts index a698755..3e450e9 100644 --- a/ledger/tasks/promise-race.ts +++ b/ledger/tasks/promise-race.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: Promise.race +// ledger: step 11, takes // Two waits side by side, the program going on when the first ends: `await Promise.race([…])`. async function wave(n: number): Promise { await sleep(frames(n)); diff --git a/ledger/tasks/start-arrow.ts b/ledger/tasks/start-arrow.ts index 186f56f..4d8e3b4 100644 --- a/ledger/tasks/start-arrow.ts +++ b/ledger/tasks/start-arrow.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task written where it is started: `start(async () => { … })`. program(async () => { start(async () => { diff --git a/ledger/tasks/start-call.ts b/ledger/tasks/start-call.ts index f8aeaa5..9cfd961 100644 --- a/ledger/tasks/start-call.ts +++ b/ledger/tasks/start-call.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task started beside the program, the program going on at once: `start(tick(3))`. async function tick(n: number) { let i = 0; diff --git a/ledger/tasks/start-max.ts b/ledger/tasks/start-max.ts index f5b3053..2387e1e 100644 --- a/ledger/tasks/start-max.ts +++ b/ledger/tasks/start-max.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A start that lets so many of its tasks run at once: `start(wave(n), { max: 4 })`. async function wave(n: number) { await sleep(frames(n)); diff --git a/ledger/tasks/start-void.ts b/ledger/tasks/start-void.ts index 18cb006..e1a9373 100644 --- a/ledger/tasks/start-void.ts +++ b/ledger/tasks/start-void.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task started by a call nothing waits for: `void later()`. async function later() { await sleep(frames(2)); diff --git a/ledger/tasks/task-awaited.ts b/ledger/tasks/task-awaited.ts index 1561a9e..5562757 100644 --- a/ledger/tasks/task-awaited.ts +++ b/ledger/tasks/task-awaited.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task waited for: `await t`, the program going on in the frame the task ends. async function short() { await sleep(frames(3)); diff --git a/ledger/tasks/task-cancel.ts b/ledger/tasks/task-cancel.ts index 4299404..c784379 100644 --- a/ledger/tasks/task-cancel.ts +++ b/ledger/tasks/task-cancel.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task stopped where it is: `t.cancel()`. async function forever() { while (true) await sleep(frames(1)); diff --git a/ledger/tasks/task-error-awaited.ts b/ledger/tasks/task-error-awaited.ts index 2981a9d..0bbc5ec 100644 --- a/ledger/tasks/task-error-awaited.ts +++ b/ledger/tasks/task-error-awaited.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // An error a task ends on, thrown again where the task is awaited and caught there: `try { await t; } catch (e) { … }`. async function wave(n: number) { await sleep(frames(2)); diff --git a/ledger/tasks/task-error-not-awaited.ts b/ledger/tasks/task-error-not-awaited.ts index 265ffa7..33f7baf 100644 --- a/ledger/tasks/task-error-not-awaited.ts +++ b/ledger/tasks/task-error-not-awaited.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: try +// ledger: step 11, takes // An error a task ends on that nothing awaits: the task ends and the game says so, and the program goes on. async function worker() { await sleep(frames(1)); diff --git a/ledger/tasks/task-outlives-program.ts b/ledger/tasks/task-outlives-program.ts index f56f90b..f7bc6f9 100644 --- a/ledger/tasks/task-outlives-program.ts +++ b/ledger/tasks/task-outlives-program.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task that runs on after the program has ended. program(async () => { start(async () => { diff --git a/ledger/tasks/task-running.ts b/ledger/tasks/task-running.ts index 18731e4..e2b96fb 100644 --- a/ledger/tasks/task-running.ts +++ b/ledger/tasks/task-running.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // Whether a task still runs: `t.running`, true until it ends or is cancelled. async function short() { await sleep(frames(3)); diff --git a/ledger/tasks/task-starts-a-task.ts b/ledger/tasks/task-starts-a-task.ts index de97475..9d7307d 100644 --- a/ledger/tasks/task-starts-a-task.ts +++ b/ledger/tasks/task-starts-a-task.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A task that starts a task of its own. async function inner(n: number) { await sleep(frames(n)); diff --git a/ledger/tasks/timers-after.ts b/ledger/tasks/timers-after.ts index 71ebfb2..5b76bfe 100644 --- a/ledger/tasks/timers-after.ts +++ b/ledger/tasks/timers-after.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // trigscript/timers: `after(delay, fn)`, a function run once, so many frames from now, in a task beside the program. import { after, SECOND } from "trigscript/timers"; program(async () => { diff --git a/ledger/tasks/timers-every-cancelled.ts b/ledger/tasks/timers-every-cancelled.ts index f94cabc..e55d5b6 100644 --- a/ledger/tasks/timers-every-cancelled.ts +++ b/ledger/tasks/timers-every-cancelled.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // trigscript/timers: an `every` stopped by the task it gives back: `const t = every(…); t.cancel()`. import { every, SECOND } from "trigscript/timers"; program(async () => { diff --git a/ledger/tasks/timers-every-times.ts b/ledger/tasks/timers-every-times.ts index ad6aaf7..4fe28fb 100644 --- a/ledger/tasks/timers-every-times.ts +++ b/ledger/tasks/timers-every-times.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // trigscript/timers: `every(period, fn, times)`, a function run so many times, so many frames apart. import { every } from "trigscript/timers"; program(async () => { diff --git a/ledger/tasks/timers-every.ts b/ledger/tasks/timers-every.ts index 8e8d063..c31466a 100644 --- a/ledger/tasks/timers-every.ts +++ b/ledger/tasks/timers-every.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // trigscript/timers: `every(period, fn)`, a function run every so many frames in a task beside the program, handed how many times it has run. import { every, SECOND } from "trigscript/timers"; program(async () => { diff --git a/ledger/texts/ui-dialog.tsx b/ledger/texts/ui-dialog.tsx index fd5919a..2bd28ac 100644 --- a/ledger/texts/ui-dialog.tsx +++ b/ledger/texts/ui-dialog.tsx @@ -1,4 +1,4 @@ -// ledger: step 12, refused: a task started +// ledger: step 12, refused: the players' input (key) // Someone speaking to a player, the words typed in, closed with Space, from `trigscript/ui`: // `await say(P1, talk, "Raynor", "…")` and ``. import { DialogBox, say } from "trigscript/ui"; diff --git a/ledger/texts/ui-lines.tsx b/ledger/texts/ui-lines.tsx index 7078b7c..c149644 100644 --- a/ledger/texts/ui-lines.tsx +++ b/ledger/texts/ui-lines.tsx @@ -1,4 +1,4 @@ -// ledger: step 11, refused: a task started +// ledger: step 11, takes // A text kept in a player's text lines and shown again as the program's variables change: `ui.lines(P1, () => …)`. program(async () => { let score = 0; diff --git a/ledger/texts/ui-menu.tsx b/ledger/texts/ui-menu.tsx index c31c2fc..c3bb5c3 100644 --- a/ledger/texts/ui-menu.tsx +++ b/ledger/texts/ui-menu.tsx @@ -1,4 +1,4 @@ -// ledger: step 12, refused: a task started +// ledger: step 12, refused: the players' input (key) // A menu a player chooses from with W, S and Space, shown in the text lines, from `trigscript/ui`: // `await choose(P1, ITEMS.length, shop)` and ``. import { MenuList, choose } from "trigscript/ui"; diff --git a/probes/own-errors.ts b/probes/own-errors.ts new file mode 100644 index 0000000..d2b6b15 --- /dev/null +++ b/probes/own-errors.ts @@ -0,0 +1,216 @@ +// TrigScript 4, the backend: errors in a map of our own (scripts/build-own.mts) — step 11 of the way off eudplib, part 1. +// A `throw` goes to the `catch` of the nearest `try` around it, out of the functions it is in: from a trigger of the +// function's own that each call points at where its error goes — nothing is looked at on the way back — or, for a +// function that calls itself, from its end, the call looking at the error once its frame is taken back. A stack that +// runs out throws a RangeError; an error nothing catches is said in red, and the program stops there. +// Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same +// frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" +// are to weigh. The last line is red — "TrigScript: uncaught RangeError: the end, line 214." — and nothing comes after. +// npx tsx scripts/build-own.mts probes/own-errors.ts /tmp/own-errors.scx +// npx tsx scripts/build-fixture.mts probes/own-errors.ts /tmp/own-errors-eud.scx +import { program, print, sleep, frames } from "trigscript"; + +class OutOfGold extends Error { + need: number; + constructor(need: number) { + super(`need ${need} more`); + this.need = need; + } +} + +/** n + 1, or a RangeError past 2. */ +function check(n: number): number { + if (n > 2) throw new RangeError(`too big: ${n}`); + return n + 1; +} +/** check's, doubled: an error goes on through it. */ +function twice(n: number): number { + return check(n) * 2; +} +/** check's, or -1: its own try keeps its errors. */ +function safe(n: number): number { + try { + return check(n); + } catch { + return -1; + } +} +function buy(gold: number, price: number): number { + if (gold < price) throw new OutOfGold(price - gold); + return gold - price; +} +/** Down to 0, then an error, up through every frame. */ +function down(n: number): number { + if (n == 0) throw new Error("bottom"); + return down(n - 1) + 1; +} +/** Each level keeps n and half around its call; the catch reads them once its frame is back. */ +function halves(n: number): number { + const half = n >> 1; + try { + if (n < 3) throw new Error("small"); + return halves(half) + n; + } catch { + return half * 100 + n; + } +} +function forever(n: number): number { + if (n > 1000000) return 0; + return forever(n + 1) + 1; +} + +program(() => { + print("Own errors: the payload runs."); + let k = 0; + while (k < 2) k += 1; + + // A: a throw in the try goes to its catch; a try with no throw runs its body only. + let a = 0; + try { + if (k > 1) throw new Error("too many"); + a = 5; + } catch (e) { + if (e instanceof Error && e.message == "too many") a = 7; + } + let quiet = 0; + try { + quiet = 1; + } catch { + quiet = 2; + } + if (a == 7 && quiet == 1) print("A: try and catch ok"); else print(`A: try and catch WRONG ${a} ${quiet}`); + + // B: classes told apart, a class's field read, a message made while the game runs. + let said = ""; + for (let i = 0; i < 4; i++) { + try { + if (i == 0) buy(k * 10, 35); + if (i == 1) throw new RangeError(`i is ${i}, k ${k}`); + if (i == 2) throw new TypeError("type"); + said += "|through"; + } catch (e) { + if (e instanceof OutOfGold) said += `|${e.need} ${e.message}`; + else if (e instanceof RangeError) said += `|range ${e.message}`; + else if (e instanceof Error) said += `|${e.name}`; + } + } + if (said == "|15 need 15 more|range i is 1, k 2|TypeError|through") print("B: classes ok"); else print(`B: classes WRONG ${said}`); + sleep(frames(2)); + + // C: a caught error thrown again to the try further out; finally on each way out. + let cleaned = 0, outer = ""; + try { + try { + if (k > 1) throw new Error("inner"); + } catch (e) { + cleaned += 100; + throw e; + } finally { + cleaned += 1; + } + } catch (e) { + if (e instanceof Error) outer = e.message; + } + for (let i = 0; i < 4; i++) { + try { + if (i == 1) continue; + if (i == 2) break; + } finally { + cleaned += 10; + } + } + if (outer == "inner" && cleaned == 131) print("C: rethrow and finally ok"); else print(`C: rethrow and finally WRONG ${outer} ${cleaned}`); + + // D: out of called functions to the try around the call; a function's own try. + let total = 0, n = 0, stopped = ""; + try { + while (n < 5) { + total += twice(n); + n += 1; + } + } catch (e) { + if (e instanceof Error) stopped = e.message; + } + try { + total += check(k + 7); + } catch { + total += 1000; + } + if (total == 1012 && n == 3 && stopped == "too big: 3" && safe(1) == 2 && safe(k + 5) == -1) print("D: out of functions ok"); + else print(`D: out of functions WRONG ${total} ${n} ${stopped}`); + sleep(frames(2)); + + // E: up through a function that calls itself, its frames taken back on the way. + let bottom = ""; + try { + print(`E: WRONG, came back with ${down(k + 3)}`); + } catch (e) { + if (e instanceof Error) bottom = e.message; + } + const h = halves(k * 20); + if (bottom == "bottom" && h == 177) print("E: through recursion ok"); else print(`E: through recursion WRONG ${bottom} ${h}`); + + // F: a stack that runs out throws a RangeError. + let ran = ""; + try { + ran = `${forever(k)}`; + } catch (e) { + if (e instanceof RangeError) ran = e.message; + } + if (ran == "Maximum call stack size exceeded") print("F: stack overflow caught ok"); else print(`F: stack overflow WRONG ${ran}`); + sleep(frames(4)); + + print("Weighed:"); + sleep(frames(2)); + // W1: 300 calls of a function that may throw and does not. + let w = 0, i = 0, j = 0; + while (i < 300) { + w += check(j); + j += 1; + if (j > 1) j = 0; + i += 1; + } + sleep(frames(4)); + // W2: 100 errors thrown in a called function, each caught in the loop. + let caught = 0; + i = 0; + while (i < 100) { + try { + w += check(i + k); + } catch { + caught += 1; + } + i += 1; + } + sleep(frames(4)); + // W3: an error up through 40 frames, four times. + i = 0; + while (i < 4) { + try { + w += down(38 + k); + } catch { + caught += 1; + } + i += 1; + } + sleep(frames(4)); + // W4: 200 turns through a try with a finally, one in two left by continue. + let f = 0; + i = 0; + while (i < 200) { + i += 1; + try { + if ((i & 1) == 0) continue; + w += 1; + } finally { + f += 1; + } + } + sleep(frames(4)); + if (caught == 103 && f == 200 && w == 450 + 3 + 100) print("W: weighed ok"); else print(`W: weighed WRONG ${caught} ${f} ${w}`); + sleep(frames(2)); + + // Z: an error nothing catches, said in red where it was thrown; the program stops there. + if (k > 1) throw new RangeError("the end"); + print("Z: WRONG, the program went on"); +}); diff --git a/probes/own-tasks.ts b/probes/own-tasks.ts new file mode 100644 index 0000000..93ded22 --- /dev/null +++ b/probes/own-tasks.ts @@ -0,0 +1,148 @@ +// TrigScript 4, the backend: tasks and Promise.all / race in a map of our own (scripts/build-own.mts) — step 11 of the +// way off eudplib, part 2. The program, each task's slot and each branch of a Promise.all or race is a coroutine: a +// trigger that wakes it once the map's count of frames reaches the frame its sleep wrote. The tasks have their turn +// after the program's, a start's slots in order; a slot with nothing in it is passed by as done. +// Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same +// frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" +// are to weigh. One line says in red that a task did not start, as a start whose tasks all run says it. +// npx tsx scripts/build-own.mts probes/own-tasks.ts /tmp/own-tasks.scx +// npx tsx scripts/build-fixture.mts probes/own-tasks.ts /tmp/own-tasks-eud.scx +import { program, print, sleep, frames } from "trigscript"; +import { every, after } from "trigscript/timers"; + +program(async () => { + let waves = 0, turns = 0, children = 0, work = 0; + /** Ends after n frames, giving back twice n. */ + async function slow(n: number): Promise { + await sleep(frames(n)); + return n * 2; + } + /** Says when it ends, n frames on. */ + async function wave(n: number) { + await sleep(frames(n)); + waves += n; + } + /** Counts its turns until cancelled. */ + async function forever() { + while (true) { + turns += 1; + await sleep(frames(1)); + } + } + /** Starts a task of its own a frame after it starts. */ + async function parent() { + await sleep(frames(1)); + start(child(2)); + } + async function child(n: number) { + await sleep(frames(n)); + children += 1; + } + /** Ends on an error after n frames. */ + async function fails(n: number) { + await sleep(frames(n)); + if (n > 0) throw new RangeError(`failed after ${n}`); + } + /** Weighed: asleep for a long while. */ + async function sleeper() { + await sleep(frames(400)); + } + /** Weighed: awake every frame, a little each turn. */ + async function busy(n: number) { + let i = 0; + while (i < n) { + work += 1; + i += 1; + await sleep(frames(1)); + } + } + + print("Own tasks: the payload runs."); + let k = 0; + while (k < 2) k += 1; + + // A: a task awaited, `running` true until it ends, a task cancelled where it waits. + const t = start(slow(3)); + let seen = 0; + while (t.running) { + seen += 1; + await sleep(frames(1)); + } + const f = start(forever()); + await sleep(frames(5)); + const was = f.running; + f.cancel(); + const counted = turns; + await sleep(frames(3)); + if (seen == 4 && was && !f.running && turns == counted && counted == 5) print("A: awaited, running, cancelled ok"); + else print(`A: awaited, running, cancelled WRONG ${seen} ${was} ${counted} ${turns}`); + + // B: a start's slots — three at once of a start that lets two run: the third does not start (said in red). + let i = 1; + let started = 0; + while (i <= 3) { + const w = start(wave(i + k), { max: 2 }); + if (w.running) started += 1; + i += 1; + } + await sleep(frames(6)); + const again = start(wave(1), { max: 2 }); + await again; + if (started == 2 && waves == 3 + 4 + 1) print("B: slots ok"); else print(`B: slots WRONG ${started} ${waves}`); + + // C: Promise.all and Promise.race, their values. + const [a, b] = await Promise.all([slow(2), slow(k + 2)]); + const r = await Promise.race([slow(5), slow(k + 1)]); + if (a == 4 && b == 8 && r == 6) print("C: all and race ok"); else print(`C: all and race WRONG ${a} ${b} ${r}`); + + // D: a task that starts one; E: every and after. + start(parent()); + let ticks = 0, fired = 0; + const e = every(2, (n) => { + ticks = n; + }); + after(5, () => { + fired += 1; + }); + await sleep(frames(9)); + e.cancel(); + await sleep(frames(2)); + if (children == 1 && ticks == 4 && fired == 1) print("D-E: a task's task, every and after ok"); else print(`D-E: WRONG ${children} ${ticks} ${fired}`); + + // F: an error a task ends on, caught where it is awaited. + let caught = ""; + const g = start(fails(k)); + try { + await g; + } catch (err) { + if (err instanceof RangeError) caught = err.message; + } + if (caught == "failed after 2") print("F: a task's error caught ok"); else print(`F: a task's error WRONG ${caught}`); + await sleep(frames(4)); + + print("Weighed:"); + // W1: sixteen tasks asleep, a frame where none wakes. + let s = 0; + while (s < 16) { + start(sleeper(), { max: 16 }); + s += 1; + } + await sleep(frames(6)); + // W2: sixteen tasks awake every frame, each a little each turn. + s = 0; + while (s < 16) { + start(busy(6), { max: 16 }); + s += 1; + } + await sleep(frames(8)); + // W3: the program alone, asleep a frame at a time. + let n = 0; + while (n < 6) { + n += 1; + await sleep(frames(1)); + } + // W4: four branches of a Promise.all, each asleep a frame at a time. + await Promise.all([slow(4), slow(4), slow(4), slow(4)]); + await sleep(frames(4)); + if (work == 16 * 6 && n == 6) print("W: weighed ok"); else print(`W: weighed WRONG ${work} ${n}`); +}); diff --git a/tests/emitter.test.ts b/tests/emitter.test.ts index c3771aa..5570dcc 100644 --- a/tests/emitter.test.ts +++ b/tests/emitter.test.ts @@ -4,7 +4,7 @@ // conditions compare memory with their own numbers, actions write memory, the walk follows next pointers), from the // same random values, every variable compared after. The game probe probes/game-emitter.ts does the same in the // lifted game, for fewer loops. -import { writeFileSync } from "node:fs"; +import { readFileSync, writeFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import type { ArrayDecl, BoolExpr, Call, FuncDecl, NumExpr, Stmt, TextPart, VarDecl } from "../compiler/ir"; import { emitBody, emitLoop, emitterCounts } from "../compiler/backend/emit"; @@ -147,7 +147,8 @@ const CALLS = { whole: false }; * 0 byte as nothing) less 0x0D — which StarCraft draws as nothing, and which a map of our own fills the room around a * number with — in hex. */ -const PRINTED: { want: string[]; got: string[] } = { want: [], got: [] }; +/** `frames`: the frame each of `got` was shown in, by a walk of several (`runOwn`'s `frames`). */ +const PRINTED: { want: string[]; got: string[]; frames: number[] } = { want: [], got: [], frames: [] }; const shown = (bytes: number[]) => bytes.filter((b) => b !== 0x0d).map((b) => b.toString(16).padStart(2, "0")).join(""); /** * The statements the evaluator may still run for the program in hand (a hundred thousand), its calls' bodies included: a program whose @@ -299,7 +300,7 @@ function key(d: Dword): string { if ("template" in d) return `template:${d.template}:${d.word}`; if ("cell" in d) return `epd:${TABLE_EPD(d.cell) + 18 * d.index}`; const f = d.field; - return f === "next" || f === "current" ? `${d.at}|${f}` : "cond" in f ? `${d.at}|cond:${f.cond}${f.part === "player" ? ":player" : f.part === "mask" ? ":mask" : ""}` : `${d.at}|act:${f.act}:${f.part}`; + return f === "next" || f === "current" || f === "flags" ? `${d.at}|${f}` : "cond" in f ? `${d.at}|cond:${f.cond}${f.part === "player" ? ":player" : f.part === "mask" ? ":mask" : ""}` : `${d.at}|act:${f.act}:${f.part}`; } function exec(g: Graph, env: Env): Env { @@ -1066,7 +1067,11 @@ describe("the emitter, a map of our own's texts", () => { const laidOut = new WeakMap; own: ReturnType; texts: string[]; templates: string[] }>(); /** `memory`: dwords of the game's as the map starts, by address (a player's name, their colour); `arrays`: the lists the graph reads. */ /** `runs`: instead of `g` alone, several runs laid out one after another, as a map of our own runs them each frame. */ -function runOwn(g: Graph, memory?: Map, arrays = new Map(), runs?: { graph: Graph; owner: number }[]): Env { +/** + * `frames`: walked that many times, as the game walks the triggers once a frame — from `@run` (the map's count of frames + * one more) when more than one, the memory kept from one to the next; each text shown noted with its frame. + */ +function runOwn(g: Graph, memory?: Map, arrays = new Map(), runs?: { graph: Graph; owner: number }[], frames = 1): Env { // (Laid out once a graph: a test may walk one many times.) let done = laidOut.get(g); if (!done || (done.arrays !== arrays && (done.arrays.size || arrays.size))) { @@ -1090,7 +1095,10 @@ function runOwn(g: Graph, memory?: Map, arrays = new Map dv.getUint8(a - base); // (A player field of 13 is the current player: the dword whose EPD the game's current player is.) const at = (epd: number) => u(DEATHS + 4 * (epd === 13 ? rd(0x6509b0) : epd)); - let pc = own.address; + PRINTED.frames = []; + for (let frame = 1; frame <= frames; frame++) { + let pc = frames > 1 ? own.triggers.get("@run")! : own.address; + const shownBefore = PRINTED.got.length; for (let steps = 0; !(pc & 0x80000000); steps++) { if (steps > 200_000_000) throw new Error("the payload does not end"); if (!inside(pc)) throw new Error(`the walk left the payload: 0x${pc.toString(16)}`); @@ -1125,6 +1133,8 @@ function runOwn(g: Graph, memory?: Map, arrays = new Map>> 0 : (rd(addr) & ~bit) >>> 0); continue; } + // (Play WAV: noted as a sound played, which sound aside.) + if (kind === 8) { PRINTED.got.push("wav"); continue; } if (kind !== 45) throw new Error(`action kind ${kind}`); const dest = at(rd(o + 16)), value = rd(o + 20), modifier = byte(o + 27), old = rd(dest); if (dv.getUint16(o + 30 - base, true) === 0x4353) { @@ -1138,6 +1148,8 @@ function runOwn(g: Graph, memory?: Map, arrays = new Map [id, rd(a)])); for (const [id, n] of WRITABLE) { const r = own.regions.get(id); @@ -1766,3 +1778,341 @@ describe("a map of our own, units", () => { ].join("\n"), UNITS, HOME); }); }); + +describe("a map of our own, errors", () => { + /** What a script prints as the simulator runs it, and as its payload does, walked from its bytes (one pass). */ + const both = (script: string): { want: string[]; got: string[] } => { + const r = compileScript(ts, { "main.ts": script }, defaultScriptNames(), { lib: defaultLib(), emitter: false }); + expect(r.diagnostics).toEqual([]); + const hex = (text: string) => shown([...new TextEncoder().encode(text)]); + const want = simulatePrograms(r.ir, 1, { strings: r.strings }).events.filter((e) => e.text !== undefined && e.action.type === 9).map((e) => hex(e.text!)); + const o = emitOwn(r); + if (typeof o === "string") throw new Error(o); + PRINTED.got = []; + runOwn(o.runs[0].graph, undefined, o.values); + return { want, got: PRINTED.got }; + }; + const same = (script: string, lines?: string[]) => { + const r = both(script); + expect(r.want.length).toBeGreaterThan(0); + expect(r.got).toEqual(r.want); + if (lines) expect(r.want).toEqual(lines.map((l) => shown([...new TextEncoder().encode(l)]))); + }; + /** A program whose `k` is 2, not known when built. */ + const program = (body: string, before = "") => `${before}\nprogram(() => {\n let k = 0;\n while (k < 2) k += 1;\n${body}\n});\n`; + + it("goes to the catch of the try around a throw, and on after it", () => { + same(program([ + " try {", + " if (k > 1) throw new Error(\"too many\");", + " print(\"fine\");", + " } catch (e) {", + " if (e instanceof Error) print(`caught ${e.message}`);", + " }", + " try { print(\"no throw\"); } catch { print(\"never\"); }", + " print(\"after\");", + ].join("\n")), ["caught too many", "no throw", "after"]); + }); + + it("tells classes of error apart, the script's own with fields among them, and makes a message while the game runs", () => { + same(program([ + " for (let i = 0; i < 4; i++) {", + " try {", + " if (i == 0) throw new Late(k * 5);", + " if (i == 1) throw new RangeError(`i is ${i}, k ${k}`);", + " if (i == 2) throw new TypeError(\"type\");", + " print(`${i} went through`);", + " } catch (e) {", + " if (e instanceof Late) print(`late by ${e.by}: ${e.message}`);", + " else if (e instanceof RangeError) print(`range: ${e.message}`);", + " else if (e instanceof Error) print(`other: ${e.message}`);", + " }", + " }", + ].join("\n"), "class Late extends Error {\n by: number;\n constructor(by: number) {\n super(\"late\");\n this.by = by;\n }\n}"), ["late by 10: late", "range: i is 1, k 2", "other: type", "3 went through"]); + }); + + it("throws a caught error again, to the try further out, and finally runs however the try is left", () => { + same(program([ + " let cleaned = 0;", + " try {", + " try {", + " if (k > 1) throw new Error(\"inner\");", + " } catch (e) {", + " print(\"seen inside\");", + " throw e;", + " } finally {", + " cleaned += 1;", + " }", + " } catch (e) {", + " if (e instanceof Error) print(`caught outside: ${e.message}, cleaned ${cleaned}`);", + " }", + " for (let i = 0; i < 4; i++) {", + " try {", + " if (i == 1) continue;", + " if (i == 2) break;", + " } finally {", + " cleaned += 10;", + " }", + " }", + " print(`${cleaned}`);", + ].join("\n")), ["seen inside", "caught outside: inner, cleaned 1", "31"]); + }); + + it("comes out of called functions to the try around the call, and a function's own try keeps its errors", () => { + same(program([ + " let total = 0, n = 0;", + " try {", + " while (n < 5) {", + " total += outer(n);", + " n += 1;", + " }", + " } catch (e) {", + " if (e instanceof Error) print(`stopped at ${n}: ${e.message}`);", + " }", + " try { total += check(k + 7); } catch { total += 1000; }", + " print(`${total} ${safe(1)} ${safe(7)}`);", + ].join("\n"), [ + "function check(n: number) {", + " if (n > 2) throw new Error(`too big: ${n}`);", + " return n + 1;", + "}", + "function outer(n: number) {", + " return check(n) * 2;", + "}", + "function safe(n: number) {", + " try {", + " return check(n);", + " } catch {", + " return -1;", + " }", + "}", + ].join("\n")), ["stopped at 3: too big: 3", "1012 2 -1"]); + }); + + it("says an error nothing catches in red, where it was thrown, and stops the program there", () => { + same(program([ + " print(\"before\");", + " if (k > 1) explode(k);", + " print(\"after\");", + ].join("\n"), "function explode(k: number) {\n throw new RangeError(`k is ${k}`);\n}"), ["before", "\x06TrigScript: uncaught RangeError: k is 2, line 2."]); + }); + + it("comes up through a function that calls itself, its frames taken back on the way", () => { + same(program([ + " try {", + " print(`${down(k + 3)}`);", + " } catch (e) {", + " if (e instanceof Error) print(`caught ${e.message}`);", + " }", + " print(`${halves(k * 20)}`);", + ].join("\n"), [ + "function down(n: number): number {", + " if (n == 0) throw new Error(\"bottom\");", + " return down(n - 1) + 1;", + "}", + // (Each level keeps `n` and `half` around the call; the catch reads them after the frame is taken back.) + "function halves(n: number): number {", + " const half = n >> 1;", + " try {", + " if (n < 3) throw new Error(\"small\");", + " return halves(half) + n;", + " } catch {", + " return half * 100 + n;", + " }", + "}", + ].join("\n")), ["caught bottom", `${40 + 20 + 10 + 5 + 102}`]); + }); + + it("prints what the simulator prints for each of the ledger's scripts of errors", () => { + for (const name of ["try-catch", "try-finally", "rethrow", "error-class", "error-fields", "error-stack", "throw-from-function", "throw-through-recursion", "finally-on-return", "throw-uncaught", "uncaught-in-function", "stack-overflow-caught"]) { + const r = both(readFileSync(`ledger/language/${name}.ts`, "utf8")); + expect([name, r.want.length > 0]).toEqual([name, true]); + expect([name, r.got]).toEqual([name, r.want]); + } + }); + + it("lays a loop out the same on an error's way, a throw after it", () => { + // (Only what runs once before a throw is cold, its reads refreshed from their cells: a loop may turn many times.) + const lines = (end: string) => { + const r = compileScript(ts, { "main.ts": `program(() => {\n let i = 0, s = 0;\n while (i < 100) {\n s += i * 3;\n i += 1;\n }\n ${end}\n});\n` }, defaultScriptNames(), { lib: defaultLib(), emitter: false }); + const o = emitOwn(r); + if (typeof o === "string") throw new Error(o); + return o.runs[0].graph.triggers.filter((t) => t.at && t.at.line >= 3 && t.at.line <= 5).map((t) => [t.conds.length, t.acts.length, !!t.copy]); + }; + expect(lines(`throw new Error("done");`)).toEqual(lines(`print("done");`)); + // (And one whose message reads the sum: its read is refreshed where it is made, no place the loop keeps.) + expect(lines(`throw new Error(\`\${s}\`);`)).toEqual(lines(`print("done");`)); + }); + + it("throws a RangeError where a function that calls itself runs out of stack", () => { + same(program([ + " try {", + " print(`${forever(k)}`);", + " } catch (e) {", + " if (e instanceof Error) print(`${e instanceof RangeError} ${e.message}`);", + " }", + ].join("\n"), "function forever(n: number): number {\n if (n > 1000000) return 0;\n return forever(n + 1) + 1;\n}"), ["true Maximum call stack size exceeded"]); + }); +}); + +describe("a map of our own, tasks", () => { + /** + * What a script prints over so many frames, each text with its frame — and each sound played, as "wav": as the + * simulator runs it, and from its payload's bytes (every run of it, one a player of a program for each). + */ + const both = (script: string, frames: number, file = "main.ts"): { want: string[]; got: string[] } => { + const r = compileScript(ts, { [file]: script }, defaultScriptNames(), { lib: defaultLib(), emitter: false }); + expect(r.diagnostics).toEqual([]); + const hex = (text: string) => shown([...new TextEncoder().encode(text)]); + const events = simulatePrograms(r.ir, frames, { strings: r.strings }).events.filter((e) => (e.text !== undefined && e.action.type === 9) || e.action.type === 8); + const first = events[0]?.frame ?? 0; + const want = events.map((e) => `${e.frame - first} ${e.action.type === 8 ? "wav" : hex(e.text!)}`); + // (Player 1 alone in the game, as the simulator runs a program for each player: for the first.) + const o = emitOwn(r, { slots: [0], forces: [0, 0, 0, 0, 0, 0, 0, 0] }); + if (typeof o === "string") throw new Error(o); + PRINTED.got = []; + runOwn(o.runs[0].graph, undefined, o.values, o.runs.length > 1 ? o.runs.map((x) => ({ graph: x.graph, owner: x.player })) : undefined, frames); + const start = PRINTED.frames[0] ?? 0; + return { want, got: PRINTED.got.map((t, i) => `${PRINTED.frames[i] - start} ${t}`) }; + }; + for (const path of ["sounds/music-loop.ts", "texts/ui-lines.tsx", "programs/owner-all-players-tasks.ts"]) it(`runs ledger/${path} as the simulator does, frame for frame`, () => { + const r = both(readFileSync(`ledger/${path}`, "utf8"), 200, path.endsWith(".tsx") ? "main.tsx" : "main.ts"); + expect(r.want.length).toBeGreaterThan(0); + expect(r.got).toEqual(r.want); + }); + const ROWS = ["start-call", "start-arrow", "start-void", "start-max", "task-awaited", "task-running", "task-cancel", "task-outlives-program", "task-starts-a-task", "generator-as-a-task", "timers-after", "timers-every", "timers-every-times", "timers-every-cancelled", "promise-all", "promise-all-values", "promise-race", "promise-any", "task-error-awaited", "task-error-not-awaited"]; + for (const name of ROWS) it(`runs ledger/tasks/${name} as the simulator does, frame for frame`, () => { + const r = both(readFileSync(`ledger/tasks/${name}.ts`, "utf8"), 200); + // (A task cancelled says nothing: the tests below do.) + if (name !== "task-cancel") expect(r.want.length).toBeGreaterThan(0); + expect(r.got).toEqual(r.want); + }); + const same = (script: string, frames = 120) => { + const r = both(script, frames); + expect(r.want.length).toBeGreaterThan(0); + expect(r.got).toEqual(r.want); + }; + + it("cancels a task where it waits, and says it no longer runs", () => { + same([ + "async function ticker(name: number) {", + " let i = 0;", + " while (true) {", + " print(`${name}: ${i}`);", + " i += 1;", + " await sleep(frames(2));", + " }", + "}", + "program(async () => {", + + " const a = start(ticker(1));", + " const b = start(ticker(2));", + " await sleep(frames(5));", + " a.cancel();", + " print(`${a.running} ${b.running}`);", + " await sleep(frames(4));", + " b.cancel();", + " a.cancel();", + " print(`${a.running} ${b.running}`);", + "});", + ].join("\n")); + }); + + it("gives a start's tasks its slots, says once when all run, and reuses a slot with a new number", () => { + // (The game says in red that a task did not start, as the Python side's build does; the simulator notes a fault.) + const r = both([ + "async function wave(n: number) {", + " await sleep(frames(n));", + " print(`wave ${n}`);", + "}", + "program(async () => {", + " let n = 1;", + " while (n <= 4) {", + " const t = start(wave(n * 2), { max: 2 });", + " print(`${n}: ${t.running}`);", + " n += 1;", + " }", + " await sleep(frames(3));", + " n = 1;", + " while (n <= 2) {", + " const t = start(wave(n), { max: 2 });", + " print(`again ${n}: ${t.running}`);", + " n += 1;", + " }", + " await sleep(frames(10));", + "});", + ].join("\n"), 120); + const said = shown([...new TextEncoder().encode("\x06TrigScript: wave did not start, line 8 - all 2 of this start's tasks are running.")]); + expect(r.got.filter((t) => t.endsWith(said))).toEqual([`0 ${said}`]); + expect(r.got.filter((t) => !t.endsWith(said))).toEqual(r.want); + }); + + it("starts a task from a task later in the turn, and earlier, in the frame it is started", () => { + same([ + "async function late(n: number) {", + " print(`late ${n} starts`);", + " await sleep(frames(1));", + " print(`late ${n} ends`);", + "}", + "async function early() {", + " await sleep(frames(2));", + " print(\"early starts late\");", + " start(late(1));", + " await sleep(frames(2));", + "}", + "program(async () => {", + " start(late(0));", + " start(early());", + " print(\"program\");", + "});", + ].join("\n")); + }); + + it("waits for branches inside a task, and a race inside a branch", () => { + same([ + "async function pause(n: number): Promise {", + " await sleep(frames(n));", + " return n;", + "}", + "async function pair(): Promise {", + " const [a, b] = await Promise.all([pause(2), pause(5)]);", + " return a + b;", + "}", + "async function task() {", + " const [x, y] = await Promise.all([pair(), Promise.race([pause(3), pause(7)])]);", + " print(`task ${x} ${y}`);", + "}", + "program(async () => {", + " start(task());", + " let i = 0;", + " while (i < 8) { print(`frame ${i}`); i += 1; await sleep(frames(1)); }", + "});", + ].join("\n")); + }); + + it("throws in a task to its own catch, and to the program waiting on it", () => { + same([ + "async function risky(n: number) {", + " await sleep(frames(n));", + " try {", + " if (n > 1) throw new RangeError(`n is ${n}`);", + " } catch (e) {", + " if (e instanceof RangeError) print(`caught in the task: ${e.message}`);", + " }", + " if (n > 2) throw new Error(`gave up at ${n}`);", + " print(`task ${n} ends`);", + "}", + "program(async () => {", + " const a = start(risky(1));", + " const b = start(risky(3));", + " await a;", + " print(\"a done\");", + " try {", + " await b;", + " } catch (e) {", + " if (e instanceof Error) print(`the program caught: ${e.message}`);", + " }", + "});", + ].join("\n")); + }); +});