Phase 3d: declaration modifiers become @annotations (Rule A)
edge/pure/export prefix keywords retired; modifiers are now leading @annotations (@export, @edge, @pure, @deterministic). parse_one_decl collects a leading @anno run and @export sets the fn export flag; dead edge-dispatch removed from parse_system. Migrated examples/lib/combat.ludic; old prefixes now rejected. Behavior-identical: the export flag is parse-only in the self-hosted emitter (emits @fn_<name> for every fn, never reads the flag), so IR is byte-identical. Reseeded, fixpoint holds, test.sh 14/14. Docs updated. Phase 3 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b643037d8f
commit
9a558d6b58
9 changed files with 782 additions and 765 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ 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: **Phases 1 & 2 landed; Phase 3 in progress** (3a records/spawn done).
|
||||
> Remaining sub-phases and Phases 4→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–3 landed.** Phases 4→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`).
|
||||
>
|
||||
> 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 +32,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 +
|
||||
|
|
@ -266,10 +265,20 @@ unused anywhere in the corpus — removed from `parse_system`. *Not done:* actua
|
|||
*storing* reads/writes on the node for an analysis pass — that's analysis
|
||||
infrastructure, out of scope for a syntax pass.
|
||||
|
||||
**3d — modifiers `edge`/`pure`/`export` → `@`-annotations.** Not started. Low
|
||||
urgency: `edge`/`pure` are unused in the corpus and `export` appears once
|
||||
([examples/lib/combat.ludic](examples/lib/combat.ludic)); it needs `parse_one_decl`
|
||||
to accept leading `@annotations` on any declaration.
|
||||
**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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -272,8 +272,18 @@ fn already_loaded(full: ptr) -> bool {
|
|||
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") }
|
||||
|
|
@ -284,12 +294,11 @@ fn parse_one_decl() -> void {
|
|||
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("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("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("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")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ fn skip_clause() -> void {
|
|||
}
|
||||
|
||||
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"
|
||||
# clauses: @anno, phase X, reads/writes/needs/uses [..], query (..) [..]
|
||||
# A `query (vars) [terms] where c` clause desugars to a body wrapped in one
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue