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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/backend/chk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function markInTransit(s: Sections, slot: number): void {
const view = new DataView(uprp.buffer);
const at = (slot - 1) * CUWP_STRIDE;
view.setUint16(at, view.getUint16(at, true) | IN_TRANSIT, true);
view.setUint16(at + 16, view.getUint16(at + 16, true) | IN_TRANSIT, true);
view.setUint16(at + 14, view.getUint16(at + 14, true) | IN_TRANSIT, true);
s.set("UPRP", uprp);
const upus = s.get("UPUS");
if (upus && upus.length >= slot) { const next = upus.slice(); next[slot - 1] = 1; s.set("UPUS", next); }
Expand Down
40 changes: 32 additions & 8 deletions compiler/backend/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,7 @@ class Scope {
case "unrolled": return s.iterations.every((it) => this.stmts(it, true));
case "if": return this.test(s.cond) ? this.stmts(s.then, inLoop) && this.stmts(s.else ?? [], inLoop) : this.island(s);
case "while": return (!s.cond || this.test(s.cond)) ? this.stmts(s.body, true) : this.island(s);
case "do": return this.test(s.cond) ? this.stmts(s.body, true) : this.island(s);
case "do": return this.stmts(s.body, true) && this.stmts(s.before ?? [], true) && (this.test(s.cond) || this.island(s));
case "for": {
if (s.cond && !this.test(s.cond)) return this.island(s);
this.counters.push(writtenIn(s.update));
Expand Down Expand Up @@ -1862,6 +1862,8 @@ class Emitter {
this.stmts(s.body);
this.loops.pop();
this.land(cont);
// (What the test needs computed first, at every turn's end — a `continue` lands here too.)
if (s.before) this.stmts(s.before);
this.jump(s.cond, false, exit, s.at);
this.goto(head);
this.land(exit);
Expand Down Expand Up @@ -5514,10 +5516,24 @@ class Rewriter {
const read = (k === 0 ? c.left : c.right) as Extract<NumExpr, { kind: "read" }>, other = k === 0 ? c.right : c.left;
const l = linear(other);
if (!l || l.terms.length > 1 || !gameRead(read.read)) return null;
const base = read.read.record;
const range: readonly [number, number] | undefined = l.terms.length ? (other as NumExpr & { range?: readonly [number, number] }).range : [l.c, l.c];
if (!range || range[0] < 0) return null;
const flip = { "<": ">", ">": "<", "<=": ">=", ">=": "<=", "==": "==", "!=": "!=" } as const;
const op = k === 0 ? c.op : flip[c.op];
// A constant below zero, compared signed (`i32(deaths(…)) >= -10`): the numbers it holds for, from the lowest to the
// highest, as bits — those below zero are above 2 147 483 647 as bits — one or two runs of the condition's own
// "at least" and "at most" (as `cellCompare` reads a cell).
if (!l.terms.length && (l.c | 0) < 0 && c.unsigned !== true && op !== "==" && op !== "!=") {
const epd = read.read.epd ? { epd: read.read.epd } : {};
const raw = (comparison: number, amount: number): BoolExpr => ({ kind: "cond", record: { ...base, comparison, amount: u(amount) }, ...epd });
const all = (items: BoolExpr[]): BoolExpr => (items.length === 0 ? { kind: "const", value: true } : items.length === 1 ? items[0] : { kind: "and", items });
const between = (lo: number, hi: number): BoolExpr => all([...(u(lo) > 0 ? [raw(0, lo)] : []), ...(u(hi) < U32 - 1 ? [raw(1, hi)] : [])]);
const K = l.c | 0, MIN_S = -2147483648, MAX_S = 2147483647;
const [lo, hi] = op === "<" ? [MIN_S, K - 1] : op === "<=" ? [MIN_S, K] : op === ">" ? [K + 1, MAX_S] : [K, MAX_S];
if (lo > hi) return { kind: "const", value: false };
return lo >= 0 || hi < 0 ? between(lo, hi) : { kind: "or", items: [between(lo, -1), between(0, hi)] };
}
if (!range || range[0] < 0) return null;
const how: Record<string, [number, number]> = { ">=": [0, 0], ">": [0, 1], "<=": [1, 0], "<": [1, -1], "==": [10, 0], "!=": [10, 0] };
const [comparison, add] = how[op];
if (range[0] + add < 0) return op === "<" && range[1] < 1 ? { kind: "const", value: false } : null;
Expand Down Expand Up @@ -5927,7 +5943,10 @@ class Rewriter {
if (!el || !inRegion(this.arrays, el.array)) return null;
const a = this.arrays.get(el.array)!;
const index = this.flat(el.index, pre, where), r = (index as NumExpr & { range?: readonly [number, number] }).range;
const at = this.side(index, pre, where), cell: NumExpr = { ...el, index: at };
// (A cell is read at one variable plus a constant: an index under a mask — `xs[i & 3]`, which `side` would keep as a
// masked side — and one of more terms — `board[y * W + x]` — are computed first.)
const shape = side(index);
const at = shape && "mask" in shape && shape.mask !== undefined ? this.sum(index, pre, where) : this.side(index, pre, where), cell: NumExpr = { ...el, index: at };
const raw = (o: CompareOp, v: number): BoolExpr => ({ kind: "compare", op: o, left: cell, right: { kind: "const", value: u(v) }, unsigned: true, ...where });
const all = (items: BoolExpr[]): BoolExpr => (items.length === 0 ? { kind: "const", value: true } : items.length === 1 ? items[0] : { kind: "and", items });
// The bits between two numbers, ends included, read as they are.
Expand Down Expand Up @@ -6167,11 +6186,14 @@ class Rewriter {
return [s.kind === "for" ? { ...s, cond: undefined, body: [...pre, leave, ...body], update } : { ...s, cond: undefined, body: [...pre, leave, ...body] }];
}
case "do": {
// (A map of our own: the test as rewritten when nothing comes before it — something that did would have to run
// at a `continue` too.)
// (A map of our own: the test as rewritten; what it needs computed first runs before it at every turn's end,
// `before`, where a `continue` lands too — the emitter lays it out at the loop's continue label.)
const pre: Stmt[] = [];
const cond = this.own ? this.cond(s.cond, pre, where) : s.cond;
return [{ ...s, body: this.stmts(s.body), ...(this.own && !pre.length ? { cond } : {}) }];
if (!this.own) return [{ ...s, body: this.stmts(s.body) }];
// (What the texts pass put before the test stays, rewritten as statements, and the test's own needs follow it.)
const before = [...this.stmts(s.before ?? []), ...pre];
return [{ ...s, body: this.stmts(s.body), cond, ...(before.length ? { before } : {}) }];
}
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) }];
Expand Down Expand Up @@ -6360,9 +6382,11 @@ export function emitBody(body: Stmt[], decls: Map<string, VarDecl>, key: string,
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.
// At its end the program wakes no more — unless it started tasks, which run on after it (the simulator's rule,
// and the Python side's): its wake then comes every frame and goes straight to the tasks' turn.
end: () => {
e.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: U32 - 1 } });
if (scope.slots.size) e.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: 0 } }, { act: { dword: { data: WAKE_AT }, modifier: "set", value: { label: e.tasksLabel } } });
else e.open().items.push({ act: { dword: { data: WAKE_TICK }, modifier: "set", value: U32 - 1 } });
e.goto(e.tasksLabel);
},
};
Expand Down
7 changes: 5 additions & 2 deletions compiler/backend/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* line goes as a *chat* message: the pattern's number, then its values. On arrival the pattern's flag is up for a
* frame and its values are in cells.
*/
import { STATEMENT_KINDS } from "./emit";
import type { ArrayDecl, At, BoolExpr, NumExpr, Program, Stmt, VarDecl } from "../ir";
import type { ActionRecord, TriggerRecord } from "../../vendor/triggers";
import { MAX_CHAT_BYTES, MAX_CHAT_NUMBER, MOUSE_BUTTONS, inputsOf, parseChatPattern, virtualKey, type ChatPattern, type InputSource, type MouseButton } from "../input";
Expand Down Expand Up @@ -113,7 +114,8 @@ const KIND_STATE = 0, KIND_SYNC = 1, KIND_CHAT = 2;

const global = (id: string): VarDecl => ({ id, name: "(input)", kind: "number", shared: true, routed: true, global: true, at: AT0 });
export const INPUT_GLOBALS: readonly VarDecl[] = [global(MADE), global(RING), global(NAME_LENGTH), global(LINE_LENGTH), global(TYPED)];
export const isInputGlobal = (id: string) => INPUT_GLOBALS.some((d) => d.id === id);
/** An id of the input's: its arrays, globals and temps are the map's, one of each however many programs and runs read them (as the events' `@ev.`). */
export const isInputGlobal = (id: string) => id.startsWith("@in.");
/** The mouse of a player on the map, as the receive pass keeps it: what `underMouse()`'s pick measures from. */
export const MOUSE_CELLS = { x: MX, y: MY };

Expand Down Expand Up @@ -315,7 +317,8 @@ export function inputValues(program: Program, plan: OwnInputPlan, passes: boolea
};

const fix = (x: unknown): unknown => {
if (Array.isArray(x)) return x.map(fix);
// (A list of statements inside a statement — an if's, a loop's, a try's — as statements: a sync() in it is lowered too.)
if (Array.isArray(x)) return x.length && x[0] && typeof x[0] === "object" && STATEMENT_KINDS.has((x[0] as { kind?: string }).kind ?? "") ? stmts(x as Stmt[]) : x.map(fix);
if (!x || typeof x !== "object") return x;
const o = x as Record<string, unknown> & { kind?: string };
if (o.kind === "input") return read(o.input as InputSource);
Expand Down
11 changes: 9 additions & 2 deletions compiler/backend/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,21 @@ export function layout(input: LayoutInput): OwnMap {
// 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}`;
// A run with tasks: the current player set to its owner before its wake, every frame. The game set it to the player
// whose list carries the walk (the last one's), and the wake sets it to the owner only when the program runs; the
// tasks' turn comes while the program sleeps too, and what a task showed then, the owner did not see. (The first
// play of the events probe, 2026-10-24: "back", printed by a task two seconds after a death, never showed.)
const cur = (i: number) => (i ? `@cur#${i}` : "@cur");
const before = (i: number) => (runs[i].graph.tasks ? cur(i) : wake(i));
// (A run whose graph holds the map's events pass goes through it first.)
const first = (i: number) => runs[i].graph.pass ?? wake(i);
const first = (i: number) => runs[i].graph.pass ?? before(i);
// (A run whose graph holds the map's send pass: the walk goes through it after the last run, before it stops.)
const sendPass = runs.find((r) => r.graph.after)?.graph.after;
const after = (i: number) => (i + 1 < runs.length ? first(i + 1) : sendPass ?? stop);
const list = (p: number) => TRIGGER_LISTS + 12 * p;
const listEnd = (p: number) => u(~(list(p) + 4));
// (Where each run's graph ends: the next run.)
const ends = new Map<string, string>(runs.flatMap((_, i): [string, string][] => [[i ? `${END}#${i}` : END, after(i)], [i ? `${PASS_END}#${i}` : PASS_END, wake(i)], [i ? `${AFTER_END}#${i}` : AFTER_END, stop]]));
const ends = new Map<string, string>(runs.flatMap((_, i): [string, string][] => [[i ? `${END}#${i}` : END, after(i)], [i ? `${PASS_END}#${i}` : PASS_END, before(i)], [i ? `${AFTER_END}#${i}` : AFTER_END, stop]]));
const endOf = (l: string) => ends.get(l) ?? l;
// (`flags`: a node's execution flags as written, a trigger of the map's own; else preserved, or `off`.)
type T = { label: string; conds: Cond[]; acts: Act[]; next?: string | number; prev?: number; off?: boolean; flags?: number };
Expand All @@ -323,6 +329,7 @@ export function layout(input: LayoutInput): OwnMap {
return { label: own(p, k), prev: 0, conds: t.conds, acts: t.acts, next: chain[i + 1] ?? gate(p), flags: t.record.flags };
}));
const triggers: T[] = runs.flatMap(({ graph, owner }, i) => [
...(graph.tasks ? [{ label: cur(i), conds: [], acts: [{ dword: { game: CURRENT_PLAYER }, modifier: "set", value: owner }], next: wake(i) } satisfies T] : []),
{
label: wake(i),
conds: [{ dword: { var: NOW }, compare: "atLeast", value: 0 }, { dword: { game: list(owner) + 8 }, compare: "atMost", value: 0x7fffffff }],
Expand Down
Loading
Loading