Skip to content

Latest commit

 

History

History
79 lines (73 loc) · 5.97 KB

compiler.md

File metadata and controls

79 lines (73 loc) · 5.97 KB

How the compiler is built

Notes for working on TrigScript's compiler. The README has the commands and the file list; ir.md describes the IR.

names.ts turns the map into name tables (players, units, locations, switches, AI scripts, and the fixed weapons, upgrades and technologies stats() takes). Each entry's keys are an identifier derived from the display name first (Terran Marine → TerranMarine), the display name itself second, then custom names, unique per table by construction. declarations.ts generates the .d.ts from them plus the library: values are branded literal types (UnitType<0> = 0 & Brand<"unit">; plain numbers still pass, a Location where a UnitType belongs does not), enumerated kinds are string unions of the canonical words, every condition and action is a declare function whose identifier is its ConditionType / ActionType key in camel case, and the whole thing is declared twice: as globals and as the ambient module "trigscript".

runtime.ts is the same vocabulary as values: the tables as frozen objects and one function per condition and action that validates its arguments and returns a record. trigger() pushes a record onto a Collector; program() pushes the descriptor the transformer made for it. Strings are not interned here: text and WAV fields hold local ids into CompileResult.strings, and the build resolves them through the map's string table.

compiler.ts builds a real ts.createProgram over the files, the declarations and the standard library (in-memory host; module: commonjs, moduleResolution: bundler), collects every reference to the map's tables while the checker is there (so a rename can follow them even when they no longer type-check), and stops at the first type error. It then plans every program() body and every game() arrow (hoist.ts): the game bindings are the body's let / var, the parameters and the functions declared in it; a hoisted expression is a maximal subexpression that mentions none of those (and is not random(), nor a call of a game() function, whose type carries a brand), and a const of the body whose initialiser is hoistable is a build-time constant. The emit transformer appends its position to every trigger() call and replaces each planned arrow with a descriptor holding a function that returns the hoisted expressions as thunks and the constants as memoised thunks, every reference to a constant rewritten to a call of its thunk, so a constant is computed when first needed and never in a pruned branch. The emitted CommonJS is linked by link.ts (relative imports against the files, "trigscript" to the runtime, the library's names in scope as globals) and run; an error the script throws is placed through the emitted source map. game(descriptor) returns a function that throws if the script calls it and carries the descriptor for the walker.

structured.ts then walks each program body against the same plan, with the thunks the descriptor's function returned: where the plan says an expression was hoisted, the walker takes its value: a number, a boolean, a condition, an action or a list of them. let → a variable of the IR (a number, a boolean, a unit, a text), a record of them (an object literal, an instance of a class) or an array, bound in a scope keyed by declaration node so shadowing and inlining resolve as the checker does; expressions become IR expressions as written; && / || / ! stay what they are, and the lowering short-circuits them; functions declared in the body and game() functions are call nodes, inlined where a function is first met, the walker switching to the function's own plan, file and thunks for the duration, and from the second time on a call of one body kept in Program.functions, when the function compiles with every parameter a variable (the attempt's diagnostics are thrown away, and a first call already inlined is changed to match; settleFunctions then inlines again whatever is left with one call); a call, member access or arithmetic over values a parameter was bound to is evaluated on the spot; a for whose bounds evaluate is unrolled like a for…of; an action with a variable argument carries the expression beside its record. The thunks are memoised in the bodies, so the script's build-time parts run once.

TrigScript's own build is compiler/backend/ (see the compiler/ row above): a program's body becomes one graph of triggers, laid out after the map's strings. Arithmetic is 32 bits that wrap, and each comparison, division, shift right, min, max and printed number reads them signed or unsigned as numbers.ts marked it. An array that grows is a block of one heap the programs share (sizes double, and a block given back serves the next array of its size), and a made text is such a block of bytes; an array's cells sit beside the heap's bytes as cell triggers, 72 bytes each, so that a read or a write is a jump into the cell. A function that is called is one body with its parameters as variables; one that calls itself keeps what a call would overwrite on a stack of its own, a region beside the heap whose depth the script's Settings give. A per-player program is laid out once for each of its owners, with variables of its own but for the shared ones. A read is the condition itself where a table of the game is the value (a player's deaths, kills, ore, gas; the player bytes and the supply tables) and otherwise a search with the condition, "at least" a bit at a time from the top, so it means what the condition means. A text with values in it is written over a string of the map's table as long as its template, which is then shown. docs/ir.md says what each node means to the backend. simulateIr.ts mirrors all of it, wrap included, so that what Simulate shows is what the game does. (Until TrigScript 4's step 17 a second lowering, python/trigscript.py, built through eudplib what the backend did not take yet; step 16's random whole programs, run both ways, held the two to the same lines before it went.)