Phase 6a: rename core vocabulary (game/main/component/archetype/system)

game/module -> program, main -> entry, component -> property,
archetype -> model, system -> handler. Done via a transitional self-hosting
bootstrap: parser accepts both -> reseed -> compiler source moved to new
keywords + parser tightened to new-only -> reseed (fixpoint holds). Old keywords
now rejected.

Token-safe corpus migration (tools/ludic-tools/rename_kw.c) leaves the LLVM
@main/entry: labels in emit strings untouched; goldens byte-identical. Editor
vocab (header/JetBrains/TextMate/emacs), check-docs wrapper, and doc fences
updated; error string 'unknown component' -> 'unknown property'. test.sh 14/14,
test-tools 28/0, check-vocabulary green. (Doc prose pass to follow.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 18:22:13 +03:00
parent 3227e1e1d3
commit 42f955fd22
35 changed files with 2632 additions and 2646 deletions

View file

@ -852,9 +852,9 @@ current tree (`./test.sh` = 64/64).
**Recursion** ✅ → `55` **Recursion** ✅ → `55`
```ludic ```ludic
game P { program P {
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) }
system B phase Start { print_int(fib(10)); quit() } handler B phase Start { print_int(fib(10)); quit() }
} }
``` ```
@ -908,19 +908,19 @@ spelling is equally legal (§5.3).
**R1 — statements now require a separator (Rule B, syntax-redesign Phase 2)** → parse error **R1 — statements now require a separator (Rule B, syntax-redesign Phase 2)** → parse error
```ludic ```ludic
# doc-check: skip — intentionally rejected under Rule B: needs a newline or ';' # 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() } } program P { handler 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 Statements no longer sit adjacent with only spaces between them; the compiler
reports `expected newline or ';' between statements`. Put each on its own line, reports `expected newline or ';' between statements`. Put each on its own line,
or separate them with `;` (both lex to the same separator token): or separate them with `;` (both lex to the same separator token):
```ludic ```ludic
game P { system B phase Start { let x = 1; x = x + 1; print_int(x); quit() } } program P { handler B phase Start { let x = 1; x = x + 1; print_int(x); quit() } }
``` ```
**R2 — commas omitted throughout** → `7` **R2 — commas omitted throughout** → `7`
```ludic ```ludic
# doc-check: skip — composite: declaration plus statements # doc-check: skip — composite: declaration plus statements
component Pos { x: int = 0 y: int = 0 } property Pos { x: int = 0 y: int = 0 }
spawn Hero { Pos { x: 7 y: 2 } } spawn Hero { Pos { x: 7 y: 2 } }
``` ```
@ -950,7 +950,7 @@ print_int(half(8))
``` ```
And a system may declare read-only access, then write — also compiles: And a system may declare read-only access, then write — also compiles:
```ludic ```ludic
system Violate phase Update reads [Pos] query (p) [Pos] { p.x = 99 } handler Violate phase Update reads [Pos] query (p) [Pos] { p.x = 99 }
``` ```
**R8 — the two formatters disagree about what "format" means.** Given **R8 — the two formatters disagree about what "format" means.** Given

View file

@ -91,7 +91,7 @@ A source file opens with `game Name { … }` or `module Name { … }`.
```ludic ```ludic
# doc-check: skip — illustrative: elided body # doc-check: skip — illustrative: elided body
module Combat { program 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 fn curve(level: int) -> int { … } # private: not a symbol
} }

View file

@ -20,15 +20,15 @@ A program is one `game` block containing declarations:
```ludic ```ludic
# doc-check: skip — illustrative: elided import list # doc-check: skip — illustrative: elided import list
game Name { program Name {
import ... # pull declarations in from another file import ... # pull declarations in from another file
component ... # data (per entity) property ... # data (per entity)
struct ... # a plain record, not tied to an entity struct ... # a plain record, not tied to an entity
archetype ... # a named entity KIND (bundle of components) model ... # a named entity KIND (bundle of components)
const ... # compile-time constants const ... # compile-time constants
fn ... # functions fn ... # functions
extern fn ... # bind a C library symbol (FFI) extern fn ... # bind a C library symbol (FFI)
system ... # behavior, grouped into phases handler ... # behavior, grouped into phases
} }
``` ```
@ -36,7 +36,7 @@ game Name {
```ludic ```ludic
# doc-check: skip — paths resolve only inside the repo # doc-check: skip — paths resolve only inside the repo
game ChronoRift { program ChronoRift {
import "chronorift/world.ludic" # path is relative to THIS file import "chronorift/world.ludic" # path is relative to THIS file
import "chronorift/combat.ludic" import "chronorift/combat.ludic"
} }
@ -61,11 +61,11 @@ integer per entity, not a parallel boolean array.
```ludic ```ludic
# doc-check: skip — composite: declarations and statements together # doc-check: skip — composite: declarations and statements together
component Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
component Stats { hp: int = 10 } property Stats { hp: int = 10 }
archetype Player { Pos, Stats } # Player IS a kind, not a component model Player { Pos, Stats } # Player IS a kind, not a component
archetype Enemy { Pos, Stats } model 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 # (seeding field defaults), then overrides
@ -120,17 +120,17 @@ Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
systems: systems:
```ludic ```ludic
system Boot phase Start { handler Boot phase Start {
setreg(R_FONT, font_load("…Arial.ttf")) setreg(R_FONT, font_load("…Arial.ttf"))
ui_build() # construct the tree (loads skins/images) ui_build() # construct the tree (loads skins/images)
ui_open(UI_MainMenu) # make it active, focus the first button ui_open(UI_MainMenu) # make it active, focus the first button
} }
system Nav phase Update { handler Nav phase Update {
ui_tick(key()) # w/s move focus, space/enter activate ui_tick(key()) # w/s move focus, space/enter activate
if ui_clicked(UI_Quit) { quit() } if ui_clicked(UI_Quit) { quit() }
ui_set_int(UI_HpLabel, hp) # poke dynamic values by id ui_set_int(UI_HpLabel, hp) # poke dynamic values by id
} }
system Draw phase Render { clear(0x0e0e16); ui_render(); present() } handler Draw phase Render { clear(0x0e0e16); ui_render(); present() }
``` ```
See `examples/menu.ludic` for a complete title screen. See `examples/menu.ludic` for a complete title screen.
@ -155,8 +155,8 @@ point (`1.5`) is `fixed`. Arithmetic on two `fixed` values lowers to
```ludic ```ludic
# doc-check: skip — composite: declarations and statements together # doc-check: skip — composite: declarations and statements together
component Pos { x: int = 0, y: int = 0 } # typed fields with defaults property Pos { x: int = 0, y: int = 0 } # typed fields with defaults
component Player { } # a tag (no fields) property Player { } # a tag (no fields)
spawn Hero { # create an entity spawn Hero { # create an entity
Pos { x: 10, y: 5 } Pos { x: 10, y: 5 }
@ -175,7 +175,7 @@ program. `self()` yields the entity of the innermost `query` loop.
## Systems & phases ## Systems & phases
```ludic ```ludic
system Move @deterministic handler Move @deterministic
reads [Vel] # declared data access (parsed and reserved; not yet reads [Vel] # declared data access (parsed and reserved; not yet
writes [Pos] # consumed by any analysis pass — see "Not yet implemented") writes [Pos] # consumed by any analysis pass — see "Not yet implemented")
phase FixedUpdate phase FixedUpdate
@ -201,7 +201,7 @@ that entity — the query header is simply hoisted out of the body into the
signature: signature:
```ludic ```ludic
system CleanBattle phase LateUpdate handler CleanBattle phase LateUpdate
query (b, p) [Battle, Pos, {Enemy}] where b.hp <= 0 query (b, p) [Battle, Pos, {Enemy}] where b.hp <= 0
{ despawn self() } { despawn self() }
``` ```
@ -209,7 +209,7 @@ system CleanBattle phase LateUpdate
is the same program as is the same program as
```ludic ```ludic
system CleanBattle phase LateUpdate { handler CleanBattle phase LateUpdate {
for (b, p) in query [Battle, Pos, {Enemy}] where b.hp <= 0 { despawn self() } for (b, p) in query [Battle, Pos, {Enemy}] where b.hp <= 0 { despawn self() }
} }
``` ```
@ -255,7 +255,7 @@ the data a program keeps outside the ECS.
```ludic ```ludic
struct Tok { kind: int = 0, line: int = 0, next: Tok } struct Tok { kind: int = 0, line: int = 0, next: Tok }
system Lex phase Update { handler Lex phase Update {
let t = new Tok # allocates; every field seeded from its default let t = new Tok # allocates; every field seeded from its default
t.kind = 1 t.kind = 1
} }
@ -269,7 +269,7 @@ struct Tok { kind: int = 0, line: int = 0, next: Tok }
fn bump(t: Tok) -> void { t.kind = t.kind + 1 } fn bump(t: Tok) -> void { t.kind = t.kind + 1 }
system Share phase Update { handler Share phase Update {
let a = new Tok let a = new Tok
let b = a # b and a are the SAME object let b = a # b and a are the SAME object
b.kind = 9 b.kind = 9
@ -283,7 +283,7 @@ Fields chain, so a struct can refer to its own type and be walked without
temporaries — which is what an AST or a linked list needs: temporaries — which is what an AST or a linked list needs:
```ludic ```ludic
system Walk phase Update { handler Walk phase Update {
let a = new Tok let a = new Tok
let b = new Tok let b = new Tok
a.next = b a.next = b
@ -300,7 +300,7 @@ snippet below does not compile today. Programs use `[]T` slices for now.
```ludic ```ludic
# doc-check: skip — [T; N] fixed arrays are not yet implemented (design target) # doc-check: skip — [T; N] fixed arrays are not yet implemented (design target)
var table: [int; 8] # module-level storage var table: [int; 8] # module-level storage
system S phase Update { handler S phase Update {
let buf: [int; 4] # a local; no initializer needed let buf: [int; 4] # a local; no initializer needed
buf[0] = 10 buf[0] = 10
table[2] = buf[0] table[2] = buf[0]
@ -312,7 +312,7 @@ capacity. `push` appends, doubling the storage when it is full; because the
header never moves, an append is visible to everything holding that slice. header never moves, an append is visible to everything holding that slice.
```ludic ```ludic
system Collect phase Update { handler Collect phase Update {
let toks = new []Tok let toks = new []Tok
push(toks, new Tok) push(toks, new Tok)
for i in 0 .. len(toks) { print_int(toks[i].kind) } for i in 0 .. len(toks) { print_int(toks[i].kind) }
@ -553,7 +553,7 @@ scene Title start {
on exit { ui_visible(UI_Menu, 0) } on exit { ui_visible(UI_Menu, 0) }
layer Main { layer Main {
system Choose phase Update { handler Choose phase Update {
if ui_clicked(UI_NewGame) { enter Overworld } if ui_clicked(UI_NewGame) { enter Overworld }
} }
} }
@ -562,8 +562,8 @@ scene Title start {
scene Overworld { scene Overworld {
on enter { spawn_party() } on enter { spawn_party() }
layer World { system Move phase Update { … } } layer World { handler Move phase Update { … } }
layer Hud { system Draw phase Render { … } } layer Hud { handler Draw phase Render { … } }
} }
``` ```
@ -589,7 +589,7 @@ When a system's whole body is one query loop, the loop header can move into the
declaration: declaration:
```ludic ```ludic
system CleanBattle phase LateUpdate handler CleanBattle phase LateUpdate
query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0 query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0
{ {
despawn self() despawn self()

View file

@ -112,11 +112,11 @@ alternative if the paren form is disliked: keep whitespace separation but coloni
```ludic ```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax) # doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before # before
edge system Move @deterministic reads [Vel] writes [Pos] phase FixedUpdate edge handler Move @deterministic reads [Vel] writes [Pos] phase FixedUpdate
query (p, v) [Pos, Vel] where a.x > 0 { … } query (p, v) [Pos, Vel] where a.x > 0 { … }
# after # after
@edge @deterministic @edge @deterministic
system Move handler Move
phase FixedUpdate phase FixedUpdate
reads [Vel] writes [Pos] reads [Vel] writes [Pos]
query (p, v) [Pos, Vel] where p.x > 0 query (p, v) [Pos, Vel] where p.x > 0

View file

@ -7,7 +7,7 @@
# chronorift/, pulled in with `import`. Paths resolve relative to this file, and # chronorift/, pulled in with `import`. Paths resolve relative to this file, and
# each import is include-guarded so re-imports are free. # each import is include-guarded so re-imports are free.
# ============================================================================ # ============================================================================
game ChronoRift { program ChronoRift {
import "chronorift/world.ludic" # components, entity archetypes, constants import "chronorift/world.ludic" # components, entity archetypes, constants
import "chronorift/rules.ludic" # pure rules + map data (functions) import "chronorift/rules.ludic" # pure rules + map data (functions)
import "chronorift/overworld.ludic" # boot, movement, encounters, shop import "chronorift/overworld.ludic" # boot, movement, encounters, shop

View file

@ -8,7 +8,7 @@
enum KnightAct { Attack, Guard, Item, Flee } enum KnightAct { Attack, Guard, Item, Flee }
enum MageAct { Attack, Heal, Guard } enum MageAct { Attack, Heal, Guard }
system Battle phase Update { handler Battle phase Update {
if reg(R_MODE) == 1 { if reg(R_MODE) == 1 {
let k = key() let k = key()
for (e) in query [Stats, {Enemy}] { for (e) in query [Stats, {Enemy}] {
@ -144,7 +144,7 @@ system Battle phase Update {
} }
} }
system CleanBattle phase LateUpdate { handler CleanBattle phase LateUpdate {
if reg(R_MODE) != 1 { if reg(R_MODE) != 1 {
for (e) in query [Stats, {Enemy}] { for (e) in query [Stats, {Enemy}] {
despawn self() despawn self()
@ -152,7 +152,7 @@ system CleanBattle phase LateUpdate {
} }
} }
system Meta phase Update { handler Meta phase Update {
let k = key() let k = key()
if reg(R_MODE) == 2 { if reg(R_MODE) == 2 {
if k == 0 { setreg(R_ACK, 1) } if k == 0 { setreg(R_ACK, 1) }

View file

@ -1,6 +1,6 @@
# draw.ludic — the Render system (overworld, battle, and overlay screens). # draw.ludic — the Render system (overworld, battle, and overlay screens).
system Draw phase Render { handler Draw phase Render {
let m = reg(R_MODE) let m = reg(R_MODE)
clear(0x101018) clear(0x101018)

View file

@ -1,6 +1,6 @@
# overworld.ludic — boot, movement/encounter input, and the shop system. # overworld.ludic — boot, movement/encounter input, and the shop system.
system Boot phase Start { handler Boot phase Start {
load_png("assets/kenney/tiny-town/Tiles/tile_0000.png") # 0 grass load_png("assets/kenney/tiny-town/Tiles/tile_0000.png") # 0 grass
load_png("assets/kenney/tiny-town/Tiles/tile_0004.png") # 1 tree load_png("assets/kenney/tiny-town/Tiles/tile_0004.png") # 1 tree
load_png("assets/kenney/tiny-town/Tiles/tile_0025.png") # 2 path load_png("assets/kenney/tiny-town/Tiles/tile_0025.png") # 2 path
@ -33,7 +33,7 @@ system Boot phase Start {
setreg(R_POTION, 2) setreg(R_POTION, 2)
} }
system Control phase Input { handler Control phase Input {
if reg(R_MODE) == 0 { if reg(R_MODE) == 0 {
for (p) in query [Pos, {Player}] { for (p) in query [Pos, {Player}] {
let k = key() let k = key()
@ -122,7 +122,7 @@ system Control phase Input {
} }
# shop (at the house 'h' tile): buy potions with gold # shop (at the house 'h' tile): buy potions with gold
system Shop phase Update { handler Shop phase Update {
if reg(R_MODE) == 4 { if reg(R_MODE) == 4 {
let k = key() let k = key()
if k == 'w' { setreg(R_CUR, max(0, reg(R_CUR) - 1)) } if k == 'w' { setreg(R_CUR, max(0, reg(R_CUR) - 1)) }

View file

@ -1,17 +1,17 @@
# world.ludic — the data model: components, entity archetypes, constants. # world.ludic — the data model: components, entity archetypes, constants.
# Imported by examples/chronorift.ludic. No `game` wrapper (this is a fragment). # Imported by examples/chronorift.ludic. No `game` wrapper (this is a fragment).
component Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
component Actor { kind: int = 0 } property Actor { kind: int = 0 }
component Stats { hp: int = 1, maxhp: int = 1, mp: int = 0, maxmp: int = 0, property Stats { hp: int = 1, maxhp: int = 1, mp: int = 0, maxmp: int = 0,
atk: int = 1, def: int = 0, lvl: int = 1, xp: int = 0, guard: int = 0 } atk: int = 1, def: int = 0, lvl: int = 1, xp: int = 0, guard: int = 0 }
component Party { slot: int = 0 } property Party { slot: int = 0 }
# Entity KINDS. Identity is stored as one integer per entity (L_kind), not as # Entity KINDS. Identity is stored as one integer per entity (L_kind), not as
# an empty marker component. Query membership with {Player} / {Enemy}. # an empty marker component. Query membership with {Player} / {Enemy}.
archetype Player { Pos, Actor, Party, Stats } # the controllable knight model Player { Pos, Actor, Party, Stats } # the controllable knight
archetype Ally { Party, Stats } # backline mage (no overworld body) model Ally { Party, Stats } # backline mage (no overworld body)
archetype Enemy { Actor, Stats } # battle foe model Enemy { Actor, Stats } # battle foe
const MW: int = 40 const MW: int = 40
const MH: int = 18 const MH: int = 18

View file

@ -1,10 +1,10 @@
# hello.ludic — smallest program that exercises the ECS pipeline # hello.ludic — smallest program that exercises the ECS pipeline
game Hello { program Hello {
component Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
component Vel { dx: int = 0, dy: int = 0 } property Vel { dx: int = 0, dy: int = 0 }
system Boot phase Start { handler Boot phase Start {
spawn Mob { spawn Mob {
Pos { x: 3, y: 4 } Pos { x: 3, y: 4 }
Vel { dx: 1, dy: 0 } Vel { dx: 1, dy: 0 }
@ -17,14 +17,14 @@ game Hello {
# a system can declare the entities it operates on: the body then runs once # a system can declare the entities it operates on: the body then runs once
# per match, with the components bound and self() giving that entity. # per match, with the components bound and self() giving that entity.
system Move phase FixedUpdate handler Move phase FixedUpdate
query (p, v) [Pos, Vel] query (p, v) [Pos, Vel]
{ {
p.x = p.x + v.dx p.x = p.x + v.dx
p.y = p.y + v.dy p.y = p.y + v.dy
} }
system Report phase Update { handler Report phase Update {
for (p) in query [Pos] { for (p) in query [Pos] {
print_int(p.x) print_int(p.x)
print_int(p.y) print_int(p.y)

View file

@ -9,20 +9,20 @@
# that: the same declaration binds a C, Rust or Zig library just as well. This # that: the same declaration binds a C, Rust or Zig library just as well. This
# is how Ludic reaches the outside world without a C shim. # is how Ludic reaches the outside world without a C shim.
# ============================================================================ # ============================================================================
game Arena { program Arena {
extern fn damage(attack: int, armour: int, roll: int) -> int = "damage" extern fn damage(attack: int, armour: int, roll: int) -> int = "damage"
extern fn hits_to_kill(hp: int, attack: int, armour: int) -> int = "hits_to_kill" extern fn hits_to_kill(hp: int, attack: int, armour: int) -> int = "hits_to_kill"
extern fn xp_for(level: int, kills: int) -> int = "xp_for" extern fn xp_for(level: int, kills: int) -> int = "xp_for"
component Fighter { hp: int = 30, attack: int = 9, armour: int = 2 } property Fighter { hp: int = 30, attack: int = 9, armour: int = 2 }
system Boot phase Start { handler Boot phase Start {
spawn Hero { Fighter { hp: 40, attack: 12, armour: 3 } } spawn Hero { Fighter { hp: 40, attack: 12, armour: 3 } }
spawn Orc { Fighter { hp: 24, attack: 7, armour: 1 } } spawn Orc { Fighter { hp: 24, attack: 7, armour: 1 } }
} }
system Fight phase Update { handler Fight phase Update {
for (f) in query [Fighter] { for (f) in query [Fighter] {
print_int(damage(f.attack, f.armour, 0)) print_int(damage(f.attack, f.armour, 0))
print_int(damage(f.attack, f.armour, 20)) print_int(damage(f.attack, f.armour, 20))

View file

@ -11,7 +11,7 @@
# engine, a test harness, a scripting host. Non-exported functions stay private # engine, a test harness, a scripting host. Non-exported functions stay private
# to the library. # to the library.
# ============================================================================ # ============================================================================
module Combat { program Combat {
const CRIT_MULT: int = 2 const CRIT_MULT: int = 2

View file

@ -2,7 +2,7 @@
# block); the engine lays it out (stacked panel, padding/gap/alignment), draws # block); the engine lays it out (stacked panel, padding/gap/alignment), draws
# it (background, TrueType labels, focusable buttons), and routes keyboard focus # it (background, TrueType labels, focusable buttons), and routes keyboard focus
# and activation. The game only loads a font, opens the UI, and reads clicks. # and activation. The game only loads a font, opens the UI, and reads clicks.
game Menu { program Menu {
const R_FONT: int = 0 const R_FONT: int = 0
const R_MSG: int = 1 const R_MSG: int = 1
@ -19,13 +19,13 @@ game Menu {
} }
} }
system Boot phase Start { handler Boot phase Start {
setreg(R_FONT, font_load("/System/Library/Fonts/Supplemental/Arial.ttf")) setreg(R_FONT, font_load("/System/Library/Fonts/Supplemental/Arial.ttf"))
ui_build() ui_build()
ui_open(UI_MainMenu) ui_open(UI_MainMenu)
} }
system Nav phase Update { handler Nav phase Update {
ui_tick(key()) ui_tick(key())
if ui_clicked(UI_NewGame) { ui_set_text(UI_NewGame, "Starting...") } if ui_clicked(UI_NewGame) { ui_set_text(UI_NewGame, "Starting...") }
if ui_clicked(UI_Continue) { ui_set_text(UI_Continue, "No save") } if ui_clicked(UI_Continue) { ui_set_text(UI_Continue, "No save") }
@ -33,7 +33,7 @@ game Menu {
if ui_clicked(UI_Quit) { quit() } if ui_clicked(UI_Quit) { quit() }
} }
system Draw phase Render { handler Draw phase Render {
clear(0x0e0e16) clear(0x0e0e16)
ui_render() ui_render()
present() present()

View file

@ -9,29 +9,29 @@
# the whole body — the loop header just moves into the declaration. The body runs # the whole body — the loop header just moves into the declaration. The body runs
# once per matching entity, and self() is that entity. # once per matching entity, and self() is that entity.
# ============================================================================ # ============================================================================
game QueryDecl { program QueryDecl {
component Battle { hp: int = 0, side: int = 0 } property Battle { hp: int = 0, side: int = 0 }
component Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
archetype Foe { Battle, Pos } model Foe { Battle, Pos }
system Seed phase Start { handler Seed phase Start {
spawn Foe { Battle { hp: 3, side: 1 } } spawn Foe { Battle { hp: 3, side: 1 } }
spawn Foe { Battle { hp: 0, side: 1 } } spawn Foe { Battle { hp: 0, side: 1 } }
spawn Foe { Battle { hp: -2, side: 0 } } spawn Foe { Battle { hp: -2, side: 0 } }
} }
system CleanBattle phase LateUpdate handler CleanBattle phase LateUpdate
query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0 and b.side == 1 query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0 and b.side == 1
{ {
print_int(p.x) print_int(p.x)
despawn self() despawn self()
} }
system Census phase Render handler Census phase Render
query (b) [Battle] query (b) [Battle]
{ {
print_int(b.hp) print_int(b.hp)
} }
system Bye phase Render { quit() } handler Bye phase Render { quit() }
} }

View file

@ -4,16 +4,16 @@
# LANGUAGE.md §"Scenes & layers"). It is deliberately excluded from test.sh. # LANGUAGE.md §"Scenes & layers"). It is deliberately excluded from test.sh.
# Games that need scene-like states use a mode register + `machine` today, as # Games that need scene-like states use a mode register + `machine` today, as
# examples/chronorift does. # examples/chronorift does.
game SceneDemo { program SceneDemo {
const R_N: int = 0 const R_N: int = 0
system Boot phase Start { setreg(R_N, 0); print_int(1000) } handler Boot phase Start { setreg(R_N, 0); print_int(1000) }
scene Title start { scene Title start {
on enter { print_int(1) } on enter { print_int(1) }
on exit { print_int(2) } on exit { print_int(2) }
layer Main { layer Main {
system Tick phase Update { handler Tick phase Update {
setreg(R_N, reg(R_N) + 1) setreg(R_N, reg(R_N) + 1)
print_int(100 + reg(R_N)) print_int(100 + reg(R_N))
if reg(R_N) >= 2 { enter Play } if reg(R_N) >= 2 { enter Play }
@ -25,14 +25,14 @@ game SceneDemo {
on enter { print_int(3); setreg(R_N, 0) } on enter { print_int(3); setreg(R_N, 0) }
on exit { print_int(4) } on exit { print_int(4) }
layer World { layer World {
system Step phase Update { handler Step phase Update {
setreg(R_N, reg(R_N) + 1) setreg(R_N, reg(R_N) + 1)
print_int(200 + reg(R_N)) print_int(200 + reg(R_N))
if reg(R_N) >= 2 { quit() } if reg(R_N) >= 2 { quit() }
} }
} }
layer Hud { layer Hud {
system Draw phase Render { print_int(900) } handler Draw phase Render { print_int(900) }
} }
} }
} }

View file

@ -3,10 +3,10 @@
# general: compiled by the SAME unmodified ludicc + runtime as the JRPG. # general: compiled by the SAME unmodified ludicc + runtime as the JRPG.
# No sprites/assets — pure primitives. Build: ./build.sh examples/snake.ludic # No sprites/assets — pure primitives. Build: ./build.sh examples/snake.ludic
# ============================================================================ # ============================================================================
game Snake { program Snake {
component Pos { x: int = 0, y: int = 0 } property Pos { x: int = 0, y: int = 0 }
component Seg { order: int = 0 } property Seg { order: int = 0 }
const GW: int = 20 const GW: int = 20
const GH: int = 15 const GH: int = 15
@ -39,9 +39,9 @@ game Snake {
return 0 return 0
} }
system Boot phase Start { reset(0) } handler Boot phase Start { reset(0) }
system Input phase Input { handler Input phase Input {
let k = key() let k = key()
if reg(R_MODE) == 0 { if reg(R_MODE) == 0 {
if k == 'w' { if reg(R_DIR) != 1 { setreg(R_DIR, 0) } } if k == 'w' { if reg(R_DIR) != 1 { setreg(R_DIR, 0) } }
@ -55,7 +55,7 @@ game Snake {
} }
} }
system Move phase Update { handler Move phase Update {
if reg(R_MODE) == 0 { if reg(R_MODE) == 0 {
setreg(R_TICK, reg(R_TICK) + 1) setreg(R_TICK, reg(R_TICK) + 1)
if reg(R_TICK) >= 6 { if reg(R_TICK) >= 6 {
@ -110,7 +110,7 @@ game Snake {
} }
} }
system Draw phase Render { handler Draw phase Render {
clear(0x0d1020) clear(0x0d1020)
for gy in 0 .. GH { for gy in 0 .. GH {
for gx in 0 .. GW { for gx in 0 .. GW {

View file

@ -24,7 +24,7 @@ FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludi
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic" selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic" { echo "program SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
# the seed-built compiler compiles its own source # the seed-built compiler compiles its own source
"$B/sh_seed" "$B/selfhost.ludic" > "$B/out.ll" 2>/dev/null || { echo "FAIL: seed compiler self-compile"; exit 1; } "$B/sh_seed" "$B/selfhost.ludic" > "$B/out.ll" 2>/dev/null || { echo "FAIL: seed compiler self-compile"; exit 1; }

View file

@ -33,7 +33,7 @@ selfhost/emit_ui.ludic
selfhost/emit_decl.ludic selfhost/emit_decl.ludic
selfhost/main.ludic" selfhost/main.ludic"
SRC=build/selfhost.ludic SRC=build/selfhost.ludic
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$SRC" { echo "program SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$SRC"
# the compiler emits IR to stdout; clang assembles and links it # the compiler emits IR to stdout; clang assembles and links it
"$LC" "$SRC" > "$OUT.ll" 2>/dev/null && ${LUDIC_CC:-clang} "$OUT.ll" -o "$OUT" 2>/dev/null "$LC" "$SRC" > "$OUT.ll" 2>/dev/null && ${LUDIC_CC:-clang} "$OUT.ll" -o "$OUT" 2>/dev/null
rc=$?; rm -f "$OUT.ll"; [ $rc -eq 0 ] rc=$?; rm -f "$OUT.ll"; [ $rc -eq 0 ]

View file

@ -4,7 +4,7 @@
fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void { fn emit_init_component(e: ptr, comp: ptr, rec: Node) -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
let c = find_comp(comp) let c = find_comp(comp)
if ptr_is_null(c) { perr(sconcat("spawn: unknown component ", comp)) } if ptr_is_null(c) { perr(sconcat("spawn: unknown property ", 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") 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") emit(" store i8 1, ptr "); emit(hp); emit("\n")
let slot = nreg() let slot = nreg()

File diff suppressed because it is too large Load diff

View file

@ -51,7 +51,7 @@ fn die(msg: ptr) -> void {
os_exit(1) os_exit(1)
} }
main { entry {
let path = ptr_null() let path = ptr_null()
let out = ptr_null() let out = ptr_null()
let want = 0 # 0 = auto, 1 = windowed, 2 = headless let want = 0 # 0 = auto, 1 = windowed, 2 = headless

View file

@ -297,15 +297,15 @@ fn parse_one_decl() -> void {
} }
if is_id("struct") { push(prog, parse_struct()); return } if is_id("struct") { push(prog, parse_struct()); return }
if is_id("enum") { push(prog, parse_enum()); return } if is_id("enum") { push(prog, parse_enum()); return }
if is_id("component") { push(prog, parse_component()); return } if is_id("property") { push(prog, parse_component()); return }
if is_id("archetype") { push(prog, parse_archetype()); return } if is_id("model") { push(prog, parse_archetype()); return }
if is_id("system") { push(prog, parse_system()); return } if is_id("handler") { push(prog, parse_system()); return }
if is_id("ui") { push(prog, parse_ui()); return } if is_id("ui") { push(prog, parse_ui()); return }
if is_id("var") { push(prog, parse_var()); return } if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); 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("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("extern") { push(prog, parse_extern()); return }
if is_id("main") { push(prog, parse_main()); return } if is_id("entry") { push(prog, parse_main()); return }
perr("expected declaration") perr("expected declaration")
} }
@ -342,7 +342,7 @@ fn parse_program() -> void {
g_game_name = "Ludic" g_game_name = "Ludic"
# imports may precede the game block # 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() } 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("{") } if is_id("program") { pi = pi + 1; g_game_name = eat_id(); skipnl(); eat_op("{") }
while true { while true {
skipnl() skipnl()
if toks[pi].kind == TK_EOF { break } if toks[pi].kind == TK_EOF { break }

View file

@ -11,7 +11,7 @@ FRAGS="selfhost/str.ludic selfhost/buf.ludic selfhost/io.ludic selfhost/ast.ludi
selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic selfhost/lex.ludic selfhost/parse.ludic selfhost/parse_game.ludic selfhost/emit_core.ludic selfhost/emit_head.ludic
selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic selfhost/emit_addr.ludic selfhost/emit_intrin.ludic selfhost/emit_intrin2.ludic selfhost/emit_math.ludic selfhost/emit_new.ludic
selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic" selfhost/emit_expr.ludic selfhost/emit_stmt.ludic selfhost/emit_ecs.ludic selfhost/emit_query.ludic selfhost/emit_spawn.ludic selfhost/emit_game.ludic selfhost/emit_machine.ludic selfhost/emit_save.ludic selfhost/emit_ui.ludic selfhost/emit_decl.ludic selfhost/main.ludic"
{ echo "game SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic" { echo "program SelfHost {"; for f in $FRAGS; do cat "$f"; echo; done; echo "}"; } > "$B/selfhost.ludic"
if $CC selfhost/ludicc.seed.ll -o "$B/sh_old" 2>/dev/null; then if $CC selfhost/ludicc.seed.ll -o "$B/sh_old" 2>/dev/null; then
# compile once with the old seed, then AGAIN with the freshly built one so the # compile once with the old seed, then AGAIN with the freshly built one so the
# seed is a fixed point of the NEW compiler, not a one-step image of the old. # seed is a fixed point of the NEW compiler, not a one-step image of the old.

View file

@ -1,6 +1,6 @@
game T { program 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 { entry {
print_int(fib(10)) # 55 print_int(fib(10)) # 55
let s = 0 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 }

View file

@ -1,5 +1,5 @@
game T { program T {
main { entry {
let half = 0.5 let half = 0.5
let a = 1.5 let a = 1.5
print_int(flr(a + half)) # flr(2.0) = 2 print_int(flr(a + half)) # flr(2.0) = 2

View file

@ -1,9 +1,9 @@
game T { program T {
fn classify(c: int) -> int { 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 return 0
} }
main { entry {
print_int(classify(65)) # 1 print_int(classify(65)) # 1
print_int(classify(67)) # 2 print_int(classify(67)) # 2
print_int(classify(90)) # 9 print_int(classify(90)) # 9

View file

@ -1,5 +1,5 @@
game T { program T {
main { entry {
let xs = new []int let xs = new []int
print_int(len(xs)) # 0 print_int(len(xs)) # 0
for i in 0 .. 20 { push(xs, i * i) } for i in 0 .. 20 { push(xs, i * i) }

View file

@ -1,7 +1,7 @@
game T { program T {
struct P { x: int = 0, y: int = 7, next: P } struct P { x: int = 0, y: int = 7, next: P }
fn bump(p: P) -> void { p.x = p.x + 100 } fn bump(p: P) -> void { p.x = p.x + 100 }
main { entry {
let a = new P let a = new P
print_int(a.y) # 7 default print_int(a.y) # 7 default
a.x = 5 a.x = 5

View file

@ -72,7 +72,7 @@ if ./ludicc examples/snake.ludic --emit-llvm -o /tmp/cli_snake.ll >/tmp/cli.out
else bad "ludicc --emit-llvm: $(tail -1 /tmp/cli.out)"; fi else bad "ludicc --emit-llvm: $(tail -1 /tmp/cli.out)"; fi
# ludic compiles, runs, and forwards the program's exit code. # ludic compiles, runs, and forwards the program's exit code.
printf 'main { os_exit(42) }\n' > /tmp/cli_exit.ludic printf 'entry { os_exit(42) }\n' > /tmp/cli_exit.ludic
./ludic /tmp/cli_exit.ludic >/tmp/cli.out 2>&1 ./ludic /tmp/cli_exit.ludic >/tmp/cli.out 2>&1
if [ "$?" -eq 42 ]; then ok "ludic app.ludic -> compiles, runs, forwards exit code" if [ "$?" -eq 42 ]; then ok "ludic app.ludic -> compiles, runs, forwards exit code"
else bad "ludic run: expected exit 42, got $? ($(tail -1 /tmp/cli.out))"; fi else bad "ludic run: expected exit 42, got $? ($(tail -1 /tmp/cli.out))"; fi

View file

@ -19,22 +19,22 @@ A bare fragment is wrapped first: a declaration list goes inside
import re, subprocess, sys, os, tempfile import re, subprocess, sys, os, tempfile
LC = './build/ludicc' LC = './build/ludicc'
DECL = ('game','module','component','archetype','system','edge','fn','pure', DECL = ('program','property','model','handler','enum','fn',
'extern','const','var','ui','scene','import') 'extern','const','var','ui','struct','import')
def classify(body): def classify(body):
first = next((l.strip() for l in body.split('\n') first = next((l.strip() for l in body.split('\n')
if l.strip() and not l.strip().startswith('#')), '') if l.strip() and not l.strip().startswith('#')), '')
head = first.split('(')[0].split()[0] if first else '' head = first.split('(')[0].split()[0] if first else ''
if head in ('game','module'): return 'whole' if head == 'program': return 'whole'
return 'decls' if head in DECL else 'stmts' return 'decls' if head in DECL else 'stmts'
def wraps(body, kind): def wraps(body, kind):
"""Candidate framings, best guess first. An excerpt often mixes declarations """Candidate framings, best guess first. An excerpt often mixes declarations
with loose statements, so both are tried and either parsing counts.""" with loose statements, so both are tried and either parsing counts."""
if kind == 'whole': return [body] if kind == 'whole': return [body]
as_decls = 'game DocCheck {\n' + body + '\n}\n' as_decls = 'program DocCheck {\n' + body + '\n}\n'
as_stmts = 'game DocCheck {\n system DocS phase Start {\n' + body + '\n }\n}\n' as_stmts = 'program DocCheck {\n handler DocS phase Start {\n' + body + '\n }\n}\n'
return [as_decls, as_stmts] if kind == 'decls' else [as_stmts, as_decls] return [as_decls, as_stmts] if kind == 'decls' else [as_stmts, as_decls]
def fences(path): def fences(path):

View file

@ -11,8 +11,8 @@
;;; Code: ;;; Code:
(defconst ludic--declaration-keywords (defconst ludic--declaration-keywords
'("game" "module" "import" "component" "struct" "archetype" "enum" "ui" '("program" "import" "property" "struct" "model" "enum" "ui"
"const" "var" "fn" "extern" "system" "main")) "const" "var" "fn" "extern" "handler" "entry"))
(defconst ludic--clause-keywords (defconst ludic--clause-keywords
'("phase" "query" "reads" "writes")) '("phase" "query" "reads" "writes"))

View file

@ -41,8 +41,8 @@ object LudicTokens {
*/ */
object LudicVocabulary { object LudicVocabulary {
val DECL = setOf( val DECL = setOf(
"game", "module", "import", "component", "struct", "archetype", "enum", "ui", "program", "import", "property", "struct", "model", "enum", "ui",
"const", "var", "fn", "extern", "system", "main" "const", "var", "fn", "extern", "handler", "entry"
) )
val CLAUSE = setOf( val CLAUSE = setOf(
"phase", "query", "reads", "writes" "phase", "query", "reads", "writes"

View file

@ -54,21 +54,21 @@
"declaration": { "declaration": {
"patterns": [ "patterns": [
{ {
"match": "\\b(game|module)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(program)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "keyword.control.unit.ludic" }, "1": { "name": "keyword.control.unit.ludic" },
"2": { "name": "entity.name.type.unit.ludic" } "2": { "name": "entity.name.type.unit.ludic" }
} }
}, },
{ {
"match": "\\b(component)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(property)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.component.ludic" }, "1": { "name": "storage.type.component.ludic" },
"2": { "name": "entity.name.type.component.ludic" } "2": { "name": "entity.name.type.component.ludic" }
} }
}, },
{ {
"match": "\\b(archetype)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(model)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.archetype.ludic" }, "1": { "name": "storage.type.archetype.ludic" },
"2": { "name": "entity.name.type.archetype.ludic" } "2": { "name": "entity.name.type.archetype.ludic" }
@ -82,7 +82,7 @@
} }
}, },
{ {
"match": "\\b(system)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(handler)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.system.ludic" }, "1": { "name": "storage.type.system.ludic" },
"2": { "name": "entity.name.function.system.ludic" } "2": { "name": "entity.name.function.system.ludic" }
@ -165,7 +165,7 @@
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\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": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\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.boolean.ludic", "match": "\\b(true|false)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -54,21 +54,21 @@
"declaration": { "declaration": {
"patterns": [ "patterns": [
{ {
"match": "\\b(game|module)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(program)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "keyword.control.unit.ludic" }, "1": { "name": "keyword.control.unit.ludic" },
"2": { "name": "entity.name.type.unit.ludic" } "2": { "name": "entity.name.type.unit.ludic" }
} }
}, },
{ {
"match": "\\b(component)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(property)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.component.ludic" }, "1": { "name": "storage.type.component.ludic" },
"2": { "name": "entity.name.type.component.ludic" } "2": { "name": "entity.name.type.component.ludic" }
} }
}, },
{ {
"match": "\\b(archetype)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(model)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.archetype.ludic" }, "1": { "name": "storage.type.archetype.ludic" },
"2": { "name": "entity.name.type.archetype.ludic" } "2": { "name": "entity.name.type.archetype.ludic" }
@ -82,7 +82,7 @@
} }
}, },
{ {
"match": "\\b(system)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(handler)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "storage.type.system.ludic" }, "1": { "name": "storage.type.system.ludic" },
"2": { "name": "entity.name.function.system.ludic" } "2": { "name": "entity.name.function.system.ludic" }
@ -165,7 +165,7 @@
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\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": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\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.boolean.ludic", "match": "\\b(true|false)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }

View file

@ -52,8 +52,8 @@ typedef struct {
* mirror the compiler's parser: anything parse_decl() dispatches on is a * mirror the compiler's parser: anything parse_decl() dispatches on is a
* declaration keyword, anything stmt() dispatches on is a statement keyword. */ * declaration keyword, anything stmt() dispatches on is a statement keyword. */
static const char* LUDIC_KW_DECL[] = { static const char* LUDIC_KW_DECL[] = {
"game","module","import","component","struct","archetype","enum","ui", "program","import","property","struct","model","enum","ui",
"const","var","fn","extern","system","main", 0 "const","var","fn","extern","handler","entry", 0
}; };
static const char* LUDIC_KW_CLAUSE[] = { static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","reads","writes", 0 "phase","query","reads","writes", 0