Phase 3b+3c: ui props use ':'; drop dead needs/uses synonyms (Rule A)
3b: widget props go from key=value to key: value (parse_widget). emit_ui and ludic-fmt unchanged (AST-identical; ':' formats correctly by default). Chose the colonized form over parenthesized named-args — minimal churn, no new grammar. Migration: tools/ludic-tools/migrate_ui.c. menu golden byte-identical. 3c: removed the undocumented, unused 'needs'/'uses' clause synonyms from parse_system; reads/writes stay. Reseeded, fixpoint holds, test.sh 14/14, check-docs + check-vocabulary green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
39bd430ce8
commit
b643037d8f
6 changed files with 2687 additions and 2651 deletions
12
LANGUAGE.md
12
LANGUAGE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -250,14 +250,26 @@ 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`).
|
||||
**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 — collapse `reads/writes/needs/uses` → `reads`/`writes`, stored on the node.**
|
||||
Not started.
|
||||
**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 `edge`/`pure`/`export` → `@`-annotations, parsed into a list.**
|
||||
Not started.
|
||||
**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.
|
||||
|
||||
Each sub-phase follows the proven pattern: parser change → verification-gated
|
||||
migration (IR byte-identical / goldens identical) → reseed → docs. The migration
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -34,7 +34,7 @@ fn parse_system() -> Node {
|
|||
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("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
|
||||
if is_op("(") { eat_op("(") # optional (vars); omitted when nothing binds
|
||||
|
|
@ -126,8 +126,8 @@ fn parse_extern() -> Node {
|
|||
fn parse_widget() -> Node {
|
||||
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
|
||||
|
|
|
|||
52
tools/ludic-tools/migrate_ui.c
Normal file
52
tools/ludic-tools/migrate_ui.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue