Phase 3a: spawn/record initializers use ':' (Rule A)
Comp = { f = v } -> Comp { f: v }. record() requires ':' and parse_spawn()
drops the '=' before the record; '=' is now assignment/binding only. Records
appear only in games, so the compiler seed is unaffected.
Migration: tools/ludic-tools/migrate_records.c (spawn-context aware). Verified
every golden byte-identical, old '=' form rejected, reseeded, test.sh 14/14.
Docs updated (LANGUAGE.md, BOOTSTRAP.md R2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eac8f8f335
commit
39bd430ce8
12 changed files with 2499 additions and 2425 deletions
|
|
@ -921,7 +921,7 @@ game P { system B phase Start { let x = 1; x = x + 1; print_int(x); quit() } }
|
|||
```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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -390,7 +390,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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ 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** (see below). Phases 3→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 & 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`).
|
||||
>
|
||||
> Coordinated with the toolchain agent (CLI front-end / `ludicc`+`ludic`
|
||||
> binaries) via serialized reseeds of `selfhost/ludicc.seed.ll`; Phase 1 rode in
|
||||
|
|
@ -240,13 +240,29 @@ 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.
|
||||
|
||||
**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).
|
||||
|
||||
**3b — ui props → named-argument form.** `panel id=Root w=288` → `panel(id: Root,
|
||||
w: 288)`. Not started (touches `parse_widget` + `emit_ui`).
|
||||
|
||||
**3c — collapse `reads/writes/needs/uses` → `reads`/`writes`, stored on the node.**
|
||||
Not started.
|
||||
|
||||
**3d — modifiers `edge`/`pure`/`export` → `@`-annotations, parsed into a list.**
|
||||
Not started.
|
||||
|
||||
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 & operator consolidation (largest)
|
||||
- Remove `when`; confirm `if` covers all uses in the corpus.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -116,12 +116,13 @@ fn p_or() -> Node {
|
|||
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)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ fn parse_spawn() -> Node {
|
|||
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, ... }
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
59
tools/ludic-tools/migrate_records.c
Normal file
59
tools/ludic-tools/migrate_records.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue