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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
216 changes: 210 additions & 6 deletions compiler/backend/emit.ts

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions compiler/backend/own.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* A map of our own, as far as the emitter goes: a compiled script's one program lowered whole by `emitBody`, or why
* it is not. `scripts/build-own.mts` makes the map from what this gives; the ledger (`tests/ledger.test.ts`) asks it
* of every feature, so that the two refuse the same.
*/
import type { CompileResult } from "../compiler";
import type { ArrayDecl, FuncDecl, Program, VarDecl } from "../ir";
import { emitBody } from "./emit";
import type { Graph } from "./graph";

export function emitOwn(r: Pick<CompileResult, "ir" | "triggers">): { program: Program; graph: Graph } | string {
if (r.ir.length !== 1) return `a map of our own has one program; this script has ${r.ir.length}`;
const program = r.ir[0];
if (program.perPlayer) return "a map of our own has no per-player program yet";
// (Its start-up is in the owner's triggers, and the game runs those of P1 to P8 only.)
if (program.owner >= 8) return `a program owned by P${program.owner + 1}: a map of our own starts a program from its owner's triggers, which the game runs for P1 to P8 only`;
// (A map of our own is built without the map's triggers, so `trigger()` records would be lost without a word.)
if (r.triggers.length) return `a map of our own keeps no trigger() or hyperTriggers() records yet; this script has ${r.triggers.length}`;
const decls = new Map<string, VarDecl>();
const collect = (x: unknown): void => {
if (Array.isArray(x)) x.forEach(collect);
else if (x && typeof x === "object") {
const o = x as Record<string, unknown>;
if (o.decl && typeof o.decl === "object" && "id" in (o.decl as object)) decls.set((o.decl as VarDecl).id, o.decl as VarDecl);
for (const v of Object.values(o)) if (v && typeof v === "object") collect(v);
}
};
collect(program);
const functions = new Map<string, FuncDecl>((program.functions ?? []).map((f) => [f.id, f]));
const arrays = new Map<string, ArrayDecl>((program.arrays ?? []).map((a) => [a.id, a]));
const graph = emitBody(program.body, decls, "own", functions, arrays, undefined, program.owner);
return typeof graph === "string" ? graph : { program, graph };
}
2 changes: 1 addition & 1 deletion compiler/generated/stdlib.ts

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions compiler/hoist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ export function clockMessage(call: string): string {

/** The library calls the game answers: never hoisted, the structured compiler lowers them. */
const GAME_CALLS = new Set(["random", "sleep", "nextFrame", "until", "sync", "select", "start", "rose", "once", "shared", "lines", "centerLocation", ...ON_EVENTS]);
const EVENT_NAMES: ReadonlySet<string> = new Set(ON_EVENTS);
/**
* Whether a library name is one of `GAME_CALLS`. An event's name is the game's as `on.order(…)` or a unit's
* `u.order(…)`, not as the action of the same name, `order(…)`, a function of the library's own whose arguments the
* script works out as any action's.
*/
function answeredByGame(ts: typeof TS, checker: TS.TypeChecker, id: TS.Identifier, lib: string): boolean {
if (!GAME_CALLS.has(lib)) return false;
if (!EVENT_NAMES.has(lib)) return true;
let sym = checker.getSymbolAtLocation(id);
if (sym && sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym);
const decl = sym?.valueDeclaration ?? sym?.declarations?.[0];
return !(decl && ts.isFunctionDeclaration(decl));
}
/**
* Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison
* (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a
Expand Down Expand Up @@ -311,7 +325,7 @@ export function usesGame(ts: typeof TS, checker: TS.TypeChecker, fn: TS.Node, se
if (clockCall(ts, checker, n)) { found = true; return; }
if (ts.isIdentifier(n)) {
const lib = libraryName(ts, checker, n);
if (lib && (GAME_CALLS.has(lib) || READ_CALLS.has(lib) || READ_ARITY.has(lib) || GAME_CELL_ROOTS.has(lib))) { found = true; return; }
if (lib && (answeredByGame(ts, checker, n, lib) || READ_CALLS.has(lib) || READ_ARITY.has(lib) || GAME_CELL_ROOTS.has(lib))) { found = true; return; }
const decl = declarationOf(ts, checker, n);
const other = decl && decl !== fn && (isScriptFunction(ts, decl) ? decl : scriptArrow(ts, decl) ?? (classMember(ts, decl) ? decl : undefined));
if (other && usesGame(ts, checker, other, seen)) { found = true; return; }
Expand Down Expand Up @@ -577,7 +591,7 @@ function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, a
const property = ts.isPropertyAccessExpression(p) && p.name === n;
if ((ts.isPropertyAssignment(p) && p.name === n) || (ts.isMethodDeclaration(p) && p.name === n) || ts.isQualifiedName(p)) return;
const lib = libraryName(ts, checker, n);
if (lib && GAME_CALLS.has(lib)) { ok = false; return; }
if (lib && answeredByGame(ts, checker, n, lib)) { ok = false; return; }
if (lib && FORBIDDEN_INSIDE.has(lib) && ts.isCallExpression(p.parent) && p.parent.expression === p && property) { ok = false; return; }
if (lib && FORBIDDEN_INSIDE.has(lib) && ts.isCallExpression(p) && p.expression === n) { ok = false; return; }
if (property) return;
Expand Down Expand Up @@ -679,7 +693,7 @@ function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, a
let callee: TS.Expression = e.expression;
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
if (isTaskFunction(ts, checker, callee)) callback(callee, items);
else if (!lib || !GAME_CALLS.has(lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items);
else if (!lib || !answeredByGame(ts, checker, ts.isPropertyAccessExpression(callee) ? callee.name as TS.Identifier : callee as TS.Identifier, lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items);
for (const a of e.arguments ?? []) {
let fn: TS.Expression = a;
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
Expand Down
5 changes: 3 additions & 2 deletions compiler/structured.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,8 +806,9 @@ export class Structured {
}
const obj = sub(e.expression);
if (!obj || obj.value === null || obj.value === undefined) return undefined;
// A field of the unit selected (\`local.selected.hp\`) is read when the map is played, as any unit's is.
if (isSelected(obj.value) || isSelectionCount(obj.value)) return undefined;
// A field of the unit selected (\`local.selected.hp\`) is read when the map is played, as any unit's is — and so is
// one of a unit picked (`first(…)?.hp`), which, worked out here, was the pick's own field: undefined.
if (isSelected(obj.value) || isSelectionCount(obj.value) || isUnitPick(obj.value)) return undefined;
return { value: readField(obj.value, e.name.text) };
}
if (ts.isElementAccessExpression(e)) {
Expand Down
87 changes: 82 additions & 5 deletions docs/v4-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -3849,7 +3849,10 @@ clears the map's triggers: refused now.
**Why the arithmetic stayed hidden.** Most refusals read `a assign it does not lower (no reason given), line N`:
`Scope.test()` drops the reasons `bool()` gives, `lowers()` and `withCells()` give none, and `why ||=` keeps an
earlier reason that is not the one. `a * b`, `a / 4`, `random(6)` and `minerals(P1)` were all refused in the same
words. Step 14 asks for a refusal that names what it refuses; the ledger (below) asks for it to be right.
words. Step 14 asks for a refusal that names what it refuses; the ledger (below) asks for it to be right. Since the
ledger, a refusal names the first part of the statement that the emitter never takes (`unlowered` in `emit.ts`) and
its line — "a product of two values, line 51", "the action Set Resources, line 10", "a write of unit.sight, line 550",
"a unit's hp" — and falls back on the checks' own reason, now kept statement by statement.

**Smaller things**, to fix inside the step that meets them:
- A boolean `?:` is refused, with constants on both sides too (`Rewriter.cond` has no case for it).
Expand All @@ -3872,10 +3875,84 @@ Bad Apple: 5, 6, 7, as the fog library. The bullets: 5 (a death count read), 9.
- *Magenta.* It keeps plugin-eudplib alive. Until it builds on the own layout, a map with both could have TrigScript's
program built into Magenta's eudplib build, or refuse the pair.

**The ledger, so the list stays whole.** A script for each feature — the audit's 230 are the start — each with its
step and whether a map of our own takes it, run by a test in `tests/`. It fails when a script's answer changes, and
when a kind of the IR or a name the library exports has no row. A step is done when its rows say so. Proposed as the
first thing before step 4.
**The ledger, so the list stays whole.** Built before step 4: see the next section.

### The ledger (2026-10-19)

`ledger/` holds a script for each feature of the language and the library — 831 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:

// ledger: step 4, refused: a product of two values
// ledger: step 1, takes

`tests/ledger.test.ts` compiles each one as the editor does and gives it to `emitOwn` (`compiler/backend/own.ts`, the
checks `build-own` makes, now shared); it fails when a first line no longer says what happens, and when a kind of the IR
(129, less six that never survive the front end), a name a script has from the library, a member of `on`, `ui`,
`memory`, `local`, a unit, a set of units or a task, or an export of a module of the standard library (340 names in
all) is in no script. When a step lands, `npx tsx scripts/ledger.mts --write` writes the first lines anew and the diff
is the step's account; `npx tsx scripts/ledger.mts` prints the table, `--check <files>` looks at a few, `--missing`
lists what no script reaches. A step is done when its scripts say so.

| Step | Taken today |
| --- | ---: |
| 0 The start | 157 of 158 |
| 1 Calls and recursion | 33 of 33 |
| 2 Texts with numbers | 6 of 6 |
| 3 Arrays of a fixed length | 53 of 54 |
| 4 Numbers | 0 of 59 |
| 5 The game's conditions and actions | 0 of 193 |
| 6 The game's memory | 0 of 49 |
| 7 Programs, owners and players | 1 of 22 |
| 8 Texts as values | 0 of 43 |
| 9 Arrays that grow, the heap | 0 of 27 |
| 10 Units | 0 of 99 |
| 11 Tasks, `async` and errors | 0 of 44 |
| 12 Input | 0 of 31 |
| 13 The map's own triggers | 0 of 13 |

Two steps called done are not quite: a `?:` of two booleans (step 0) and a cell read at a shifted index, `xs[a >> 1]`
(step 3 — the store there and `const i = a >> 1; xs[i]` are taken). Every script a map of our own takes was also run
from its laid-out bytes against the simulator — the numbers, arrays, texts and language scripts, and about forty edge
cases besides (signs of `>>`, `u32` printed and compared, wrap-around, reads past the ends, negative indexes): all
printed the same. The README's reference to the language predates TrigScript 4 (it says `try` and `async` are missing):
the ledger is the list it can be written again from at 4.0.

**Found while writing it**, in the front end — both builds — unless said:

*Fixed with the ledger:*
- A field or flag read straight off a pick was worked out when the script was built, as `undefined`:
`` print(`${first(…)?.hp}`) `` printed "undefined", `if (first(…)?.cloaked)` was nothing at all, and
`n = first(…)?.hp ?? 5` was `n = 5`. Now a read of the game's, as `u.hp` is (`tests/units.test.ts`); a `??` after
it is a compile error, as after `u?.hp` (below).
- The action `order(…)` did not compile inside a program: `on.order`'s name was taken for it, and its arguments were
never worked out (`tests/events.test.ts`).
- `music.loop(song)` never compiled — the loop showed no wait of its own (`tests/music.test.ts`).
- A map of our own refused a print to the owner named by its slot (`{ to: P1 }` in P1's program), and took a program
owned by P9 to P12, whose triggers the game does not run — refused now (to be confirmed in the game).

*Open:*
- **Built wrong without a word:** a function written outside the program that keeps a state and is called inside it
(`` const next = counter(); while (…) print(`${next()}`) ``) is worked out once when the script is built:
1, 1, 1 for 1, 2, 3. Kept out of the ledger until it is refused or right.
- **Built wrong without a word:** `order(…, orders.Move)` type-checks (`OrderKind | number`) and makes an Order action
of modifier 6; the action knows move, patrol and attack (0, 1, 2).
- `displayText(text, always)` made a print loses its flag — to check against the flags a print gives Display Text.
- `disabled(…)` inside a program is neither refused nor said: a disabled condition in an `if` holds, as in a trigger.
`preserve()` and `preserveTrigger()` there are dropped (every program is preserved), also without a word.
- False errors: `n++` used as a value in a loop's body ("This loop's condition never changes" — `checkSleeps` does not
see writes inside expressions or calls); a comparison passed as a boolean argument (`step(n, turns < 2)`); a `switch`
on a text known when built; `?:` between two closures; an arrow using `this` inside a method (a JavaScript exception
as the message); `first(…)?.order(…)` on a pick; `u?.hp ?? 0`; `await sleep(…).then(() => 7)`.
- `return` at the top of a program is "return outside a function", and `trigscript/cutscene`'s own example uses it (and
`first(unitsAt(…))`, which `first` does not take).
- Not in a program, against JavaScript: `~a`, `**`, `Math.pow`, `Math.sign`, `Math.log2`, `fill(v, start, end)`,
`reduce` with no first value. A `const HALF = 7 / 2` worked out when built is 3.5, a fixed number; the same `7 / 2` in
the game is 3.
- A map of our own: `displayText("known text")` is refused where `print("known text")` is taken (step 5's first);
JSX inside a program is "made while the game runs" even when it holds nothing of the program's (step 8);
`a >> 0` is refused where `a >> 1` to `a >> 31` are taken; `Math.tan` and `Math.hypot` are refused as what they
become (`Math.sin`, a product).

## Part C — the game, pushed

Expand Down
12 changes: 12 additions & 0 deletions ledger/arrays/array-compound-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// ledger: step 3, takes
// A cell of an array changed where it is: `hp[i] += 5`, `hp[i]++`.
program(() => {
const hp = [10, 20, 30];
let i = 0;
while (i < 3) {
hp[i] += 5;
hp[i]++;
i += 1;
}
print(`${hp[1]}`);
});
12 changes: 12 additions & 0 deletions ledger/arrays/array-destructured.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// ledger: step 3, takes
// The items of an array taken out into variables: `const [a, b] = xs`.
program(() => {
const xs = [3, 4, 5];
let i = 0;
while (i < 3) {
xs[i] += i;
i += 1;
}
const [a, b] = xs;
print(`${a + b}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-fill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// Every cell of an array of the program set to a value that changes while the game runs: `board.fill(v)`.
program(() => {
const board = [1, 2, 3, 4];
let v = 0;
while (v < 5) v += 2;
board.fill(v);
print(`${board[3]}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-for-of.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// A loop over the cells of an array of the program, within one frame: `for (const x of xs)`.
program(() => {
const xs = [4, 8, 15, 16];
xs[0] = 5;
let total = 0;
for (const x of xs) total += x;
print(`${total}`);
});
10 changes: 10 additions & 0 deletions ledger/arrays/array-from.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// ledger: step 3, takes
// An array made from a length written in the script and a function of each place:
// `Array.from({ length: 4 }, (_, i) => i * 2)`.
program(() => {
const xs = Array.from({ length: 4 }, (_, i) => i * 2);
let i = 0;
while (i < 3) i += 1;
xs[0] = i;
print(`${xs[i] + xs[0]}`);
});
14 changes: 14 additions & 0 deletions ledger/arrays/array-given-to-a-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ledger: step 3, takes
// An array handed to a function of the script, which reads and stores into the array itself.
function bump(xs: number[], i: number) {
xs[i] += 10;
}
program(() => {
const xs = [1, 2, 3];
let i = 0;
while (i < 2) {
bump(xs, i);
i += 1;
}
print(`${xs[0] + xs[1]}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-in-a-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// An array of a fixed length that is a field of a record: `shop.stock[i]`.
program(() => {
const shop = { gold: 10, stock: [3, 3, 3] };
let i = 0;
while (i < 2) i += 1;
shop.stock[i] -= 1;
print(`${shop.stock[i] + shop.gold}`);
});
10 changes: 10 additions & 0 deletions ledger/arrays/array-includes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// ledger: step 3, takes
// Whether an array of the program has a value, a loop the game runs: `xs.includes(v)`.
program(() => {
const xs = [4, 8, 15, 16];
xs[0] = 5;
let v = 0;
while (v < 15) v += 5;
if (xs.includes(v)) print("found");
else print("not found");
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-index-of.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// Where a value is in an array of the program, −1 when it is not: `xs.indexOf(v)`.
program(() => {
const xs = [4, 8, 15, 16];
xs[0] = 5;
let v = 0;
while (v < 15) v += 5;
print(`${xs.indexOf(v)}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-length-of-a-fixed-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 0, takes
// The length of an array of a fixed length, known when the map is built: `xs.length`.
program(() => {
const xs = [4, 8, 15, 16];
xs[0] = 5;
let i = 0;
while (i < xs.length) i += 3;
print(`${i}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-new-filled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// An array of a length written in the script, every cell one value: `new Array(8).fill(3)`.
program(() => {
const board = new Array(8).fill(3);
let i = 0;
while (i < 5) i += 2;
board[i] = 7;
print(`${board[i] + board[0]}`);
});
14 changes: 14 additions & 0 deletions ledger/arrays/array-of-booleans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ledger: step 3, takes
// An array of booleans, stored and tested at a place that changes while the game runs: `seen[i] = true`.
program(() => {
const seen = [false, false, false, false];
let i = 0;
while (i < 4) {
seen[i] = i > 1;
i += 1;
}
let j = 0;
while (j < 3) j += 1;
if (seen[j]) print("seen");
else print("not seen");
});
15 changes: 15 additions & 0 deletions ledger/arrays/array-of-instances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// ledger: step 3, takes
// An array of instances of a class of the script, a method called on one: `towers[i].upgrade()`.
class Tower {
level = 1;
upgrade() {
this.level += 1;
}
}
program(() => {
const towers = [new Tower(), new Tower()];
let i = 0;
while (i < 1) i += 1;
towers[i].upgrade();
print(`${towers[0].level} ${towers[1].level}`);
});
11 changes: 11 additions & 0 deletions ledger/arrays/array-of-records-that-grows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// ledger: step 9, refused: an array that grows
// An array of records that grows a record at a time: `enemies.push({ hp, x })`.
program(() => {
const enemies: { hp: number; x: number }[] = [];
let x = 0;
while (x < 10) {
enemies.push({ hp: 20, x });
x += 4;
}
print(`${enemies.length} ${enemies[1].x}`);
});
13 changes: 13 additions & 0 deletions ledger/arrays/array-of-records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// ledger: step 3, takes
// An array of records, each with its fields, read and written at a place that changes while the game runs:
// `waves[i].count`.
program(() => {
const waves = [
{ count: 5, gold: 10 },
{ count: 8, gold: 15 },
];
let i = 0;
while (i < 1) i += 1;
waves[i].count += 2;
print(`${waves[i].count + waves[i].gold}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-read-at-a-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// A cell of an array of the program read at a value that changes while the game runs: `hp[i]`.
program(() => {
const hp = [10, 20, 30, 40];
hp[0] = 15;
let i = 0;
while (i < 2) i += 1;
print(`${hp[i]}`);
});
9 changes: 9 additions & 0 deletions ledger/arrays/array-read-past-the-end.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// ledger: step 3, takes
// A cell read at a place past an array's end, which reads 0 (and is said when the script is tested).
program(() => {
const hp = [10, 20, 30];
hp[0] = 5;
let i = 0;
while (i < 5) i += 2;
print(`${hp[i]}`);
});
Loading
Loading