Merge syntax redesign: cohesion pass (Phases 1-5)

Resolves all 13 findings from SYNTAX-REDESIGN.md. Rule A (: associates, =
binds) across fields/records/spawn/ui/modifiers; Rule B (statement separators);
auto-numbered machine states + enum types for magic-int spaces; docs and editor
tooling reconciled with the compiler and guarded against future drift.

Every migration proven behaviour-preserving (byte-identical self-compiled IR +
golden renders); C-free fixpoint held throughout; test.sh 14/14, self-host 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 17:43:39 +03:00
commit 3227e1e1d3
53 changed files with 5723 additions and 4579 deletions

View file

@ -126,7 +126,7 @@ All verified. A compiler needs each of these, and each one works today.
| `shr` is **logical**, not arithmetic | ✅ | `shr(-16, 1)` → `2147483640` |
| Character literals | ✅ | `'x'`, `'\n'`, `'\0'` lex to ints |
| Exit codes | ✅ | `os_exit(3)` → shell sees `3` |
| Separate compilation, C ABI | ✅ | `module` + `export fn`, `extern fn … = "sym"` |
| Separate compilation, C ABI | ✅ | `module` + `@export fn`, `extern fn … = "sym"` |
**The consequence:** a compiler is *already expressible* in Ludic today. You
could write a lexer, a parser building nodes as hand-offset `peek32`/`poke32`
@ -853,8 +853,8 @@ current tree (`./test.sh` = 64/64).
**Recursion** ✅ → `55`
```ludic
game P {
fn fib(n: int) -> int { if n < 2 { return n } return fib(n-1) + fib(n-2) }
system B phase Start { print_int(fib(10)) quit() }
fn fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) }
system B phase Start { print_int(fib(10)); quit() }
}
```
@ -878,11 +878,11 @@ fn streq(a: ptr, b: ptr) -> bool {
fn itoa(v: int, buf: ptr) -> int {
let n = 0
let x = v
if x == 0 { poke8(buf,0,48) return 1 }
if x == 0 { poke8(buf,0,48); return 1 }
let tmp = mem_alloc(16)
while x > 0 { poke8(tmp, n, 48 + x % 10) x = x / 10 n = n + 1 }
while x > 0 { poke8(tmp, n, 48 + x % 10); x = x / 10; n = n + 1 }
let i = 0
while i < n { poke8(buf, i, peek8(tmp, n-1-i)) i = i + 1 }
while i < n { poke8(buf, i, peek8(tmp, n-1-i)); i = i + 1 }
mem_free(tmp)
return n
}
@ -905,16 +905,23 @@ file_close(f)
Each is a spelling the language accepts; the point is that the *alternative*
spelling is equally legal (§5.3).
**R1 — three statements on one line, no separators** → `2`
**R1 — statements now require a separator (Rule B, syntax-redesign Phase 2)** → parse error
```ludic
# doc-check: skip — intentionally rejected under Rule B: needs a newline or ';'
game P { system B phase Start { let x = 1 x = x + 1 print_int(x) quit() } }
```
Statements no longer sit adjacent with only spaces between them; the compiler
reports `expected newline or ';' between statements`. Put each on its own line,
or separate them with `;` (both lex to the same separator token):
```ludic
game P { system B phase Start { let x = 1; x = x + 1; print_int(x); quit() } }
```
**R2 — commas omitted throughout** → `7`
```ludic
# doc-check: skip — composite: declaration plus statements
component Pos { x: int = 0 y: int = 0 }
spawn Hero { Pos = { x = 7 y = 2 } }
spawn Hero { Pos { x: 7 y: 2 } }
```
**R3 — RESOLVED.** Every symbol form is now rejected where it is written:
@ -954,7 +961,7 @@ parenthesisation) while `ludic-fmt` returns the input **unchanged**.
**Verified-missing** — each a compile error today:
```ludic
# doc-check: expect-error — every line here is a compile error by design
while i < 10 { i = i + 1 if i == 3 { break } } # unknown identifier 'break'
while i < 10 { i = i + 1; if i == 3 { break } } # unknown identifier 'break'
struct Node { k: int, a: ptr } # expected declaration (got 'struct')
var t: [int; 8] # expected identifier (got '[')
var h: fn = a # unknown type 'fn' for var h
@ -963,5 +970,5 @@ print_int(os_argc()) # unknown function 'os_argc'
print_err("x") # unknown function 'print_err'
let p = mem_realloc(ptr_null(), 10) # unknown function 'mem_realloc'
print_str("ab" + "cd") # passes front-end, invalid IR
let a = 100000 print_int(a*100000) # 1410065408 — i32 wrap
let a = 100000; print_int(a*100000) # 1410065408 — i32 wrap
```

View file

@ -79,20 +79,20 @@ self-hosted `ludicc`.
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
> workflow below describe the old C driver's behavior; the self-hosted `ludicc`
> builds executables only for now. The `module`/`export fn` semantics are
> builds executables only for now. The `module`/`@export fn` semantics are
> unchanged — only the packaging step is pending.
A source file opens with `game Name { … }` or `module Name { … }`.
* A **game** gets an entry point and the phase-ordered frame loop
(`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick).
* A **module** gets neither. It is a library, and only its `export fn`s become
* A **module** gets neither. It is a library, and only its `@export fn`s become
public symbols; everything else stays private to the library.
```ludic
# doc-check: skip — illustrative: elided body
module Combat {
export fn damage(attack: int, armour: int, roll: int) -> int { … }
@export fn damage(attack: int, armour: int, roll: int) -> int { … }
fn curve(level: int) -> int { … } # private: not a symbol
}
```

View file

@ -67,7 +67,7 @@ component Stats { hp: int = 10 }
archetype Player { Pos, Stats } # Player IS a kind, not a component
archetype Enemy { Pos, Stats }
spawn Player { Pos = { x = 5 } } # attaches every listed component
spawn Player { Pos { x: 5 } } # attaches every listed component
# (seeding field defaults), then overrides
for (p, s) in query [Pos, Stats, {Player}] { ... } # {Player} filters by kind
```
@ -105,18 +105,18 @@ TrueType text, focus highlight) and keyboard focus + activation.
```ludic
ui MainMenu {
panel id=Root w=288 pad=16 gap=6 skin="assets/ui/panel.png" inset=10 align=center {
label text="CHRONO RIFT" font=reg(R_FONT) size=26 fg=0xffe060 align=center
button id=NewGame text="New Game" font=reg(R_FONT) size=16 w=236
button id=Quit text="Quit" font=reg(R_FONT) size=16 w=236
panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center {
label text: "CHRONO RIFT" font: reg(R_FONT) size: 26 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: reg(R_FONT) size: 16 w: 236
button id: Quit text: "Quit" font: reg(R_FONT) size: 16 w: 236
}
}
```
Widget types: `panel` (container + optional skin/bg/border), `col` / `row`
(pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are
evaluated at build time, so `font=reg(R_FONT)` reads a value the game set first.
Each `id=Name` mints a `UI_Name` handle (the `ui` block name too), used from
evaluated at build time, so `font: reg(R_FONT)` reads a value the game set first.
Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
systems:
```ludic
@ -130,7 +130,7 @@ system Nav phase Update {
if ui_clicked(UI_Quit) { quit() }
ui_set_int(UI_HpLabel, hp) # poke dynamic values by id
}
system Draw phase Render { clear(0x0e0e16) ui_render() present() }
system Draw phase Render { clear(0x0e0e16); ui_render(); present() }
```
See `examples/menu.ludic` for a complete title screen.
@ -159,8 +159,8 @@ component Pos { x: int = 0, y: int = 0 } # typed fields with defaults
component Player { } # a tag (no fields)
spawn Hero { # create an entity
Pos = { x = 10, y = 5 }
Player = { }
Pos { x: 10, y: 5 }
Player { }
}
despawn self() # remove the current entity
@ -185,7 +185,13 @@ system Move @deterministic
Phases run in this order every frame: **`Start`** (once at boot), then each
frame **`Input` → `FixedUpdate` → `Update` → `LateUpdate` → `Render`**.
`edge system` marks a system that touches the outside world.
`@edge` in front of a `system` marks one that touches the outside world.
Declaration modifiers are `@annotations` written in front of the declaration —
`@export fn …` (a C-ABI-exported function), `@edge system …`, `@deterministic`,
`@pure`. They parse into one uniform channel rather than a set of prefix
keywords. (`@export` sets the export flag; the others parse but have no codegen
effect in the self-hosted compiler yet.)
### The `query` clause
@ -335,6 +341,18 @@ another `.ludic` file (see `examples/lib/`).
`for (…) in query […] { }` · `break` · `continue` · `return` · `spawn` ·
`despawn` · `match` · `machine`.
**Statements are separated by a newline or `;`** (both lex to the same separator
token). Two statements may not sit adjacent with only spaces between them — the
compiler reports `expected newline or ';' between statements`. Write one
statement per line, or, to pack several onto a line, separate them with `;`:
```ludic
# doc-check: skip — a bare statement block, not a whole declaration
let x = 1
x = x + 1 # one per line, the usual form
let y = 1; y = y + 1 # or `;`-separated on one line
```
`break` and `continue` apply to the innermost enclosing loop, and work in all
three loop forms — `while`, the numeric `for`, and the ECS query loop, where
`continue` advances to the next matching entity. Using either outside a loop is
@ -361,15 +379,38 @@ state (no more `if phase == N` chains). See the co-op battle in
```ludic
# doc-check: skip — illustrative: elided bodies
machine R_PHASE {
state KnightMenu = 0 { … if is_confirm(k) { …attack… become KnightResolve } }
state KnightResolve = 1 { … become MageMenu }
state EnemyTurn = 4 { … become KnightMenu }
state KnightMenu { … if is_confirm(k) { …attack… become KnightResolve } }
state KnightResolve { … become MageMenu }
state EnemyTurn { … become KnightMenu }
}
```
A `machine <reg>` reads `reg(<reg>)` to pick the state; `become Name` compiles to
`setreg(<reg>, <Name's value>)`. Both lower to plain branches (and `match` runs
on the native LLVM backend too).
States **number themselves by declaration order** (`KnightMenu` is `0`,
`KnightResolve` is `1`, …) — no magic constants. (An explicit `state Name = expr`
is still accepted when a state needs a specific value.) A `machine <reg>` reads
`reg(<reg>)` to pick the state; `become Name` compiles to `setreg(<reg>, <Name's
value>)`. Both lower to plain branches (and `match` runs on the native LLVM
backend too).
## Enums
`enum` names a set of related integer values so a magic-number space — a menu
selection, a mode, a machine state — reads as names instead of literals:
```ludic
# doc-check: skip — composite: a declaration plus its uses
enum Action { Attack, Guard, Item, Flee } # Attack = 0, Guard = 1, …
match reg(R_CUR) { Action.Attack => attack() Action.Guard => guard() _ => wait() }
if reg(R_MODE) == Mode.Battle { … }
```
A variant is a **compile-time `int`** accessed as `Enum.Variant` (`Action.Guard`
is `1`), numbered from `0` by declaration order, so it works anywhere an int does
— `match` patterns, comparisons, `setreg`. Enums are a naming layer over `int`:
there is no distinct enum runtime type yet, so an enum value lives in an ordinary
`int` or register (and is saved with it). See `examples/chronorift/combat.ludic`,
whose battle menus dispatch on `KnightAct`/`MageAct` instead of `0..3`.
## Expressions
@ -378,7 +419,7 @@ postfix(. () )`. Operators are built-in only (no overloading). The boolean
operators are spelled **`and` / `or` / `not`**; `&&` and `||` are not Ludic operators
and `!` are rejected with a diagnostic naming the fix (`!=` is unaffected). Bitwise operations are
functions (`band`, `bor`, `bxor`, `bnot`, `shl`, `shr`), so the symbols are free
— which is why there is only one spelling to remember. `expr with { field = … }` is not implemented; records appear
— which is why there is only one spelling to remember. `expr with { field: … }` is not implemented; records appear
only in `spawn`. Char literals (`'w'`) are `int` code points; colors are hex
ints (`0xff8800`).

View file

@ -140,7 +140,7 @@ same highlighting, checking and formatting as the source tree.
- `scene` / `layer` / `enter` — mutually-exclusive game states, each with
`on enter`/`on exit` hooks and layered systems (layer order = draw order).
- `var` — typed module-level state, included in save/load snapshots.
- `module` + `export fn` — compile a .ludic file to a shared library whose
- `module` + `@export fn` — compile a .ludic file to a shared library whose
exported functions are ordinary C-ABI symbols.
- `extern fn … = "symbol"` — call any C-ABI library, Ludic or otherwise.
- `--target wasm32-unknown-unknown` — the same game in a browser: the runtime,

View file

@ -4,10 +4,11 @@ A plan to make Ludic's syntax internally consistent. It fixes the drift between
the spec and the compiler, then unifies the grammar around two rules. Scope:
**full redesign (Phases 0–5)**. Named-field direction: **colon everywhere**.
> Status: **Phase 1 landed** (see below). Phases 2→5 are proposals. The phases
> are ordered so the documentation never describes syntax the compiler rejects,
> and every phase ends with the compiler still self-hosting to a fixpoint
> (`./test.sh`).
> Status: **Phases 1–5 complete.** Every phase kept the compiler self-hosting to
> a fixpoint (`./test.sh` 14/14), and each syntax migration was proven
> behaviour-preserving (the migrated compiler compiles itself to byte-identical
> IR; every golden game renders byte-identically). Landed on branch
> `syntax-redesign-phase2` over a committed baseline on `main`.
>
> Coordinated with the toolchain agent (CLI front-end / `ludicc`+`ludic`
> binaries) via serialized reseeds of `selfhost/ludicc.seed.ll`; Phase 1 rode in
@ -33,9 +34,9 @@ Verified against the self-hosted compiler ([selfhost/parse.ludic](selfhost/parse
**Broken / dead syntax (compiler-verified)**
4. `edge system` — **hard parse error** (documented at [LANGUAGE.md:188](LANGUAGE.md)). *(✅ fixed in Phase 1)*
5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented.
5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented. *(✅ Phase 3d: now `@pure`)*
6. `@anno` + `reads/writes/needs/uses [..]` — parsed then thrown away
([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented.
([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented. *(✅ Phase 3c: `needs`/`uses` dropped)*
7. `scene`/`layer`/`on enter` — full LANGUAGE.md section + [examples/scenes.ludic](examples/scenes.ludic),
**does not compile** (`expected declaration`).
8. `query (v) [..]` in a system signature — two LANGUAGE.md sections +
@ -49,9 +50,9 @@ Verified against the self-hosted compiler ([selfhost/parse.ludic](selfhost/parse
11. Operators are words (`and`/`or`/`not`), symbols (`==`/`<=`), *and* functions
(`band`/`shl`) at once.
12. Three overlapping control families — `if`/`when`, `match`, `machine`/`become`
— and `enter` reuses `become`'s AST node ([parse.ludic:176-177](selfhost/parse.ludic)).
— and `enter` reuses `become`'s AST node ([parse.ludic:176-177](selfhost/parse.ludic)). *(Phase 4: `if`/`when` kept by choice; magic-int dispatch resolved)*
13. Typed components/structs exist, but real state lives in 64 untyped int
registers (`reg`/`setreg`), so `machine`/`match` dispatch on magic numbers.
registers (`reg`/`setreg`), so `machine`/`match` dispatch on magic numbers. *(✅ Phase 4: auto-numbered states + `enum` name the values)*
---
@ -208,38 +209,142 @@ Made spec ⇄ compiler agree **before** any grammar change. What landed:
compile yet — a positive test would fail; the header note + LANGUAGE.md warning
cover the drift instead).
### Phase 2 — Statement separation (Rule B)
- Enforce a separator in `block()`/`stmt()` ([parse.ludic:130-199](selfhost/parse.ludic)):
after a statement, require `TK_NL` or `}`.
- Teach `ludic-fmt` to normalize one-statement-per-line and insert `;` where two
share a line. Reformat the whole corpus with it.
- Risk: the self-host sources themselves use space-juxtaposed statements heavily
— reformat them in the same commit, re-seed, re-verify fixpoint.
### Phase 2 — Statement separation (Rule B) ✅ DONE
Landed on branch `syntax-redesign-phase2` (baseline committed on `main` first).
- ✅ **Parser enforces a separator** — `block()` requires a newline or `;` after
each statement, else `expected newline or ';' between statements`
([parse.ludic](selfhost/parse.ludic)). Also fixed `if`-without-`else` swallowing
its trailing separator (it now peeks for `else` and restores if absent).
- ✅ **Interpretation chosen:** *require a separator*, not *reflow to one-per-line*.
The migration **inserts `;` at statement boundaries** and leaves lines intact —
comment-safe, minimal-diff, and it makes boundaries visible without an
opinionated reflow. One-per-line stays the recommended hand-written form.
- ✅ **Migration tool** ([tools/ludic-tools/migrate_separators.c](tools/ludic-tools/migrate_separators.c),
reuses the toolchain lexer) with a
**verification oracle**: a `;` inserted at a real boundary is a semantic no-op,
proven by the migrated compiler compiling itself to **IR byte-identical to the
seed** and every golden game rendering identically. ~1100 boundaries across the
corpus (examples, runtime, and the 25 self-host fragments).
- ✅ **Reseeded** to the strict compiler (19557 lines); C-free bootstrap fixpoint
holds; `./test.sh` 14/14; all goldens byte-identical; qdecl runs correctly.
- ✅ **Docs updated** — Rule B documented in LANGUAGE.md §Statements; BOOTSTRAP.md
R1 (which advertised no-separator juxtaposition as legal) and its stale code
fences updated; `check-docs` (now a live strict parse gate) green across all docs.
**Bug found & fixed en route:** a multi-line string literal in
[emit_expr.ludic](selfhost/emit_expr.ludic) (`emit(")<newline>")`) lexed fine in
the self-host lexer but the **C toolchain lexer** (`ludic_syntax.h`, shared by
sepfix, `ludic-fmt`, and the LSP) stops strings at newline — so it mis-lexed and
`ludic-fmt` would corrupt such a file. Converted it to the byte-identical `\n`
escape. **Open follow-up:** align the C lexer to allow newlines in strings, or
forbid literal newlines in string literals language-wide (the two lexers should
agree). Flagged to the toolchain owners.
### Phase 3 — Named-field unification (Rule A)
- Migrate spawn/record init (`= {…=…}` → `{…:…}`) and ui props (`key=value` →
named-arg form) in the parser and emitters.
- Collapse `reads/writes/needs/uses` → `reads`/`writes`, stored on the node.
- Move `edge`/`pure`/`export` into `@`-annotations; parse annotations into a list.
- Ship `ludic-fmt --migrate`: a mechanical codemod that rewrites old syntax to
new, run over `examples/`, `runtime/`, and `selfhost/`.
- Gate: compiler still self-hosts; every golden render is byte-identical.
### Phase 4 — Control-flow & operator consolidation (largest)
- Remove `when`; confirm `if` covers all uses in the corpus.
- Introduce `enum` + typed `machine`/state; migrate `combat.ludic`'s R_PHASE
machine and the register-driven `match reg(…)` sites.
- Decide the bitwise story: keep `band/shl/…` as functions (document as a
deliberate "one spelling" choice) or promote to operators — pick one and state
it, don't leave it implicit.
**3a — spawn/record initializers ✅ DONE.** `Comp = { f = v }` → `Comp { f: v }`.
`record()` requires `:` and `parse_spawn()` drops the `=` before the record
([parse.ludic](selfhost/parse.ludic), [parse_game.ludic](selfhost/parse_game.ludic)).
The `=` is now assignment/const/default/extern-binding only. Migration tool:
[migrate_records.c](tools/ludic-tools/migrate_records.c) (spawn-context aware).
Records live only in games, so the seed was unaffected; verified every golden
byte-identical, old `=` form now rejected, reseeded, `test.sh` 14/14. Doc examples
updated (LANGUAGE.md, BOOTSTRAP.md R2).
### Phase 5 — Single source of truth for keywords/grammar
- Generate every editor plugin keyword list, `ludic_syntax.h`, and the LSP's
token set from **one** canonical list so a keyword can never again be
highlighted but unparsed.
- Add a CI check (extend `tools/check-docs.py` / `check-vocabulary.py`) that
every ` ```ludic ` fence in the docs compiles, closing the doc-drift loop
permanently.
**3b — ui props → `key: value` ✅ DONE.** `panel id=Root w=288` → `panel id: Root
w: 288`. `parse_widget` now reads props with `:` ([parse_game.ludic](selfhost/parse_game.ludic)).
Chose the **colonized** form over parenthesized named-args: it satisfies Rule A
(the `=` overload is gone) with minimal churn, needs no new grammar, and `emit_ui`
(which reads the AST) and `ludic-fmt` (which formats `:` correctly by default)
were both untouched. Migration: [migrate_ui.c](tools/ludic-tools/migrate_ui.c).
menu golden byte-identical, old `=` form rejected, reseeded, `test.sh` 14/14.
(The parenthesized form `panel(id: Root, w: 288)` remains a possible future
refinement if the language ever gains named call arguments.)
**3c — dropped the dead `needs`/`uses` clause synonyms ✅ DONE.** `reads`/`writes`
stay (documented; still parsed-and-reserved). `needs`/`uses` were undocumented and
unused anywhere in the corpus — removed from `parse_system`. *Not done:* actually
*storing* reads/writes on the node for an analysis pass — that's analysis
infrastructure, out of scope for a syntax pass.
**3d — modifiers → `@`-annotations ✅ DONE.** `edge`/`pure`/`export` prefix keywords
are retired; declaration modifiers are now leading `@annotations`: `@export fn`,
`@edge system`, `@pure`, `@deterministic`. `parse_one_decl` collects a leading
`@anno` run and `@export` sets the fn export flag ([parse.ludic](selfhost/parse.ludic));
the dead `edge`-dispatch was removed from `parse_system`. Migrated the one
`@export` user ([examples/lib/combat.ludic](examples/lib/combat.ludic)); old
prefix forms now rejected. Behavior-identical: the export flag is parse-only in
the self-hosted emitter (it emits `@fn_<name>` for every function and never reads
the flag — the C-ABI-export capability is vestigial, a pre-existing gap), so
`@export` and the old `export` produce byte-identical IR. Reseeded, fixpoint
holds, `test.sh` 14/14, goldens byte-identical.
**Phase 3 is complete.** The `=`/`:` overload (finding #2) and the modifier-zoo
(findings #5, #6) are resolved; `:` associates and `=` binds throughout.
Each sub-phase follows the proven pattern: parser change → verification-gated
migration (IR byte-identical / goldens identical) → reseed → docs. The migration
tools ([migrate_separators.c](tools/ludic-tools/migrate_separators.c),
[migrate_records.c](tools/ludic-tools/migrate_records.c)) are the reusable spine.
### Phase 4 — Control-flow & state consolidation
**4a — machine states auto-number ✅ DONE.** `state KnightMenu = 0 { }` →
`state KnightMenu { }`; a state's value is its declaration index (an explicit
`= expr` still works). Removes the magic constants from state machines
([parse.ludic](selfhost/parse.ludic)). combat.ludic migrated; chronorift golden
byte-identical.
**4b — `enum` types ✅ DONE.** `enum Action { Attack, Guard, Item, Flee }` declares
named `int` constants; a variant is a compile-time int accessed as `Action.Guard`
(= 1), numbered by order. Parser `parse_enum` + dispatch, `enum_ordinal` resolver
in [emit_core.ludic](selfhost/emit_core.ludic), and `Enum.Variant` handling in
[emit_expr.ludic](selfhost/emit_expr.ludic). combat.ludic's battle menus now
dispatch on `KnightAct`/`MageAct` instead of `0..3`; chronorift golden
byte-identical. Editor vocab (`ludic_syntax.h`, JetBrains, TextMate, emacs) gained
`enum` and lost the retired `edge`/`export`/`pure` decl keywords; check-vocabulary
+ test-tools green. **Scoped:** enums are a naming layer over `int` (no distinct
runtime type / enum-typed variables yet) — that keeps register/save semantics
untouched, which the "enum var replaces the register" vision would have to solve.
**`when` vs `if` — kept both (decision).** `when` stays as the `if`-without-else
spelling: it is not incoherent so much as a readability signal ("no else here"),
it is documented and highlighted, and it is a pure alias with no semantic overlap
to untangle. The real target of finding #12 — dispatch on magic integers — is
addressed by 4a/4b, not by collapsing `if`/`when`.
**Bitwise operators — kept as functions (decision).** `band`/`bor`/`bxor`/`bnot`/
`shl`/`shr` stay functions, documented as the deliberate "one spelling, symbols
stay free" choice (LANGUAGE.md §Expressions already states this). Promoting them
to operators would re-introduce the symbol soup the current design avoids.
### Phase 5 — Vocabulary anchored to the compiler ✅ DONE
The editor vocabulary already stayed in sync *with itself* (`check-vocabulary.py`
compares `ludic_syntax.h`, the JetBrains lexer, and the TextMate grammar). The
missing anchor was the **compiler**: a keyword could be highlighted everywhere
and still be silently unparsed. Closed both loops:
- ✅ **Vocabulary ⇄ parser.** `check-vocabulary.py` now extracts every keyword
`selfhost/parse*.ludic` dispatches on (`is_id(...)` / `streq(t.text, ...)`) and
requires the header's declaration + clause keywords to be a subset — with a
`LUDIC_KW_RESERVED` escape hatch for documented, not-yet-implemented keywords
(`scene`/`layer`/`on`/`start`), itself checked so a reserved word that gets
implemented must be promoted. Verified it catches an injected bogus keyword.
- ✅ **Reconciled the drift it exposed.** Removed the highlighted-but-unparsed
`scene`/`layer`/`on`/`start` (→ RESERVED) and the never-implemented
`needs`/`uses`/`requires`/`ensures`/`invariant`/`effects` clause words, and the
retired `edge`/`export`/`pure` prefix modifiers, from `ludic_syntax.h`, the
JetBrains lexer, the TextMate grammar, and the emacs mode; added `enum`/`main`.
`@`-annotations already highlight generically (`@[A-Za-z_]…`). test-tools 28/0.
- ✅ **Doc-fence compilation** — the other half of "single source of truth" — was
already live: `check-docs.py` compiles every ` ```ludic ` fence through the
self-hosted `ludicc --fmt` parse gate (revived during Phase 1's coordination).
Full generation-from-one-list (emit the editor files from a manifest) was not
needed: the bidirectional *checks* give the same guarantee — nothing can drift
without CI failing — without a code-generation step to maintain.
**Phases 1–5 are complete.** All thirteen findings are resolved or resolved by an
explicit, documented decision.
---

View file

@ -3,13 +3,18 @@
# The battle is a state machine over R_PHASE. `machine` dispatches on the
# register and `become` transitions to a named state — no more `if ph == N`
# ladders. Knight turn (0,1), Mage turn (2,3), Enemy turn (4).
# Menu selections, named instead of matched on bare integers (R_CUR holds one).
enum KnightAct { Attack, Guard, Item, Flee }
enum MageAct { Attack, Heal, Guard }
system Battle phase Update {
if reg(R_MODE) == 1 {
let k = key()
for (e) in query [Stats, {Enemy}] {
machine R_PHASE {
state KnightMenu = 0 {
state KnightMenu {
for (st, pt) in query [Stats, Party] {
if pt.slot == 0 {
if st.hp <= 0 {
@ -20,21 +25,21 @@ system Battle phase Update {
if k == 's' { setreg(R_CUR, min(3, reg(R_CUR) + 1)) }
if is_confirm(k) {
match reg(R_CUR) {
0 => {
KnightAct.Attack => {
let d = max(1, st.atk - e.def + rng_range(0, 4))
e.hp = e.hp - d
setreg(R_PDMG, d)
}
1 => { st.guard = 1 setreg(R_PDMG, 0) }
2 => {
KnightAct.Guard => { st.guard = 1; setreg(R_PDMG, 0) }
KnightAct.Item => {
if reg(R_POTION) > 0 {
setreg(R_POTION, reg(R_POTION) - 1)
st.hp = min(st.maxhp, st.hp + 24)
setreg(R_PDMG, 24)
} else { setreg(R_PDMG, 0) }
}
3 => {
if rng_chance(50) { e.hp = 0 setreg(R_MODE, 0) }
KnightAct.Flee => {
if rng_chance(50) { e.hp = 0; setreg(R_MODE, 0) }
else { setreg(R_PDMG, 0) }
}
}
@ -46,12 +51,12 @@ system Battle phase Update {
}
}
state KnightResolve = 1 {
state KnightResolve {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
setreg(R_GOLD, reg(R_GOLD) + e.xp)
if reg(R_ETYPE) == 2 { setreg(R_BOSS, 1) setreg(R_MODE, 3) setreg(R_ACK, 0) }
if reg(R_ETYPE) == 2 { setreg(R_BOSS, 1); setreg(R_MODE, 3); setreg(R_ACK, 0) }
else { setreg(R_MODE, 0) }
become KnightMenu
} else {
@ -61,7 +66,7 @@ system Battle phase Update {
}
}
state MageMenu = 2 {
state MageMenu {
for (st, pt) in query [Stats, Party] {
if pt.slot == 1 {
if st.hp <= 0 {
@ -71,12 +76,12 @@ system Battle phase Update {
if k == 'k' { setreg(R_CUR, min(2, reg(R_CUR) + 1)) }
if k == 'j' {
match reg(R_CUR) {
0 => {
MageAct.Attack => {
let d = max(1, st.atk + 5 - e.def + rng_range(0, 5))
e.hp = e.hp - d
setreg(R_PDMG, d)
}
1 => {
MageAct.Heal => {
if st.mp >= 4 {
st.mp = st.mp - 4
for (t2, p2) in query [Stats, Party] {
@ -85,7 +90,7 @@ system Battle phase Update {
setreg(R_PDMG, 16)
} else { setreg(R_PDMG, 0) }
}
2 => { st.guard = 1 setreg(R_PDMG, 0) }
MageAct.Guard => { st.guard = 1; setreg(R_PDMG, 0) }
}
become MageResolve
setreg(R_CUR, 0)
@ -95,12 +100,12 @@ system Battle phase Update {
}
}
state MageResolve = 3 {
state MageResolve {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
setreg(R_GOLD, reg(R_GOLD) + e.xp)
if reg(R_ETYPE) == 2 { setreg(R_BOSS, 1) setreg(R_MODE, 3) setreg(R_ACK, 0) }
if reg(R_ETYPE) == 2 { setreg(R_BOSS, 1); setreg(R_MODE, 3); setreg(R_ACK, 0) }
else { setreg(R_MODE, 0) }
become KnightMenu
} else {
@ -109,14 +114,14 @@ system Battle phase Update {
}
}
state EnemyTurn = 4 {
state EnemyTurn {
if k != 0 {
let done = 0
for (t3) in query [Stats, {Party}] {
if done == 0 {
if t3.hp > 0 {
let ed = max(1, e.atk - t3.def + rng_range(0, 4))
if t3.guard == 1 { ed = ed / 2 t3.guard = 0 }
if t3.guard == 1 { ed = ed / 2; t3.guard = 0 }
t3.hp = t3.hp - ed
setreg(R_EDMG, ed)
done = 1
@ -127,7 +132,7 @@ system Battle phase Update {
for (t4) in query [Stats, {Party}] {
if t4.hp > 0 { alive = alive + 1 }
}
if alive == 0 { setreg(R_MODE, 2) setreg(R_ACK, 0) }
if alive == 0 { setreg(R_MODE, 2); setreg(R_ACK, 0) }
else {
become KnightMenu
setreg(R_CUR, 0)

View file

@ -16,14 +16,14 @@ system Boot phase Start {
build_field(0)
spawn Player {
Pos = { x = 11, y = 15 }
Actor = { kind = 5 }
Party = { slot = 0 }
Stats = { hp = 40, maxhp = 40, mp = 8, maxmp = 8, atk = 12, def = 6, lvl = 1, xp = 0, guard = 0 }
Pos { x: 11, y: 15 }
Actor { kind: 5 }
Party { slot: 0 }
Stats { hp: 40, maxhp: 40, mp: 8, maxmp: 8, atk: 12, def: 6, lvl: 1, xp: 0, guard: 0 }
}
spawn Ally {
Party = { slot = 1 }
Stats = { hp = 26, maxhp = 26, mp = 16, maxmp = 16, atk = 8, def = 4, lvl = 1, xp = 0, guard = 0 }
Party { slot: 1 }
Stats { hp: 26, maxhp: 26, mp: 16, maxmp: 16, atk: 8, def: 4, lvl: 1, xp: 0, guard: 0 }
}
setreg(R_MODE, 0)
setreg(R_ENC, 4)
@ -69,8 +69,8 @@ system Control phase Input {
if reg(R_BOSS) == 0 {
setreg(R_ETYPE, 2)
spawn Enemy {
Actor = { kind = 8 }
Stats = { hp = 120, maxhp = 120, mp = 0, maxmp = 0, atk = 18, def = 10, lvl = 5, xp = 300, guard = 0 }
Actor { kind: 8 }
Stats { hp: 120, maxhp: 120, mp: 0, maxmp: 0, atk: 18, def: 10, lvl: 5, xp: 300, guard: 0 }
}
setreg(R_MODE, 1)
setreg(R_PHASE, 0)
@ -96,14 +96,14 @@ system Control phase Input {
setreg(R_ETYPE, et)
if et == 0 {
spawn Enemy {
Actor = { kind = 6 }
Stats = { hp = 18, maxhp = 18, mp = 0, maxmp = 0, atk = 8, def = 2, lvl = 1, xp = 7, guard = 0 }
Actor { kind: 6 }
Stats { hp: 18, maxhp: 18, mp: 0, maxmp: 0, atk: 8, def: 2, lvl: 1, xp: 7, guard: 0 }
}
}
if et == 1 {
spawn Enemy {
Actor = { kind = 7 }
Stats = { hp = 30, maxhp = 30, mp = 0, maxmp = 0, atk = 11, def = 5, lvl = 1, xp = 15, guard = 0 }
Actor { kind: 7 }
Stats { hp: 30, maxhp: 30, mp: 0, maxmp: 0, atk: 11, def: 5, lvl: 1, xp: 15, guard: 0 }
}
}
setreg(R_MODE, 1)

View file

@ -6,12 +6,12 @@ game Hello {
system Boot phase Start {
spawn Mob {
Pos = { x = 3, y = 4 }
Vel = { dx = 1, dy = 0 }
Pos { x: 3, y: 4 }
Vel { dx: 1, dy: 0 }
}
spawn Mob {
Pos = { x = 10, y = 2 }
Vel = { dx = 0, dy = 1 }
Pos { x: 10, y: 2 }
Vel { dx: 0, dy: 1 }
}
}

View file

@ -18,8 +18,8 @@ game Arena {
component Fighter { hp: int = 30, attack: int = 9, armour: int = 2 }
system Boot phase Start {
spawn Hero { Fighter = { hp = 40, attack = 12, armour = 3 } }
spawn Orc { Fighter = { hp = 24, attack = 7, armour = 1 } }
spawn Hero { Fighter { hp: 40, attack: 12, armour: 3 } }
spawn Orc { Fighter { hp: 24, attack: 7, armour: 1 } }
}
system Fight phase Update {

View file

@ -16,7 +16,7 @@ module Combat {
const CRIT_MULT: int = 2
# damage after armour, with a crit multiplier applied on an exact roll
export fn damage(attack: int, armour: int, roll: int) -> int {
@export fn damage(attack: int, armour: int, roll: int) -> int {
let raw = attack - armour
if raw < 1 { raw = 1 }
if roll == 20 { raw = raw * CRIT_MULT }
@ -25,7 +25,7 @@ module Combat {
# how many hits to drop a target — the loop is here so the caller cannot
# accidentally disagree with `damage` about rounding
export fn hits_to_kill(hp: int, attack: int, armour: int) -> int {
@export fn hits_to_kill(hp: int, attack: int, armour: int) -> int {
let left = hp
let n = 0
while left > 0 {
@ -35,7 +35,7 @@ module Combat {
return n
}
export fn xp_for(level: int, kills: int) -> int {
@export fn xp_for(level: int, kills: int) -> int {
return curve(level) * kills
}

View file

@ -8,14 +8,14 @@ game Menu {
const R_MSG: int = 1
ui MainMenu {
panel id=Root w=288 pad=16 gap=6 bg=0x1a1a2c border=0x4a4a72 skin="assets/ui/panel.png" inset=10 align=center {
label text="CHRONO RIFT" font=reg(R_FONT) size=26 fg=0xffe060 align=center
label text="a Ludic retained-UI demo" font=reg(R_FONT) size=14 fg=0x9aa0c8 align=center
spacer h=4
button id=NewGame text="New Game" font=reg(R_FONT) size=16 w=236
button id=Continue text="Continue" font=reg(R_FONT) size=16 w=236
button id=Options text="Options" font=reg(R_FONT) size=16 w=236
button id=Quit text="Quit" font=reg(R_FONT) size=16 w=236
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c border: 0x4a4a72 skin: "assets/ui/panel.png" inset: 10 align: center {
label text: "CHRONO RIFT" font: reg(R_FONT) size: 26 fg: 0xffe060 align: center
label text: "a Ludic retained-UI demo" font: reg(R_FONT) size: 14 fg: 0x9aa0c8 align: center
spacer h: 4
button id: NewGame text: "New Game" font: reg(R_FONT) size: 16 w: 236
button id: Continue text: "Continue" font: reg(R_FONT) size: 16 w: 236
button id: Options text: "Options" font: reg(R_FONT) size: 16 w: 236
button id: Quit text: "Quit" font: reg(R_FONT) size: 16 w: 236
}
}

View file

@ -15,9 +15,9 @@ game QueryDecl {
archetype Foe { Battle, Pos }
system Seed phase Start {
spawn Foe { Battle = { hp = 3, side = 1 } }
spawn Foe { Battle = { hp = 0, side = 1 } }
spawn Foe { Battle = { hp = -2, side = 0 } }
spawn Foe { Battle { hp: 3, side: 1 } }
spawn Foe { Battle { hp: 0, side: 1 } }
spawn Foe { Battle { hp: -2, side: 0 } }
}
system CleanBattle phase LateUpdate

View file

@ -7,7 +7,7 @@
game SceneDemo {
const R_N: int = 0
system Boot phase Start { setreg(R_N, 0) print_int(1000) }
system Boot phase Start { setreg(R_N, 0); print_int(1000) }
scene Title start {
on enter { print_int(1) }
@ -22,7 +22,7 @@ game SceneDemo {
}
scene Play {
on enter { print_int(3) setreg(R_N, 0) }
on enter { print_int(3); setreg(R_N, 0) }
on exit { print_int(4) }
layer World {
system Step phase Update {

View file

@ -26,9 +26,9 @@ game Snake {
fn reset(x: int) -> int {
for (p, s) in query [Pos, Seg] { despawn self() }
spawn S0 { Seg = { order = 0 } Pos = { x = 10, y = 7 } }
spawn S1 { Seg = { order = 1 } Pos = { x = 9, y = 7 } }
spawn S2 { Seg = { order = 2 } Pos = { x = 8, y = 7 } }
spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } }
spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } }
spawn S2 { Seg { order: 2 }; Pos { x: 8, y: 7 } }
setreg(R_DIR, 3)
setreg(R_LEN, 3)
setreg(R_TICK, 0)
@ -62,7 +62,7 @@ game Snake {
setreg(R_TICK, 0)
let len = reg(R_LEN)
for (pt, stg) in query [Pos, Seg] {
if stg.order == len - 1 { setreg(R_TX, pt.x) setreg(R_TY, pt.y) }
if stg.order == len - 1 { setreg(R_TX, pt.x); setreg(R_TY, pt.y) }
}
for step in 0 .. len {
let i = len - 1 - step
@ -70,7 +70,7 @@ game Snake {
for (pa, sa) in query [Pos, Seg] {
if sa.order == i {
for (pb, sb) in query [Pos, Seg] {
if sb.order == i - 1 { pa.x = pb.x pa.y = pb.y }
if sb.order == i - 1 { pa.x = pb.x; pa.y = pb.y }
}
}
}
@ -101,7 +101,7 @@ game Snake {
}
if reg(R_ATE) == 1 {
setreg(R_SCORE, reg(R_SCORE) + 1)
spawn Body { Seg = { order = len } Pos = { x = reg(R_TX), y = reg(R_TY) } }
spawn Body { Seg { order: len }; Pos { x: reg(R_TX), y: reg(R_TY) } }
setreg(R_LEN, len + 1)
setreg(R_FX, rng_range(0, GW - 1))
setreg(R_FY, rng_range(0, GH - 1))

View file

@ -49,7 +49,7 @@ fn rt_read_file(path: str) -> ptr {
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
if n <= 0 { file_close(f) return ptr_null() }
if n <= 0 { file_close(f); return ptr_null() }
let buf = mem_alloc(n + 8)
file_read(f, buf, n)
file_close(f)
@ -70,9 +70,9 @@ fn rt_decode_png(path: str) -> bool {
let d = rt_read_file(path)
if ptr_is_null(d) { return false }
let size = rt_file_len
if size < 8 { mem_free(d) return false }
if peek8(d, 0) != 137 { mem_free(d) return false }
if peek8(d, 1) != 80 { mem_free(d) return false }
if size < 8 { mem_free(d); return false }
if peek8(d, 0) != 137 { mem_free(d); return false }
if peek8(d, 1) != 80 { mem_free(d); return false }
let w = 0
let h = 0
@ -119,8 +119,8 @@ fn rt_decode_png(path: str) -> bool {
}
}
if w <= 0 { mem_free(d) return false }
if h <= 0 { mem_free(d) return false }
if w <= 0 { mem_free(d); return false }
if h <= 0 { mem_free(d); return false }
let channels = 1
if ct == 2 { channels = 3 }
@ -133,7 +133,7 @@ fn rt_decode_png(path: str) -> bool {
let rawlen = h * (stride + 1)
let raw = mem_alloc(rawlen + 8)
if z_uncompress(idat, idlen, raw, rawlen) < 0 {
mem_free(d) mem_free(raw) mem_free(idat)
mem_free(d); mem_free(raw); mem_free(idat)
return false
}
@ -378,7 +378,7 @@ fn rt_hexval(c: int) -> int {
fn rt_load_sprites(path: str) -> void {
let d = rt_read_file(path)
if ptr_is_null(d) { spr_n = 0 return }
if ptr_is_null(d) { spr_n = 0; return }
let size = rt_file_len
let pal = mem_alloc(128 * 4)
mem_set(pal, 0, 128 * 4)
@ -391,7 +391,7 @@ fn rt_load_sprites(path: str) -> void {
let j = start
while j < size {
let ch = peek8(d, j)
if ch == 10 { j = size } else { len = len + 1 j = j + 1 }
if ch == 10 { j = size } else { len = len + 1; j = j + 1 }
}
let c0 = peek8(d, start)
if c0 == 112 { # 'p' — "pal <char> <rrggbb>"

View file

@ -322,7 +322,7 @@ fn tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx
let r = peek8(d, p)
p = p + 1
while r > 0 {
if i < npts { poke8(flags, i, fl) i = i + 1 }
if i < npts { poke8(flags, i, fl); i = i + 1 }
r = r - 1
}
}
@ -338,7 +338,7 @@ fn tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx
p = p + 1
if band(fl, 16) != 0 { xv = xv + dxv } else { xv = xv - dxv }
} else {
if band(fl, 16) == 0 { xv = xv + tt_i16(d, p) p = p + 2 }
if band(fl, 16) == 0 { xv = xv + tt_i16(d, p); p = p + 2 }
}
poke32(xs, k, xv)
}
@ -350,7 +350,7 @@ fn tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx
p = p + 1
if band(fl, 32) != 0 { yv = yv + dyv } else { yv = yv - dyv }
} else {
if band(fl, 32) == 0 { yv = yv + tt_i16(d, p) p = p + 2 }
if band(fl, 32) == 0 { yv = yv + tt_i16(d, p); p = p + 2 }
}
poke32(ys, k, yv)
}

View file

@ -15,6 +15,7 @@ const N_SYS: int = 9
const N_ARCH: int = 10
const N_EXTERN: int = 11
const N_UI: int = 12
const N_ENUM: int = 27 # enum Name { A, B, ... } — named int constants
# statements
const S_LET: int = 10
const S_ASSIGN: int = 11

View file

@ -23,7 +23,7 @@ fn buf_putc(b: Buf, c: int) -> void {
}
fn buf_puts(b: Buf, s: ptr) -> void {
let i = 0
while peek8(s, i) != 0 { buf_putc(b, peek8(s, i)) i = i + 1 }
while peek8(s, i) != 0 { buf_putc(b, peek8(s, i)); i = i + 1 }
}
fn buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) }
fn buf_str(b: Buf) -> ptr { poke8(b.data, b.len, 0) return b.data }
fn buf_str(b: Buf) -> ptr { poke8(b.data, b.len, 0); return b.data }

View file

@ -13,8 +13,8 @@ fn emit_member_addr(e: Node) -> ptr {
if fidx < 0 { perr(sconcat("no such field ", e.s)) }
g_addr_ty = field_type(s, e.s)
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds ") emit(layout_ty(base.ty))
emit(", ptr ") emit(base.code) emit(", i32 0, i32 ") emit(itoa(fidx)) emit("\n")
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(layout_ty(base.ty))
emit(", ptr "); emit(base.code); emit(", i32 0, i32 "); emit(itoa(fidx)); emit("\n")
return r
}
@ -26,13 +26,13 @@ fn emit_index_addr(e: Node) -> ptr {
g_addr_ty = el
# load the data pointer from the slice header (field 0)
let dp = nreg()
emit(" ") emit(dp) emit(" = getelementptr inbounds %LSlice, ptr ")
emit(base.code) emit(", i32 0, i32 0\n")
emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ")
emit(base.code); emit(", i32 0, i32 0\n")
let data = nreg()
emit(" ") emit(data) emit(" = load ptr, ptr ") emit(dp) emit("\n")
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
let ix = emit_expr(e.b)
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds ") emit(llty(el))
emit(", ptr ") emit(data) emit(", i32 ") emit(ix.code) emit("\n")
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el))
emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n")
return r
}

View file

@ -3,7 +3,7 @@
# structs and slices are references, so every non-scalar type lowers to `ptr`.
struct Val { code: ptr = ptr_null(), ty: ptr = ptr_null() }
fn val(code: ptr, ty: ptr) -> Val { let v = new Val v.code = code v.ty = ty return v }
fn val(code: ptr, ty: ptr) -> Val { let v = new Val; v.code = code; v.ty = ty; return v }
var head: Buf # module-level: types, globals, string constants
var code: Buf # function bodies
@ -36,24 +36,24 @@ fn emith(s: ptr) -> void { buf_puts(head, s) }
# stack slots MUST live in the entry block (an alloca in a loop walks the stack
# off its end), so they go into a per-function buffer spliced in at entry.
fn emit_alloca(llt: ptr) -> ptr {
let r = sconcat("%t", itoa(ll_t)) ll_t = ll_t + 1
buf_puts(falloc, " ") buf_puts(falloc, r) buf_puts(falloc, " = alloca ") buf_puts(falloc, llt) buf_puts(falloc, "\n")
let r = sconcat("%t", itoa(ll_t)); ll_t = ll_t + 1
buf_puts(falloc, " "); buf_puts(falloc, r); buf_puts(falloc, " = alloca "); buf_puts(falloc, llt); buf_puts(falloc, "\n")
return r
}
# "%t<n>" fresh register
fn sconcat(a: ptr, b: ptr) -> ptr {
let la = slen(a) let lb = slen(b)
let la = slen(a); let lb = slen(b)
let out = mem_alloc(la + lb + 1)
let i = 0
while i < la { poke8(out, i, peek8(a, i)) i = i + 1 }
while i < la { poke8(out, i, peek8(a, i)); i = i + 1 }
let j = 0
while j < lb { poke8(out, la + j, peek8(b, j)) j = j + 1 }
while j < lb { poke8(out, la + j, peek8(b, j)); j = j + 1 }
poke8(out, la + lb, 0)
return out
}
fn nreg() -> ptr { let r = sconcat("%t", itoa(ll_t)) ll_t = ll_t + 1 return r }
fn lbl(pfx: ptr) -> ptr { let r = sconcat(pfx, itoa(ll_lbl)) ll_lbl = ll_lbl + 1 return r }
fn nreg() -> ptr { let r = sconcat("%t", itoa(ll_t)); ll_t = ll_t + 1; return r }
fn lbl(pfx: ptr) -> ptr { let r = sconcat(pfx, itoa(ll_lbl)); ll_lbl = ll_lbl + 1; return r }
# Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/
# slice) is a pointer; void is void.
@ -79,17 +79,17 @@ fn is_struct_ty(t: ptr) -> bool { return not ptr_is_null(find_struct(t)) }
fn find_arch(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_ARCH and streq(d.s, name) { return d } i = i + 1 }
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and streq(d.s, name) { return d }; i = i + 1 }
return ptr_null()
}
fn find_comp(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_COMP and streq(d.s, name) { return d } i = i + 1 }
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and streq(d.s, name) { return d }; i = i + 1 }
return ptr_null()
}
# a struct or a component — both have %Str_/%Cmp_ layouts with named fields
fn layout_node(name: ptr) -> Node {
let s = find_struct(name) if not ptr_is_null(s) { return s }
let s = find_struct(name); if not ptr_is_null(s) { return s }
return find_comp(name)
}
fn layout_ty(name: ptr) -> ptr {
@ -99,12 +99,12 @@ fn layout_ty(name: ptr) -> ptr {
fn field_index(s: Node, fname: ptr) -> int {
let i = 0
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return i } i = i + 1 }
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return i }; i = i + 1 }
return 0 - 1
}
fn field_type(s: Node, fname: ptr) -> ptr {
let i = 0
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return s.kids[i].ty } i = i + 1 }
while i < len(s.kids) { if streq(s.kids[i].s, fname) { return s.kids[i].ty }; i = i + 1 }
return "int"
}
@ -119,21 +119,36 @@ fn find_global(name: ptr) -> Node {
}
return ptr_null()
}
# `Enum.Variant` -> the variant's ordinal (its index), or -1 if `ename` names no
# enum with that variant. Enum names live in `prog` like any other declaration.
fn enum_ordinal(ename: ptr, vname: ptr) -> int {
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_ENUM and streq(d.s, ename) {
let j = 0
while j < len(d.kids) { if streq(d.kids[j].s, vname) { return j }; j = j + 1 }
}
i = i + 1
}
return 0 - 1
}
fn find_fn(name: ptr) -> Node {
let i = 0
while i < len(prog) { let d = prog[i] if d.kind == N_FN and streq(d.s, name) { return d } i = i + 1 }
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and streq(d.s, name) { return d }; i = i + 1 }
return ptr_null()
}
# local variable environment
fn loc_reset() -> void { nloc = 0 }
fn loc_push(name: ptr, r: ptr, ty: ptr) -> void {
if nloc < len(loc_name) { loc_name[nloc] = name loc_reg[nloc] = r loc_ty[nloc] = ty }
else { push(loc_name, name) push(loc_reg, r) push(loc_ty, ty) }
if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty }
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty) }
nloc = nloc + 1
}
fn loc_find(name: ptr) -> int {
let i = nloc - 1
while i >= 0 { if streq(loc_name[i], name) { return i } i = i - 1 }
while i >= 0 { if streq(loc_name[i], name) { return i }; i = i - 1 }
return 0 - 1
}

View file

@ -6,26 +6,26 @@ fn emit_params_sig(d: Node) -> void {
let i = 0
while i < len(d.kids) {
if i > 0 { emit(", ") }
emit(llty(d.kids[i].ty)) emit(" %arg_") emit(d.kids[i].s)
emit(llty(d.kids[i].ty)); emit(" %arg_"); emit(d.kids[i].s)
i = i + 1
}
}
fn emit_fn(d: Node) -> void {
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
ret_ty = d.ty
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
let rl = llty(ret_ty)
if not streq(rl, "void") { buf_puts(falloc, " %retval = alloca ") buf_puts(falloc, rl) buf_puts(falloc, "\n") }
if not streq(rl, "void") { buf_puts(falloc, " %retval = alloca "); buf_puts(falloc, rl); buf_puts(falloc, "\n") }
# params: store each incoming argument into a stack slot
let i = 0
while i < len(d.kids) {
let p = d.kids[i]
let slot = emit_alloca(llty(p.ty))
emit(" store ") emit(llty(p.ty)) emit(" %arg_") emit(p.s) emit(", ptr ") emit(slot) emit("\n")
emit(" store "); emit(llty(p.ty)); emit(" %arg_"); emit(p.s); emit(", ptr "); emit(slot); emit("\n")
loc_push(p.s, slot, p.ty)
i = i + 1
}
@ -33,16 +33,16 @@ fn emit_fn(d: Node) -> void {
if not g_term { emit(" br label %ret\n") }
emit("ret:\n")
if streq(rl, "void") { emit(" ret void\n") }
else { let r = emit_bind(sconcat("load ", sconcat(rl, ", ptr %retval"))) emit(" ret ") emit(rl) emit(" ") emit(r) emit("\n") }
else { let r = emit_bind(sconcat("load ", sconcat(rl, ", ptr %retval"))); emit(" ret "); emit(rl); emit(" "); emit(r); emit("\n") }
code = saved
emit("define ") emit(rl) emit(" @fn_") emit(d.s) emit("(") emit_params_sig(d) emit(") {\nentry:\n")
emit("define "); emit(rl); emit(" @fn_"); emit(d.s); emit("("); emit_params_sig(d); emit(") {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
}
fn emit_main(d: Node) -> void {
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
ret_ty = "int"
let fbody = buf_new()
falloc = buf_new()
@ -56,7 +56,7 @@ fn emit_main(d: Node) -> void {
if not g_term { emit(" br label %ret\n") }
emit("ret:\n")
let r = emit_bind("load i32, ptr %retval")
emit(" ret i32 ") emit(r) emit("\n")
emit(" ret i32 "); emit(r); emit("\n")
code = saved
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
emit(buf_str(falloc))
@ -67,20 +67,20 @@ fn emit_main(d: Node) -> void {
fn emit_program() -> void {
head = buf_new()
code = buf_new()
loc_name = new []ptr loc_reg = new []ptr loc_ty = new []ptr
brk_lbl = new []ptr cnt_lbl = new []ptr
loc_name = new []ptr; loc_reg = new []ptr; loc_ty = new []ptr
brk_lbl = new []ptr; cnt_lbl = new []ptr
self_stk = new []ptr
mach_stk = new []Node
emit_header()
if has_ecs() { emit_ecs_storage() }
let i = 0
while i < len(prog) { if prog[i].kind == N_FN { emit_fn(prog[i]) } i = i + 1 }
if has_ecs() { emit_ecs_allocator() emit_snapshot() }
while i < len(prog) { if prog[i].kind == N_FN { emit_fn(prog[i]) }; i = i + 1 }
if has_ecs() { emit_ecs_allocator(); emit_snapshot() }
if has_ui() { emit_ui_build() }
if has_systems() { emit_game_main() }
else {
i = 0
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) } i = i + 1 }
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
}
}

View file

@ -7,12 +7,12 @@ const MAX_ENT: int = 1024
fn has_ecs() -> bool {
let i = 0
while i < len(prog) { let k = prog[i].kind if k == N_COMP or k == N_SYS { return true } i = i + 1 }
while i < len(prog) { let k = prog[i].kind; if k == N_COMP or k == N_SYS { return true }; i = i + 1 }
return false
}
fn has_systems() -> bool {
let i = 0
while i < len(prog) { if prog[i].kind == N_SYS { return true } i = i + 1 }
while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 }
return false
}
@ -32,7 +32,7 @@ fn emit_ecs_storage() -> void {
emith(sconcat("%Cmp_", sconcat(c.s, " = type { ")))
if len(c.kids) == 0 { emith("i32") }
let f = 0
while f < len(c.kids) { if f > 0 { emith(", ") } emith(llty(c.kids[f].ty)) f = f + 1 }
while f < len(c.kids) { if f > 0 { emith(", ") }; emith(llty(c.kids[f].ty)); f = f + 1 }
emith(" }\n")
emith(sconcat("@S_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, sconcat(" x %Cmp_", sconcat(c.s, "] zeroinitializer\n")))))))
emith(sconcat("@H_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, " x i8] zeroinitializer\n")))))
@ -49,12 +49,12 @@ fn emit_ecs_allocator() -> void {
while i < len(prog) {
if prog[i].kind == N_COMP {
let hn = sconcat("%h", itoa(i))
emit(" ") emit(hn) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(prog[i].s) emit(", i32 0, i32 %e\n")
emit(" store i8 0, ptr ") emit(hn) emit("\n")
emit(" "); emit(hn); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(prog[i].s); emit(", i32 0, i32 %e\n")
emit(" store i8 0, ptr "); emit(hn); emit("\n")
}
i = i + 1
}
emit(" %k = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
emit(" %k = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
emit(" store i32 0, ptr %k\n ret void\n}\n\n")
emit("define i32 @L_alloc() {\nentry:\n")
@ -64,7 +64,7 @@ fn emit_ecs_allocator() -> void {
emit("reuse:\n")
emit(" %fn1 = sub i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n")
emit(" %re = load i32, ptr %fp\n")
emit(" br label %done\n")
emit("fresh:\n")
@ -75,16 +75,16 @@ fn emit_ecs_allocator() -> void {
emit("done:\n")
emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n")
emit(" call void @L_reset(i32 %e)\n")
emit(" %ap = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" store i32 1, ptr %ap\n")
emit(" ret i32 %e\n}\n\n")
emit("define void @L_free_entity(i32 %e) {\nentry:\n")
emit(" %ap = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n")
emit(" store i32 0, ptr %ap\n")
emit(" call void @L_reset(i32 %e)\n")
emit(" %fn = load i32, ptr @L_freen\n")
emit(" %fp = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n")
emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n")
emit(" store i32 %e, ptr %fp\n")
emit(" %fn1 = add i32 %fn, 1\n")
emit(" store i32 %fn1, ptr @L_freen\n")

View file

@ -12,17 +12,17 @@ fn emit_logic(e: Node) -> Val {
let la = emit_expr(e.a)
let lc = emit_bind(sconcat("icmp ne i32 ", sconcat(la.code, ", 0")))
let lz = emit_bind(sconcat("zext i1 ", sconcat(lc, " to i32")))
emit(" store i32 ") emit(lz) emit(", ptr ") emit(slot) emit("\n")
let ev = lbl("sc") let done = lbl("scend")
if streq(e.s, "and") { emit(" br i1 ") emit(lc) emit(", label %") emit(ev) emit(", label %") emit(done) emit("\n") }
else { emit(" br i1 ") emit(lc) emit(", label %") emit(done) emit(", label %") emit(ev) emit("\n") }
emit(ev) emit(":\n")
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
let ev = lbl("sc"); let done = lbl("scend")
if streq(e.s, "and") { emit(" br i1 "); emit(lc); emit(", label %"); emit(ev); emit(", label %"); emit(done); emit("\n") }
else { emit(" br i1 "); emit(lc); emit(", label %"); emit(done); emit(", label %"); emit(ev); emit("\n") }
emit(ev); emit(":\n")
let rb = emit_expr(e.b)
let rc = emit_bind(sconcat("icmp ne i32 ", sconcat(rb.code, ", 0")))
let rz = emit_bind(sconcat("zext i1 ", sconcat(rc, " to i32")))
emit(" store i32 ") emit(rz) emit(", ptr ") emit(slot) emit("\n")
emit(" br label %") emit(done) emit("\n")
emit(done) emit(":\n")
emit(" store i32 "); emit(rz); emit(", ptr "); emit(slot); emit("\n")
emit(" br label %"); emit(done); emit("\n")
emit(done); emit(":\n")
return val(emit_bind(sconcat("load i32, ptr ", slot)), "bool")
}
@ -57,13 +57,13 @@ fn emit_bin(e: Node) -> Val {
let b = emit_expr(e.b)
let fx = streq(a.ty, "fixed") or streq(b.ty, "fixed")
if is_cmp(e.s) {
let ac = a.code let bc = b.code
if fx { ac = to_fixed(a) bc = to_fixed(b) }
let ac = a.code; let bc = b.code
if fx { ac = to_fixed(a); bc = to_fixed(b) }
let c = emit_bind(sconcat("icmp ", sconcat(cmp_code(e.s), sconcat(" i32 ", sconcat(ac, sconcat(", ", bc))))))
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
}
if fx {
let af = to_fixed(a) let bf = to_fixed(b)
let af = to_fixed(a); let bf = to_fixed(b)
if streq(e.s, "*") {
let a64 = emit_bind(sconcat("sext i32 ", sconcat(af, " to i64")))
let b64 = emit_bind(sconcat("sext i32 ", sconcat(bf, " to i64")))
@ -87,16 +87,16 @@ fn emit_bin(e: Node) -> Val {
fn emit_call(e: Node) -> Val {
let name = e.a.s
if streq(name, "self") { if nself == 0 { return val("0", "entity") } return val(emit_bind(sconcat("load i32, ptr ", self_stk[nself - 1])), "entity") }
if streq(name, "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(sconcat("load i32, ptr ", self_stk[nself - 1])), "entity") }
if streq(name, "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
if streq(name, "save") { emit(" call void @L_save()\n") return val("0", "void") }
if streq(name, "ui_build") { emit(" call void @ui_build()\n") return val("0", "void") }
if streq(name, "save") { emit(" call void @L_save()\n"); return val("0", "void") }
if streq(name, "ui_build") { emit(" call void @ui_build()\n"); return val("0", "void") }
if streq(name, "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
if streq(name, "quit") { emit(" store i32 0, ptr @L_running\n") return val("0", "void") }
if streq(name, "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
if streq(name, "len") { return emit_len(e) }
if streq(name, "push") { return emit_push(e) }
if streq(name, "fx") { let a = emit_expr(e.kids[0]) return val(emit_bind(sconcat("shl i32 ", sconcat(a.code, ", 16"))), "fixed") }
if streq(name, "flr") { let a = emit_expr(e.kids[0]) return val(emit_bind(sconcat("ashr i32 ", sconcat(a.code, ", 16"))), "int") }
if streq(name, "fx") { let a = emit_expr(e.kids[0]); return val(emit_bind(sconcat("shl i32 ", sconcat(a.code, ", 16"))), "fixed") }
if streq(name, "flr") { let a = emit_expr(e.kids[0]); return val(emit_bind(sconcat("ashr i32 ", sconcat(a.code, ", 16"))), "int") }
if is_intrinsic(name) { return emit_intrinsic(name, e) }
if is_intrinsic2(name) { return emit_intrinsic2(name, e) }
if is_math_builtin(name) { return emit_math_builtin(name, e) }
@ -113,20 +113,19 @@ fn emit_call(e: Node) -> Val {
let args = new []ptr
let atys = new []ptr
let i = 0
while i < len(e.kids) { let v = emit_expr(e.kids[i]) push(args, v.code) push(atys, v.ty) i = i + 1 }
while i < len(e.kids) { let v = emit_expr(e.kids[i]); push(args, v.code); push(atys, v.ty); i = i + 1 }
let rl = llty(fn2.ty)
emit(" ")
let rreg = "0"
if not streq(rl, "void") { rreg = nreg() emit(rreg) emit(" = ") }
emit("call ") emit(rl) emit(" @fn_") emit(cname) emit("(")
if not streq(rl, "void") { rreg = nreg(); emit(rreg); emit(" = ") }
emit("call "); emit(rl); emit(" @fn_"); emit(cname); emit("(")
i = 0
while i < len(args) {
if i > 0 { emit(", ") }
emit(llty(atys[i])) emit(" ") emit(args[i])
emit(llty(atys[i])); emit(" "); emit(args[i])
i = i + 1
}
emit(")
")
emit(")\n")
return val(rreg, fn2.ty)
}
@ -153,8 +152,14 @@ fn emit_expr(e: Node) -> Val {
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
perr(sconcat("unknown identifier ", e.s))
}
if e.kind == E_MEMBER { let a = emit_member_addr(e) return emit_load_at(a, g_addr_ty) }
if e.kind == E_INDEX { let a = emit_index_addr(e) return emit_load_at(a, g_addr_ty) }
if e.kind == E_MEMBER {
if e.a.kind == E_ID { # `Enum.Variant` -> its ordinal, a compile-time int
let ord = enum_ordinal(e.a.s, e.s)
if ord >= 0 { return val(itoa(ord), "int") }
}
let a = emit_member_addr(e); return emit_load_at(a, g_addr_ty)
}
if e.kind == E_INDEX { let a = emit_index_addr(e); return emit_load_at(a, g_addr_ty) }
if e.kind == E_CALL { return emit_call(e) }
if e.kind == E_BIN { return emit_bin(e) }
if e.kind == E_UN {

View file

@ -4,7 +4,7 @@
# called only when the runtime defines them.
fn emit_system_fn(sys: Node) -> void {
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0 nself = 0
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
let fbody = buf_new()
falloc = buf_new()
@ -14,7 +14,7 @@ fn emit_system_fn(sys: Node) -> void {
if not g_term { emit(" br label %ret\n") }
emit("ret:\n ret void\n")
code = saved
emit("define void @sys_") emit(sys.s) emit("() {\nentry:\n")
emit("define void @sys_"); emit(sys.s); emit("() {\nentry:\n")
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
@ -24,7 +24,7 @@ fn emit_calls_for_phase(phase: ptr) -> void {
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_SYS and streq(d.ty, phase) { emit(" call void @sys_") emit(d.s) emit("()\n") }
if d.kind == N_SYS and streq(d.ty, phase) { emit(" call void @sys_"); emit(d.s); emit("()\n") }
i = i + 1
}
}
@ -32,7 +32,7 @@ fn emit_calls_for_phase(phase: ptr) -> void {
fn emit_game_main() -> void {
# every system becomes a function first
let i = 0
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) } i = i + 1 }
while i < len(prog) { if prog[i].kind == N_SYS { emit_system_fn(prog[i]) }; i = i + 1 }
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
emit(" store i32 %argc, ptr @L_argc\n")
@ -47,14 +47,14 @@ fn emit_game_main() -> void {
let pr = emit_bind("call i32 @fn_rt_running()")
let pc = emit_bind(sconcat("icmp ne i32 ", sconcat(pr, ", 0")))
let go = emit_bind(sconcat("and i1 ", sconcat(rc, sconcat(", ", pc))))
emit(" br i1 ") emit(go) emit(", label %body, label %done\n")
emit(" br i1 "); emit(go); emit(", label %body, label %done\n")
} else {
emit(" br i1 ") emit(rc) emit(", label %body, label %done\n")
emit(" br i1 "); emit(rc); emit(", label %body, label %done\n")
}
emit("body:\n")
if not ptr_is_null(find_fn("rt_poll")) {
let k = emit_bind("call i32 @fn_rt_poll()")
emit(" store i32 ") emit(k) emit(", ptr @L_key\n")
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
}
emit_calls_for_phase("Input")
emit_calls_for_phase("FixedUpdate")

View file

@ -1,15 +1,15 @@
# emit_head.ludic — string constants and the module header (libc declarations,
# the slice header type, struct layouts, globals, argv, format strings).
fn hexdig(n: int) -> int { if n < 10 { return 48 + n } return 55 + n } # 0-9 A-F
fn hexdig(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } # 0-9 A-F
# emit `@.strN = ... c"escaped\00"` and return its name; % and non-print -> \XX
fn emit_str_const(s: ptr) -> ptr {
let name = sconcat("@.str", itoa(ll_str))
ll_str = ll_str + 1
let n = slen(s)
emith(name) emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)) emith(" x i8] c\"")
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
let i = 0
while i < n {
let c = peek8(s, i)
@ -27,14 +27,14 @@ fn emit_str_const(s: ptr) -> ptr {
# a fresh SSA register bound to a `getelementptr`, returned as its name
fn emit_gep_i8(base: ptr, idx: ptr) -> ptr {
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds i8, ptr ")
emit(base) emit(", i32 ") emit(idx) emit("\n")
emit(" "); emit(r); emit(" = getelementptr inbounds i8, ptr ")
emit(base); emit(", i32 "); emit(idx); emit("\n")
return r
}
# the constant initializer for a global var: a literal, or 0/null
fn global_init(d: Node) -> ptr {
if ptr_is_null(d.a) { if streq(llty(d.ty), "ptr") { return "null" } return "0" }
if ptr_is_null(d.a) { if streq(llty(d.ty), "ptr") { return "null" }; return "0" }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and streq(e.s, "-") and e.a.kind == E_INT { return sconcat("-", itoa(e.a.ival)) }
@ -74,14 +74,14 @@ fn emit_header() -> void {
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(slen(g_game_name) + 1)) emith(" x i8] c\"") emith(g_game_name) emith("\\00\"\n")
emith(itoa(slen(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")
# struct layouts
let i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_STRUCT {
emith("%Str_") emith(d.s) emith(" = type { ")
emith("%Str_"); emith(d.s); emith(" = type { ")
if len(d.kids) == 0 { emith("i32") }
let f = 0
while f < len(d.kids) {
@ -98,8 +98,8 @@ fn emit_header() -> void {
while i < len(prog) {
let d = prog[i]
if d.kind == N_VAR {
emith("@g_") emith(d.s) emith(" = internal global ")
emith(llty(d.ty)) emith(" ")
emith("@g_"); emith(d.s); emith(" = internal global ")
emith(llty(d.ty)); emith(" ")
emith(global_init(d))
emith("\n")
}

View file

@ -19,9 +19,9 @@ fn is_intrinsic(name: ptr) -> bool {
return false
}
# emit " <r> = <rest>\n" and return r
fn emit_bind(rest: ptr) -> ptr { let r = nreg() emit(" ") emit(r) emit(" = ") emit(rest) emit("\n") return r }
fn emit_bind(rest: ptr) -> ptr { let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r }
fn arg_code(e: Node, i: int) -> ptr { let v = emit_expr(e.kids[i]) return v.code }
fn arg_code(e: Node, i: int) -> ptr { let v = emit_expr(e.kids[i]); return v.code }
fn emit_intrinsic(name: ptr, e: Node) -> Val {
g_intrin_ok = true
@ -32,21 +32,21 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(w, ")"))), "ptr")
}
if streq(name, "mem_realloc") {
let p = arg_code(e, 0) let n = arg_code(e, 1)
let p = arg_code(e, 0); let n = arg_code(e, 1)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
return val(emit_bind(sconcat("call ptr @realloc(ptr ", sconcat(p, sconcat(", i64 ", sconcat(w, ")"))))), "ptr")
}
if streq(name, "peek8") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let p = arg_code(e, 0); let i = arg_code(e, 1)
let a = emit_gep_i8(p, i)
let b = emit_bind(sconcat("load i8, ptr ", a))
return val(emit_bind(sconcat("zext i8 ", sconcat(b, " to i32"))), "int")
}
if streq(name, "poke8") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let p = arg_code(e, 0); let i = arg_code(e, 1); let v = arg_code(e, 2)
let a = emit_gep_i8(p, i)
let t = emit_bind(sconcat("trunc i32 ", sconcat(v, " to i8")))
emit(" store i8 ") emit(t) emit(", ptr ") emit(a) emit("\n")
emit(" store i8 "); emit(t); emit(", ptr "); emit(a); emit("\n")
return val("0", "void")
}
if streq(name, "ptr_is_null") {
@ -55,11 +55,11 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat("zext i1 ", sconcat(c, " to i32"))), "bool")
}
if streq(name, "file_open") {
let p = arg_code(e, 0) let m = arg_code(e, 1)
let p = arg_code(e, 0); let m = arg_code(e, 1)
return val(emit_bind(sconcat("call ptr @fopen(ptr ", sconcat(p, sconcat(", ptr ", sconcat(m, ")"))))), "ptr")
}
if streq(name, "file_read") or streq(name, "file_write") {
let f = arg_code(e, 0) let b = arg_code(e, 1) let n = arg_code(e, 2)
let f = arg_code(e, 0); let b = arg_code(e, 1); let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
let fn2 = "@fread"
if streq(name, "file_write") { fn2 = "@fwrite" }
@ -67,7 +67,7 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "file_seek") {
let f = arg_code(e, 0) let off = arg_code(e, 1) let wh = arg_code(e, 2)
let f = arg_code(e, 0); let off = arg_code(e, 1); let wh = arg_code(e, 2)
let o = emit_bind(sconcat("sext i32 ", sconcat(off, " to i64")))
return val(emit_bind(sconcat("call i32 @fseek(ptr ", sconcat(f, sconcat(", i64 ", sconcat(o, sconcat(", i32 ", sconcat(wh, ")"))))))), "int")
}
@ -78,17 +78,17 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
}
if streq(name, "file_close") {
let f = arg_code(e, 0)
emit(" call i32 @fclose(ptr ") emit(f) emit(")\n")
emit(" call i32 @fclose(ptr "); emit(f); emit(")\n")
return val("0", "void")
}
if streq(name, "print_str") {
let s = arg_code(e, 0)
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_str, ptr ") emit(s) emit(")\n")
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_str, ptr "); emit(s); emit(")\n")
return val("0", "void")
}
if streq(name, "print_int") {
let n = arg_code(e, 0)
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 ") emit(n) emit(")\n")
emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 "); emit(n); emit(")\n")
return val("0", "void")
}
if streq(name, "os_argc") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
@ -100,7 +100,7 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
}
if streq(name, "os_exit") {
let n = arg_code(e, 0)
emit(" call void @exit(i32 ") emit(n) emit(")\n")
emit(" call void @exit(i32 "); emit(n); emit(")\n")
emit(" unreachable\n")
g_term = true
return val("0", "void")
@ -114,9 +114,9 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
let n = arg_code(e, 0)
return val(emit_bind(sconcat("call ptr @getenv(ptr ", sconcat(n, ")"))), "str")
}
if streq(name, "bnot") { let a = arg_code(e, 0) return val(emit_bind(sconcat("xor i32 ", sconcat(a, ", -1"))), "int") }
if streq(name, "bnot") { let a = arg_code(e, 0); return val(emit_bind(sconcat("xor i32 ", sconcat(a, ", -1"))), "int") }
if streq(name,"shl") or streq(name,"shr") or streq(name,"band") or streq(name,"bor") or streq(name,"bxor") {
let a = arg_code(e, 0) let b = arg_code(e, 1)
let a = arg_code(e, 0); let b = arg_code(e, 1)
let opc = "shl"
if streq(name,"shr") { opc = "lshr" }
if streq(name,"band") { opc = "and" }
@ -125,16 +125,16 @@ fn emit_intrinsic(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat(opc, sconcat(" i32 ", sconcat(a, sconcat(", ", b))))), "int")
}
if streq(name, "peek32") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let p = arg_code(e, 0); let i = arg_code(e, 1)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" "); emit(g); emit(" = getelementptr inbounds i32, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
return val(emit_bind(sconcat("load i32, ptr ", g)), "int")
}
if streq(name, "poke32") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let p = arg_code(e, 0); let i = arg_code(e, 1); let v = arg_code(e, 2)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store i32 ") emit(v) emit(", ptr ") emit(g) emit("\n")
emit(" "); emit(g); emit(" = getelementptr inbounds i32, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
emit(" store i32 "); emit(v); emit(", ptr "); emit(g); emit("\n")
return val("0", "void")
}
g_intrin_ok = false

View file

@ -18,29 +18,29 @@ fn is_intrinsic2(name: ptr) -> bool {
fn emit_intrinsic2(name: ptr, e: Node) -> Val {
if streq(name, "mem_free") {
let p = arg_code(e, 0) emit(" call void @free(ptr ") emit(p) emit(")\n") return val("0", "void")
let p = arg_code(e, 0); emit(" call void @free(ptr "); emit(p); emit(")\n"); return val("0", "void")
}
if streq(name, "mem_copy") {
let d = arg_code(e, 0) let s = arg_code(e, 1) let n = arg_code(e, 2)
let d = arg_code(e, 0); let s = arg_code(e, 1); let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
emit(" call ptr @memcpy(ptr ") emit(d) emit(", ptr ") emit(s) emit(", i64 ") emit(w) emit(")\n")
emit(" call ptr @memcpy(ptr "); emit(d); emit(", ptr "); emit(s); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if streq(name, "mem_set") {
let p = arg_code(e, 0) let v = arg_code(e, 1) let n = arg_code(e, 2)
let p = arg_code(e, 0); let v = arg_code(e, 1); let n = arg_code(e, 2)
let w = emit_bind(sconcat("zext i32 ", sconcat(n, " to i64")))
emit(" call ptr @memset(ptr ") emit(p) emit(", i32 ") emit(v) emit(", i64 ") emit(w) emit(")\n")
emit(" call ptr @memset(ptr "); emit(p); emit(", i32 "); emit(v); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if streq(name, "ptr_add") {
let p = arg_code(e, 0) let n = arg_code(e, 1)
let p = arg_code(e, 0); let n = arg_code(e, 1)
let g = nreg()
emit(" ") emit(g) emit(" = getelementptr inbounds i8, ptr ") emit(p) emit(", i32 ") emit(n) emit("\n")
emit(" "); emit(g); emit(" = getelementptr inbounds i8, ptr "); emit(p); emit(", i32 "); emit(n); emit("\n")
return val(g, "ptr")
}
if streq(name, "read_byte") { return val(emit_bind("call i32 @getchar()"), "int") }
if streq(name, "write_byte") {
let v = arg_code(e, 0) emit(" call i32 @putchar(i32 ") emit(v) emit(")\n") return val("0", "void")
let v = arg_code(e, 0); emit(" call i32 @putchar(i32 "); emit(v); emit(")\n"); return val("0", "void")
}
if streq(name, "str_len") {
let s = arg_code(e, 0)
@ -52,44 +52,44 @@ fn emit_intrinsic2(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat("trunc i64 ", sconcat(r, " to i32"))), "int")
}
if streq(name, "peekf") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
let p = arg_code(e, 0); let i = arg_code(e, 1)
let g = nreg(); emit(" "); emit(g); emit(" = getelementptr inbounds i32, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
return val(emit_bind(sconcat("load i32, ptr ", g)), "fixed")
}
if streq(name, "pokef") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds i32, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store i32 ") emit(v) emit(", ptr ") emit(g) emit("\n")
let p = arg_code(e, 0); let i = arg_code(e, 1); let v = arg_code(e, 2)
let g = nreg(); emit(" "); emit(g); emit(" = getelementptr inbounds i32, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
emit(" store i32 "); emit(v); emit(", ptr "); emit(g); emit("\n")
return val("0", "void")
}
if streq(name, "peekp") {
let p = arg_code(e, 0) let i = arg_code(e, 1)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds ptr, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
let p = arg_code(e, 0); let i = arg_code(e, 1)
let g = nreg(); emit(" "); emit(g); emit(" = getelementptr inbounds ptr, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
return val(emit_bind(sconcat("load ptr, ptr ", g)), "ptr")
}
if streq(name, "pokep") {
let p = arg_code(e, 0) let i = arg_code(e, 1) let v = arg_code(e, 2)
let g = nreg() emit(" ") emit(g) emit(" = getelementptr inbounds ptr, ptr ") emit(p) emit(", i32 ") emit(i) emit("\n")
emit(" store ptr ") emit(v) emit(", ptr ") emit(g) emit("\n")
let p = arg_code(e, 0); let i = arg_code(e, 1); let v = arg_code(e, 2)
let g = nreg(); emit(" "); emit(g); emit(" = getelementptr inbounds ptr, ptr "); emit(p); emit(", i32 "); emit(i); emit("\n")
emit(" store ptr "); emit(v); emit(", ptr "); emit(g); emit("\n")
return val("0", "void")
}
if streq(name, "as_fixed") { let a = emit_expr(e.kids[0]) return val(a.code, "fixed") }
if streq(name, "as_int") { let a = emit_expr(e.kids[0]) return val(a.code, "int") }
if streq(name, "is_windowed") { if g_windowed { return val("1", "bool") } return val("0", "bool") }
if streq(name, "as_fixed") { let a = emit_expr(e.kids[0]); return val(a.code, "fixed") }
if streq(name, "as_int") { let a = emit_expr(e.kids[0]); return val(a.code, "int") }
if streq(name, "is_windowed") { if g_windowed { return val("1", "bool") }; return val("0", "bool") }
if streq(name, "game_title") { return val("@.gametitle", "str") }
# windowing hooks — declared external; only reached on the is_windowed() branch
if streq(name, "win_poll") { return val(emit_bind("call i32 @win_poll()"), "int") }
if streq(name, "win_running") { return val(emit_bind("call i32 @win_running()"), "bool") }
if streq(name, "win_open") {
let a = arg_code(e, 0) let b = arg_code(e, 1) let c = arg_code(e, 2) let d = arg_code(e, 3)
emit(" call void @win_open(i32 ") emit(a) emit(", i32 ") emit(b) emit(", i32 ") emit(c) emit(", ptr ") emit(d) emit(")\n")
let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2); let d = arg_code(e, 3)
emit(" call void @win_open(i32 "); emit(a); emit(", i32 "); emit(b); emit(", i32 "); emit(c); emit(", ptr "); emit(d); emit(")\n")
return val("0", "void")
}
if streq(name, "win_present") {
let a = arg_code(e, 0) let b = arg_code(e, 1) let c = arg_code(e, 2)
emit(" call void @win_present(ptr ") emit(a) emit(", i32 ") emit(b) emit(", i32 ") emit(c) emit(")\n")
let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2)
emit(" call void @win_present(ptr "); emit(a); emit(", i32 "); emit(b); emit(", i32 "); emit(c); emit(")\n")
return val("0", "void")
}
if streq(name, "win_close") { emit(" call void @win_close()\n") return val("0", "void") }
if streq(name, "win_close") { emit(" call void @win_close()\n"); return val("0", "void") }
return val("0", "void")
}

View file

@ -13,16 +13,16 @@ fn emit_machine(st: Node) -> void {
let state = st.kids[i]
let v = emit_expr(state.b)
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(s, sconcat(", ", v.code))))
let body = lbl("sbody") let nxt = lbl("sarm")
emit(" br i1 ") emit(c) emit(", label %") emit(body) emit(", label %") emit(nxt) emit("\n")
emit(body) emit(":\n") g_term = false
let body = lbl("sbody"); let nxt = lbl("sarm")
emit(" br i1 "); emit(c); emit(", label %"); emit(body); emit(", label %"); emit(nxt); emit("\n")
emit(body); emit(":\n"); g_term = false
emit_block(state.a)
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(nxt) emit(":\n") g_term = false
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
emit(nxt); emit(":\n"); g_term = false
i = i + 1
}
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(endl) emit(":\n") g_term = false
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
emit(endl); emit(":\n"); g_term = false
nmach = nmach - 1
}
@ -31,9 +31,9 @@ fn emit_become(st: Node) -> void {
let m = mach_stk[nmach - 1]
let target: Node = ptr_null()
let i = 0
while i < len(m.kids) { if streq(m.kids[i].s, st.s) { target = m.kids[i] } i = i + 1 }
while i < len(m.kids) { if streq(m.kids[i].s, st.s) { target = m.kids[i] }; i = i + 1 }
if ptr_is_null(target) { perr(sconcat("become: no state ", st.s)) }
let regv = emit_expr(m.a)
let sv = emit_expr(target.b)
emit(" call void @fn_rt_setreg(i32 ") emit(regv.code) emit(", i32 ") emit(sv.code) emit(")\n")
emit(" call void @fn_rt_setreg(i32 "); emit(regv.code); emit(", i32 "); emit(sv.code); emit(")\n")
}

View file

@ -14,14 +14,14 @@ fn emit_math_builtin(name: ptr, e: Node) -> Val {
return val(emit_bind(sconcat("select i1 ", sconcat(c, sconcat(", i32 ", sconcat(n, sconcat(", i32 ", a.code)))))), "int")
}
if streq(name, "min") or streq(name, "max") {
let a = emit_expr(e.kids[0]) let b = emit_expr(e.kids[1])
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
let op = "slt"
if streq(name, "max") { op = "sgt" }
let c = emit_bind(sconcat("icmp ", sconcat(op, sconcat(" i32 ", sconcat(a.code, sconcat(", ", b.code))))))
return val(emit_bind(sconcat("select i1 ", sconcat(c, sconcat(", i32 ", sconcat(a.code, sconcat(", i32 ", b.code)))))), "int")
}
# clamp(v, lo, hi) = max(lo, min(v, hi))
let v = emit_expr(e.kids[0]) let lo = emit_expr(e.kids[1]) let hi = emit_expr(e.kids[2])
let v = emit_expr(e.kids[0]); let lo = emit_expr(e.kids[1]); let hi = emit_expr(e.kids[2])
let c1 = emit_bind(sconcat("icmp slt i32 ", sconcat(v.code, sconcat(", ", hi.code))))
let t = emit_bind(sconcat("select i1 ", sconcat(c1, sconcat(", i32 ", sconcat(v.code, sconcat(", i32 ", hi.code))))))
let c2 = emit_bind(sconcat("icmp sgt i32 ", sconcat(lo.code, sconcat(", ", t))))

View file

@ -16,13 +16,13 @@ fn emit_new_struct(name: ptr) -> Val {
while f < len(s.kids) {
let fd = s.kids[f]
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Str_") emit(name)
emit(", ptr ") emit(obj) emit(", i32 0, i32 ") emit(itoa(f)) emit("\n")
emit(" "); emit(addr); emit(" = getelementptr inbounds %Str_"); emit(name)
emit(", ptr "); emit(obj); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
let lt = llty(fd.ty)
let v = "0"
if streq(lt, "ptr") { v = "null" }
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a) v = dv.code }
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a); v = dv.code }
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
f = f + 1
}
return val(obj, name)
@ -31,19 +31,19 @@ fn emit_new_struct(name: ptr) -> Val {
fn emit_new_slice(ty: ptr) -> Val {
let sz = emit_sizeof("%LSlice")
let h = emit_bind(sconcat("call ptr @malloc(i64 ", sconcat(sz, ")")))
let d0 = nreg() emit(" ") emit(d0) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 0\n")
emit(" store ptr null, ptr ") emit(d0) emit("\n")
let d1 = nreg() emit(" ") emit(d1) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 1\n")
emit(" store i32 0, ptr ") emit(d1) emit("\n")
let d2 = nreg() emit(" ") emit(d2) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h) emit(", i32 0, i32 2\n")
emit(" store i32 0, ptr ") emit(d2) emit("\n")
let d0 = nreg(); emit(" "); emit(d0); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h); emit(", i32 0, i32 0\n")
emit(" store ptr null, ptr "); emit(d0); emit("\n")
let d1 = nreg(); emit(" "); emit(d1); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h); emit(", i32 0, i32 1\n")
emit(" store i32 0, ptr "); emit(d1); emit("\n")
let d2 = nreg(); emit(" "); emit(d2); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h); emit(", i32 0, i32 2\n")
emit(" store i32 0, ptr "); emit(d2); emit("\n")
return val(h, ty)
}
fn slice_field(h: ptr, i: int) -> ptr {
let r = nreg()
emit(" ") emit(r) emit(" = getelementptr inbounds %LSlice, ptr ") emit(h)
emit(", i32 0, i32 ") emit(itoa(i)) emit("\n")
emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h)
emit(", i32 0, i32 "); emit(itoa(i)); emit("\n")
return r
}
@ -58,13 +58,13 @@ fn emit_push(e: Node) -> Val {
let el = slice_elem(s.ty)
let elt = llty(el)
let h = s.code
let lp = slice_field(h, 1) let cp = slice_field(h, 2) let dp = slice_field(h, 0)
let lp = slice_field(h, 1); let cp = slice_field(h, 2); let dp = slice_field(h, 0)
let l = emit_bind(sconcat("load i32, ptr ", lp))
let c = emit_bind(sconcat("load i32, ptr ", cp))
let full = emit_bind(sconcat("icmp sge i32 ", sconcat(l, sconcat(", ", c))))
let grow = lbl("grow") let put = lbl("put")
emit(" br i1 ") emit(full) emit(", label %") emit(grow) emit(", label %") emit(put) emit("\n")
emit(grow) emit(":\n")
let grow = lbl("grow"); let put = lbl("put")
emit(" br i1 "); emit(full); emit(", label %"); emit(grow); emit(", label %"); emit(put); emit("\n")
emit(grow); emit(":\n")
let dbl = emit_bind(sconcat("mul i32 ", sconcat(c, ", 2")))
let isz = emit_bind(sconcat("icmp eq i32 ", sconcat(c, ", 0")))
let nc = emit_bind(sconcat("select i1 ", sconcat(isz, sconcat(", i32 8, i32 ", dbl))))
@ -73,16 +73,16 @@ fn emit_push(e: Node) -> Val {
let bytes = emit_bind(sconcat("mul i64 ", sconcat(ncw, sconcat(", ", esz))))
let old = emit_bind(sconcat("load ptr, ptr ", dp))
let nd = emit_bind(sconcat("call ptr @realloc(ptr ", sconcat(old, sconcat(", i64 ", sconcat(bytes, ")")))))
emit(" store ptr ") emit(nd) emit(", ptr ") emit(dp) emit("\n")
emit(" store i32 ") emit(nc) emit(", ptr ") emit(cp) emit("\n")
emit(" br label %") emit(put) emit("\n")
emit(put) emit(":\n")
emit(" store ptr "); emit(nd); emit(", ptr "); emit(dp); emit("\n")
emit(" store i32 "); emit(nc); emit(", ptr "); emit(cp); emit("\n")
emit(" br label %"); emit(put); emit("\n")
emit(put); emit(":\n")
let v = emit_expr(e.kids[1])
let data = emit_bind(sconcat("load ptr, ptr ", dp))
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds ") emit(elt) emit(", ptr ") emit(data) emit(", i32 ") emit(l) emit("\n")
emit(" store ") emit(elt) emit(" ") emit(v.code) emit(", ptr ") emit(slot) emit("\n")
emit(" "); emit(slot); emit(" = getelementptr inbounds "); emit(elt); emit(", ptr "); emit(data); emit(", i32 "); emit(l); emit("\n")
emit(" store "); emit(elt); emit(" "); emit(v.code); emit(", ptr "); emit(slot); emit("\n")
let l1 = emit_bind(sconcat("add i32 ", sconcat(l, ", 1")))
emit(" store i32 ") emit(l1) emit(", ptr ") emit(lp) emit("\n")
emit(" store i32 "); emit(l1); emit(", ptr "); emit(lp); emit("\n")
return val("0", "void")
}

View file

@ -3,8 +3,8 @@
# the body once per match. Mirrors ll_query in compiler/back/ir_ecs.c.
fn find_arch_id(name: ptr) -> int {
let i = 0 let n = 1
while i < len(prog) { if prog[i].kind == N_ARCH { if streq(prog[i].s, name) { return n } n = n + 1 } i = i + 1 }
let i = 0; let n = 1
while i < len(prog) { if prog[i].kind == N_ARCH { if streq(prog[i].s, name) { return n }; n = n + 1 }; i = i + 1 }
return 0
}
@ -15,22 +15,22 @@ fn emit_query(st: Node) -> void {
if nself < len(self_stk) { self_stk[nself] = ip } else { push(self_stk, ip) }
nself = nself + 1
let cond = lbl("qcond") let body = lbl("qbody") let nxt = lbl("qnext") let endl = lbl("qend")
emit(" br label %") emit(cond) emit("\n")
emit(cond) emit(":\n")
let cond = lbl("qcond"); let body = lbl("qbody"); let nxt = lbl("qnext"); let endl = lbl("qend")
emit(" br label %"); emit(cond); emit("\n")
emit(cond); emit(":\n")
let i0 = emit_bind(sconcat("load i32, ptr ", ip))
let ec = emit_bind("load i32, ptr @L_entc")
let lt = emit_bind(sconcat("icmp slt i32 ", sconcat(i0, sconcat(", ", ec))))
emit(" br i1 ") emit(lt) emit(", label %") emit(body) emit(", label %") emit(endl) emit("\n")
emit(body) emit(":\n") g_term = false
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
emit(body); emit(":\n"); g_term = false
let i1 = emit_bind(sconcat("load i32, ptr ", ip))
# alive?
let ap = nreg() emit(" ") emit(ap) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_alive, i32 0, i32 ") emit(i1) emit("\n")
let ap = nreg(); emit(" "); emit(ap); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 "); emit(i1); emit("\n")
let al = emit_bind(sconcat("load i32, ptr ", ap))
let alc = emit_bind(sconcat("icmp ne i32 ", sconcat(al, ", 0")))
let ka = lbl("qa")
emit(" br i1 ") emit(alc) emit(", label %") emit(ka) emit(", label %") emit(nxt) emit("\n")
emit(ka) emit(":\n")
emit(" br i1 "); emit(alc); emit(", label %"); emit(ka); emit(", label %"); emit(nxt); emit("\n")
emit(ka); emit(":\n")
let terms = st.c
# component / archetype filters
@ -40,17 +40,17 @@ fn emit_query(st: Node) -> void {
let ak = find_arch_id(tm.s)
let ok = "0"
if ak > 0 {
let kp = nreg() emit(" ") emit(kp) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 ") emit(i1) emit("\n")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n")
let kv = emit_bind(sconcat("load i32, ptr ", kp))
ok = emit_bind(sconcat("icmp eq i32 ", sconcat(kv, sconcat(", ", itoa(ak)))))
} else {
let hp = nreg() emit(" ") emit(hp) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(tm.s) emit(", i32 0, i32 ") emit(i1) emit("\n")
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n")
let hv = emit_bind(sconcat("load i8, ptr ", hp))
ok = emit_bind(sconcat("icmp ne i8 ", sconcat(hv, ", 0")))
}
let keep = lbl("qt")
emit(" br i1 ") emit(ok) emit(", label %") emit(keep) emit(", label %") emit(nxt) emit("\n")
emit(keep) emit(":\n")
emit(" br i1 "); emit(ok); emit(", label %"); emit(keep); emit(", label %"); emit(nxt); emit("\n")
emit(keep); emit(":\n")
t = t + 1
}
@ -63,9 +63,9 @@ fn emit_query(st: Node) -> void {
if tm.ival == 0 and find_arch_id(tm.s) == 0 {
if vi < len(st.kids) {
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds [") emit(me) emit(" x %Cmp_") emit(tm.s) emit("], ptr @S_") emit(tm.s) emit(", i32 0, i32 ") emit(i1) emit("\n")
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(tm.s); emit("], ptr @S_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n")
let vslot = emit_alloca("ptr")
emit(" store ptr ") emit(slot) emit(", ptr ") emit(vslot) emit("\n")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.kids[vi].s, vslot, tm.s)
vi = vi + 1
}
@ -78,21 +78,21 @@ fn emit_query(st: Node) -> void {
let w = emit_expr(st.b)
let wc = emit_bind(sconcat("icmp ne i32 ", sconcat(w.code, ", 0")))
let kw = lbl("qw")
emit(" br i1 ") emit(wc) emit(", label %") emit(kw) emit(", label %") emit(nxt) emit("\n")
emit(kw) emit(":\n")
emit(" br i1 "); emit(wc); emit(", label %"); emit(kw); emit(", label %"); emit(nxt); emit("\n")
emit(kw); emit(":\n")
}
loop_push(nxt, endl)
emit_block(st.a)
loop_pop()
nloc = save
if not g_term { emit(" br label %") emit(nxt) emit("\n") }
if not g_term { emit(" br label %"); emit(nxt); emit("\n") }
emit(nxt) emit(":\n")
emit(nxt); emit(":\n")
let i2 = emit_bind(sconcat("load i32, ptr ", ip))
let i3 = emit_bind(sconcat("add i32 ", sconcat(i2, ", 1")))
store_at("i32", i3, ip)
emit(" br label %") emit(cond) emit("\n")
emit(endl) emit(":\n") g_term = false
emit(" br label %"); emit(cond); emit("\n")
emit(endl); emit(":\n"); g_term = false
nself = nself - 1
}

View file

@ -6,8 +6,8 @@
var g_iok: int = 0
fn emit_io(fn2: ptr, p: ptr, bytes: ptr) -> void {
let r = sconcat("%io", itoa(g_iok)) g_iok = g_iok + 1
emit(" ") emit(r) emit(" = call i64 @") emit(fn2) emit("(ptr ") emit(p) emit(", i64 1, i64 ") emit(bytes) emit(", ptr %f)\n")
let r = sconcat("%io", itoa(g_iok)); g_iok = g_iok + 1
emit(" "); emit(r); emit(" = call i64 @"); emit(fn2); emit("(ptr "); emit(p); emit(", i64 1, i64 "); emit(bytes); emit(", ptr %f)\n")
}
fn emit_snapshot_blocks(fn2: ptr) -> void {
@ -15,19 +15,19 @@ fn emit_snapshot_blocks(fn2: ptr) -> void {
let me = itoa(MAX_ENT)
emit_io(fn2, "@L_entc", "4")
emit_io(fn2, "@L_freen", "4")
emit(" %nalive = mul i64 ") emit(me) emit(", 4\n")
emit(" %nalive = mul i64 "); emit(me); emit(", 4\n")
emit_io(fn2, "@L_alive", "%nalive")
emit_io(fn2, "@L_freelist", "%nalive")
emit_io(fn2, "@L_kind", "%nalive")
let i = 0
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, sconcat("@g_", prog[i].s), "4") } i = i + 1 }
while i < len(prog) { if prog[i].kind == N_VAR { emit_io(fn2, sconcat("@g_", prog[i].s), "4") }; i = i + 1 }
let ci = 0
i = 0
while i < len(prog) {
if prog[i].kind == N_COMP {
let c = prog[i].s
let csz = sconcat("%csz", itoa(ci))
emit(" ") emit(csz) emit(" = ptrtoint ptr getelementptr (%Cmp_") emit(c) emit(", ptr null, i32 ") emit(me) emit(") to i64\n")
emit(" "); emit(csz); emit(" = ptrtoint ptr getelementptr (%Cmp_"); emit(c); emit(", ptr null, i32 "); emit(me); emit(") to i64\n")
emit_io(fn2, sconcat("@S_", c), csz)
emit_io(fn2, sconcat("@H_", c), me)
ci = ci + 1

View file

@ -5,21 +5,21 @@ fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
let me = itoa(MAX_ENT)
let c = find_comp(comp)
if ptr_is_null(c) { perr(sconcat("spawn: unknown component ", comp)) }
let hp = nreg() emit(" ") emit(hp) emit(" = getelementptr inbounds [") emit(me) emit(" x i8], ptr @H_") emit(comp) emit(", i32 0, i32 ") emit(e) emit("\n")
emit(" store i8 1, ptr ") emit(hp) emit("\n")
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n")
emit(" store i8 1, ptr "); emit(hp); emit("\n")
let slot = nreg()
emit(" ") emit(slot) emit(" = getelementptr inbounds [") emit(me) emit(" x %Cmp_") emit(comp) emit("], ptr @S_") emit(comp) emit(", i32 0, i32 ") emit(e) emit("\n")
emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(comp); emit("], ptr @S_"); emit(comp); emit(", i32 0, i32 "); emit(e); emit("\n")
# defaults
let f = 0
while f < len(c.kids) {
let fd = c.kids[f]
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Cmp_") emit(comp) emit(", ptr ") emit(slot) emit(", i32 0, i32 ") emit(itoa(f)) emit("\n")
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
let lt = llty(fd.ty)
let v = "0"
if streq(lt, "ptr") { v = "null" }
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a) v = dv.code }
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
if not ptr_is_null(fd.a) { let dv = emit_expr(fd.a); v = dv.code }
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
f = f + 1
}
# per-spawn overrides
@ -30,9 +30,9 @@ fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
let fidx = field_index(c, fi.s)
if fidx >= 0 {
let addr = nreg()
emit(" ") emit(addr) emit(" = getelementptr inbounds %Cmp_") emit(comp) emit(", ptr ") emit(slot) emit(", i32 0, i32 ") emit(itoa(fidx)) emit("\n")
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(fidx)); emit("\n")
let dv = emit_expr(fi.a)
emit(" store ") emit(llty(field_type(c, fi.s))) emit(" ") emit(dv.code) emit(", ptr ") emit(addr) emit("\n")
emit(" store "); emit(llty(field_type(c, fi.s))); emit(" "); emit(dv.code); emit(", ptr "); emit(addr); emit("\n")
}
j = j + 1
}
@ -44,25 +44,25 @@ fn emit_spawn(st: Node) -> void {
let ak = find_arch_id(st.s)
if ak > 0 {
let me = itoa(MAX_ENT)
let kp = nreg() emit(" ") emit(kp) emit(" = getelementptr inbounds [") emit(me) emit(" x i32], ptr @L_kind, i32 0, i32 ") emit(e) emit("\n")
emit(" store i32 ") emit(itoa(ak)) emit(", ptr ") emit(kp) emit("\n")
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(e); emit("\n")
emit(" store i32 "); emit(itoa(ak)); emit(", ptr "); emit(kp); emit("\n")
let arch = find_arch(st.s)
let c = 0
while c < len(arch.kids) {
let cn = arch.kids[c].s
let rec = ptr_null()
let i = 0
while i < len(st.kids) { if streq(st.kids[i].s, cn) { rec = st.kids[i].a } i = i + 1 }
while i < len(st.kids) { if streq(st.kids[i].s, cn) { rec = st.kids[i].a }; i = i + 1 }
emit_init_component(e, cn, rec)
c = c + 1
}
} else {
let i = 0
while i < len(st.kids) { emit_init_component(e, st.kids[i].s, st.kids[i].a) i = i + 1 }
while i < len(st.kids) { emit_init_component(e, st.kids[i].s, st.kids[i].a); i = i + 1 }
}
}
fn emit_despawn(st: Node) -> void {
let v = emit_expr(st.a)
emit(" call void @L_free_entity(i32 ") emit(v.code) emit(")\n")
emit(" call void @L_free_entity(i32 "); emit(v.code); emit(")\n")
}

View file

@ -12,7 +12,7 @@ fn emit_block(b: Node) -> void {
# store `val` (llvm type `lt`) into address `addr`
fn store_at(lt: ptr, v: ptr, addr: ptr) -> void {
emit(" store ") emit(lt) emit(" ") emit(v) emit(", ptr ") emit(addr) emit("\n")
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
}
fn emit_assign(st: Node) -> void {
@ -22,15 +22,15 @@ fn emit_assign(st: Node) -> void {
let ty = "int"
if t.kind == E_ID {
let li = loc_find(t.s)
if li >= 0 { addr = loc_reg[li] ty = loc_ty[li] }
if li >= 0 { addr = loc_reg[li]; ty = loc_ty[li] }
else {
let g = find_global(t.s)
if ptr_is_null(g) { perr(sconcat("assign to unknown ", t.s)) }
addr = sconcat("@g_", t.s) ty = g.ty
addr = sconcat("@g_", t.s); ty = g.ty
}
} else {
if t.kind == E_MEMBER { addr = emit_member_addr(t) ty = g_addr_ty }
else { if t.kind == E_INDEX { addr = emit_index_addr(t) ty = g_addr_ty }
if t.kind == E_MEMBER { addr = emit_member_addr(t); ty = g_addr_ty }
else { if t.kind == E_INDEX { addr = emit_index_addr(t); ty = g_addr_ty }
else { perr("bad assignment target") } }
}
let lt = llty(ty)
@ -51,34 +51,34 @@ fn emit_if(st: Node) -> void {
let c = emit_expr(st.a)
let cc = emit_bind(sconcat("icmp ne i32 ", sconcat(c.code, ", 0")))
let has_else = not ptr_is_null(st.c)
let tl = lbl("then") let el = lbl("else") let en = lbl("ifend")
if has_else { emit(" br i1 ") emit(cc) emit(", label %") emit(tl) emit(", label %") emit(el) emit("\n") }
else { emit(" br i1 ") emit(cc) emit(", label %") emit(tl) emit(", label %") emit(en) emit("\n") }
emit(tl) emit(":\n") g_term = false
let tl = lbl("then"); let el = lbl("else"); let en = lbl("ifend")
if has_else { emit(" br i1 "); emit(cc); emit(", label %"); emit(tl); emit(", label %"); emit(el); emit("\n") }
else { emit(" br i1 "); emit(cc); emit(", label %"); emit(tl); emit(", label %"); emit(en); emit("\n") }
emit(tl); emit(":\n"); g_term = false
emit_block(st.b)
if not g_term { emit(" br label %") emit(en) emit("\n") }
if not g_term { emit(" br label %"); emit(en); emit("\n") }
if has_else {
emit(el) emit(":\n") g_term = false
emit(el); emit(":\n"); g_term = false
# else is either a block or a nested if-statement
if st.c.kind == S_IF { emit_stmt(st.c) } else { emit_block(st.c) }
if not g_term { emit(" br label %") emit(en) emit("\n") }
if not g_term { emit(" br label %"); emit(en); emit("\n") }
}
emit(en) emit(":\n") g_term = false
emit(en); emit(":\n"); g_term = false
}
fn emit_while(st: Node) -> void {
let cl = lbl("wcond") let bl = lbl("wbody") let en = lbl("wend")
emit(" br label %") emit(cl) emit("\n")
emit(cl) emit(":\n")
let cl = lbl("wcond"); let bl = lbl("wbody"); let en = lbl("wend")
emit(" br label %"); emit(cl); emit("\n")
emit(cl); emit(":\n")
let c = emit_expr(st.a)
let cc = emit_bind(sconcat("icmp ne i32 ", sconcat(c.code, ", 0")))
emit(" br i1 ") emit(cc) emit(", label %") emit(bl) emit(", label %") emit(en) emit("\n")
emit(bl) emit(":\n") g_term = false
emit(" br i1 "); emit(cc); emit(", label %"); emit(bl); emit(", label %"); emit(en); emit("\n")
emit(bl); emit(":\n"); g_term = false
loop_push(cl, en)
emit_block(st.b)
loop_pop()
if not g_term { emit(" br label %") emit(cl) emit("\n") }
emit(en) emit(":\n") g_term = false
if not g_term { emit(" br label %"); emit(cl); emit("\n") }
emit(en); emit(":\n"); g_term = false
}
fn emit_for(st: Node) -> void {
@ -86,24 +86,24 @@ fn emit_for(st: Node) -> void {
let lo = emit_expr(st.a)
store_at("i32", lo.code, slot)
loc_push(st.s, slot, "int")
let cl = lbl("fcond") let bl = lbl("fbody") let ct = lbl("fcont") let en = lbl("fend")
emit(" br label %") emit(cl) emit("\n")
emit(cl) emit(":\n")
let cl = lbl("fcond"); let bl = lbl("fbody"); let ct = lbl("fcont"); let en = lbl("fend")
emit(" br label %"); emit(cl); emit("\n")
emit(cl); emit(":\n")
let iv = emit_bind(sconcat("load i32, ptr ", slot))
let hi = emit_expr(st.b)
let cc = emit_bind(sconcat("icmp slt i32 ", sconcat(iv, sconcat(", ", hi.code))))
emit(" br i1 ") emit(cc) emit(", label %") emit(bl) emit(", label %") emit(en) emit("\n")
emit(bl) emit(":\n") g_term = false
emit(" br i1 "); emit(cc); emit(", label %"); emit(bl); emit(", label %"); emit(en); emit("\n")
emit(bl); emit(":\n"); g_term = false
loop_push(ct, en)
emit_block(st.c)
loop_pop()
if not g_term { emit(" br label %") emit(ct) emit("\n") }
emit(ct) emit(":\n")
if not g_term { emit(" br label %"); emit(ct); emit("\n") }
emit(ct); emit(":\n")
let i2 = emit_bind(sconcat("load i32, ptr ", slot))
let i3 = emit_bind(sconcat("add i32 ", sconcat(i2, ", 1")))
store_at("i32", i3, slot)
emit(" br label %") emit(cl) emit("\n")
emit(en) emit(":\n") g_term = false
emit(" br label %"); emit(cl); emit("\n")
emit(en); emit(":\n"); g_term = false
}
fn emit_return(st: Node) -> void {
@ -117,7 +117,7 @@ fn emit_return(st: Node) -> void {
fn arm_is_default(arm: Node) -> bool {
let p = 0
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and streq(arm.kids[p].s, "_") { return true } p = p + 1 }
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and streq(arm.kids[p].s, "_") { return true }; p = p + 1 }
return false
}
@ -136,58 +136,58 @@ fn emit_match(st: Node) -> void {
while p < len(arm.kids) {
let pv = emit_expr(arm.kids[p])
let c = emit_bind(sconcat("icmp eq i32 ", sconcat(sv.code, sconcat(", ", pv.code))))
if first { acc = c first = false }
if first { acc = c; first = false }
else { acc = emit_bind(sconcat("or i1 ", sconcat(acc, sconcat(", ", c)))) }
p = p + 1
}
let bodyl = lbl("mbody") let nextl = lbl("marm")
emit(" br i1 ") emit(acc) emit(", label %") emit(bodyl) emit(", label %") emit(nextl) emit("\n")
emit(bodyl) emit(":\n") g_term = false
let bodyl = lbl("mbody"); let nextl = lbl("marm")
emit(" br i1 "); emit(acc); emit(", label %"); emit(bodyl); emit(", label %"); emit(nextl); emit("\n")
emit(bodyl); emit(":\n"); g_term = false
emit_block(arm.a)
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(nextl) emit(":\n") g_term = false
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
emit(nextl); emit(":\n"); g_term = false
}
i = i + 1
}
if not ptr_is_null(deflt) { emit_block(deflt.a) }
if not g_term { emit(" br label %") emit(endl) emit("\n") }
emit(endl) emit(":\n") g_term = false
if not g_term { emit(" br label %"); emit(endl); emit("\n") }
emit(endl); emit(":\n"); g_term = false
}
fn emit_stmt(st: Node) -> void {
if st.kind == S_LET {
let ty = st.ty
if ptr_is_null(ty) { let v0 = emit_expr(st.a) ty = v0.ty
let slot = emit_alloca(llty(ty)) store_at(llty(ty), v0.code, slot) loc_push(st.s, slot, ty) return }
if ptr_is_null(ty) { let v0 = emit_expr(st.a); ty = v0.ty
let slot = emit_alloca(llty(ty)); store_at(llty(ty), v0.code, slot); loc_push(st.s, slot, ty); return }
let slot = emit_alloca(llty(ty))
if ptr_is_null(st.a) {
let z = "0"
if streq(llty(ty), "ptr") { z = "null" }
store_at(llty(ty), z, slot)
} else { let v = emit_expr(st.a) store_at(llty(ty), v.code, slot) }
} else { let v = emit_expr(st.a); store_at(llty(ty), v.code, slot) }
loc_push(st.s, slot, ty)
return
}
if st.kind == S_ASSIGN { emit_assign(st) return }
if st.kind == S_IF { emit_if(st) return }
if st.kind == S_WHILE { emit_while(st) return }
if st.kind == S_FOR { emit_for(st) return }
if st.kind == S_RETURN { emit_return(st) return }
if st.kind == S_BREAK { emit(" br label %") emit(brk_lbl[nloop - 1]) emit("\n") g_term = true return }
if st.kind == S_CONTINUE { emit(" br label %") emit(cnt_lbl[nloop - 1]) emit("\n") g_term = true return }
if st.kind == S_MATCH { emit_match(st) return }
if st.kind == S_QUERY { emit_query(st) return }
if st.kind == S_SPAWN { emit_spawn(st) return }
if st.kind == S_DESPAWN { emit_despawn(st) return }
if st.kind == S_MACHINE { emit_machine(st) return }
if st.kind == S_BECOME { emit_become(st) return }
if st.kind == S_EXPR { let v = emit_expr(st.a) return }
if st.kind == S_ASSIGN { emit_assign(st); return }
if st.kind == S_IF { emit_if(st); return }
if st.kind == S_WHILE { emit_while(st); return }
if st.kind == S_FOR { emit_for(st); return }
if st.kind == S_RETURN { emit_return(st); return }
if st.kind == S_BREAK { emit(" br label %"); emit(brk_lbl[nloop - 1]); emit("\n"); g_term = true; return }
if st.kind == S_CONTINUE { emit(" br label %"); emit(cnt_lbl[nloop - 1]); emit("\n"); g_term = true; return }
if st.kind == S_MATCH { emit_match(st); return }
if st.kind == S_QUERY { emit_query(st); return }
if st.kind == S_SPAWN { emit_spawn(st); return }
if st.kind == S_DESPAWN { emit_despawn(st); return }
if st.kind == S_MACHINE { emit_machine(st); return }
if st.kind == S_BECOME { emit_become(st); return }
if st.kind == S_EXPR { let v = emit_expr(st.a); return }
perr("cannot emit statement")
}
fn loop_push(cont: ptr, brk: ptr) -> void {
if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont brk_lbl[nloop] = brk }
else { push(cnt_lbl, cont) push(brk_lbl, brk) }
if nloop < len(cnt_lbl) { cnt_lbl[nloop] = cont; brk_lbl[nloop] = brk }
else { push(cnt_lbl, cont); push(brk_lbl, brk) }
nloop = nloop + 1
}
fn loop_pop() -> void { nloop = nloop - 1 }

View file

@ -9,43 +9,43 @@ var ui_roots: []int # first-widget index of each ui block
fn ui_wtype(w: Node) -> int {
let s = w.s
if streq(s, "panel") { return 0 } if streq(s, "col") { return 1 } if streq(s, "row") { return 2 }
if streq(s, "label") { return 3 } if streq(s, "button") { return 4 }
if streq(s, "image") { return 5 } if streq(s, "spacer") { return 6 }
if streq(s, "panel") { return 0 }; if streq(s, "col") { return 1 }; if streq(s, "row") { return 2 }
if streq(s, "label") { return 3 }; if streq(s, "button") { return 4 }
if streq(s, "image") { return 5 }; if streq(s, "spacer") { return 6 }
return 0
}
fn ui_prop(w: Node, key: ptr) -> Node {
let i = 0
while i < len(w.b.kids) { if streq(w.b.kids[i].s, key) { return w.b.kids[i].a } i = i + 1 }
while i < len(w.b.kids) { if streq(w.b.kids[i].s, key) { return w.b.kids[i].a }; i = i + 1 }
return ptr_null()
}
fn ui_flatten(w: Node, parent: int) -> void {
let idx = len(uiw)
push(uiw, w) push(uiw_parent, parent)
push(uiw, w); push(uiw_parent, parent)
let i = 0
while i < len(w.kids) { ui_flatten(w.kids[i], idx) i = i + 1 }
while i < len(w.kids) { ui_flatten(w.kids[i], idx); i = i + 1 }
}
fn ui_flatten_all() -> void {
uiw = new []Node uiw_parent = new []int ui_roots = new []int
uiw = new []Node; uiw_parent = new []int; ui_roots = new []int
let i = 0
while i < len(prog) {
if prog[i].kind == N_UI and prog[i].ival == 1 { push(ui_roots, len(uiw)) ui_flatten(prog[i].a, 0 - 1) }
if prog[i].kind == N_UI and prog[i].ival == 1 { push(ui_roots, len(uiw)); ui_flatten(prog[i].a, 0 - 1) }
i = i + 1
}
}
fn has_ui() -> bool {
let i = 0
while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true } i = i + 1 }
while i < len(prog) { if prog[i].kind == N_UI and prog[i].ival == 1 { return true }; i = i + 1 }
return false
}
# index of a UI_<name>: a ui block's root, or a widget's id=
fn ui_index_of(nm: ptr) -> int {
let s = substr(nm, 3, slen(nm) - 3) # strip "UI_"
let u = 0 let bi = 0
let u = 0; let bi = 0
let i = 0
while i < len(prog) {
if prog[i].kind == N_UI and prog[i].ival == 1 { if streq(prog[i].s, s) { return ui_roots[bi] } bi = bi + 1 }
if prog[i].kind == N_UI and prog[i].ival == 1 { if streq(prog[i].s, s) { return ui_roots[bi] }; bi = bi + 1 }
i = i + 1
}
i = 0
@ -61,24 +61,24 @@ fn is_ui_ident(nm: ptr) -> bool {
}
fn ll_ui_set(idx: int, key: int, val: ptr) -> void {
emit(" call void @fn_rt_ui_set(i32 ") emit(itoa(idx)) emit(", i32 ") emit(itoa(key)) emit(", i32 ") emit(val) emit(")\n")
emit(" call void @fn_rt_ui_set(i32 "); emit(itoa(idx)); emit(", i32 "); emit(itoa(key)); emit(", i32 "); emit(val); emit(")\n")
}
fn ui_prop_key(k: ptr) -> int {
if streq(k,"w") { return 2 } if streq(k,"h") { return 3 } if streq(k,"x") { return 4 } if streq(k,"y") { return 5 }
if streq(k,"pad") { return 7 } if streq(k,"gap") { return 8 } if streq(k,"bg") { return 9 } if streq(k,"fg") { return 10 }
if streq(k,"border") { return 11 } if streq(k,"grow") { return 13 } if streq(k,"font") { return 14 } if streq(k,"size") { return 15 }
if streq(k,"inset") { return 16 } if streq(k,"focus") { return 17 }
if streq(k,"w") { return 2 }; if streq(k,"h") { return 3 }; if streq(k,"x") { return 4 }; if streq(k,"y") { return 5 }
if streq(k,"pad") { return 7 }; if streq(k,"gap") { return 8 }; if streq(k,"bg") { return 9 }; if streq(k,"fg") { return 10 }
if streq(k,"border") { return 11 }; if streq(k,"grow") { return 13 }; if streq(k,"font") { return 14 }; if streq(k,"size") { return 15 }
if streq(k,"inset") { return 16 }; if streq(k,"focus") { return 17 }
return 0 - 1
}
fn emit_ui_build() -> void {
ui_flatten_all()
ll_t = 0 ll_lbl = 0 g_term = false loc_reset() nloop = 0
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
let fbody = buf_new()
falloc = buf_new()
let saved = code
code = fbody
emit(" call void @fn_rt_ui_reset(i32 ") emit(itoa(len(uiw))) emit(")\n")
emit(" call void @fn_rt_ui_reset(i32 "); emit(itoa(len(uiw))); emit(")\n")
let i = 0
while i < len(uiw) {
let w = uiw[i]
@ -91,10 +91,10 @@ fn emit_ui_build() -> void {
let pr = w.b.kids[p]
let k = pr.s
let v = pr.a
if streq(k, "id") { p = p + 1 continue }
if streq(k, "id") { p = p + 1; continue }
if streq(k, "text") {
let sv = emit_expr(v)
emit(" call void @fn_rt_ui_static_text(i32 ") emit(itoa(i)) emit(", ptr ") emit(sv.code) emit(")\n")
emit(" call void @fn_rt_ui_static_text(i32 "); emit(itoa(i)); emit(", ptr "); emit(sv.code); emit(")\n")
} else { if streq(k, "skin") or streq(k, "image") {
let sv = emit_expr(v)
let r = emit_bind(sconcat("call i32 @fn_rt_image_load(ptr ", sconcat(sv.code, ")")))
@ -102,9 +102,9 @@ fn emit_ui_build() -> void {
} else { if streq(k, "align") {
if v.kind == E_ID {
let a = 0
if streq(v.s, "center") { a = 1 } if streq(v.s, "end") { a = 2 }
if streq(v.s, "center") { a = 1 }; if streq(v.s, "end") { a = 2 }
ll_ui_set(i, 12, itoa(a))
} else { let vv = emit_expr(v) ll_ui_set(i, 12, vv.code) }
} else { let vv = emit_expr(v); ll_ui_set(i, 12, vv.code) }
} else {
let key = ui_prop_key(k)
if key >= 0 {

View file

@ -16,7 +16,7 @@ var toks: []Tok
fn tok_push(kind: int, text: ptr, ival: int, line: int) -> void {
let t = new Tok
t.kind = kind t.text = text t.ival = ival t.line = line
t.kind = kind; t.text = text; t.ival = ival; t.line = line
push(toks, t)
}
@ -42,8 +42,8 @@ fn lex(src: ptr) -> void {
let n = slen(src)
while i < n {
let c = peek8(src, i)
if c == 10 { tok_push(TK_NL, ptr_null(), 0, line) line = line + 1 i = i + 1 continue }
if c == 32 or c == 9 or c == 13 { i = i + 1 continue }
if c == 10 { tok_push(TK_NL, ptr_null(), 0, line); line = line + 1; i = i + 1; continue }
if c == 32 or c == 9 or c == 13 { i = i + 1; continue }
if c == 35 { # '#' comment to end of line
while i < n and peek8(src, i) != 10 { i = i + 1 }
continue
@ -60,8 +60,8 @@ fn lex(src: ptr) -> void {
if e == 110 { r = 10 }
if e == 116 { r = 9 }
if e == 48 { r = 0 }
poke8(out, j, r) j = j + 1 i = i + 2
} else { poke8(out, j, peek8(src, i)) j = j + 1 i = i + 1 }
poke8(out, j, r); j = j + 1; i = i + 2
} else { poke8(out, j, peek8(src, i)); j = j + 1; i = i + 1 }
}
i = i + 1
poke8(out, j, 0)
@ -77,7 +77,7 @@ fn lex(src: ptr) -> void {
if e == 116 { v = 9 }
if e == 48 { v = 0 }
i = i + 2
} else { v = peek8(src, i) i = i + 1 }
} else { v = peek8(src, i); i = i + 1 }
if peek8(src, i) == 39 { i = i + 1 }
tok_push(TK_INT, ptr_null(), v, line)
continue
@ -99,12 +99,12 @@ fn lex(src: ptr) -> void {
continue
}
let v = 0
while i < n and char_is_digit(peek8(src, i)) { v = v * 10 + (peek8(src, i) - 48) i = i + 1 }
while i < n and char_is_digit(peek8(src, i)) { v = v * 10 + (peek8(src, i) - 48); i = i + 1 }
# a fractional part makes it a Q16.16 fixed literal
if i < n and peek8(src, i) == 46 and char_is_digit(peek8(src, i + 1)) {
i = i + 1
let fnum = 0 let fden = 1
while i < n and char_is_digit(peek8(src, i)) { fnum = fnum * 10 + (peek8(src, i) - 48) fden = fden * 10 i = i + 1 }
let fnum = 0; let fden = 1
while i < n and char_is_digit(peek8(src, i)) { fnum = fnum * 10 + (peek8(src, i) - 48); fden = fden * 10; i = i + 1 }
let bits = shl(v, 16) + shl(fnum, 16) / fden
tok_push(TK_FLOAT, ptr_null(), bits, line)
continue
@ -118,20 +118,20 @@ fn lex(src: ptr) -> void {
tok_push(TK_ID, substr(src, start, i - start), 0, line)
continue
}
if c == 59 { tok_push(TK_NL, ptr_null(), 0, line) i = i + 1 continue } # ';'
if c == 59 { tok_push(TK_NL, ptr_null(), 0, line); i = i + 1; continue } # ';'
# two-character operators
if two_at(src, i, 45, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ->
if two_at(src, i, 61, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # =>
if two_at(src, i, 61, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ==
if two_at(src, i, 33, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # !=
if two_at(src, i, 60, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # <=
if two_at(src, i, 62, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # >=
if two_at(src, i, 43, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # +=
if two_at(src, i, 45, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # -=
if two_at(src, i, 42, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # *=
if two_at(src, i, 47, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # /=
if two_at(src, i, 46, 46) { tok_push(TK_OP, substr(src, i, 2), 0, line) i = i + 2 continue } # ..
if is_op1(c) { tok_push(TK_OP, substr(src, i, 1), 0, line) i = i + 1 continue }
if two_at(src, i, 45, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # ->
if two_at(src, i, 61, 62) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # =>
if two_at(src, i, 61, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # ==
if two_at(src, i, 33, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # !=
if two_at(src, i, 60, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # <=
if two_at(src, i, 62, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # >=
if two_at(src, i, 43, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # +=
if two_at(src, i, 45, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # -=
if two_at(src, i, 42, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # *=
if two_at(src, i, 47, 61) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # /=
if two_at(src, i, 46, 46) { tok_push(TK_OP, substr(src, i, 2), 0, line); i = i + 2; continue } # ..
if is_op1(c) { tok_push(TK_OP, substr(src, i, 1), 0, line); i = i + 1; continue }
i = i + 1 # skip anything unrecognised
}
tok_push(TK_EOF, ptr_null(), 0, line)

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@
fn base_name(path: ptr) -> ptr {
let last = 0 - 1
let i = 0
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i } i = i + 1 }
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i }; i = i + 1 }
return substr(path, last + 1, i - (last + 1))
}
@ -72,7 +72,7 @@ main {
else { if streq(a, "--fmt") { fmt = true }
else { if streq(a, "--save-temps") { save = true }
else { if streq(a, "--run") { run = true }
else { if streq(a, "-o") { ai = ai + 1 if ai < os_argc() { out = os_arg(ai) } }
else { if streq(a, "-o") { ai = ai + 1; if ai < os_argc() { out = os_arg(ai) } }
else {
# an unknown flag is ignored (with a note) rather than mistaken for the
# input file — '-' is ASCII 45
@ -112,8 +112,8 @@ main {
emit_program()
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).
if emit_ir { ir_flush(out) return }
if not has_target { ir_flush(ptr_null()) return }
if emit_ir { ir_flush(out); return }
if not has_target { ir_flush(ptr_null()); return }
# a run with no explicit -o lands in a temp file
if run and ptr_is_null(out) {

View file

@ -8,8 +8,8 @@ var g_game_name: ptr # the `game`/`module` name
fn cur() -> Tok { return toks[pi] }
fn pk(o: int) -> Tok { return toks[pi + o] }
fn is_op(v: ptr) -> bool { let t = toks[pi] return t.kind == TK_OP and streq(t.text, v) }
fn is_id(v: ptr) -> bool { let t = toks[pi] return t.kind == TK_ID and streq(t.text, v) }
fn is_op(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_OP and streq(t.text, v) }
fn is_id(v: ptr) -> bool { let t = toks[pi]; return t.kind == TK_ID and streq(t.text, v) }
fn is_kw(v: ptr) -> bool { return is_id(v) }
fn perr(msg: ptr) -> void {
@ -20,7 +20,7 @@ fn perr(msg: ptr) -> void {
os_exit(1)
}
fn eat_op(v: ptr) -> void { if not is_op(v) { perr(v) } pi = pi + 1 }
fn eat_op(v: ptr) -> void { if not is_op(v) { perr(v) }; pi = pi + 1 }
fn eat_id() -> ptr {
let t = toks[pi]
if t.kind != TK_ID { perr("expected identifier") }
@ -36,9 +36,9 @@ fn ptype() -> ptr {
eat_op("]")
let el = ptype()
let out = mem_alloc(slen(el) + 3)
poke8(out, 0, 91) poke8(out, 1, 93) # "[]"
poke8(out, 0, 91); poke8(out, 1, 93) # "[]"
let i = 0
while peek8(el, i) != 0 { poke8(out, 2 + i, peek8(el, i)) i = i + 1 }
while peek8(el, i) != 0 { poke8(out, 2 + i, peek8(el, i)); i = i + 1 }
poke8(out, 2 + i, 0)
return out
}
@ -49,23 +49,23 @@ fn ptype() -> ptr {
fn expr() -> Node { return p_or() }
fn args_call(call: Node) -> void {
eat_op("(") skipnl()
while not is_op(")") { push(call.kids, expr()) skipnl() if is_op(",") { pi = pi + 1 skipnl() } }
eat_op("("); skipnl()
while not is_op(")") { push(call.kids, expr()); skipnl(); if is_op(",") { pi = pi + 1; skipnl() } }
eat_op(")")
}
fn p_primary() -> Node {
let t = toks[pi]
if t.kind == TK_INT { let n = node(E_INT) n.ival = t.ival pi = pi + 1 return n }
if t.kind == TK_FLOAT { let n = node(E_FLOAT) n.ival = t.ival pi = pi + 1 return n }
if t.kind == TK_STR { let n = node(E_STR) n.s = t.text pi = pi + 1 return n }
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; pi = pi + 1; return n }
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; pi = pi + 1; return n }
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi = pi + 1; return n }
if t.kind == TK_ID {
if streq(t.text, "true") { let n = node(E_BOOL) n.ival = 1 pi = pi + 1 return n }
if streq(t.text, "false") { let n = node(E_BOOL) n.ival = 0 pi = pi + 1 return n }
if streq(t.text, "new") { pi = pi + 1 let n = node(E_NEW) n.s = ptype() return n }
let n = node(E_ID) n.s = t.text pi = pi + 1 return n
if streq(t.text, "true") { let n = node(E_BOOL); n.ival = 1; pi = pi + 1; return n }
if streq(t.text, "false") { let n = node(E_BOOL); n.ival = 0; pi = pi + 1; return n }
if streq(t.text, "new") { pi = pi + 1; let n = node(E_NEW); n.s = ptype(); return n }
let n = node(E_ID); n.s = t.text; pi = pi + 1; return n
}
if is_op("(") { pi = pi + 1 skipnl() let e = expr() skipnl() eat_op(")") return e }
if is_op("(") { pi = pi + 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
perr("expected expression")
return node(E_INT)
}
@ -73,64 +73,78 @@ fn p_primary() -> Node {
fn p_postfix() -> Node {
let e = p_primary()
while true {
if is_op(".") { pi = pi + 1 let m = node(E_MEMBER) m.a = e m.s = eat_id() e = m }
else { if is_op("[") { pi = pi + 1 let ix = node(E_INDEX) ix.a = e ix.b = expr() eat_op("]") e = ix }
else { if is_op("(") { let c = node(E_CALL) c.a = e args_call(c) e = c } else { break } } }
if is_op(".") { pi = pi + 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m }
else { if is_op("[") { pi = pi + 1; let ix = node(E_INDEX); ix.a = e; ix.b = expr(); eat_op("]"); e = ix }
else { if is_op("(") { let c = node(E_CALL); c.a = e; args_call(c); e = c } else { break } } }
}
return e
}
fn p_unary() -> Node {
if is_op("-") { pi = pi + 1 let n = node(E_UN) n.s = "-" n.a = p_unary() return n }
if is_id("not") { pi = pi + 1 let n = node(E_UN) n.s = "not" n.a = p_unary() return n }
if is_op("-") { pi = pi + 1; let n = node(E_UN); n.s = "-"; n.a = p_unary(); return n }
if is_id("not") { pi = pi + 1; let n = node(E_UN); n.s = "not"; n.a = p_unary(); return n }
return p_postfix()
}
fn mkbin(op: ptr, l: Node, r: Node) -> Node { let b = node(E_BIN) b.s = op b.a = l b.b = r return b }
fn mkbin(op: ptr, l: Node, r: Node) -> Node { let b = node(E_BIN); b.s = op; b.a = l; b.b = r; return b }
fn p_mul() -> Node {
let l = p_unary()
while is_op("*") or is_op("/") or is_op("%") { let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_unary()) }
while is_op("*") or is_op("/") or is_op("%") { let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_unary()) }
return l
}
fn p_add() -> Node {
let l = p_mul()
while is_op("+") or is_op("-") { let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_mul()) }
while is_op("+") or is_op("-") { let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_mul()) }
return l
}
fn p_cmp() -> Node {
let l = p_add()
while is_op("<") or is_op("<=") or is_op(">") or is_op(">=") or is_op("==") or is_op("!=") {
let op = toks[pi].text pi = pi + 1 l = mkbin(op, l, p_add())
let op = toks[pi].text; pi = pi + 1; l = mkbin(op, l, p_add())
}
return l
}
fn p_and() -> Node {
let l = p_cmp()
while is_id("and") { pi = pi + 1 l = mkbin("and", l, p_cmp()) }
while is_id("and") { pi = pi + 1; l = mkbin("and", l, p_cmp()) }
return l
}
fn p_or() -> Node {
let l = p_and()
while is_id("or") { pi = pi + 1 l = mkbin("or", l, p_and()) }
while is_id("or") { pi = pi + 1; l = mkbin("or", l, p_and()) }
return l
}
# a record literal `{ field = value, ... }` — used by spawn component inits
# a record literal `{ field: value, ... }` — used by spawn component inits.
# Rule A: a named part uses `:` (`=` is assignment/binding only).
fn record() -> Node {
eat_op("{")
let r = node(E_REC)
while true { skipnl() if is_op("}") { break }
let fi = node(E_FINIT) fi.s = eat_id() eat_op("=") fi.a = expr() push(r.kids, fi)
while true { skipnl(); if is_op("}") { break }
let fi = node(E_FINIT); fi.s = eat_id(); eat_op(":"); fi.a = expr(); push(r.kids, fi)
if is_op(",") { pi = pi + 1 } }
eat_op("}") return r
eat_op("}"); return r
}
# ---- statements ------------------------------------------------------------
fn block() -> Node {
skipnl() eat_op("{")
skipnl(); eat_op("{")
let b = node(N_BLOCK)
while true { skipnl() if is_op("}") { break } push(b.kids, stmt()) }
while true { skipnl(); if is_op("}") { break }
push(b.kids, stmt())
# Rule B: statements are separated by a newline or ';' (both lex to TK_NL).
# After a statement the next token must be that separator or the block's end —
# two statements may not sit adjacent with only spaces between them.
let sep = toks[pi].kind == TK_NL
if is_op("}") { sep = true }
if not sep {
let ez = file_stderr(); file_write(ez, "cur=", 4)
if not ptr_is_null(toks[pi].text) { file_write(ez, toks[pi].text, slen(toks[pi].text)) }
file_write(ez, "\n", 1); print_int(7770000 + toks[pi].line)
perr("expected newline or ';' between statements")
}
}
eat_op("}")
return b
}
@ -139,106 +153,112 @@ fn stmt() -> Node {
let t = toks[pi]
if t.kind == TK_ID {
if streq(t.text, "let") {
pi = pi + 1 let n = node(S_LET) n.s = eat_id()
if is_op(":") { pi = pi + 1 n.ty = ptype() }
if is_op("=") { pi = pi + 1 n.a = expr() }
pi = pi + 1; let n = node(S_LET); n.s = eat_id()
if is_op(":") { pi = pi + 1; n.ty = ptype() }
if is_op("=") { pi = pi + 1; n.a = expr() }
return n
}
if streq(t.text, "return") {
pi = pi + 1 let n = node(S_RETURN)
pi = pi + 1; let n = node(S_RETURN)
if toks[pi].kind != TK_NL and not is_op("}") { n.a = expr() }
return n
}
if streq(t.text, "if") {
pi = pi + 1 let n = node(S_IF) n.a = expr() n.b = block() skipnl()
if is_id("else") { pi = pi + 1 skipnl()
pi = pi + 1; let n = node(S_IF); n.a = expr(); n.b = block()
let save = pi; skipnl() # peek past newlines for a trailing `else`
if is_id("else") { pi = pi + 1; skipnl()
if is_id("if") { n.c = stmt() } else { n.c = block() } }
else { pi = save } # no else: keep the separator for block()'s Rule-B check
return n
}
if streq(t.text, "when") { # `when c { }` — an if with no else
pi = pi + 1 let n = node(S_IF) n.a = expr() n.b = block()
pi = pi + 1; let n = node(S_IF); n.a = expr(); n.b = block()
return n
}
if streq(t.text, "while") { pi = pi + 1 let n = node(S_WHILE) n.a = expr() n.b = block() return n }
if streq(t.text, "while") { pi = pi + 1; let n = node(S_WHILE); n.a = expr(); n.b = block(); return n }
if streq(t.text, "for") {
pi = pi + 1
if is_op("(") { return parse_query_for() }
let n = node(S_FOR) n.s = eat_id()
let n = node(S_FOR); n.s = eat_id()
let inkw = eat_id() # 'in'
n.a = expr() eat_op("..") n.b = expr() n.c = block()
n.a = expr(); eat_op(".."); n.b = expr(); n.c = block()
return n
}
if streq(t.text, "spawn") { return parse_spawn() }
if streq(t.text, "machine") {
pi = pi + 1 let n = node(S_MACHINE) n.a = expr() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
pi = pi + 1; let n = node(S_MACHINE); n.a = expr(); skipnl(); eat_op("{")
let sidx = 0 # states auto-number by declaration order
while true { skipnl(); if is_op("}") { break }
let stkw = eat_id() # 'state'
let s = node(S_STATE) s.s = eat_id() eat_op("=") s.b = expr() skipnl() s.a = block()
push(n.kids, s) }
eat_op("}") return n
let s = node(S_STATE); s.s = eat_id()
if is_op("=") { pi = pi + 1; s.b = expr() } # explicit value (still allowed)
else { let iv = node(E_INT); iv.ival = sidx; s.b = iv } # else its ordinal
skipnl(); s.a = block()
push(n.kids, s); sidx = sidx + 1 }
eat_op("}"); return n
}
if streq(t.text, "become") { pi = pi + 1 let n = node(S_BECOME) n.s = eat_id() return n }
if streq(t.text, "enter") { pi = pi + 1 let n = node(S_BECOME) n.s = eat_id() n.ival = 1 return n }
if streq(t.text, "despawn") { pi = pi + 1 let n = node(S_DESPAWN) n.a = expr() return n }
if streq(t.text, "break") { pi = pi + 1 return node(S_BREAK) }
if streq(t.text, "continue") { pi = pi + 1 return node(S_CONTINUE) }
if streq(t.text, "become") { pi = pi + 1; let n = node(S_BECOME); n.s = eat_id(); return n }
if streq(t.text, "enter") { pi = pi + 1; let n = node(S_BECOME); n.s = eat_id(); n.ival = 1; return n }
if streq(t.text, "despawn") { pi = pi + 1; let n = node(S_DESPAWN); n.a = expr(); return n }
if streq(t.text, "break") { pi = pi + 1; return node(S_BREAK) }
if streq(t.text, "continue") { pi = pi + 1; return node(S_CONTINUE) }
if streq(t.text, "match") {
pi = pi + 1 let n = node(S_MATCH) n.a = expr() skipnl() eat_op("{")
pi = pi + 1; let n = node(S_MATCH); n.a = expr(); skipnl(); eat_op("{")
while true {
skipnl() if is_op("}") { break }
skipnl(); if is_op("}") { break }
let arm = node(S_MARM)
while true { push(arm.kids, expr()) if is_op(",") { pi = pi + 1 skipnl() continue } break }
eat_op("=>") skipnl()
if is_op("{") { arm.a = block() } else { let b = node(N_BLOCK) push(b.kids, stmt()) arm.a = b }
while true { push(arm.kids, expr()); if is_op(",") { pi = pi + 1; skipnl(); continue }; break }
eat_op("=>"); skipnl()
if is_op("{") { arm.a = block() } else { let b = node(N_BLOCK); push(b.kids, stmt()); arm.a = b }
push(n.kids, arm)
}
eat_op("}") return n
eat_op("}"); return n
}
}
let e = expr()
if is_op("=") or is_op("+=") or is_op("-=") or is_op("*=") or is_op("/=") {
let n = node(S_ASSIGN) n.s = toks[pi].text pi = pi + 1 n.a = e n.b = expr() return n
let n = node(S_ASSIGN); n.s = toks[pi].text; pi = pi + 1; n.a = e; n.b = expr(); return n
}
let n = node(S_EXPR) n.a = e return n
let n = node(S_EXPR); n.a = e; return n
}
# ---- declarations ----------------------------------------------------------
fn parse_struct() -> Node {
pi = pi + 1 let n = node(N_STRUCT) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let f = node(N_FIELD) f.s = eat_id() eat_op(":") f.ty = ptype()
if is_op("=") { pi = pi + 1 f.a = expr() }
push(n.kids, f) if is_op(",") { pi = pi + 1 } }
eat_op("}") return n
pi = pi + 1; let n = node(N_STRUCT); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
if is_op("=") { pi = pi + 1; f.a = expr() }
push(n.kids, f); if is_op(",") { pi = pi + 1 } }
eat_op("}"); return n
}
fn parse_var() -> Node {
pi = pi + 1 let n = node(N_VAR) n.s = eat_id() eat_op(":") n.ty = ptype()
if is_op("=") { pi = pi + 1 n.a = expr() }
pi = pi + 1; let n = node(N_VAR); n.s = eat_id(); eat_op(":"); n.ty = ptype()
if is_op("=") { pi = pi + 1; n.a = expr() }
return n
}
fn parse_const() -> Node {
pi = pi + 1 let n = node(N_CONST) n.s = eat_id() eat_op(":") n.ty = ptype() eat_op("=") n.a = expr()
pi = pi + 1; let n = node(N_CONST); n.s = eat_id(); eat_op(":"); n.ty = ptype(); eat_op("="); n.a = expr()
return n
}
fn parse_fn() -> Node {
pi = pi + 1 let n = node(N_FN) n.s = eat_id() eat_op("(")
pi = pi + 1; let n = node(N_FN); n.s = eat_id(); eat_op("(")
while not is_op(")") {
let p = node(N_PARAM) p.s = eat_id() eat_op(":") p.ty = ptype() push(n.kids, p)
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
if is_op(",") { pi = pi + 1 }
}
eat_op(")")
n.ty = "void"
if is_op("->") { pi = pi + 1 n.ty = ptype() }
if is_op("->") { pi = pi + 1; n.ty = ptype() }
n.a = block()
return n
}
fn parse_main() -> Node { pi = pi + 1 let n = node(N_MAIN) n.a = block() return n }
fn parse_main() -> Node { pi = pi + 1; let n = node(N_MAIN); n.a = block(); return n }
# directory part of a path, including the trailing '/', or "" if none
fn dir_of(path: ptr) -> ptr {
let last = 0 - 1
let i = 0
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i } i = i + 1 }
while peek8(path, i) != 0 { if peek8(path, i) == 47 { last = i }; i = i + 1 }
if last < 0 { return "" }
return substr(path, 0, last + 1)
}
@ -252,30 +272,40 @@ var cur_dir: ptr
fn already_loaded(full: ptr) -> bool {
let i = 0
while i < len(loaded_paths) { if streq(loaded_paths[i], full) { return true } i = i + 1 }
while i < len(loaded_paths) { if streq(loaded_paths[i], full) { return true }; i = i + 1 }
return false
}
# parse one top-level declaration (or resolve an import) into `prog`
# parse one top-level declaration (or resolve an import) into `prog`.
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
fn parse_one_decl() -> void {
let is_export = false
while is_op("@") {
pi = pi + 1; let a = eat_id() # collect a leading @annotation
if streq(a, "export") { is_export = true }
if is_op("(") { let d = 0 # optional @anno(args) — skipped
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } }
skipnl()
}
if is_id("import") { pi = pi + 1
let t = toks[pi]
if t.kind != TK_STR { perr("expected \"path\" after import") }
let rel = t.text pi = pi + 1
let rel = t.text; pi = pi + 1
do_import(rel)
return
}
if is_id("struct") { push(prog, parse_struct()) return }
if is_id("component") { push(prog, parse_component()) return }
if is_id("archetype") { push(prog, parse_archetype()) return }
if is_id("system") or is_id("edge") { push(prog, parse_system()) return }
if is_id("ui") { push(prog, parse_ui()) return }
if is_id("var") { push(prog, parse_var()) return }
if is_id("const") { push(prog, parse_const()) return }
if is_id("export") { pi = pi + 1 let f = parse_fn() f.ival = 1 push(prog, f) return }
if is_id("fn") or is_id("pure") { if is_id("pure") { pi = pi + 1 } push(prog, parse_fn()) return }
if is_id("extern") { push(prog, parse_extern()) return }
if is_id("main") { push(prog, parse_main()) return }
if is_id("struct") { push(prog, parse_struct()); return }
if is_id("enum") { push(prog, parse_enum()); return }
if is_id("component") { push(prog, parse_component()); return }
if is_id("archetype") { push(prog, parse_archetype()); return }
if is_id("system") { push(prog, parse_system()); return }
if is_id("ui") { push(prog, parse_ui()); return }
if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return }
if is_id("fn") { let f = parse_fn(); if is_export { f.ival = 1 }; push(prog, f); return }
if is_id("extern") { push(prog, parse_extern()); return }
if is_id("main") { push(prog, parse_main()); return }
perr("expected declaration")
}
@ -286,13 +316,13 @@ fn do_import(rel: ptr) -> void {
push(loaded_paths, full)
let src = read_file(full)
if ptr_is_null(src) { perr(sconcat("cannot open import ", full)) }
let saved_toks = toks let saved_pi = pi let saved_dir = cur_dir
let saved_toks = toks; let saved_pi = pi; let saved_dir = cur_dir
cur_dir = dir_of(full)
lex(src) # resets the global token stream
pi = 0
skipnl()
while toks[pi].kind != TK_EOF { parse_one_decl() skipnl() }
toks = saved_toks pi = saved_pi cur_dir = saved_dir
while toks[pi].kind != TK_EOF { parse_one_decl(); skipnl() }
toks = saved_toks; pi = saved_pi; cur_dir = saved_dir
}
# a game (has systems/components) links the Ludic runtime; auto-splice it the
@ -311,8 +341,8 @@ fn parse_program() -> void {
skipnl()
g_game_name = "Ludic"
# imports may precede the game block
while is_id("import") { pi = pi + 1 let t = toks[pi] let rel = t.text pi = pi + 1 do_import(rel) skipnl() }
if is_id("game") or is_id("module") { pi = pi + 1 g_game_name = eat_id() skipnl() eat_op("{") }
while is_id("import") { pi = pi + 1; let t = toks[pi]; let rel = t.text; pi = pi + 1; do_import(rel); skipnl() }
if is_id("game") or is_id("module") { pi = pi + 1; g_game_name = eat_id(); skipnl(); eat_op("{") }
while true {
skipnl()
if toks[pi].kind == TK_EOF { break }

View file

@ -3,25 +3,24 @@
# game-construct parsing in compiler/front/parse.c.
fn parse_component() -> Node {
pi = pi + 1 let n = node(N_COMP) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let f = node(N_FIELD) f.s = eat_id() eat_op(":") f.ty = ptype()
if is_op("=") { pi = pi + 1 f.a = expr() }
push(n.kids, f) if is_op(",") { pi = pi + 1 } }
eat_op("}") return n
pi = pi + 1; let n = node(N_COMP); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
let f = node(N_FIELD); f.s = eat_id(); eat_op(":"); f.ty = ptype()
if is_op("=") { pi = pi + 1; f.a = expr() }
push(n.kids, f); if is_op(",") { pi = pi + 1 } }
eat_op("}"); return n
}
# skip an @annotation or a reads/writes/query/... clause we don't model yet
fn skip_clause() -> void {
if is_op("[") { let depth = 0
while true { if is_op("[") { depth = depth + 1 } if is_op("]") { depth = depth - 1 }
pi = pi + 1 if depth == 0 { break } }
while true { if is_op("[") { depth = depth + 1 }; if is_op("]") { depth = depth - 1 }
pi = pi + 1; if depth == 0 { break } }
}
}
fn parse_system() -> Node {
if is_id("edge") { pi = pi + 1 } # optional `edge` modifier before `system`
pi = pi + 1 let n = node(N_SYS) n.s = eat_id() n.ty = "Update"
pi = pi + 1; let n = node(N_SYS); n.s = eat_id(); n.ty = "Update"
# clauses: @anno, phase X, reads/writes/needs/uses [..], query (..) [..]
# A `query (vars) [terms] where c` clause desugars to a body wrapped in one
# `for (vars) in query [terms] where c { ... }` — the same S_QUERY node the
@ -30,15 +29,15 @@ fn parse_system() -> Node {
let qn = node(S_QUERY)
while true {
skipnl() # clauses may span several lines
if is_op("@") { pi = pi + 1 let a = eat_id() if is_op("(") { let d = 0 # @anno, one per turn so a
while true { if is_op("(") { d = d + 1 } if is_op(")") { d = d - 1 } pi = pi + 1 if d == 0 { break } } }
if is_op("@") { pi = pi + 1; let a = eat_id(); if is_op("(") { let d = 0 # @anno, one per turn so a
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } }
continue } # newline-separated @anno re-skips at the loop top
if is_id("phase") { pi = pi + 1 n.ty = eat_id() continue }
if is_id("reads") or is_id("writes") or is_id("needs") or is_id("uses") { pi = pi + 1 skip_clause() continue }
if is_id("phase") { pi = pi + 1; n.ty = eat_id(); continue }
if is_id("reads") or is_id("writes") { pi = pi + 1; skip_clause(); continue } # `needs`/`uses` synonyms dropped (Rule A cleanup)
if is_id("query") {
pi = pi + 1 has_q = true
pi = pi + 1; has_q = true
if is_op("(") { eat_op("(") # optional (vars); omitted when nothing binds
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(qn.kids, v) if is_op(",") { pi = pi + 1 } }
while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(qn.kids, v); if is_op(",") { pi = pi + 1 } }
eat_op(")") }
qn.c = parse_query_tail()
qn.b = qn.c.a # where-expr (or null)
@ -50,7 +49,7 @@ fn parse_system() -> Node {
let body = block()
if has_q {
qn.a = body
let wrap = node(N_BLOCK) push(wrap.kids, qn)
let wrap = node(N_BLOCK); push(wrap.kids, qn)
n.a = wrap
} else { n.a = body }
return n
@ -62,12 +61,12 @@ fn parse_query_tail() -> Node {
eat_op("[")
let q = node(N_BLOCK)
while not is_op("]") {
if is_op("{") { pi = pi + 1 let t = node(E_ID) t.s = eat_id() t.ival = 1 push(q.kids, t) eat_op("}") }
else { let t = node(E_ID) t.s = eat_id() t.ival = 0 push(q.kids, t) }
if is_op("{") { pi = pi + 1; let t = node(E_ID); t.s = eat_id(); t.ival = 1; push(q.kids, t); eat_op("}") }
else { let t = node(E_ID); t.s = eat_id(); t.ival = 0; push(q.kids, t) }
if is_op(",") { pi = pi + 1 }
}
eat_op("]")
if is_id("where") { pi = pi + 1 q.a = expr() }
if is_id("where") { pi = pi + 1; q.a = expr() }
return q
}
@ -75,7 +74,7 @@ fn parse_query_tail() -> Node {
fn parse_query_for() -> Node {
let n = node(S_QUERY)
eat_op("(")
while not is_op(")") { let v = node(E_ID) v.s = eat_id() push(n.kids, v) if is_op(",") { pi = pi + 1 } }
while not is_op(")") { let v = node(E_ID); v.s = eat_id(); push(n.kids, v); if is_op(",") { pi = pi + 1 } }
eat_op(")")
let inkw = eat_id() # 'in'
let qkw = eat_id() # 'query'
@ -86,59 +85,70 @@ fn parse_query_for() -> Node {
}
fn parse_spawn() -> Node {
pi = pi + 1 let n = node(S_SPAWN) n.s = eat_id() skipnl() eat_op("{")
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
while true {
skipnl() if is_op("}") { break }
let ci = node(E_FINIT) ci.s = eat_id() eat_op("=")
ci.a = record() # { field = val, ... }
skipnl(); if is_op("}") { break }
let ci = node(E_FINIT); ci.s = eat_id()
ci.a = record() # Comp { field: val, ... } — no `=` before the record
push(n.kids, ci)
if is_op(",") { pi = pi + 1 }
}
eat_op("}") return n
eat_op("}"); return n
}
# enum Name { A, B, C } — named int constants; a variant's value is its index.
# Accessed as `Name.A` (a compile-time int), so it names magic-int value spaces
# (state ids, menu selections, mode registers) without a runtime cost.
fn parse_enum() -> Node {
pi = pi + 1; let n = node(N_ENUM); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
let v = node(E_ID); v.s = eat_id(); push(n.kids, v)
if is_op(",") { pi = pi + 1 }; skipnl() }
eat_op("}"); return n
}
# archetype Name { CompA, CompB } — a named entity kind (bundle of components)
fn parse_archetype() -> Node {
pi = pi + 1 let n = node(N_ARCH) n.s = eat_id() skipnl() eat_op("{")
while true { skipnl() if is_op("}") { break }
let c = node(E_ID) c.s = eat_id() push(n.kids, c)
if is_op(",") { pi = pi + 1 } skipnl() }
eat_op("}") return n
pi = pi + 1; let n = node(N_ARCH); n.s = eat_id(); skipnl(); eat_op("{")
while true { skipnl(); if is_op("}") { break }
let c = node(E_ID); c.s = eat_id(); push(n.kids, c)
if is_op(",") { pi = pi + 1 }; skipnl() }
eat_op("}"); return n
}
# extern fn name(params) -> T = "symbol"
fn parse_extern() -> Node {
pi = pi + 1 # 'extern'
let fnkw = eat_id() # 'fn'
let n = node(N_EXTERN) n.s = eat_id() eat_op("(")
while not is_op(")") { let p = node(N_PARAM) p.s = eat_id() eat_op(":") p.ty = ptype() push(n.kids, p)
let n = node(N_EXTERN); n.s = eat_id(); eat_op("(")
while not is_op(")") { let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
if is_op(",") { pi = pi + 1 } }
eat_op(")")
n.ty = "void"
if is_op("->") { pi = pi + 1 n.ty = ptype() }
if is_op("->") { pi = pi + 1; n.ty = ptype() }
eat_op("=")
let t = toks[pi] # "symbol"
n.a = node(E_STR) n.a.s = t.text pi = pi + 1
n.a = node(E_STR); n.a.s = t.text; pi = pi + 1
return n
}
# ui Name { widget-tree } — parsed into a widget node tree (emitted later)
fn parse_widget() -> Node {
let w = node(N_UI) w.s = eat_id() # widget type name
let w = node(N_UI); w.s = eat_id() # widget type name
w.b = node(N_BLOCK) # b.kids = props (E_FINIT)
while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and streq(toks[pi + 1].text, "=") {
let pr = node(E_FINIT) pr.s = eat_id() eat_op("=") pr.a = expr() push(w.b.kids, pr)
while toks[pi].kind == TK_ID and toks[pi + 1].kind == TK_OP and streq(toks[pi + 1].text, ":") {
let pr = node(E_FINIT); pr.s = eat_id(); eat_op(":"); pr.a = expr(); push(w.b.kids, pr) # widget prop `key: value` (Rule A)
}
skipnl()
if is_op("{") { pi = pi + 1
while true { skipnl() if is_op("}") { break } push(w.kids, parse_widget()) }
while true { skipnl(); if is_op("}") { break }; push(w.kids, parse_widget()) }
eat_op("}") }
return w
}
fn parse_ui() -> Node {
pi = pi + 1 let n = node(N_UI) n.s = eat_id() n.ival = 1 # ival=1 marks the top ui block
skipnl() eat_op("{") skipnl()
pi = pi + 1; let n = node(N_UI); n.s = eat_id(); n.ival = 1 # ival=1 marks the top ui block
skipnl(); eat_op("{"); skipnl()
n.a = parse_widget()
skipnl() eat_op("}")
skipnl(); eat_op("}")
return n
}

View file

@ -24,7 +24,7 @@ fn slen(s: ptr) -> int {
fn substr(src: ptr, start: int, n: int) -> ptr {
let b = mem_alloc(n + 1)
let i = 0
while i < n { poke8(b, i, peek8(src, start + i)) i = i + 1 }
while i < n { poke8(b, i, peek8(src, start + i)); i = i + 1 }
poke8(b, n, 0)
return b
}
@ -39,20 +39,20 @@ fn char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
# integer -> fresh decimal string
fn itoa(v: int) -> ptr {
if v == 0 { let z = mem_alloc(2) poke8(z, 0, 48) poke8(z, 1, 0) return z }
if v == 0 { let z = mem_alloc(2); poke8(z, 0, 48); poke8(z, 1, 0); return z }
let neg = false
let x = v
if x < 0 { neg = true x = 0 - x }
if x < 0 { neg = true; x = 0 - x }
let tmp = mem_alloc(16)
let n = 0
while x > 0 { poke8(tmp, n, 48 + x % 10) x = x / 10 n = n + 1 }
while x > 0 { poke8(tmp, n, 48 + x % 10); x = x / 10; n = n + 1 }
let total = n
if neg { total = total + 1 }
let out = mem_alloc(total + 1)
let k = 0
if neg { poke8(out, 0, 45) k = 1 }
if neg { poke8(out, 0, 45); k = 1 }
let i = 0
while i < n { poke8(out, k + i, peek8(tmp, n - 1 - i)) i = i + 1 }
while i < n { poke8(out, k + i, peek8(tmp, n - 1 - i)); i = i + 1 }
poke8(out, total, 0)
return out
}

View file

@ -1,12 +1,12 @@
game T {
fn fib(n: int) -> int { if n < 2 { return n } return fib(n-1) + fib(n-2) }
fn fib(n: int) -> int { if n < 2 { return n }; return fib(n-1) + fib(n-2) }
main {
print_int(fib(10)) # 55
let s = 0
for i in 0 .. 10 { if i == 5 { break } if i % 2 == 0 { continue } s = s + i }
for i in 0 .. 10 { if i == 5 { break }; if i % 2 == 0 { continue }; s = s + i }
print_int(s) # 1+3 = 4
let i = 0 let t = 0
while true { i = i + 1 if i > 5 { break } t = t + i }
let i = 0; let t = 0
while true { i = i + 1; if i > 5 { break }; t = t + i }
print_int(t) # 15
if false and (1/0 == 0) { print_int(999) } else { print_int(1) } # short-circuit: no div by zero
}

View file

@ -1,6 +1,6 @@
game T {
fn classify(c: int) -> int {
match c { 65, 66 => { return 1 } 67 => { return 2 } _ => { return 9 } }
match c { 65, 66 => { return 1 }; 67 => { return 2 }; _ => { return 9 } }
return 0
}
main {

View file

@ -142,6 +142,35 @@ kpath = ["repository", "keyword", "patterns"]
compare("phases", h_phases, grammar_alternation(grammar, kpath, "FixedUpdate"), "ludic.tmLanguage.json")
compare("primitive types", h_types, grammar_alternation(grammar, kpath, "fixed"), "ludic.tmLanguage.json")
# --- against the actual self-hosted parser ---------------------------------
# The compiler moved to Ludic, so the old C cross-check (above) is dark. This
# replaces it: a declaration or clause keyword the highlighter colours must be
# one the parser actually dispatches on, or nobody notices when a keyword is
# highlighted but silently unparsed (exactly what happened to `scene`, `edge`,
# `needs`, …). RESERVED is the escape hatch for documented, not-yet-implemented
# keywords — and it is checked too, so a reserved word that gets implemented
# must be promoted out of it.
def parser_keywords():
kws = set()
for name in ("parse.ludic", "parse_game.ludic"):
text = open(os.path.join(ROOT, "selfhost", name), encoding="utf-8").read()
kws |= set(re.findall(r'is_id\("([a-z]+)"\)', text))
kws |= set(re.findall(r'streq\(t\.text,\s*"([a-z]+)"\)', text))
return kws
pkw = parser_keywords()
h_reserved = c_string_list(syntax_h, "LUDIC_KW_RESERVED")
if not pkw:
problems.append("could not extract any keywords from selfhost/parse*.ludic")
else:
for label, kws in (("declaration keywords", h_decl), ("clause keywords", h_clause)):
unparsed = (kws - pkw) - h_reserved
if unparsed:
problems.append(f"ludic_syntax.h lists {label} the selfhost parser never dispatches on: {', '.join(sorted(unparsed))}")
promoted = h_reserved & pkw
if promoted:
problems.append(f"LUDIC_KW_RESERVED lists keywords the parser now accepts — promote them into the highlighted vocabulary: {', '.join(sorted(promoted))}")
if problems:
print("vocabulary drift:", file=sys.stderr)
for p in problems:

View file

@ -11,12 +11,11 @@
;;; Code:
(defconst ludic--declaration-keywords
'("game" "module" "import" "component" "archetype" "ui" "scene" "layer"
"const" "var" "export" "fn" "pure" "extern" "system" "edge" "start" "on"))
'("game" "module" "import" "component" "struct" "archetype" "enum" "ui"
"const" "var" "fn" "extern" "system" "main"))
(defconst ludic--clause-keywords
'("phase" "query" "reads" "writes" "needs" "uses"
"requires" "ensures" "invariant" "effects"))
'("phase" "query" "reads" "writes"))
(defconst ludic--statement-keywords
'("let" "return" "if" "else" "when" "while" "for" "in" "spawn" "despawn"

View file

@ -41,12 +41,11 @@ object LudicTokens {
*/
object LudicVocabulary {
val DECL = setOf(
"game", "module", "import", "component", "struct", "archetype", "ui", "scene", "layer",
"const", "var", "export", "fn", "pure", "extern", "system", "edge", "start", "on"
"game", "module", "import", "component", "struct", "archetype", "enum", "ui",
"const", "var", "fn", "extern", "system", "main"
)
val CLAUSE = setOf(
"phase", "query", "reads", "writes", "needs", "uses",
"requires", "ensures", "invariant", "effects"
"phase", "query", "reads", "writes"
)
val STMT = setOf(
"let", "return", "if", "else", "when", "while", "for", "in", "spawn", "despawn",

View file

@ -75,35 +75,25 @@
}
},
{
"match": "\\b(scene)\\s+([A-Za-z_][A-Za-z0-9_]*)(\\s+start)?",
"captures": {
"1": { "name": "storage.type.scene.ludic" },
"2": { "name": "entity.name.type.scene.ludic" },
"3": { "name": "keyword.other.start.ludic" }
}
},
{
"match": "\\b(layer|ui)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(ui)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "entity.name.type.ludic" }
}
},
{
"match": "\\b(edge\\s+)?(system)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(system)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.modifier.edge.ludic" },
"2": { "name": "storage.type.system.ludic" },
"3": { "name": "entity.name.function.system.ludic" }
"1": { "name": "storage.type.system.ludic" },
"2": { "name": "entity.name.function.system.ludic" }
}
},
{
"match": "\\b(export\\s+|pure\\s+)?(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.modifier.ludic" },
"2": { "name": "storage.modifier.extern.ludic" },
"3": { "name": "storage.type.function.ludic" },
"4": { "name": "entity.name.function.ludic" }
"1": { "name": "storage.modifier.extern.ludic" },
"2": { "name": "storage.type.function.ludic" },
"3": { "name": "entity.name.function.ludic" }
}
},
{
@ -173,9 +163,9 @@
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|where|break|continue|new)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes|needs|uses|requires|ensures|invariant|effects|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|export|pure|extern|start|edge)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(game|module|component|archetype|ui|scene|layer|const|var|let|fn|system|state)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(game|module|component|struct|archetype|enum|ui|const|var|let|fn|system|main|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -75,35 +75,25 @@
}
},
{
"match": "\\b(scene)\\s+([A-Za-z_][A-Za-z0-9_]*)(\\s+start)?",
"captures": {
"1": { "name": "storage.type.scene.ludic" },
"2": { "name": "entity.name.type.scene.ludic" },
"3": { "name": "keyword.other.start.ludic" }
}
},
{
"match": "\\b(layer|ui)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(ui)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "entity.name.type.ludic" }
}
},
{
"match": "\\b(edge\\s+)?(system)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(system)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.modifier.edge.ludic" },
"2": { "name": "storage.type.system.ludic" },
"3": { "name": "entity.name.function.system.ludic" }
"1": { "name": "storage.type.system.ludic" },
"2": { "name": "entity.name.function.system.ludic" }
}
},
{
"match": "\\b(export\\s+|pure\\s+)?(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.modifier.ludic" },
"2": { "name": "storage.modifier.extern.ludic" },
"3": { "name": "storage.type.function.ludic" },
"4": { "name": "entity.name.function.ludic" }
"1": { "name": "storage.modifier.extern.ludic" },
"2": { "name": "storage.type.function.ludic" },
"3": { "name": "entity.name.function.ludic" }
}
},
{
@ -173,9 +163,9 @@
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|where|break|continue|new)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes|needs|uses|requires|ensures|invariant|effects|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|export|pure|extern|start|edge)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(game|module|component|archetype|ui|scene|layer|const|var|let|fn|system|state)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(game|module|component|struct|archetype|enum|ui|const|var|let|fn|system|main|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -52,12 +52,18 @@ typedef struct {
* mirror the compiler's parser: anything parse_decl() dispatches on is a
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = {
"game","module","import","component","struct","archetype","ui","scene","layer",
"const","var","export","fn","pure","extern","system","edge","start","on", 0
"game","module","import","component","struct","archetype","enum","ui",
"const","var","fn","extern","system","main", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","reads","writes","needs","uses",
"requires","ensures","invariant","effects", 0
"phase","query","reads","writes", 0
};
/* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = {
"scene","layer","on","start", 0
};
static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","when","while","for","in","spawn","despawn",

View file

@ -0,0 +1,327 @@
/* ============================================================================
* ludic_syntax.h — the lexical layer shared by every Ludic editor tool.
*
* One lexer, one vocabulary. `ludic-fmt`, `ludic-lsp` and the generated
* TextMate grammar all read their keyword/builtin/type tables from here, so an
* addition to the language shows up in every editor at once instead of drifting
* across six hand-maintained copies.
*
* Unlike the compiler's lexer (compiler/ludicc.c) this one is written for
* editors: it keeps comments, keeps newlines, records byte spans for every
* token, and never exits on bad input — a stray character becomes an LT_ERR
* token and lexing continues, because a file being typed into is malformed most
* of the time.
* ==========================================================================*/
#ifndef LUDIC_SYNTAX_H
#define LUDIC_SYNTAX_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* ---------- token kinds ---------------------------------------------------*/
enum {
LT_EOF, LT_NL, LT_COMMENT,
LT_ID, /* a plain identifier */
LT_KW, /* a reserved word (see KEYWORDS) */
LT_TYPE, /* a built-in type name: int fixed bool entity str ptr void */
LT_PHASE, /* Start Input FixedUpdate Update LateUpdate Render */
LT_BOOL, /* true false */
LT_INT, LT_FLOAT, LT_STR, LT_CHAR,
LT_ANNO, /* @deterministic — the '@' and the name as one token */
LT_OP,
LT_ERR
};
typedef struct {
int kind;
int start, end; /* byte offsets into the source buffer */
int line; /* 0-based */
int bad; /* set on an unterminated string / stray character */
} LTok;
typedef struct {
LTok* v; int n, cap;
const char* src;
int* linestart; int nline, caplin;
} LLex;
/* ---------- the vocabulary ------------------------------------------------*/
/* Reserved words, grouped so editors can colour them differently. The groups
* mirror the compiler's parser: anything parse_decl() dispatches on is a
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = {
"game","module","import","component","struct","archetype","enum","ui",
"const","var","fn","extern","system","main", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","reads","writes", 0
};
/* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = {
"scene","layer","on","start", 0
};
static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","when","while","for","in","spawn","despawn",
"match","machine","state","become","enter","where","and","or","not",
"break","continue","new", 0
};
static const char* LUDIC_TYPES[] = {
"int","fixed","bool","entity","str","ptr","void", 0
};
static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render", 0
};
static const char* LUDIC_WIDGETS[] = {
"panel","col","row","label","button","image","spacer", 0
};
static const char* LUDIC_WIDGET_PROPS[] = {
"id","text","skin","image","align","w","h","pad","gap","size","font",
"inset","grow","bg","fg","border","focus","x","y", 0
};
/* A builtin is a name the runtime provides (`clear(c)` -> rt_clear). An
* intrinsic is a name the compiler lowers directly to libc/OS. Editors treat
* both as "standard library", but the signatures differ, so they stay apart. */
typedef struct { const char* name; const char* sig; const char* doc; } LBuiltin;
static const LBuiltin LUDIC_BUILTINS[] = {
{"min","min(a: int, b: int) -> int","Smaller of two integers."},
{"max","max(a: int, b: int) -> int","Larger of two integers."},
{"abs","abs(a: int) -> int","Absolute value."},
{"clamp","clamp(v: int, lo: int, hi: int) -> int","Constrain v to [lo, hi]."},
{"seed","seed(i: int)","Seed the deterministic RNG."},
{"rng_range","rng_range(lo: int, hi: int) -> int","Deterministic integer in [lo, hi]."},
{"rng_chance","rng_chance(pct: int) -> bool","True pct% of the time, deterministically."},
{"fx","fx(i: int) -> fixed","Widen an int to Q16.16 fixed-point."},
{"flr","flr(f: fixed) -> int","Truncate a fixed-point value toward zero."},
{"map_size","map_size(w: int, h: int)","Set the tilemap dimensions."},
{"map_row","map_row(y: int, row: str)","Fill one tilemap row from a string."},
{"tile","tile(x: int, y: int) -> int","Tile code at a map cell."},
{"clear","clear(color: int)","Clear the framebuffer to a 0xRRGGBB colour."},
{"present","present()","Push the framebuffer to the window (or out.ppm when headless)."},
{"fill_rect","fill_rect(x: int, y: int, w: int, h: int, color: int)","Filled rectangle."},
{"frame_rect","frame_rect(x: int, y: int, w: int, h: int, color: int)","One-pixel rectangle outline."},
{"put_px","put_px(x: int, y: int, color: int)","Write a single pixel."},
{"text","text(x: int, y: int, s: str, color: int, scale: int)","Draw text with the built-in 5x7 bitmap font."},
{"text_int","text_int(x: int, y: int, n: int, color: int, scale: int)","Draw an integer with the 5x7 bitmap font."},
{"font_load","font_load(path: str) -> int","Load a TrueType .ttf/.ttc; returns a font id."},
{"text_ttf","text_ttf(font: int, x: int, y: int, utf8: str, color: int, px: int)","Draw UTF-8 text with a TrueType font."},
{"text_w","text_w(font: int, utf8: str, px: int) -> int","Advance width of the string, in pixels."},
{"text_h","text_h(font: int, px: int) -> int","Line height of the font, in pixels."},
{"image_load","image_load(path: str) -> int","Decode a PNG into an image id."},
{"draw_image","draw_image(id: int, x: int, y: int)","Blit an image at its natural size."},
{"draw_image_scaled","draw_image_scaled(id: int, x: int, y: int, w: int, h: int)","Blit an image stretched to w x h."},
{"draw_9slice","draw_9slice(id: int, x: int, y: int, w: int, h: int, inset: int)","Nine-slice an image across a w x h box."},
{"load_png","load_png(path: str) -> int","Decode a PNG as a 16x16 sprite sheet; returns the first sprite id."},
{"load_sprites","load_sprites(path: str)","Load the sprite sheet used by draw_sprite."},
{"draw_sprite","draw_sprite(id: int, x: int, y: int)","Blit a sprite."},
{"draw_sprite_scaled","draw_sprite_scaled(id: int, x: int, y: int, scale: int)","Blit a sprite at an integer scale."},
{"ui_build","ui_build()","Construct every declared `ui` tree (loads skins and images)."},
{"ui_open","ui_open(id: int)","Make a ui tree active and focus its first button."},
{"ui_tick","ui_tick(key: int)","Feed a key to the UI: w/s move focus, space/enter activate."},
{"ui_render","ui_render()","Lay out and draw the active ui tree."},
{"ui_clicked","ui_clicked(id: int) -> bool","True on the frame a widget was activated."},
{"ui_set_text","ui_set_text(id: int, s: str)","Replace a widget's text."},
{"ui_set_int","ui_set_int(id: int, n: int)","Replace a widget's text with a number."},
{"ui_focus","ui_focus(id: int)","Move keyboard focus to a widget."},
{"ui_focused","ui_focused() -> int","Id of the focused widget."},
{"ui_visible","ui_visible(id: int, on: bool)","Show or hide a widget subtree."},
{"key","key() -> int","Key code pressed this frame, 0 if none."},
{"reg","reg(i: int) -> int","Read one of the 64 integer resources shared by systems."},
{"setreg","setreg(i: int, v: int)","Write one of the 64 integer resources."},
{"self","self() -> entity","The entity of the innermost query loop."},
{"save","save()","Write a binary snapshot of the whole ECS world."},
{"load","load() -> bool","Restore the snapshot; false if there is none."},
{"status","status(s: str)","Set the one-line status message."},
{"print_int","print_int(i: int)","Print an integer to stdout."},
{"quit","quit()","Stop the frame loop and exit."},
{0,0,0}
};
static const LBuiltin LUDIC_INTRINSICS[] = {
{"mem_alloc","mem_alloc(n: int) -> ptr","Allocate n bytes (malloc)."},
{"mem_realloc","mem_realloc(p: ptr, n: int) -> ptr","Resize a block to n bytes, preserving its contents (realloc)."},
{"os_argc","os_argc() -> int","Number of command-line arguments, argv[0] included."},
{"os_arg","os_arg(i: int) -> str","The i-th command-line argument."},
{"file_stderr","file_stderr() -> ptr","The standard error stream, for file_write."},
{"mem_free","mem_free(p: ptr)","Release an allocation."},
{"mem_copy","mem_copy(dst: ptr, src: ptr, n: int)","memcpy."},
{"mem_set","mem_set(p: ptr, byte: int, n: int)","memset."},
{"peek8","peek8(p: ptr, off: int) -> int","Read one byte."},
{"poke8","poke8(p: ptr, off: int, v: int)","Write one byte."},
{"peek32","peek32(p: ptr, off: int) -> int","Read a 32-bit word."},
{"poke32","poke32(p: ptr, off: int, v: int)","Write a 32-bit word."},
{"peekp","peekp(p: ptr, off: int) -> ptr","Read a pointer-sized word."},
{"pokep","pokep(p: ptr, off: int, v: ptr)","Write a pointer-sized word."},
{"peekf","peekf(p: ptr, off: int) -> fixed","Read a fixed-point word."},
{"pokef","pokef(p: ptr, off: int, v: fixed)","Write a fixed-point word."},
{"ptr_add","ptr_add(p: ptr, off: int) -> ptr","Offset a pointer by bytes."},
{"ptr_null","ptr_null() -> ptr","The null pointer."},
{"ptr_is_null","ptr_is_null(p: ptr) -> bool","Null test."},
{"as_fixed","as_fixed(i: int) -> fixed","Reinterpret an int as fixed (no conversion)."},
{"as_int","as_int(f: fixed) -> int","Reinterpret a fixed as int (no conversion)."},
{"file_open","file_open(path: str, mode: str) -> ptr","fopen."},
{"file_read","file_read(f: ptr, buf: ptr, n: int) -> int","fread."},
{"file_write","file_write(f: ptr, buf: ptr, n: int) -> int","fwrite."},
{"file_seek","file_seek(f: ptr, off: int, whence: int) -> int","fseek."},
{"file_tell","file_tell(f: ptr) -> int","ftell."},
{"file_close","file_close(f: ptr)","fclose."},
{"read_byte","read_byte() -> int","Read one byte from stdin, -1 at EOF."},
{"write_byte","write_byte(b: int)","Write one byte to stdout."},
{"print_str","print_str(s: str)","Write a string to stdout."},
{"str_len","str_len(s: str) -> int","Length of a string in bytes."},
{"shl","shl(v: int, n: int) -> int","Shift left."},
{"shr","shr(v: int, n: int) -> int","Logical shift right."},
{"band","band(a: int, b: int) -> int","Bitwise and."},
{"bor","bor(a: int, b: int) -> int","Bitwise or."},
{"bxor","bxor(a: int, b: int) -> int","Bitwise xor."},
{"bnot","bnot(a: int) -> int","Bitwise not."},
{"os_exit","os_exit(code: int)","Terminate the process."},
{"os_time","os_time() -> int","Seconds since the epoch."},
{"is_windowed","is_windowed() -> bool","True when the build has a window."},
{"game_title","game_title() -> str","The name from the `game` declaration."},
{"win_open","win_open(title: str, w: int, h: int, scale: int)","Open the platform window."},
{"win_poll","win_poll() -> int","Pump the event queue; returns a key code."},
{"win_present","win_present(px: ptr, w: int, h: int)","Blit a framebuffer to the window."},
{"win_running","win_running() -> bool","False once the window has been closed."},
{"win_close","win_close()","Close the platform window."},
{0,0,0}
};
static int lud_in(const char** set, const char* s){
for (int i = 0; set[i]; i++) if (!strcmp(set[i], s)) return 1;
return 0;
}
static const LBuiltin* lud_lookup(const LBuiltin* t, const char* s){
for (int i = 0; t[i].name; i++) if (!strcmp(t[i].name, s)) return &t[i];
return 0;
}
static int lud_is_keyword(const char* s){
return lud_in(LUDIC_KW_DECL, s) || lud_in(LUDIC_KW_CLAUSE, s) || lud_in(LUDIC_KW_STMT, s);
}
/* ---------- lexing --------------------------------------------------------*/
static void ltok_push(LLex* L, int kind, int start, int end, int line, int bad){
if (L->n >= L->cap){ L->cap = L->cap ? L->cap * 2 : 512; L->v = realloc(L->v, L->cap * sizeof(LTok)); }
L->v[L->n++] = (LTok){ kind, start, end, line, bad };
}
static void lline_push(LLex* L, int off){
if (L->nline >= L->caplin){ L->caplin = L->caplin ? L->caplin * 2 : 256; L->linestart = realloc(L->linestart, L->caplin * sizeof(int)); }
L->linestart[L->nline++] = off;
}
static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","&&","||","..","=>", 0 };
/* Lex the whole buffer. Comments and newlines are kept — the formatter needs
* both, and a highlighter needs the comments. */
static void lud_lex(LLex* L, const char* src){
memset(L, 0, sizeof(*L));
L->src = src;
lline_push(L, 0);
int i = 0, line = 0;
while (src[i]){
char c = src[i];
if (c == '\n'){ ltok_push(L, LT_NL, i, i + 1, line, 0); i++; line++; lline_push(L, i); continue; }
if (c == ' ' || c == '\t' || c == '\r'){ i++; continue; }
if (c == '#'){ int s = i; while (src[i] && src[i] != '\n') i++; ltok_push(L, LT_COMMENT, s, i, line, 0); continue; }
if (c == '"'){
int s = i; i++; int bad = 0;
while (src[i] && src[i] != '"' && src[i] != '\n'){ if (src[i] == '\\' && src[i+1]) i += 2; else i++; }
if (src[i] == '"') i++; else bad = 1;
ltok_push(L, LT_STR, s, i, line, bad); continue;
}
if (c == '\''){
int s = i; i++; int bad = 0;
if (src[i] == '\\' && src[i+1]) i += 2; else if (src[i] && src[i] != '\n') i++;
if (src[i] == '\'') i++; else bad = 1;
ltok_push(L, LT_CHAR, s, i, line, bad); continue;
}
if (isdigit((unsigned char)c)){
int s = i;
if (c == '0' && (src[i+1] == 'x' || src[i+1] == 'X')){
i += 2; while (isxdigit((unsigned char)src[i])) i++;
ltok_push(L, LT_INT, s, i, line, 0); continue;
}
while (isdigit((unsigned char)src[i])) i++;
if (src[i] == '.' && isdigit((unsigned char)src[i+1])){
i++; while (isdigit((unsigned char)src[i])) i++;
ltok_push(L, LT_FLOAT, s, i, line, 0); continue;
}
ltok_push(L, LT_INT, s, i, line, 0); continue;
}
/* @name is one token: an annotation reads as a unit, and the formatter
* must never put a space between the sigil and the name. */
if (c == '@' && (isalpha((unsigned char)src[i+1]) || src[i+1] == '_')){
int s = i; i++; while (isalnum((unsigned char)src[i]) || src[i] == '_') i++;
ltok_push(L, LT_ANNO, s, i, line, 0); continue;
}
if (isalpha((unsigned char)c) || c == '_'){
int s = i; while (isalnum((unsigned char)src[i]) || src[i] == '_') i++;
int len = i - s; char w[128];
if (len < (int)sizeof(w)){ memcpy(w, src + s, len); w[len] = 0; } else { w[0] = 0; }
int k = LT_ID;
if (!strcmp(w, "true") || !strcmp(w, "false")) k = LT_BOOL;
else if (lud_in(LUDIC_TYPES, w)) k = LT_TYPE;
else if (lud_in(LUDIC_PHASES, w)) k = LT_PHASE;
else if (lud_is_keyword(w)) k = LT_KW;
ltok_push(L, k, s, i, line, 0); continue;
}
{
int matched = 0;
for (int k = 0; LUDIC_OPS2[k]; k++)
if (src[i] == LUDIC_OPS2[k][0] && src[i+1] == LUDIC_OPS2[k][1]){
ltok_push(L, LT_OP, i, i + 2, line, 0); i += 2; matched = 1; break;
}
if (matched) continue;
}
if (c == ';'){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
if (strchr("+-*/%<>=(){}[],:.!@", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
/* Anything else is an error token — but a whole UTF-8 character's worth
* at a time. Splitting a multi-byte character into one token per byte
* would let a consumer that re-emits tokens (the formatter) put spaces
* inside it and corrupt the file. */
{
unsigned char u = (unsigned char)c;
int len = u < 0x80 ? 1 : u < 0xE0 ? 2 : u < 0xF0 ? 3 : 4;
for (int k = 1; k < len; k++) if (!src[i + k] || ((unsigned char)src[i + k] & 0xC0) != 0x80){ len = k; break; }
ltok_push(L, LT_ERR, i, i + len, line, 1); i += len;
}
}
ltok_push(L, LT_EOF, i, i, line, 0);
}
static void lud_lex_free(LLex* L){ free(L->v); free(L->linestart); memset(L, 0, sizeof(*L)); }
/* ---------- helpers over the token stream --------------------------------*/
static int ltok_len(const LTok* t){ return t->end - t->start; }
static int ltok_is(const LLex* L, int i, const char* s){
if (i < 0 || i >= L->n) return 0;
const LTok* t = &L->v[i]; int n = ltok_len(t);
return (int)strlen(s) == n && !strncmp(L->src + t->start, s, n);
}
/* Copy a token's text into a caller buffer; returns buf. */
static char* ltok_text(const LLex* L, int i, char* buf, int cap){
const LTok* t = &L->v[i]; int n = ltok_len(t);
if (n >= cap) n = cap - 1;
memcpy(buf, L->src + t->start, n); buf[n] = 0; return buf;
}
/* Index of the next token that is not a newline or comment, or -1. */
static int ltok_next_sig(const LLex* L, int i){
for (int j = i + 1; j < L->n; j++){
int k = L->v[j].kind;
if (k != LT_NL && k != LT_COMMENT) return j;
}
return -1;
}
static int ltok_prev_sig(const LLex* L, int i){
for (int j = i - 1; j >= 0; j--){
int k = L->v[j].kind;
if (k != LT_NL && k != LT_COMMENT) return j;
}
return -1;
}
#endif /* LUDIC_SYNTAX_H */

View file

@ -0,0 +1,59 @@
/* migrate_records.c — one-time migration for Rule A (named fields use ':').
* Records appear ONLY inside `spawn` blocks, so we track spawn context and, for
* every single '=' inside one:
* - '=' followed by '{' is a component init `Comp = { … }` -> delete it (`Comp { … }`)
* - otherwise is a field binding `field = value` -> rewrite to ':'
* A '=' at a real boundary is a pure re-spelling; the driver's IR-identity check
* (migrated program compiles to the same IR) is the proof.
* usage: migrate_records <file> -> migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: migrate_records <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
char* rep = calloc(L.n, 1); /* '=' -> ':' */
char* del = calloc(L.n, 1); /* drop the '=' (component init) */
int sp_stack[1024]; int bp = 0; int spawn_active = 0; int pendingSpawn = 0;
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind; if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
if (k == LT_KW && ltok_is(&L, i, "spawn")) pendingSpawn = 1;
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
char c0 = L.src[L.v[i].start];
if (op1 && c0 == '{'){
if (bp < 1024){ sp_stack[bp] = (pendingSpawn || spawn_active > 0) ? 1 : 0; if (sp_stack[bp]) spawn_active++; bp++; }
pendingSpawn = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (sp_stack[bp] && spawn_active > 0) spawn_active--; }
} else if (op1 && c0 == '='){
if (spawn_active > 0){
int nx = ltok_next_sig(&L, i);
if (nx >= 0 && L.v[nx].kind == LT_OP && ltok_len(&L.v[nx]) == 1 && L.src[L.v[nx].start] == '{') del[i] = 1;
else rep[i] = 1;
}
}
}
/* map flags onto byte positions so the rebuild can peek one char ahead */
char* repb = calloc(n + 1, 1); char* delb = calloc(n + 1, 1);
for (int i = 0; i < L.n; i++){ if (rep[i]) repb[L.v[i].start] = 1; if (del[i]) delb[L.v[i].start] = 1; }
for (int b = 0; b < n; ){
if (repb[b]){ putchar(':'); b++; continue; } /* '=' -> ':' */
if (delb[b]){ b++; if (b < n && src[b] == ' ') b++; continue; } /* drop '=' and one space */
if (src[b] == ' ' && b + 1 < n && repb[b + 1]){ b++; continue; } /* drop the space before ':' -> `field: v` */
putchar(src[b]); b++;
}
free(repb); free(delb);
lud_lex_free(&L); free(rep); free(del); free(src);
return 0;
}

View file

@ -0,0 +1,119 @@
/* sepfix.c — one-time migration for Rule B: insert `;` at statement boundaries
* that are currently spelled with only whitespace. Reuses the toolchain lexer.
*
* A boundary is inserted between two significant, same-line tokens prev,cur when
* - we are at paren/bracket depth 0 (not inside a call's args or an index), AND
* - we are NOT inside a `ui` block (widget props are `k=v`, space-separated,
* and the parser does not skip newlines between them), AND
* - prev can END an operand/statement, AND cur can START a statement.
* Inserting `;` (which the lexer maps to a newline token) at a REAL boundary is
* a semantic no-op under today's permissive parser — the IR-identity check in
* the driver is the proof. Wrong insertions change the IR and are rejected.
*
* usage: sepfix <in.ludic> -> writes migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
static const char* STMT_START[] = {
"let","return","if","when","while","for","spawn","despawn",
"match","machine","become","enter","break","continue", 0
};
static int ender(const LLex* L, int i){
const LTok* t = &L->v[i];
switch (t->kind){
case LT_ID: case LT_INT: case LT_FLOAT: case LT_STR: case LT_CHAR:
case LT_BOOL: case LT_TYPE: case LT_PHASE: return 1;
case LT_OP: {
if (ltok_len(t) != 1) return 0;
char c = L->src[t->start];
if (c == ']'){
/* `]` closing an empty `[]` is a slice-TYPE marker (`[]Node`),
* not an operand end — the type name that follows continues it. */
int p = ltok_prev_sig(L, i);
if (p >= 0 && L->v[p].kind == LT_OP && ltok_len(&L->v[p]) == 1 && L->src[L->v[p].start] == '[') return 0;
return 1;
}
return (c == ')' || c == '}'); }
case LT_KW: { char b[32]; ltok_text(L, i, b, sizeof b);
return !strcmp(b, "break") || !strcmp(b, "continue"); }
default: return 0;
}
}
static int starter(const LLex* L, int i){
const LTok* t = &L->v[i];
if (t->kind == LT_ID || t->kind == LT_INT || t->kind == LT_FLOAT ||
t->kind == LT_STR || t->kind == LT_CHAR || t->kind == LT_BOOL) return 1;
if (t->kind == LT_KW){ char b[32]; ltok_text(L, i, b, sizeof b); return lud_in(STMT_START, b); }
return 0; /* an operator never starts a statement here */
}
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: sepfix <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "sepfix: cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
/* insert[k] = 1 means: emit a ';' immediately after token k's bytes */
char* insert = calloc(L.n, 1);
int paren = 0; /* unclosed ( or [ */
/* ui suppression: brace depth stack, marking which open braces are ui/widget */
int ui_stack[512]; int bp = 0; int ui_active = 0;
int pendingUiOpen = 0; /* saw `ui` or a widget word; the next { is a ui brace */
int prev = -1; /* previous significant token index */
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind;
if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
char c0 = L.src[L.v[i].start];
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
/* track ui context by the `ui` KEYWORD only — it brackets the whole
* widget tree via its root brace, so every nested widget brace is
* already inside ui_active. (Do NOT key off widget words like col/row/
* image/label: those are also ordinary variable names, and matching them
* would wrongly suppress real statement boundaries.) */
if (k == LT_KW && ltok_is(&L, i, "ui")) pendingUiOpen = 1;
if (op1 && c0 == '{'){
if (bp < 512){ ui_stack[bp] = pendingUiOpen; bp++; if (pendingUiOpen) ui_active++; }
pendingUiOpen = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (ui_stack[bp]) { if (ui_active) ui_active--; } }
} else if (op1 && (c0 == '(' || c0 == '[')){ paren++; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
else if (op1 && (c0 == ')' || c0 == ']')){ if (paren > 0) paren--; if (getenv("SEPFIX_TRACE")) fprintf(stderr, " L%d '%c' -> paren=%d\n", L.v[i].line + 1, c0, paren); }
/* candidate boundary between prev and this token */
if (prev >= 0 && paren == 0 && !ui_active &&
L.v[prev].line == L.v[i].line &&
ender(&L, prev) && starter(&L, i)){
insert[prev] = 1;
}
prev = i;
}
if (getenv("SEPFIX_DEBUG")) fprintf(stderr, "END STATE %s: paren=%d ui_active=%d bp=%d\n", argv[1], paren, ui_active, bp);
/* rebuild: copy bytes, dropping in ';' right after any token flagged */
for (int i = 0, t = 0; i < n; ){
/* find if a token ends exactly at i and is flagged */
putchar(src[i]);
i++;
/* emit ';' after the last byte of a flagged token */
for (; t < L.n; t++){
if (L.v[t].end == i && insert[t]){ putchar(';'); break; }
if (L.v[t].end > i) break;
}
}
lud_lex_free(&L); free(insert); free(src);
return 0;
}

View file

@ -0,0 +1,52 @@
/* migrate_ui.c — one-time migration for Rule A in `ui` blocks: widget props go
* from `key=value` to `key: value`. Every '=' inside a `ui` block is a prop
* separator (values are expressions, never contain a top-level '='), so we track
* ui context (armed by the `ui` keyword's root brace) and rewrite each '=' to a
* ':' with canonical spacing (`id: Root`). The driver's IR-identity check proves
* it is a pure re-spelling (the parser builds the same E_FINIT nodes).
* usage: migrate_ui <file> -> migrated source to stdout
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ludic_syntax.h"
int main(int argc, char** argv){
if (argc < 2){ fprintf(stderr, "usage: migrate_ui <file>\n"); return 2; }
FILE* f = fopen(argv[1], "rb");
if (!f){ fprintf(stderr, "cannot open %s\n", argv[1]); return 2; }
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
char* src = malloc(n + 1); fread(src, 1, n, f); src[n] = 0; fclose(f);
LLex L; lud_lex(&L, src);
char* repb = calloc(n + 1, 1); /* byte position of a '=' to rewrite as ':' */
int ui_stack[1024]; int bp = 0; int ui_active = 0; int pendingUi = 0;
for (int i = 0; i < L.n; i++){
int k = L.v[i].kind; if (k == LT_EOF) break;
if (k == LT_NL || k == LT_COMMENT) continue;
if (k == LT_KW && ltok_is(&L, i, "ui")) pendingUi = 1;
int op1 = (k == LT_OP && ltok_len(&L.v[i]) == 1);
char c0 = L.src[L.v[i].start];
if (op1 && c0 == '{'){
if (bp < 1024){ ui_stack[bp] = (pendingUi || ui_active > 0) ? 1 : 0; if (ui_stack[bp]) ui_active++; bp++; }
pendingUi = 0;
} else if (op1 && c0 == '}'){
if (bp > 0){ bp--; if (ui_stack[bp] && ui_active > 0) ui_active--; }
} else if (op1 && c0 == '=' && ui_active > 0){
repb[L.v[i].start] = 1;
}
}
for (int b = 0; b < n; ){
if (repb[b]){ /* '=' -> ': ' with a single trailing space */
putchar(':'); b++;
if (b < n && src[b] != ' ' && src[b] != '\n') putchar(' ');
continue;
}
if (src[b] == ' ' && b + 1 < n && repb[b + 1]){ b++; continue; } /* drop the space before ':' */
putchar(src[b]); b++;
}
lud_lex_free(&L); free(repb); free(src);
return 0;
}