Merge vocabulary rename + annotation DSL (Phase 6)
program (was game/module), entry (was main), property (was component),
model (was archetype), handler (was system). Plus an annotation DSL:
@Queries(these: [Prop{constraint}, ...], on: Model) on a handler desugars to the
query loop, and @Handles on a program. Behaviour-preserving (goldens
byte-identical, C-free fixpoint holds); test.sh 15/15, test-tools 28/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
ecead0564e
43 changed files with 3962 additions and 3277 deletions
42
BOOTSTRAP.md
42
BOOTSTRAP.md
|
|
@ -110,7 +110,7 @@ All verified. A compiler needs each of these, and each one works today.
|
|||
| Heap allocation | ✅ | `mem_alloc`, `mem_free`, `mem_copy`, `mem_set`; 1 MiB alloc verified |
|
||||
| Byte-level memory | ✅ | `peek8`/`poke8`, `peek32`/`poke32`, `peekp`/`pokep`, `ptr_add` |
|
||||
| `ptr` locals, params, returns | ✅ | `fn make(n: int) -> ptr` |
|
||||
| `ptr` in a component field | ✅ | `component Nd { kind: int = 0, a: ptr = ptr_null() }` |
|
||||
| `ptr` in a property field | ✅ | `property Nd { kind: int = 0, a: ptr = ptr_null() }` |
|
||||
| String literals as readable bytes | ✅ | `peek8("hello", 1)` → `101` |
|
||||
| `str` accepted where `ptr` expected | ✅ | `f("A")` into `fn f(p: ptr)` |
|
||||
| String comparison, **hand-written in Ludic** | ✅ | `streq` over `peek8` |
|
||||
|
|
@ -186,10 +186,10 @@ records. Today there are two workarounds, and both are bad at compiler scale:
|
|||
what `truetype.ludic` does, and it works, but every field access becomes a
|
||||
magic number. Across a 6,000-line compiler this is the difference between
|
||||
maintainable and not.
|
||||
- **ECS entities as nodes** — verified working (`component Nd { kind, a: ptr }`),
|
||||
- **ECS entities as nodes** — verified working (`property Nd { kind, a: ptr }`),
|
||||
and initially seductive because queries give you free traversal. **Do not do
|
||||
this.** `LUDIC_MAX_ENT` is 1024 in `native.c:18`; the entity world is a fixed
|
||||
array of per-component storage. A compiler needs hundreds of thousands of
|
||||
array of per-property storage. A compiler needs hundreds of thousands of
|
||||
nodes. This is a dead end, and it is worth writing down because it is the
|
||||
obvious wrong turn.
|
||||
|
||||
|
|
@ -211,11 +211,11 @@ No copying, no by-value passing, no nested-struct inlining — a `struct` value
|
|||
a layout table.
|
||||
|
||||
**Lowering.** This is largely already built. `native.c` already emits
|
||||
`%Cmp_<Name>` LLVM struct types for components and already resolves
|
||||
`%Cmp_<Name>` LLVM struct types for properties and already resolves
|
||||
`a.b` through `ll_member_addr` with `getelementptr`. A `struct` is a
|
||||
`%Cmp_`-style type *without* the parallel entity arrays: `new` is
|
||||
`malloc(sizeof)` plus a default-seeding memset/store sequence, and `.field` is
|
||||
the existing `getelementptr` path. Reusing the component machinery is why this
|
||||
the existing `getelementptr` path. Reusing the property machinery is why this
|
||||
is far cheaper than it looks.
|
||||
|
||||
**Cost.** ~250 lines of C across `ludicc.c` (parse) and `native.c` (layout,
|
||||
|
|
@ -423,7 +423,7 @@ confident nonsense.
|
|||
| **No silent no-ops** | If the language accepts a construct it must either honour it or reject it. Accepting-and-ignoring teaches a falsehood (see R6 — the worst thing in the audit). |
|
||||
| **Recoverable structure** — explicit terminators | A slightly-wrong generation fails *locally*, with an error pointing at the mistake, instead of cascading into a confusing error 40 lines later. |
|
||||
| **Locality** — meaning readable from the construct | No action-at-a-distance. Ludic is already strong here; keep it. |
|
||||
| **Greppable unique anchors** | `component Pos` is findable. Retrieval quality is a language design property. |
|
||||
| **Greppable unique anchors** | `property Pos` is findable. Retrieval quality is a language design property. |
|
||||
| **Errors that name the fix** | Already partly true: a missing builtin errors naming `rt_<name>`. Extend that everywhere. |
|
||||
|
||||
**Folklore, and false:**
|
||||
|
|
@ -452,12 +452,12 @@ Each row verified by compiling a probe, not by reading docs.
|
|||
| **R1** | **No statement terminator at all.** `block()` is `skipnl(); stmt()` in a loop. A newline *stops* an expression (it lexes as `T_NL`, and `binlevel` only continues on `T_OP`) but is never *required*. `let x = 1 x = x + 1 print_int(x)` on one line is three legal statements — verified compiling. | `ludicc.c` `block()`, `binlevel` | The reader cannot see where a statement ends without re-deriving operator precedence. Blocks error recovery entirely. |
|
||||
| **R2** | **Commas are optional everywhere.** `if(isop(",")) pi++` appears in `comp()`, `arche()`, `fn` params and `spawn`. `{ x: int = 0 y: int = 0 }` and the comma'd form both compile. | 4 parser sites | Two spellings, zero semantic difference. |
|
||||
| ~~**R3**~~ | ~~**`and`/`or` alias `&&`/`\|\|`.**~~ **RESOLVED** — `and`/`or`/`not` are the only boolean operators; `&&`, `\|\|` and `!` are each rejected with a diagnostic naming the fix, and all three words are reserved. `!=` is unaffected. | landed via S3 | — |
|
||||
| **R4** | **`{ }` means seven different things** — statement block; component fields (`n: T = e`); archetype list (bare idents); spawn initialisers (`N = { … }`); ui props + children (`k=v` juxtaposed, no commas); match arms (`p, p => …`); machine states (`state N = v { … }`). | `block/comp/arche/spawn/parse_widget/match/machine` | The delimiter carries no information. You must already know the head keyword to know the inner grammar. |
|
||||
| **R4** | **`{ }` means seven different things** — statement block; property fields (`n: T = e`); model list (bare idents); spawn initialisers (`N = { … }`); ui props + children (`k=v` juxtaposed, no commas); match arms (`p, p => …`); machine states (`state N = v { … }`). | `block/comp/arche/spawn/parse_widget/match/machine` | The delimiter carries no information. You must already know the head keyword to know the inner grammar. |
|
||||
| **R5** | **Contextual keywords, not reserved.** `phase`, `query`, `reads`, `writes`, `needs`, `uses`, `where`, `in`, `on`, `layer`, `state`, `start` are matched with `isid()` — ordinary identifiers. `let query = 5 let phase = 6` compiles and prints `11`. | `sys()`, `scene_decl()` | A local named `enter` or `match` produces a baffling error far from the cause. |
|
||||
| **R6** | **Contracts are parsed and thrown away.** `requires`/`ensures`/`invariant` parse an expression and **discard it** (`pi++; expr();`). `reads`/`writes`/`needs`/`uses`/`effects` are `skip_brackets()`. `pure` is consumed and ignored. Verified: `fn half(n: int) -> int requires n > 100000 ensures false` compiles, and `half(8)` returns `4`. Verified: a system declaring `reads [Pos]` that **writes** `p.x = 99` compiles. | `fn()`, `sys()` | **The worst item in the audit.** The language accepts a contract and does nothing. A model writing `requires n > 0` is rewarded with a clean compile and zero enforcement — it learns a lie, and so does a human reader trusting the annotation. |
|
||||
| **R7** | **`str + str` typechecks, then emits invalid IR.** | verified (§4 B2) | The front-end accepts what the backend cannot lower. |
|
||||
| **R8** | **Two formatters, opposite philosophies, both called "format".** `ludicc --fmt` canonicalises hard (one statement per line, `and`→`&&`, full parenthesisation) but drops comments and inlines imports. `ludic-fmt` is token-based and preserves comments — but **normalises nothing**: handed the one-line `let a = 1 a = a + 1 if true and false { … }`, it returned it unchanged. | verified side-by-side | **Neither tool enforces a single spelling.** The canonicaliser is unusable on real source; the source formatter has no opinion. |
|
||||
| **R9** | **Two ways to spell a tag** — `component Player { }` (empty component) or `archetype`. | LANGUAGE.md | |
|
||||
| **R9** | **Two ways to spell a tag** — `property Player { }` (empty property) or `model`. | LANGUAGE.md | |
|
||||
| **R10** | **Stale docs are stale training data.** LANGUAGE.md still says "the current compiler is a tree-to-C translator" (it emits LLVM IR) and lists arrays under "Not yet implemented" beside things never planned. | LANGUAGE.md | Docs are the highest-leverage model input in the repo. A wrong doc is worse than a missing one. |
|
||||
|
||||
### 5.4 Proposals
|
||||
|
|
@ -497,7 +497,7 @@ point of the mistake. *Fixes R5. Cost:* ~40 lines.
|
|||
section.** Two honest options per construct, no third:
|
||||
|
||||
- `reads` / `writes`: **implement them.** The compiler already knows every
|
||||
component a system touches — it builds the query and walks the body. Checking
|
||||
property a system touches — it builds the query and walks the body. Checking
|
||||
the declaration against actual access is a genuine static analysis the
|
||||
language claims to have and doesn't. This converts dead syntax into a real
|
||||
guarantee, which is exactly what an "AI-first" language should offer a model
|
||||
|
|
@ -542,7 +542,7 @@ Recording these so they are not relitigated:
|
|||
|
||||
- **Braces, not indentation** (§5.2).
|
||||
- **`#` comments** — unambiguous, one spelling already.
|
||||
- **The ECS vocabulary** — `component` / `system` / `query` / `phase` are
|
||||
- **The ECS vocabulary** — `property` / `system` / `query` / `phase` are
|
||||
unusually self-describing and greppable. This is the language's best existing
|
||||
readability asset.
|
||||
- **`fixed` / Q16.16** — determinism is a design constraint, not a style choice.
|
||||
|
|
@ -598,7 +598,7 @@ The self-host compiler (`selfhost/`) implements the **compiler-subset**: `struct
|
|||
(reference), `[]T` slices with `push`/`len`, functions, a plain `main` entry,
|
||||
the full control flow, the operators (with short-circuit `and`/`or`), and the
|
||||
low-level intrinsics. It deliberately does **not** implement the game half of
|
||||
Ludic — ECS, queries, archetypes, scenes, UI, save/load, `match`/`machine`,
|
||||
Ludic — ECS, queries, models, scenes, UI, save/load, `match`/`machine`,
|
||||
fixed-point. It targets native (macOS/clang) and emits LLVM IR text that clang
|
||||
assembles, exactly the posture the C `ludicc` has.
|
||||
|
||||
|
|
@ -634,8 +634,8 @@ compiler is now a historical seed, not a dependency.
|
|||
|
||||
### Stage 4+ — `ludicc.c` is deleted
|
||||
|
||||
The self-host compiler was extended to the **whole** language — components,
|
||||
archetypes, systems, phases, `for … in query` (with `where`), spawn/despawn,
|
||||
The self-host compiler was extended to the **whole** language — properties,
|
||||
models, systems, phases, `for … in query` (with `where`), spawn/despawn,
|
||||
`self()`, `machine`/`become`, `match`, `save`/`load` snapshots, the retained
|
||||
`ui` widget tree, multi-file `import`, fixed-point Q16.16, and every runtime
|
||||
intrinsic. It auto-splices the Ludic runtime exactly as the C compiler did.
|
||||
|
|
@ -791,7 +791,7 @@ existing framing ("the same floor Rust and Swift stand on") already covers it.
|
|||
## 8. Risks and gotchas
|
||||
|
||||
- **Do not build the AST out of ECS entities.** `LUDIC_MAX_ENT` is 1024
|
||||
(`native.c:18`) and component storage is fixed arrays. It compiles, it looks
|
||||
(`native.c:18`) and property storage is fixed arrays. It compiles, it looks
|
||||
elegant, and it caps the compiler at 1024 nodes. Use `struct` (A2).
|
||||
- **Do not inherit the C compiler's fixed caps.** `ludicc.c:22` has
|
||||
`g_srcpath[128]`; `native.c:161` has `Val a[8]`. The Ludic port should grow
|
||||
|
|
@ -852,9 +852,9 @@ current tree (`./test.sh` = 64/64).
|
|||
|
||||
**Recursion** ✅ → `55`
|
||||
```ludic
|
||||
game P {
|
||||
program 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() }
|
||||
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
|
||||
```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() } }
|
||||
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
|
||||
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() } }
|
||||
program P { handler 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 }
|
||||
property Pos { x: int = 0 y: int = 0 }
|
||||
spawn Hero { Pos { x: 7 y: 2 } }
|
||||
```
|
||||
|
||||
|
|
@ -950,11 +950,11 @@ print_int(half(8))
|
|||
```
|
||||
And a system may declare read-only access, then write — also compiles:
|
||||
```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
|
||||
`component Pos { x: int = 0 y: int = 0 }` and a multi-statement one-liner,
|
||||
`property Pos { x: int = 0 y: int = 0 }` and a multi-statement one-liner,
|
||||
`ludicc --fmt` rewrites both (one statement per line, `and`→`&&`, full
|
||||
parenthesisation) while `ludic-fmt` returns the input **unchanged**.
|
||||
|
||||
|
|
|
|||
12
COMPILING.md
12
COMPILING.md
|
|
@ -37,7 +37,7 @@ and find your program rewritten in another language.
|
|||
app.ludic
|
||||
│ ludicc — lex, parse, check, lower (compiler/ludicc.c,
|
||||
▼ compiler/native.c)
|
||||
app.ll LLVM IR: your systems, your components, your runtime
|
||||
app.ll LLVM IR: your systems, your properties, your runtime
|
||||
│ IR assembler (compiler/driver.c)
|
||||
▼
|
||||
app.o Mach-O / ELF / COFF object code
|
||||
|
|
@ -91,7 +91,7 @@ A source file opens with `game Name { … }` or `module Name { … }`.
|
|||
|
||||
```ludic
|
||||
# doc-check: skip — illustrative: elided body
|
||||
module Combat {
|
||||
program Combat {
|
||||
@export fn damage(attack: int, armour: int, roll: int) -> int { … }
|
||||
fn curve(level: int) -> int { … } # private: not a symbol
|
||||
}
|
||||
|
|
@ -216,7 +216,7 @@ python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
|
|||
itch.io — and the game runs. It needs no server-side anything, and no
|
||||
cross-origin isolation headers.
|
||||
|
||||
**No game logic passes through JavaScript.** The systems, the queries, the
|
||||
**No game logic passes through JavaScript.** The handlers, the queries, the
|
||||
fixed-point arithmetic, the PNG decoder, the TrueType rasteriser and the UI are
|
||||
all compiled Ludic executing as wasm. `platform.js` is 300 lines and implements
|
||||
the same five-function window protocol `cocoa.ll` implements, plus the host
|
||||
|
|
@ -248,7 +248,7 @@ A browser tab cannot be held inside that loop — it would never paint, and the
|
|||
key events the loop is waiting on would never be delivered. So a web build
|
||||
exports those four functions instead of `main`, and `platform.js` calls
|
||||
`ludic_frame` from `requestAnimationFrame`. Both targets emit the four from the
|
||||
same code in `ll_emit_loop_parts`, so the systems that run, and the phase order
|
||||
same code in `ll_emit_loop_parts`, so the handlers that run, and the phase order
|
||||
they run in, are identical; only the owner of the loop differs.
|
||||
|
||||
### The floor
|
||||
|
|
@ -302,8 +302,8 @@ asserts exactly that, which is a much stronger check on the backend than
|
|||
|
||||
## What a build contains
|
||||
|
||||
Everything: components and archetypes, spawn/despawn, queries with bindings,
|
||||
`where` filters and archetype filters, `match`, `machine`/`become`,
|
||||
Everything: properties and models, spawn/despawn, queries with bindings,
|
||||
`where` filters and model filters, `match`, `machine`/`become`,
|
||||
`scene`/`layer`/`enter`, module state (`var`), `const`, int and Q16.16
|
||||
fixed-point arithmetic, control flow, functions, `extern fn` FFI, strings, the
|
||||
entity allocator, save/load snapshots, the frame loop, the window, and the whole
|
||||
|
|
|
|||
182
LANGUAGE.md
182
LANGUAGE.md
|
|
@ -12,23 +12,23 @@ program.ludic ──ludicc──▶ program.ll ──▶ program.o ──▶ nat
|
|||
`ludicc` lowers Ludic to **LLVM IR itself** and links the result — see
|
||||
[COMPILING.md](COMPILING.md) for the pipeline, `module`/`export`, and
|
||||
cross-targets. There is one backend: no C is generated, compiled or linked at
|
||||
any point, and the runtime a game calls is itself written in Ludic.
|
||||
any point, and the runtime a program calls is itself written in Ludic.
|
||||
|
||||
## Program structure
|
||||
|
||||
A program is one `game` block containing declarations:
|
||||
A program is one `program` block containing declarations:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — illustrative: elided import list
|
||||
game Name {
|
||||
program Name {
|
||||
import ... # pull declarations in from another file
|
||||
component ... # data (per entity)
|
||||
property ... # data (per 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 properties)
|
||||
const ... # compile-time constants
|
||||
fn ... # functions
|
||||
extern fn ... # bind a C library symbol (FFI)
|
||||
system ... # behavior, grouped into phases
|
||||
handler ... # behavior, grouped into phases
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -36,43 +36,43 @@ game Name {
|
|||
|
||||
```ludic
|
||||
# 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/combat.ludic"
|
||||
}
|
||||
```
|
||||
|
||||
An imported file is a **fragment**: bare declarations, no `game` wrapper. Its
|
||||
An imported file is a **fragment**: bare declarations, no `program` wrapper. Its
|
||||
declarations are spliced into the importing program. Imports may appear inside
|
||||
the `game` block or before it, they may nest (a fragment may import fragments),
|
||||
the `program` block or before it, they may nest (a fragment may import fragments),
|
||||
and each resolved path is **include-guarded**, so importing the same file twice
|
||||
(even via different chains) pulls it in once. Diagnostics report the true file:
|
||||
|
||||
```
|
||||
error: line 1: unknown type 'nope' for field Pos.x
|
||||
chronorift/world.ludic:1 | component Pos { x: nope = 0 }
|
||||
chronorift/world.ludic:1 | property Pos { x: nope = 0 }
|
||||
```
|
||||
|
||||
## Archetypes (entity kinds)
|
||||
## Models (entity kinds)
|
||||
|
||||
An `archetype` names a *kind* of entity and the fixed set of components it
|
||||
carries. It replaces the empty "tag component" idiom: identity is stored as one
|
||||
An `model` names a *kind* of entity and the fixed set of properties it
|
||||
carries. It replaces the empty "tag property" idiom: identity is stored as one
|
||||
integer per entity, not a parallel boolean array.
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — composite: declarations and statements together
|
||||
component Pos { x: int = 0, y: int = 0 }
|
||||
component Stats { hp: int = 10 }
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
property Stats { hp: int = 10 }
|
||||
|
||||
archetype Player { Pos, Stats } # Player IS a kind, not a component
|
||||
archetype Enemy { Pos, Stats }
|
||||
model Player { Pos, Stats } # Player IS a kind, not a property
|
||||
model Enemy { Pos, Stats }
|
||||
|
||||
spawn Player { Pos { x: 5 } } # attaches every listed component
|
||||
spawn Player { Pos { x: 5 } } # attaches every listed property
|
||||
# (seeding field defaults), then overrides
|
||||
for (p, s) in query [Pos, Stats, {Player}] { ... } # {Player} filters by kind
|
||||
```
|
||||
|
||||
Use `{Name}` (tag position) to filter a query by archetype — an archetype can't
|
||||
Use `{Name}` (tag position) to filter a query by model — an model can't
|
||||
be *bound* to a variable since it has no fields of its own. Entity kind is part
|
||||
of the saved snapshot.
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ The 5×7 bitmap `text` stays for zero-asset programs. For real typography, load
|
|||
TrueType font and draw UTF-8:
|
||||
|
||||
```ludic
|
||||
let f = font_load("/System/Library/Fonts/Supplemental/Arial.ttf")
|
||||
let f = font_load("/Handler/Library/Fonts/Supplemental/Arial.ttf")
|
||||
text_ttf(f, 20, 20, "Héllo — Καλημέρα — Привет", 0xffffff, 28) # anti-aliased
|
||||
let w = text_w(f, "measure me", 28) # pixel width
|
||||
```
|
||||
|
|
@ -115,22 +115,22 @@ ui MainMenu {
|
|||
|
||||
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.
|
||||
evaluated at build time, so `font: reg(R_FONT)` reads a value the program set first.
|
||||
Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
|
||||
systems:
|
||||
handlers:
|
||||
|
||||
```ludic
|
||||
system Boot phase Start {
|
||||
handler Boot phase Start {
|
||||
setreg(R_FONT, font_load("…Arial.ttf"))
|
||||
ui_build() # construct the tree (loads skins/images)
|
||||
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
|
||||
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() }
|
||||
handler Draw phase Render { clear(0x0e0e16); ui_render(); present() }
|
||||
```
|
||||
|
||||
See `examples/menu.ludic` for a complete title screen.
|
||||
|
|
@ -151,12 +151,12 @@ point (`1.5`) is `fixed`. Arithmetic on two `fixed` values lowers to
|
|||
`fxmul`/`fxdiv`; mixing `int` and `fixed` promotes the `int`. Convert with
|
||||
`fx(i)` (int→fixed) and `flr(f)` (fixed→int).
|
||||
|
||||
## Components, entities, queries
|
||||
## Properties, entities, queries
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — composite: declarations and statements together
|
||||
component Pos { x: int = 0, y: int = 0 } # typed fields with defaults
|
||||
component Player { } # a tag (no fields)
|
||||
property Pos { x: int = 0, y: int = 0 } # typed fields with defaults
|
||||
property Player { } # a tag (no fields)
|
||||
|
||||
spawn Hero { # create an entity
|
||||
Pos { x: 10, y: 5 }
|
||||
|
|
@ -164,44 +164,44 @@ spawn Hero { # create an entity
|
|||
}
|
||||
despawn self() # remove the current entity
|
||||
|
||||
# iterate every entity that has all listed components:
|
||||
# iterate every entity that has all listed properties:
|
||||
for (p) in query [Pos, {Player}] { p.x = p.x + 1 } # {Tag} filters, doesn't bind
|
||||
for (a, b) in query [Pos, Vel] where a.x > 0 { ... } # one var per non-tag term
|
||||
```
|
||||
|
||||
Entities are integer handles; component storage and slot reuse are generated per
|
||||
Entities are integer handles; property storage and slot reuse are generated per
|
||||
program. `self()` yields the entity of the innermost `query` loop.
|
||||
|
||||
## Systems & phases
|
||||
## Handlers & phases
|
||||
|
||||
```ludic
|
||||
system Move @deterministic
|
||||
handler Move @deterministic
|
||||
reads [Vel] # declared data access (parsed and reserved; not yet
|
||||
writes [Pos] # consumed by any analysis pass — see "Not yet implemented")
|
||||
phase FixedUpdate
|
||||
query (p, v) [Pos, Vel] # the entities this system operates on
|
||||
query (p, v) [Pos, Vel] # the entities this handler operates on
|
||||
{ p.x = p.x + v.dx }
|
||||
```
|
||||
|
||||
Phases run in this order every frame: **`Start`** (once at boot), then each
|
||||
frame **`Input` → `FixedUpdate` → `Update` → `LateUpdate` → `Render`**.
|
||||
`@edge` in front of a `system` marks one that touches the outside world.
|
||||
`@edge` in front of a `handler` 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`,
|
||||
`@export fn …` (a C-ABI-exported function), `@edge handler …`, `@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
|
||||
|
||||
A system declares the entities it works on, alongside its phase. The body then
|
||||
runs **once per matching entity**, with the components bound and `self()` giving
|
||||
A handler declares the entities it works on, alongside its phase. The body then
|
||||
runs **once per matching entity**, with the properties bound and `self()` giving
|
||||
that entity — the query header is simply hoisted out of the body into the
|
||||
signature:
|
||||
|
||||
```ludic
|
||||
system CleanBattle phase LateUpdate
|
||||
handler CleanBattle phase LateUpdate
|
||||
query (b, p) [Battle, Pos, {Enemy}] where b.hp <= 0
|
||||
{ despawn self() }
|
||||
```
|
||||
|
|
@ -209,32 +209,32 @@ system CleanBattle phase LateUpdate
|
|||
is the same program as
|
||||
|
||||
```ludic
|
||||
system CleanBattle phase LateUpdate {
|
||||
handler CleanBattle phase LateUpdate {
|
||||
for (b, p) in query [Battle, Pos, {Enemy}] where b.hp <= 0 { despawn self() }
|
||||
}
|
||||
```
|
||||
|
||||
Drop `(vars)` when nothing binds: `query [{Enemy}]`. A system declares at most
|
||||
Drop `(vars)` when nothing binds: `query [{Enemy}]`. A handler declares at most
|
||||
one query, and the number of variables must equal the number of binding terms
|
||||
(`{Tag}` terms filter without binding, so they don't count). A system with no
|
||||
(`{Tag}` terms filter without binding, so they don't count). A handler with no
|
||||
`query` clause runs once per tick, as before.
|
||||
|
||||
### Conditions
|
||||
|
||||
A query selects on more than *which* components an entity has. `where` is an
|
||||
A query selects on more than *which* properties an entity has. `where` is an
|
||||
ordinary expression evaluated with the bindings in scope, so entities can be
|
||||
matched on their field values:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — a bare system clause, not a whole declaration
|
||||
# doc-check: skip — a bare handler clause, not a whole declaration
|
||||
query (b, s) [Battle, Stats] where b.hp <= 0 and s.level > 3
|
||||
```
|
||||
|
||||
The same `where` works on an inline `for (…) in query […]`.
|
||||
|
||||
`where` is evaluated **per candidate entity**, so it is the wrong place for a
|
||||
guard that concerns the whole system (`where reg(R_MODE) != 1` would re-read the
|
||||
register for every entity). Keep whole-system guards in the body of a system
|
||||
guard that concerns the whole handler (`where reg(R_MODE) != 1` would re-read the
|
||||
register for every entity). Keep whole-handler guards in the body of a handler
|
||||
with no `query` clause, wrapping an inline query — as `CleanBattle` does in
|
||||
`examples/chronorift/combat.ludic`.
|
||||
|
||||
|
|
@ -247,6 +247,46 @@ there is no per-tick array of matched entities. Consequences worth knowing:
|
|||
* An entity **spawned during the loop at a higher id is visited in the same
|
||||
tick**. Spawn into a later phase if you don't want that.
|
||||
|
||||
## Annotations
|
||||
|
||||
Declarations carry `@annotations` in front of them — `@export`, `@edge`, `@pure`,
|
||||
`@deterministic` — one uniform channel rather than a set of prefix keywords. Two
|
||||
annotations replace a clause with a decorator.
|
||||
|
||||
**`@Queries` — a handler's query as a decorator.** Instead of the `query (v) […]`
|
||||
clause, a handler annotates its query, with each property's constraints written
|
||||
inline and the model given as `on:`:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — composite: a handler plus its property/model declarations
|
||||
property Transform { x: int = 0, scale: int = 1 }
|
||||
property Velocity { dx: int = 0, dy: int = 0 }
|
||||
model Actor { Transform, Velocity }
|
||||
|
||||
@Queries(these: [Transform{scale > 0}, Velocity{dx > 0 or dy > 0}], on: Actor)
|
||||
handler Move phase Update {
|
||||
Transform.x = Transform.x + Velocity.dx # each property is bound by its name
|
||||
}
|
||||
```
|
||||
|
||||
It desugars to the ordinary loop
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — the desugaring of the @Queries above
|
||||
for (Transform, Velocity) in query [Transform, Velocity, {Actor}]
|
||||
where Transform.scale > 0 and (Velocity.dx > 0 or Velocity.dy > 0) { … }
|
||||
```
|
||||
|
||||
— each listed property becomes a binding **named after itself**, a
|
||||
`Prop{constraint}` block reads its bare names as fields of `Prop`, and `on: Model`
|
||||
adds a `{Model}` tag filter. The body runs once per matching entity.
|
||||
|
||||
**`@Handles` — the handlers a program drives.** Written in front of the
|
||||
`program`, `@Handles(Move)` names the handlers it uses. It parses and reads as
|
||||
documentation; every declared handler still runs (registration is implicit).
|
||||
|
||||
See [`examples/annotations.ludic`](examples/annotations.ludic).
|
||||
|
||||
## Structs, arrays and slices
|
||||
|
||||
`struct` is the aggregate that is *not* tied to an entity — a plain record, for
|
||||
|
|
@ -255,7 +295,7 @@ the data a program keeps outside the ECS.
|
|||
```ludic
|
||||
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
|
||||
t.kind = 1
|
||||
}
|
||||
|
|
@ -269,7 +309,7 @@ struct Tok { kind: int = 0, line: int = 0, next: Tok }
|
|||
|
||||
fn bump(t: Tok) -> void { t.kind = t.kind + 1 }
|
||||
|
||||
system Share phase Update {
|
||||
handler Share phase Update {
|
||||
let a = new Tok
|
||||
let b = a # b and a are the SAME object
|
||||
b.kind = 9
|
||||
|
|
@ -283,7 +323,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:
|
||||
|
||||
```ludic
|
||||
system Walk phase Update {
|
||||
handler Walk phase Update {
|
||||
let a = new Tok
|
||||
let b = new Tok
|
||||
a.next = b
|
||||
|
|
@ -300,7 +340,7 @@ snippet below does not compile today. Programs use `[]T` slices for now.
|
|||
```ludic
|
||||
# doc-check: skip — [T; N] fixed arrays are not yet implemented (design target)
|
||||
var table: [int; 8] # module-level storage
|
||||
system S phase Update {
|
||||
handler S phase Update {
|
||||
let buf: [int; 4] # a local; no initializer needed
|
||||
buf[0] = 10
|
||||
table[2] = buf[0]
|
||||
|
|
@ -312,7 +352,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.
|
||||
|
||||
```ludic
|
||||
system Collect phase Update {
|
||||
handler Collect phase Update {
|
||||
let toks = new []Tok
|
||||
push(toks, new Tok)
|
||||
for i in 0 .. len(toks) { print_int(toks[i].kind) }
|
||||
|
|
@ -442,7 +482,7 @@ ints (`0xff8800`).
|
|||
# ui_focus(id) ui_focused()->int ui_visible(id,bool)
|
||||
# assets load_png(path)->id (decodes a PNG; returns a 16x16 sprite id)
|
||||
# input key()->int (current frame's key code, 0 if none)
|
||||
# state reg(i)->int setreg(i,v) (64 integer resources shared by systems)
|
||||
# state reg(i)->int setreg(i,v) (64 integer resources shared by handlers)
|
||||
# entity self()->entity
|
||||
# save save() load()->bool (binary snapshot of the whole ECS World)
|
||||
# control quit() print_int(i)
|
||||
|
|
@ -453,10 +493,10 @@ ints (`0xff8800`).
|
|||
## Tooling
|
||||
|
||||
```bash
|
||||
ludicc game.ludic -o build/game # native binary (windowed for a game)
|
||||
ludicc game.ludic --headless -o g # headless build (renders out.ppm; reads stdin)
|
||||
ludicc game.ludic --emit-llvm -o g.ll # stop at LLVM IR
|
||||
ludic game.ludic # compile AND run (forwards the exit code)
|
||||
ludicc app.ludic -o build/app # native binary (windowed for a game)
|
||||
ludicc app.ludic --headless -o app # headless build (renders out.ppm; reads stdin)
|
||||
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
|
||||
ludic app.ludic # compile AND run (forwards the exit code)
|
||||
```
|
||||
|
||||
`ludicc` (compile) and `ludic` (compile-and-run) are one multi-call binary built
|
||||
|
|
@ -464,7 +504,7 @@ by `./build-cli.sh`. **[COMPILING.md](COMPILING.md) is the authoritative CLI
|
|||
reference** — the full flag set (`-o`, `--windowed`, `--headless`, `--emit-llvm`,
|
||||
`--save-temps`, `--run`), the `LUDIC_HOME` / `LUDIC_CC` environment variables,
|
||||
and the IR-to-stdout bootstrap contract (no `-o`, invoked as `ludicc`) that
|
||||
`build.sh` / `reseed.sh` rely on. The default mode is auto: a file with `system`s
|
||||
`build.sh` / `reseed.sh` rely on. The default mode is auto: a file with `handler`s
|
||||
links windowed, otherwise headless; an explicit flag always wins.
|
||||
|
||||
The retired C driver's `--shared`, `--fmt`, `-c`, cross-compile (`--target`) and
|
||||
|
|
@ -502,7 +542,7 @@ Emacs, Sublime and Zed, are in `tools/editors/` — see
|
|||
|
||||
- `examples/chronorift.ludic` — a co-op JRPG (overworld, dungeon, boss, shop,
|
||||
save) using CC0 Kenney sprites. Split across `chronorift/*.ludic` via `import`,
|
||||
built on archetypes.
|
||||
built on models.
|
||||
- `examples/menu.ludic` — a retained-UI title screen (9-slice panel, TrueType
|
||||
labels, focusable buttons).
|
||||
- `examples/snake.ludic` — Snake, no assets — same compiler, proving generality.
|
||||
|
|
@ -519,7 +559,7 @@ are future work.
|
|||
|
||||
- **`scene` / `layer` / `on enter` / `on exit`** — the state-machine-over-scenes
|
||||
sugar is documented above but not parsed by the self-hosted compiler yet.
|
||||
- **`reads` / `writes` clauses** — parsed and reserved on the system node, but no
|
||||
- **`reads` / `writes` clauses** — parsed and reserved on the handler node, but no
|
||||
analysis pass consumes them.
|
||||
- **`[T; N]` fixed arrays** — documented above, but `ptype` parses only `[]T`
|
||||
slices; fixed inline arrays are not accepted yet. Use `[]T` slices.
|
||||
|
|
@ -542,9 +582,9 @@ as near-term — are now implemented and self-hosting; their lowerings are in
|
|||
> `machine`, as `examples/chronorift` does. This section describes the intended
|
||||
> syntax for when scene support lands.
|
||||
|
||||
A game is usually several mutually-exclusive states — a title screen, the
|
||||
A program is usually several mutually-exclusive states — a title screen, the
|
||||
overworld, a battle — and the usual way to write that is a mode register
|
||||
consulted at the top of every system. `scene` makes it structure instead:
|
||||
consulted at the top of every handler. `scene` makes it structure instead:
|
||||
|
||||
```ludic
|
||||
# doc-check: skip — illustrative: elided bodies
|
||||
|
|
@ -553,7 +593,7 @@ scene Title start {
|
|||
on exit { ui_visible(UI_Menu, 0) }
|
||||
|
||||
layer Main {
|
||||
system Choose phase Update {
|
||||
handler Choose phase Update {
|
||||
if ui_clicked(UI_NewGame) { enter Overworld }
|
||||
}
|
||||
}
|
||||
|
|
@ -562,34 +602,34 @@ scene Title start {
|
|||
scene Overworld {
|
||||
on enter { spawn_party() }
|
||||
|
||||
layer World { system Move phase Update { … } }
|
||||
layer Hud { system Draw phase Render { … } }
|
||||
layer World { handler Move phase Update { … } }
|
||||
layer Hud { handler Draw phase Render { … } }
|
||||
}
|
||||
```
|
||||
|
||||
- Exactly **one scene is active**. The one marked `start` runs first (or the
|
||||
first declared, if none is marked).
|
||||
- A scene's systems only run while it is active. Systems declared outside any
|
||||
- A scene's handlers only run while it is active. Handlers declared outside any
|
||||
scene are global and run every frame regardless.
|
||||
- **Layers group systems and declaration order is draw order**: within a phase,
|
||||
global systems run first, then the active scene's layers in the order they
|
||||
- **Layers group handlers and declaration order is draw order**: within a phase,
|
||||
global handlers run first, then the active scene's layers in the order they
|
||||
were written — so `Hud`'s `Render` paints over `World`'s.
|
||||
- `on enter` / `on exit` are lifecycle hooks, not phases. Scene setup goes in
|
||||
`on enter`; a layer system may not use phase `Start`.
|
||||
`on enter`; a layer handler may not use phase `Start`.
|
||||
- `enter Name` transitions: the current scene's `on exit` runs, the active scene
|
||||
becomes `Name`, and its `on enter` runs. Inside a layer system the compiler
|
||||
becomes `Name`, and its `on enter` runs. Inside a layer handler the compiler
|
||||
knows which scene is leaving, so a transition costs two direct calls and a
|
||||
store — there is no dispatch table.
|
||||
|
||||
`examples/scenes.ludic` is a runnable demonstration of the ordering rules.
|
||||
|
||||
## Queries in a system signature
|
||||
## Queries in a handler signature
|
||||
|
||||
When a system's whole body is one query loop, the loop header can move into the
|
||||
When a handler's whole body is one query loop, the loop header can move into the
|
||||
declaration:
|
||||
|
||||
```ludic
|
||||
system CleanBattle phase LateUpdate
|
||||
handler CleanBattle phase LateUpdate
|
||||
query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0
|
||||
{
|
||||
despawn self()
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -48,7 +48,7 @@ self-hosted native toolchain.)
|
|||
| `runtime/web/platform.js` | the browser's window — the same five `win_*` functions `cocoa.ll` implements, against a `<canvas>` |
|
||||
| `runtime/web/index.html` | the page a web build is served from |
|
||||
| `tools/ludic-web/run.mjs` | runs a headless wasm build under Node, so native and wasm output can be diffed |
|
||||
| `examples/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, archetype-based) |
|
||||
| `examples/chronorift.ludic` | the JRPG written in Ludic (multi-file via `import`, model-based) |
|
||||
| `examples/menu.ludic` | a retained-UI title screen (9-slice, TrueType, focusable buttons) |
|
||||
| `examples/snake.ludic` | a second, unrelated game — proves the language is general (same toolchain, no engine hardcoding) |
|
||||
| `build.sh` | `./build.sh examples/<name>.ludic` |
|
||||
|
|
@ -119,10 +119,10 @@ same highlighting, checking and formatting as the source tree.
|
|||
|
||||
## Language features implemented
|
||||
|
||||
- `component` (typed fields + defaults), `system` (`phase`, `@annotations`,
|
||||
- `property` (typed fields + defaults), `system` (`phase`, `@annotations`,
|
||||
`reads`/`writes` clauses), `const`, `fn` (with `requires`/`ensures` parsed).
|
||||
- `archetype` — named entity **kinds** (bundles of components); identity is one
|
||||
int per entity, replacing empty tag components. Filter with `{Kind}`.
|
||||
- `model` — named entity **kinds** (bundles of properties); identity is one
|
||||
int per entity, replacing empty tag properties. Filter with `{Kind}`.
|
||||
- `import "file"` — multi-file programs (fragments spliced in, include-guarded,
|
||||
per-file diagnostics).
|
||||
- ECS queries: `for (a, b) in query [A, B, {Tag}] where <expr> { … }`.
|
||||
|
|
@ -165,7 +165,7 @@ Controls:
|
|||
- [x] Compiler pipeline: Ludic → LLVM IR → native binary / shared library
|
||||
- [x] No C generated, compiled or linked in a build; runtime written in Ludic
|
||||
- [x] Cross-compilation to ELF (x86-64, aarch64) and Windows COFF
|
||||
- [x] ECS runtime (components, systems, phases, queries, entity pooling)
|
||||
- [x] ECS runtime (properties, systems, phases, queries, entity pooling)
|
||||
- [x] Windowed 2D rendering (Cocoa driven from LLVM IR) + headless PPM verification
|
||||
- [x] CC0 Kenney PNG sprites (`load_png`, decoder written in Ludic) + scrolling camera
|
||||
- [x] Overworld: tilemap, movement, collision
|
||||
|
|
|
|||
|
|
@ -112,11 +112,11 @@ alternative if the paren form is disliked: keep whitespace separation but coloni
|
|||
```ludic
|
||||
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
|
||||
# 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 { … }
|
||||
# after
|
||||
@edge @deterministic
|
||||
system Move
|
||||
handler Move
|
||||
phase FixedUpdate
|
||||
reads [Vel] writes [Pos]
|
||||
query (p, v) [Pos, Vel] where p.x > 0
|
||||
|
|
@ -343,7 +343,23 @@ 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
|
||||
**Phases 1–5 are complete.**
|
||||
|
||||
### Phase 6 — vocabulary rename + annotation DSL ✅ DONE (follow-on request)
|
||||
Renamed the core nouns: `game`/`module` → `program`, `main` → `entry`,
|
||||
`component` → `property`, `archetype` → `model`, `system` → `handler`. Done via a
|
||||
transitional self-hosting bootstrap (accept both → reseed → move the compiler's
|
||||
own source to new keywords + tighten → reseed); old keywords now rejected.
|
||||
Token-safe corpus migration ([rename_kw.c](tools/ludic-tools/rename_kw.c)), goldens
|
||||
byte-identical. Reconciled the LSP indexer, editor vocab, check-docs wrapper, and
|
||||
docs; fixed two pre-existing toolchain bugs (a `set -e` bug in build-tools.sh that
|
||||
blocked all editor-binary rebuilds, and a stale LSP test offset).
|
||||
|
||||
Added an **annotation DSL**: `@Queries(these: [Prop{constraint}, …], on: Model)` on
|
||||
a handler desugars to the existing `S_QUERY` loop (each property binds by its own
|
||||
name; a `Prop{…}` constraint qualifies its bare fields; `on:` adds a `{Model}`
|
||||
tag), and `@Handles(…)` on a program parses as documentation. See
|
||||
[examples/annotations.ludic](examples/annotations.ludic); test.sh 15/15. All thirteen findings are resolved or resolved by an
|
||||
explicit, documented decision.
|
||||
|
||||
---
|
||||
|
|
|
|||
30
examples/annotations.ludic
Normal file
30
examples/annotations.ludic
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# annotations.ludic — the annotation-first style. A handler's query is an
|
||||
# @Queries decorator instead of a clause, and the program lists the handlers it
|
||||
# drives with @Handles. Both lower to the same code the older spellings did.
|
||||
|
||||
@Handles(Move)
|
||||
program RPG2D {
|
||||
property Transform { x: int = 0, y: int = 0, scale: int = 1 }
|
||||
property Velocity { dx: int = 0, dy: int = 0 }
|
||||
model Actor { Transform, Velocity }
|
||||
|
||||
handler Spawn phase Start {
|
||||
spawn Actor { Transform { x: 0, scale: 2 } Velocity { dx: 3, dy: 1 } }
|
||||
spawn Actor { Transform { x: 0, scale: 0 } Velocity { dx: 9, dy: 9 } }
|
||||
}
|
||||
|
||||
# Move every Actor whose scale is positive and that is actually moving. The
|
||||
# query lives in the annotation; each bound property is addressed by its own
|
||||
# name in the body (`Transform`, `Velocity`), and a constraint like
|
||||
# `Transform{scale > 0}` reads `scale` as a field of Transform.
|
||||
@Queries(these: [Transform{scale > 0}, Velocity{dx > 0 or dy > 0}], on: Actor)
|
||||
handler Move phase Update {
|
||||
Transform.x = Transform.x + Velocity.dx
|
||||
Transform.y = Transform.y + Velocity.dy
|
||||
}
|
||||
|
||||
handler Report phase Render {
|
||||
for (t) in query [Transform] { print_int(t.x); print_int(t.y) }
|
||||
quit()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
# chronorift/, pulled in with `import`. Paths resolve relative to this file, and
|
||||
# each import is include-guarded so re-imports are free.
|
||||
# ============================================================================
|
||||
game ChronoRift {
|
||||
program ChronoRift {
|
||||
import "chronorift/world.ludic" # components, entity archetypes, constants
|
||||
import "chronorift/rules.ludic" # pure rules + map data (functions)
|
||||
import "chronorift/overworld.ludic" # boot, movement, encounters, shop
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
enum KnightAct { Attack, Guard, Item, Flee }
|
||||
enum MageAct { Attack, Heal, Guard }
|
||||
|
||||
system Battle phase Update {
|
||||
handler Battle phase Update {
|
||||
if reg(R_MODE) == 1 {
|
||||
let k = key()
|
||||
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 {
|
||||
for (e) in query [Stats, {Enemy}] {
|
||||
despawn self()
|
||||
|
|
@ -152,7 +152,7 @@ system CleanBattle phase LateUpdate {
|
|||
}
|
||||
}
|
||||
|
||||
system Meta phase Update {
|
||||
handler Meta phase Update {
|
||||
let k = key()
|
||||
if reg(R_MODE) == 2 {
|
||||
if k == 0 { setreg(R_ACK, 1) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# draw.ludic — the Render system (overworld, battle, and overlay screens).
|
||||
|
||||
system Draw phase Render {
|
||||
handler Draw phase Render {
|
||||
let m = reg(R_MODE)
|
||||
clear(0x101018)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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_0004.png") # 1 tree
|
||||
load_png("assets/kenney/tiny-town/Tiles/tile_0025.png") # 2 path
|
||||
|
|
@ -33,7 +33,7 @@ system Boot phase Start {
|
|||
setreg(R_POTION, 2)
|
||||
}
|
||||
|
||||
system Control phase Input {
|
||||
handler Control phase Input {
|
||||
if reg(R_MODE) == 0 {
|
||||
for (p) in query [Pos, {Player}] {
|
||||
let k = key()
|
||||
|
|
@ -122,7 +122,7 @@ system Control phase Input {
|
|||
}
|
||||
|
||||
# shop (at the house 'h' tile): buy potions with gold
|
||||
system Shop phase Update {
|
||||
handler Shop phase Update {
|
||||
if reg(R_MODE) == 4 {
|
||||
let k = key()
|
||||
if k == 'w' { setreg(R_CUR, max(0, reg(R_CUR) - 1)) }
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# world.ludic — the data model: components, entity archetypes, constants.
|
||||
# Imported by examples/chronorift.ludic. No `game` wrapper (this is a fragment).
|
||||
|
||||
component Pos { x: int = 0, y: int = 0 }
|
||||
component Actor { kind: int = 0 }
|
||||
component Stats { hp: int = 1, maxhp: int = 1, mp: int = 0, maxmp: int = 0,
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
property Actor { kind: 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 }
|
||||
component Party { slot: int = 0 }
|
||||
property Party { slot: int = 0 }
|
||||
|
||||
# Entity KINDS. Identity is stored as one integer per entity (L_kind), not as
|
||||
# an empty marker component. Query membership with {Player} / {Enemy}.
|
||||
archetype Player { Pos, Actor, Party, Stats } # the controllable knight
|
||||
archetype Ally { Party, Stats } # backline mage (no overworld body)
|
||||
archetype Enemy { Actor, Stats } # battle foe
|
||||
model Player { Pos, Actor, Party, Stats } # the controllable knight
|
||||
model Ally { Party, Stats } # backline mage (no overworld body)
|
||||
model Enemy { Actor, Stats } # battle foe
|
||||
|
||||
const MW: int = 40
|
||||
const MH: int = 18
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# hello.ludic — smallest program that exercises the ECS pipeline
|
||||
game Hello {
|
||||
program Hello {
|
||||
|
||||
component Pos { x: int = 0, y: int = 0 }
|
||||
component Vel { dx: int = 0, dy: int = 0 }
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
property Vel { dx: int = 0, dy: int = 0 }
|
||||
|
||||
system Boot phase Start {
|
||||
handler Boot phase Start {
|
||||
spawn Mob {
|
||||
Pos { x: 3, y: 4 }
|
||||
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
|
||||
# per match, with the components bound and self() giving that entity.
|
||||
system Move phase FixedUpdate
|
||||
handler Move phase FixedUpdate
|
||||
query (p, v) [Pos, Vel]
|
||||
{
|
||||
p.x = p.x + v.dx
|
||||
p.y = p.y + v.dy
|
||||
}
|
||||
|
||||
system Report phase Update {
|
||||
handler Report phase Update {
|
||||
for (p) in query [Pos] {
|
||||
print_int(p.x)
|
||||
print_int(p.y)
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@
|
|||
# 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.
|
||||
# ============================================================================
|
||||
game Arena {
|
||||
program Arena {
|
||||
|
||||
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 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 Orc { Fighter { hp: 24, attack: 7, armour: 1 } }
|
||||
}
|
||||
|
||||
system Fight phase Update {
|
||||
handler Fight phase Update {
|
||||
for (f) in query [Fighter] {
|
||||
print_int(damage(f.attack, f.armour, 0))
|
||||
print_int(damage(f.attack, f.armour, 20))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
# engine, a test harness, a scripting host. Non-exported functions stay private
|
||||
# to the library.
|
||||
# ============================================================================
|
||||
module Combat {
|
||||
program Combat {
|
||||
|
||||
const CRIT_MULT: int = 2
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# block); the engine lays it out (stacked panel, padding/gap/alignment), draws
|
||||
# it (background, TrueType labels, focusable buttons), and routes keyboard focus
|
||||
# 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_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"))
|
||||
ui_build()
|
||||
ui_open(UI_MainMenu)
|
||||
}
|
||||
|
||||
system Nav phase Update {
|
||||
handler Nav phase Update {
|
||||
ui_tick(key())
|
||||
if ui_clicked(UI_NewGame) { ui_set_text(UI_NewGame, "Starting...") }
|
||||
if ui_clicked(UI_Continue) { ui_set_text(UI_Continue, "No save") }
|
||||
|
|
@ -33,7 +33,7 @@ game Menu {
|
|||
if ui_clicked(UI_Quit) { quit() }
|
||||
}
|
||||
|
||||
system Draw phase Render {
|
||||
handler Draw phase Render {
|
||||
clear(0x0e0e16)
|
||||
ui_render()
|
||||
present()
|
||||
|
|
|
|||
|
|
@ -9,29 +9,29 @@
|
|||
# the whole body — the loop header just moves into the declaration. The body runs
|
||||
# once per matching entity, and self() is that entity.
|
||||
# ============================================================================
|
||||
game QueryDecl {
|
||||
component Battle { hp: int = 0, side: int = 0 }
|
||||
component Pos { x: int = 0, y: int = 0 }
|
||||
archetype Foe { Battle, Pos }
|
||||
program QueryDecl {
|
||||
property Battle { hp: int = 0, side: int = 0 }
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
model Foe { Battle, Pos }
|
||||
|
||||
system Seed phase Start {
|
||||
handler Seed phase Start {
|
||||
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
|
||||
handler CleanBattle phase LateUpdate
|
||||
query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0 and b.side == 1
|
||||
{
|
||||
print_int(p.x)
|
||||
despawn self()
|
||||
}
|
||||
|
||||
system Census phase Render
|
||||
handler Census phase Render
|
||||
query (b) [Battle]
|
||||
{
|
||||
print_int(b.hp)
|
||||
}
|
||||
|
||||
system Bye phase Render { quit() }
|
||||
handler Bye phase Render { quit() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@
|
|||
# LANGUAGE.md §"Scenes & layers"). It is deliberately excluded from test.sh.
|
||||
# Games that need scene-like states use a mode register + `machine` today, as
|
||||
# examples/chronorift does.
|
||||
game SceneDemo {
|
||||
program SceneDemo {
|
||||
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 {
|
||||
on enter { print_int(1) }
|
||||
on exit { print_int(2) }
|
||||
layer Main {
|
||||
system Tick phase Update {
|
||||
handler Tick phase Update {
|
||||
setreg(R_N, reg(R_N) + 1)
|
||||
print_int(100 + reg(R_N))
|
||||
if reg(R_N) >= 2 { enter Play }
|
||||
|
|
@ -25,14 +25,14 @@ game SceneDemo {
|
|||
on enter { print_int(3); setreg(R_N, 0) }
|
||||
on exit { print_int(4) }
|
||||
layer World {
|
||||
system Step phase Update {
|
||||
handler Step phase Update {
|
||||
setreg(R_N, reg(R_N) + 1)
|
||||
print_int(200 + reg(R_N))
|
||||
if reg(R_N) >= 2 { quit() }
|
||||
}
|
||||
}
|
||||
layer Hud {
|
||||
system Draw phase Render { print_int(900) }
|
||||
handler Draw phase Render { print_int(900) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
# general: compiled by the SAME unmodified ludicc + runtime as the JRPG.
|
||||
# No sprites/assets — pure primitives. Build: ./build.sh examples/snake.ludic
|
||||
# ============================================================================
|
||||
game Snake {
|
||||
program Snake {
|
||||
|
||||
component Pos { x: int = 0, y: int = 0 }
|
||||
component Seg { order: int = 0 }
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
property Seg { order: int = 0 }
|
||||
|
||||
const GW: int = 20
|
||||
const GH: int = 15
|
||||
|
|
@ -39,9 +39,9 @@ game Snake {
|
|||
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()
|
||||
if reg(R_MODE) == 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 {
|
||||
setreg(R_TICK, reg(R_TICK) + 1)
|
||||
if reg(R_TICK) >= 6 {
|
||||
|
|
@ -110,7 +110,7 @@ game Snake {
|
|||
}
|
||||
}
|
||||
|
||||
system Draw phase Render {
|
||||
handler Draw phase Render {
|
||||
clear(0x0d1020)
|
||||
for gy in 0 .. GH {
|
||||
for gx in 0 .. GW {
|
||||
|
|
|
|||
|
|
@ -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/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"
|
||||
{ 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
|
||||
"$B/sh_seed" "$B/selfhost.ludic" > "$B/out.ll" 2>/dev/null || { echo "FAIL: seed compiler self-compile"; exit 1; }
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ selfhost/emit_ui.ludic
|
|||
selfhost/emit_decl.ludic
|
||||
selfhost/main.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
|
||||
"$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 ]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
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)) }
|
||||
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")
|
||||
emit(" store i8 1, ptr "); emit(hp); emit("\n")
|
||||
let slot = nreg()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -51,7 +51,7 @@ fn die(msg: ptr) -> void {
|
|||
os_exit(1)
|
||||
}
|
||||
|
||||
main {
|
||||
entry {
|
||||
let path = ptr_null()
|
||||
let out = ptr_null()
|
||||
let want = 0 # 0 = auto, 1 = windowed, 2 = headless
|
||||
|
|
|
|||
|
|
@ -281,11 +281,13 @@ fn already_loaded(full: ptr) -> bool {
|
|||
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
|
||||
fn parse_one_decl() -> void {
|
||||
let is_export = false
|
||||
let qspec: Node = ptr_null()
|
||||
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 } } }
|
||||
if streq(a, "Queries") { qspec = parse_queries_anno() } # @Queries(these: [...], on: ...)
|
||||
else { if is_op("(") { let d = 0 # any other @anno(args) — parsed and 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
|
||||
|
|
@ -297,15 +299,22 @@ fn parse_one_decl() -> void {
|
|||
}
|
||||
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("property") { push(prog, parse_component()); return }
|
||||
if is_id("model") { push(prog, parse_archetype()); return }
|
||||
if is_id("handler") {
|
||||
let h = parse_system()
|
||||
if not ptr_is_null(qspec) { # @Queries wraps the body in its S_QUERY
|
||||
qspec.a = h.a
|
||||
let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap
|
||||
}
|
||||
push(prog, h); 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 }
|
||||
if is_id("entry") { push(prog, parse_main()); return }
|
||||
perr("expected declaration")
|
||||
}
|
||||
|
||||
|
|
@ -340,9 +349,16 @@ fn parse_program() -> void {
|
|||
loaded_paths = new []ptr
|
||||
skipnl()
|
||||
g_game_name = "Ludic"
|
||||
# imports may precede the game block
|
||||
# imports may precede the program 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("{") }
|
||||
# @annotations on the program itself (e.g. @Handles(Movement)) — parsed, skipped
|
||||
while is_op("@") {
|
||||
pi = pi + 1; let a = eat_id()
|
||||
if is_op("(") { let d = 0
|
||||
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("program") { pi = pi + 1; g_game_name = eat_id(); skipnl(); eat_op("{") }
|
||||
while true {
|
||||
skipnl()
|
||||
if toks[pi].kind == TK_EOF { break }
|
||||
|
|
|
|||
|
|
@ -84,6 +84,62 @@ fn parse_query_for() -> Node {
|
|||
return n
|
||||
}
|
||||
|
||||
# ---- @Queries annotation -----------------------------------------------------
|
||||
# `@Queries(these: [Prop{constraint}, ...], on: Model)` on a handler is an
|
||||
# annotation spelling of the `for (Prop, ...) in query [Prop, ..., {Model}]
|
||||
# where <constraints> { body }` loop. It desugars to the same S_QUERY node, so
|
||||
# the whole query backend (iteration, filters, binding, break/continue) is reused.
|
||||
|
||||
fn mk_and(a: Node, b: Node) -> Node {
|
||||
if ptr_is_null(a) { return b }
|
||||
let n = node(E_BIN); n.s = "and"; n.a = a; n.b = b; return n
|
||||
}
|
||||
|
||||
# In `Prop{scale > 0}` the bare names are fields of Prop; qualify each to
|
||||
# `Prop.field` (the binding var is the property name) for the desugared where.
|
||||
fn qualify_fields(e: Node, prop: ptr) -> Node {
|
||||
if ptr_is_null(e) { return e }
|
||||
if e.kind == E_ID {
|
||||
let m = node(E_MEMBER); let base = node(E_ID); base.s = prop; m.a = base; m.s = e.s; return m
|
||||
}
|
||||
if e.kind == E_BIN { e.a = qualify_fields(e.a, prop); e.b = qualify_fields(e.b, prop); return e }
|
||||
if e.kind == E_UN { e.a = qualify_fields(e.a, prop); return e }
|
||||
return e
|
||||
}
|
||||
|
||||
# parse `(these: [...], on: Model)`, returning an S_QUERY with its vars/terms/where
|
||||
# filled in (the body `.a` is attached by the caller once the handler is parsed).
|
||||
fn parse_queries_anno() -> Node {
|
||||
eat_op("(")
|
||||
let qn = node(S_QUERY)
|
||||
let terms = node(N_BLOCK)
|
||||
let wh: Node = ptr_null()
|
||||
while not is_op(")") {
|
||||
skipnl()
|
||||
if is_op(")") { break }
|
||||
let key = eat_id(); eat_op(":")
|
||||
if streq(key, "these") {
|
||||
eat_op("["); skipnl()
|
||||
while not is_op("]") {
|
||||
let pname = eat_id()
|
||||
let v = node(E_ID); v.s = pname; push(qn.kids, v) # binding var = property name
|
||||
let t = node(E_ID); t.s = pname; t.ival = 0; push(terms.kids, t)
|
||||
if is_op("{") { pi = pi + 1; let ce = expr(); eat_op("}"); wh = mk_and(wh, qualify_fields(ce, pname)) }
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
skipnl()
|
||||
}
|
||||
eat_op("]")
|
||||
} else { if streq(key, "on") {
|
||||
let mname = eat_id(); let t = node(E_ID); t.s = mname; t.ival = 1; push(terms.kids, t) # {Model} tag
|
||||
} else { expr() } } # unknown key: skip its value
|
||||
if is_op(",") { pi = pi + 1 }
|
||||
skipnl()
|
||||
}
|
||||
eat_op(")")
|
||||
qn.c = terms; qn.b = wh
|
||||
return qn
|
||||
}
|
||||
|
||||
fn parse_spawn() -> Node {
|
||||
pi = pi + 1; let n = node(S_SPAWN); n.s = eat_id(); skipnl(); eat_op("{")
|
||||
while true {
|
||||
|
|
|
|||
|
|
@ -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/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"
|
||||
{ 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
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
main {
|
||||
entry {
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
game T {
|
||||
main {
|
||||
program T {
|
||||
entry {
|
||||
let half = 0.5
|
||||
let a = 1.5
|
||||
print_int(flr(a + half)) # flr(2.0) = 2
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
game T {
|
||||
program T {
|
||||
fn classify(c: int) -> int {
|
||||
match c { 65, 66 => { return 1 }; 67 => { return 2 }; _ => { return 9 } }
|
||||
return 0
|
||||
}
|
||||
main {
|
||||
entry {
|
||||
print_int(classify(65)) # 1
|
||||
print_int(classify(67)) # 2
|
||||
print_int(classify(90)) # 9
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
game T {
|
||||
main {
|
||||
program T {
|
||||
entry {
|
||||
let xs = new []int
|
||||
print_int(len(xs)) # 0
|
||||
for i in 0 .. 20 { push(xs, i * i) }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
game T {
|
||||
program T {
|
||||
struct P { x: int = 0, y: int = 7, next: P }
|
||||
fn bump(p: P) -> void { p.x = p.x + 100 }
|
||||
main {
|
||||
entry {
|
||||
let a = new P
|
||||
print_int(a.y) # 7 default
|
||||
a.x = 5
|
||||
|
|
|
|||
7
test.sh
7
test.sh
|
|
@ -50,6 +50,11 @@ qsmoke() { # name
|
|||
else bad "$n: $(tail -1 /tmp/qs.out)"; fi
|
||||
}
|
||||
qsmoke qdecl
|
||||
# @Queries desugars to a query loop; @Handles parses. Check the desugared behavior.
|
||||
if ./selfhost/game-build.sh build/ludicc examples/annotations.ludic "/tmp/ludic_ann" >/tmp/ann.out 2>&1 \
|
||||
&& [ "$(/tmp/ludic_ann | tr '\n' ' ')" = "3 1 0 0 " ]; then
|
||||
ok "annotations.ludic (@Queries desugars to a query, @Handles parses)"
|
||||
else bad "annotations: $(tail -1 /tmp/ann.out)"; fi
|
||||
# --- Toolchain-agent CLI smoke tests append below this line ---
|
||||
echo "== self-hosted front-end binaries (ludicc / ludic) =="
|
||||
# The two commands are one multi-call native binary built from the seed with
|
||||
|
|
@ -72,7 +77,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
|
||||
|
||||
# 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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ build_one() {
|
|||
if [ -x "$OUT/$name" ]; then
|
||||
newest=$(find "$SRC" -name '*.h' -o -name "$(basename "$main")" | while read -r f; do
|
||||
[ "$f" -nt "$OUT/$name" ] && echo new
|
||||
done)
|
||||
[ -z "$newest" ] && { echo "$name: up to date"; return; }
|
||||
done) || true
|
||||
if [ -z "$newest" ]; then echo "$name: up to date"; return; fi
|
||||
fi
|
||||
echo "cc: $main -> $OUT/$name"
|
||||
$CC $CFLAGS "$main" -o "$OUT/$name"
|
||||
|
|
|
|||
|
|
@ -19,22 +19,22 @@ A bare fragment is wrapped first: a declaration list goes inside
|
|||
import re, subprocess, sys, os, tempfile
|
||||
|
||||
LC = './build/ludicc'
|
||||
DECL = ('game','module','component','archetype','system','edge','fn','pure',
|
||||
'extern','const','var','ui','scene','import')
|
||||
DECL = ('program','property','model','handler','enum','fn',
|
||||
'extern','const','var','ui','struct','import')
|
||||
|
||||
def classify(body):
|
||||
first = next((l.strip() for l in body.split('\n')
|
||||
if l.strip() and not l.strip().startswith('#')), '')
|
||||
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'
|
||||
|
||||
def wraps(body, kind):
|
||||
"""Candidate framings, best guess first. An excerpt often mixes declarations
|
||||
with loose statements, so both are tried and either parsing counts."""
|
||||
if kind == 'whole': return [body]
|
||||
as_decls = 'game DocCheck {\n' + body + '\n}\n'
|
||||
as_stmts = 'game DocCheck {\n system DocS phase Start {\n' + body + '\n }\n}\n'
|
||||
as_decls = 'program DocCheck {\n' + body + '\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]
|
||||
|
||||
def fences(path):
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
;;; Code:
|
||||
|
||||
(defconst ludic--declaration-keywords
|
||||
'("game" "module" "import" "component" "struct" "archetype" "enum" "ui"
|
||||
"const" "var" "fn" "extern" "system" "main"))
|
||||
'("program" "import" "property" "struct" "model" "enum" "ui"
|
||||
"const" "var" "fn" "extern" "handler" "entry"))
|
||||
|
||||
(defconst ludic--clause-keywords
|
||||
'("phase" "query" "reads" "writes"))
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ object LudicTokens {
|
|||
*/
|
||||
object LudicVocabulary {
|
||||
val DECL = setOf(
|
||||
"game", "module", "import", "component", "struct", "archetype", "enum", "ui",
|
||||
"const", "var", "fn", "extern", "system", "main"
|
||||
"program", "import", "property", "struct", "model", "enum", "ui",
|
||||
"const", "var", "fn", "extern", "handler", "entry"
|
||||
)
|
||||
val CLAUSE = setOf(
|
||||
"phase", "query", "reads", "writes"
|
||||
|
|
|
|||
|
|
@ -54,21 +54,21 @@
|
|||
"declaration": {
|
||||
"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": {
|
||||
"1": { "name": "keyword.control.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": {
|
||||
"1": { "name": "storage.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": {
|
||||
"1": { "name": "storage.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": {
|
||||
"1": { "name": "storage.type.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.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": "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": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -54,21 +54,21 @@
|
|||
"declaration": {
|
||||
"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": {
|
||||
"1": { "name": "keyword.control.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": {
|
||||
"1": { "name": "storage.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": {
|
||||
"1": { "name": "storage.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": {
|
||||
"1": { "name": "storage.type.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.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": "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": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -421,14 +421,14 @@ static void lp_decl(LP* p, int parent){
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (!strcmp(w, "component") || !strcmp(w, "archetype")){
|
||||
int is_comp = !strcmp(w, "component");
|
||||
if (!strcmp(w, "property") || !strcmp(w, "model")){
|
||||
int is_comp = !strcmp(w, "property");
|
||||
lp_adv(p); int nt = lp_name(p); if (nt < 0) return;
|
||||
int s = lsym_add(D, is_comp ? LS_COMPONENT : LS_ARCHETYPE, nt, parent);
|
||||
ldoc_collect_doc(D, decl_tok, D->sym[s].doc, sizeof(D->sym[s].doc));
|
||||
while (lp_kind(p) == LT_ANNO) lp_adv(p);
|
||||
snprintf(D->sym[s].detail, sizeof(D->sym[s].detail), "%s %s {",
|
||||
is_comp ? "component" : "archetype", D->sym[s].name);
|
||||
is_comp ? "property" : "model", D->sym[s].name);
|
||||
if (!lp_is(p, "{")) return;
|
||||
int close = D->match[p->i];
|
||||
D->sym[s].body_start = D->lex.v[p->i].start;
|
||||
|
|
@ -518,8 +518,8 @@ static void lp_decl(LP* p, int parent){
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (!strcmp(w, "system") || !strcmp(w, "edge")){
|
||||
if (!strcmp(w, "edge")){ lp_adv(p); if (!lp_is(p, "system")) return; }
|
||||
if (!strcmp(w, "handler") || !strcmp(w, "edge")){
|
||||
if (!strcmp(w, "edge")){ lp_adv(p); if (!lp_is(p, "handler")) return; }
|
||||
lp_adv(p);
|
||||
int nt = lp_name(p); if (nt < 0) return;
|
||||
int s = lsym_add(D, LS_SYSTEM, nt, parent);
|
||||
|
|
@ -638,15 +638,15 @@ static void ldoc_parse(LDoc* D){
|
|||
lp_skipnl(&p);
|
||||
while (lp_is(&p, "import")) { lp_decl(&p, -1); lp_skipnl(&p); }
|
||||
int unit_parent = -1;
|
||||
if (lp_is(&p, "game") || lp_is(&p, "module")){
|
||||
D->is_module = lp_is(&p, "module");
|
||||
if (lp_is(&p, "program") || lp_is(&p, "program")){
|
||||
D->is_module = lp_is(&p, "program");
|
||||
D->is_unit = 1;
|
||||
lp_adv(&p);
|
||||
int nt = lp_name(&p);
|
||||
if (nt >= 0){
|
||||
unit_parent = lsym_add(D, LS_UNIT, nt, -1);
|
||||
lcpy(D->unit, sizeof(D->unit), D->sym[unit_parent].name, (int)strlen(D->sym[unit_parent].name));
|
||||
lcatf(D->sym[unit_parent].detail, sizeof(D->sym[unit_parent].detail), "%s %s", D->is_module ? "module" : "game", D->unit);
|
||||
lcatf(D->sym[unit_parent].detail, sizeof(D->sym[unit_parent].detail), "%s %s", D->is_module ? "program" : "program", D->unit);
|
||||
}
|
||||
if (lp_is(&p, "{")){
|
||||
int close = D->match[p.i];
|
||||
|
|
|
|||
|
|
@ -1080,13 +1080,13 @@ static void on_completion(JVal* msg, JVal* id){
|
|||
comp_builtins(&c);
|
||||
comp_scope(&c, D, off, 0);
|
||||
/* the shapes people actually type */
|
||||
comp_item(&c, "system", CI_SNIPPET, "system … phase … { }", "A system with a phase and a body.",
|
||||
comp_item(&c, "handler", CI_SNIPPET, "handler … phase … { }", "A system with a phase and a body.",
|
||||
"system ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render|} {\n $0\n}");
|
||||
comp_item(&c, "system+query", CI_SNIPPET, "system … query … { }", "A system that runs once per matching entity.",
|
||||
comp_item(&c, "system+query", CI_SNIPPET, "handler … query … { }", "A system that runs once per matching entity.",
|
||||
"system ${1:Name} phase ${2|Update,FixedUpdate,LateUpdate,Render,Input|}\n query (${3:p}) [${4:Pos}]\n{\n $0\n}");
|
||||
comp_item(&c, "component", CI_SNIPPET, "component … { }", "A component with typed fields.",
|
||||
comp_item(&c, "property", CI_SNIPPET, "property … { }", "A component with typed fields.",
|
||||
"component ${1:Name} { ${2:x}: ${3:int} = ${4:0} }");
|
||||
comp_item(&c, "archetype", CI_SNIPPET, "archetype … { }", "An entity kind bundling components.",
|
||||
comp_item(&c, "model", CI_SNIPPET, "archetype … { }", "An entity kind bundling components.",
|
||||
"archetype ${1:Name} {\n ${2:Pos}\n}");
|
||||
comp_item(&c, "for-query", CI_SNIPPET, "for (…) in query […] { }", "Iterate matching entities.",
|
||||
"for (${1:p}) in query [${2:Pos}] {\n $0\n}");
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ 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","enum","ui",
|
||||
"const","var","fn","extern","system","main", 0
|
||||
"program","import","property","struct","model","enum","ui",
|
||||
"const","var","fn","extern","handler","entry", 0
|
||||
};
|
||||
static const char* LUDIC_KW_CLAUSE[] = {
|
||||
"phase","query","reads","writes", 0
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ def main():
|
|||
"signatureHelpProvider", "inlayHintProvider", "documentLinkProvider"]:
|
||||
check_true(f"advertises {cap}", cap in caps)
|
||||
|
||||
# ---- a fragment file: no `game` block, only reachable through an import --
|
||||
# ---- a fragment file: no `program` block, only reachable through an import --
|
||||
frag = os.path.join(ROOT, "examples/chronorift/combat.ludic")
|
||||
text = c.open(frag)
|
||||
uri = {"uri": "file://" + frag}
|
||||
|
|
@ -137,13 +137,13 @@ def main():
|
|||
comp = c.request("textDocument/completion", {"textDocument": uri,
|
||||
"position": position_of(text, "st.guard", 3)})
|
||||
labels = [i["label"] for i in comp["items"]]
|
||||
check_true("completion after '.' offers only that component's fields",
|
||||
check_true("completion after '.' offers only that property's fields",
|
||||
"guard" in labels and "hp" in labels and "clear" not in labels)
|
||||
|
||||
# a query's term list admits components and archetypes and nothing else —
|
||||
# a query's term list admits propertys and models and nothing else —
|
||||
# not the loop variables, not builtins, not keywords
|
||||
qsrc = ('game QT {\n component Pos { x: int = 0 }\n component Vel { dx: int = 0 }\n'
|
||||
' system S phase Update {\n for (a, b) in query [Pos, ] { }\n }\n}\n')
|
||||
qsrc = ('program QT {\n property Pos { x: int = 0 }\n property Vel { dx: int = 0 }\n'
|
||||
' handler S phase Update {\n for (a, b) in query [Pos, ] { }\n }\n}\n')
|
||||
qfile = "/tmp/ludic_lsp_test_query.ludic"
|
||||
with open(qfile, "w") as fh:
|
||||
fh.write(qsrc)
|
||||
|
|
@ -152,7 +152,7 @@ def main():
|
|||
qcomp = c.request("textDocument/completion",
|
||||
{"textDocument": quri, "position": position_of(qsrc, "[Pos, ]", 6)})
|
||||
qlabels = sorted(i["label"] for i in qcomp["items"])
|
||||
check("completion inside query terms lists components only", qlabels, ["Pos", "Vel"])
|
||||
check("completion inside query terms lists propertys only", qlabels, ["Pos", "Vel"])
|
||||
body = c.request("textDocument/completion",
|
||||
{"textDocument": quri, "position": position_of(qsrc, "] { }", 3)})
|
||||
blabels = [i["label"] for i in body["items"]]
|
||||
|
|
@ -196,7 +196,7 @@ def main():
|
|||
# any file and answers structural queries without a compiler present.)
|
||||
bad = "/tmp/ludic_lsp_test_bad.ludic"
|
||||
with open(bad, "w") as fh:
|
||||
fh.write("game Bad {\n component P { x: nosuchtype = 0 }\n}\n")
|
||||
fh.write("program Bad {\n property P { x: nosuchtype = 0 }\n}\n")
|
||||
c.open(bad)
|
||||
syms = c.request("textDocument/documentSymbol", {"textDocument": {"uri": "file://" + bad}})
|
||||
check_true("LSP opens and analyses a file with no compiler present",
|
||||
|
|
@ -206,7 +206,7 @@ def main():
|
|||
# ---- markdown: ```ludic fences are real Ludic ---------------------------
|
||||
md = "/tmp/ludic_lsp_test.md"
|
||||
with open(md, "w") as fh:
|
||||
fh.write("# Title\n\nprose\n\n```ludic\ngame D { component Pt { x: int = 0 } }\n```\n\n"
|
||||
fh.write("# Title\n\nprose\n\n```ludic\nprogram D { property Pt { x: int = 0 } }\n```\n\n"
|
||||
"```python\nz = 1\n```\n")
|
||||
c.open(md, "markdown")
|
||||
mduri = {"uri": "file://" + md}
|
||||
|
|
@ -241,17 +241,17 @@ def main():
|
|||
# Renaming any one of them must leave the other three alone.
|
||||
amb = "/tmp/ludic_lsp_test_rename.ludic"
|
||||
with open(amb, "w") as fh:
|
||||
fh.write("game R {\n"
|
||||
" component Pos { x: int = 0, y: int = 0 }\n"
|
||||
" component Vel { x: int = 0 }\n"
|
||||
fh.write("program R {\n"
|
||||
" property Pos { x: int = 0, y: int = 0 }\n"
|
||||
" property Vel { x: int = 0 }\n"
|
||||
" fn helper(x: int) -> int { return x + 1 }\n"
|
||||
" system A phase Update {\n"
|
||||
" handler A phase Update {\n"
|
||||
" for (p) in query [Pos] {\n"
|
||||
" let x = 5\n"
|
||||
" p.x = p.x + x\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" system B phase Update {\n"
|
||||
" handler B phase Update {\n"
|
||||
" for (v) in query [Vel] { v.x = v.x + 1 }\n"
|
||||
" }\n"
|
||||
"}\n")
|
||||
|
|
@ -268,7 +268,7 @@ def main():
|
|||
def lines_touched(edits):
|
||||
return sorted({e["range"]["start"]["line"] for e in edits})
|
||||
|
||||
field = rename_at(position_of(atext, "component Pos { x", 16))
|
||||
field = rename_at(position_of(atext, "property Pos { x", 15))
|
||||
check("renaming Pos.x touches only its declaration and its uses",
|
||||
lines_touched(field), [1, 7])
|
||||
local = rename_at(position_of(atext, "let x = 5", 4))
|
||||
|
|
@ -280,7 +280,7 @@ def main():
|
|||
# find-usages must be equally precise
|
||||
refs_x = c.request("textDocument/references",
|
||||
{"textDocument": auri,
|
||||
"position": position_of(atext, "component Vel { x", 16),
|
||||
"position": position_of(atext, "property Vel { x", 15),
|
||||
"context": {"includeDeclaration": True}})
|
||||
check("find-usages of Vel.x does not pick up Pos.x",
|
||||
sorted({r["range"]["start"]["line"] for r in refs_x}), [2, 11])
|
||||
|
|
@ -288,7 +288,7 @@ def main():
|
|||
|
||||
# ---- workspace-wide symbol search ---------------------------------------
|
||||
ws = c.request("workspace/symbol", {"query": "Stats"})
|
||||
check_true("workspace symbol search finds a component", any(s["name"] == "Stats" for s in ws))
|
||||
check_true("workspace symbol search finds a property", any(s["name"] == "Stats" for s in ws))
|
||||
|
||||
c.close()
|
||||
print(f" {passed} passed, {failed} failed")
|
||||
|
|
|
|||
|
|
@ -71,34 +71,34 @@ else bad "formatter altered non-whitespace bytes" "$DRIFT"; fi
|
|||
# --- multi-byte characters must survive intact ------------------------------
|
||||
# A UTF-8 character outside a string is an error to the compiler, but the
|
||||
# formatter must still not split it: doing so silently corrupts the file.
|
||||
printf 'game E {\n # \xe2\x80\xa6 ellipsis in a comment\n const S: str = "caf\xc3\xa9 \xe2\x86\x92 na\xc3\xafve"\n}\n' > "$TMP/utf8.ludic"
|
||||
printf 'program E {\n # \xe2\x80\xa6 ellipsis in a comment\n const S: str = "caf\xc3\xa9 \xe2\x86\x92 na\xc3\xafve"\n}\n' > "$TMP/utf8.ludic"
|
||||
"$FMT" "$TMP/utf8.ludic" > "$TMP/utf8.out"
|
||||
if cmp -s "$TMP/utf8.ludic" "$TMP/utf8.out"; then ok "UTF-8 preserved byte for byte"
|
||||
else bad "UTF-8 mangled" "$(diff "$TMP/utf8.ludic" "$TMP/utf8.out" | head -2)"; fi
|
||||
# the same character where the compiler cannot make sense of it at all: it must
|
||||
# still come out as one character, not as three spaced-out replacement bytes
|
||||
printf 'game F {\nfn f() -> int { return \xe2\x80\xa6 }\n}\n' > "$TMP/utf8b.ludic"
|
||||
printf 'program F {\nfn f() -> int { return \xe2\x80\xa6 }\n}\n' > "$TMP/utf8b.ludic"
|
||||
a=$(tr -d ' \t\n' < "$TMP/utf8b.ludic" | shasum | cut -d' ' -f1)
|
||||
b=$("$FMT" "$TMP/utf8b.ludic" | tr -d ' \t\n' | shasum | cut -d' ' -f1)
|
||||
check "unrecognised characters pass through whole" "$b" "$a"
|
||||
|
||||
# --- comments survive, which is the whole reason this is not ludicc --fmt ---
|
||||
printf 'game C {\n # keep me\n component P { x: int = 0 } # and me\n}\n' > "$TMP/c.ludic"
|
||||
printf 'program C {\n # keep me\n property P { x: int = 0 } # and me\n}\n' > "$TMP/c.ludic"
|
||||
N=$("$FMT" "$TMP/c.ludic" | grep -c '#')
|
||||
check "comments preserved" "$N" "2"
|
||||
|
||||
# --- messy input is normalised ----------------------------------------------
|
||||
printf 'game M{\ncomponent P{x:int=0}\nsystem S phase Update{let a=1+2\nif a== -1 {a=-a}}\n}\n' > "$TMP/m.ludic"
|
||||
printf 'program M{\nproperty P{x:int=0}\nhandler S phase Update{let a=1+2\nif a== -1 {a=-a}}\n}\n' > "$TMP/m.ludic"
|
||||
"$FMT" "$TMP/m.ludic" > "$TMP/m.out"
|
||||
grep -q '^ component P { x: int = 0 }$' "$TMP/m.out" && ok "reindents and respaces" || bad "reindents and respaces"
|
||||
grep -q '^ property P { x: int = 0 }$' "$TMP/m.out" && ok "reindents and respaces" || bad "reindents and respaces"
|
||||
grep -q 'a = -a' "$TMP/m.out" && ok "unary minus not spaced" || bad "unary minus not spaced"
|
||||
|
||||
# --- language conventions are respected, not overwritten --------------------
|
||||
printf 'game U {\n ui M {\n panel id=Root w=10 {\n button id=Go text="go"\n }\n }\n}\n' > "$TMP/u.ludic"
|
||||
printf 'program U {\n ui M {\n panel id=Root w=10 {\n button id=Go text="go"\n }\n }\n}\n' > "$TMP/u.ludic"
|
||||
"$FMT" "$TMP/u.ludic" | grep -q 'id=Root' && ok "widget props stay tight (id=Root)" || bad "widget props stay tight"
|
||||
printf 'game Q {\n system S phase Update {\n for (p) in query [Pos, {Foe}] { }\n }\n component Pos { x: int = 0 }\n component Foe { }\n}\n' > "$TMP/q.ludic"
|
||||
printf 'program Q {\n handler S phase Update {\n for (p) in query [Pos, {Foe}] { }\n }\n property Pos { x: int = 0 }\n property Foe { }\n}\n' > "$TMP/q.ludic"
|
||||
"$FMT" "$TMP/q.ludic" | grep -q '\[Pos, {Foe}\]' && ok "query tags stay tight ({Foe})" || bad "query tags stay tight"
|
||||
printf 'game A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n' > "$TMP/a.ludic"
|
||||
printf 'program A {\n const R_A: int = 0 # one\n const R_BBB: int = 1 # two\n}\n' > "$TMP/a.ludic"
|
||||
"$FMT" "$TMP/a.ludic" | grep -q 'R_A: int = 0 # one' && ok "hand alignment preserved" || bad "hand alignment preserved"
|
||||
|
||||
# --- --check exit status, for CI and pre-commit hooks -----------------------
|
||||
|
|
@ -109,11 +109,11 @@ check "--check exits 1 on unformatted input" "$?" "1"
|
|||
check "--check exits 0 after -w" "$?" "0"
|
||||
|
||||
# --- markdown: fences formatted, prose untouched ----------------------------
|
||||
printf '# T\n\nprose with spaces\n\n```ludic\ngame D{component P{x:int=0}}\n```\n\n```python\nz = 1\n```\n' > "$TMP/d.md"
|
||||
printf '# T\n\nprose with spaces\n\n```ludic\nprogram D{property P{x:int=0}}\n```\n\n```python\nz = 1\n```\n' > "$TMP/d.md"
|
||||
"$FMT" "$TMP/d.md" > "$TMP/d.out"
|
||||
grep -q 'prose with spaces' "$TMP/d.out" && ok "markdown prose untouched" || bad "markdown prose untouched"
|
||||
grep -q 'z = 1' "$TMP/d.out" && ok "non-ludic fences untouched" || bad "non-ludic fences untouched"
|
||||
grep -q 'game D { component P { x: int = 0 } }' "$TMP/d.out" && ok "ludic fences formatted" || bad "ludic fences formatted"
|
||||
grep -q 'program D { property P { x: int = 0 } }' "$TMP/d.out" && ok "ludic fences formatted" || bad "ludic fences formatted"
|
||||
|
||||
echo "language server"
|
||||
python3 tools/test-lsp.py
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue