Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete

Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling,
and docs. Phase 1 of the syntax-redesign cohesion pass has landed:
edge-system fix, signature-query, when-alias, and the documentation truth-pass.
Suite green (14/14), C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 15:15:35 +03:00
commit 985f9ad8f2
418 changed files with 39065 additions and 0 deletions

11
.claude/launch.json Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "ludic-web",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "8123", "-d", "build/web"],
"port": 8123
}
]
}

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
build
**.zip
out.ppm
# self-hosted front-end binaries (built by ./build-cli.sh) and their output
/ludic
/ludicc
/bin/
# editor toolchain build artifacts
tools/editors/vscode/node_modules/
tools/editors/vscode/*.vsix
tools/editors/jetbrains/.gradle/
tools/editors/jetbrains/build/
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
.intellijPlatform/

967
BOOTSTRAP.md Normal file
View file

@ -0,0 +1,967 @@
# Bootstrapping Ludic in Ludic
**What it would take for Ludic to compile itself.**
Today `ludicc` is a C program: 2,508 lines across `compiler/ludicc.c`,
`compiler/native.c` and `compiler/driver.c`. Everything it produces is
Ludic-or-IR — the runtime a game calls is 2,501 lines of `.ludic`, and no C is
generated, compiled or linked in a build. The compiler is the last C in the
pipeline, and this document is about removing it.
Every claim about what the language can and cannot do below was **verified
against the built compiler**, not read off the docs. The probe programs are in
the appendix; each `✅`/`❌` is a real compile-and-run.
---
## 1. What "completely bootstrapped" means
Self-hosting is not one property. It is three independent axes, and they cost
wildly different amounts:
| Axis | Today | Target |
|---|---|---|
| **Compiler independence** — is the compiler written in the language? | ❌ 2,508 lines of C | `ludicc` written in Ludic, compiling itself to a fixpoint |
| **Runtime independence** — is the library the language ships written in the language? | ✅ **already done** — 2,501 lines of `.ludic` (gfx, PNG/DEFLATE, TrueType, UI) | keep |
| **Toolchain independence** — does a build need a foreign compiler? | ❌ `clang` assembles the IR and links | see §7 — three levels, only one is worth reaching |
The runtime axis is already won, and that is the unusual part. Most languages
self-host the compiler long before they stop leaning on a C standard library;
Ludic did it backwards. **The remaining work is concentrated in one axis.**
There is also a fourth, smaller thing: `runtime/native/cocoa.ll` (327 lines) and
`runtime/web/wasm.ll` are hand-written LLVM IR, not Ludic. §7.4 covers whether
that matters.
Running alongside all of this is a question the bootstrap forces rather than
raises: **what the syntax should finally be.** A self-hosted compiler is written
in the language it compiles, so the grammar wants to be settled *before* the
port, not after. §5 audits what is irregular today and proposes the freeze; it
is scheduled as Stage 0.5, between the language features and the libraries.
### The honest bar
"Bootstrapped by itself completely" should mean:
1. `ludicc` is written in Ludic.
2. A `ludicc` binary compiles the Ludic source of `ludicc` and produces a
**byte-identical** binary to itself (the fixpoint test, §6).
3. The C compiler is needed **only** to build the very first seed, and that seed
is a checked-in artifact rather than a live dependency.
4. No C source remains in the repo outside that seed.
It should *not* mean writing an object-file writer and a linker. Rust and Swift
are self-hosted and both stand on LLVM; standing on `clang` as an IR assembler
is the same posture. §7 argues this explicitly so the goal does not quietly
inflate.
---
## 2. Where the tree stands
```
compiler/ C split by concern; every file under 500 lines
ludicc.c 435 pipeline + codegen glue + main
util/ sb, diag 103 string builder; source registry + diagnostics
front/ lex, ast, parse 469 tokens; Node; recursive descent + imports
sem/ tables, uitree, validate 208 decl tables; widget flattening; static checks
back/ ir_* x10 953 the LLVM IR backend, one file per concern
driver/ toolchain, webbundle 382 IR -> object -> exe/dylib; the wasm bundle
fmt/ fmt 162 canonical AST printer (--fmt)
------
2,712 C <- all of it, and all that must go
tools/ludic-tools/* 3,260 C ludic-fmt + ludic-lsp (not yet split)
runtime/native/core.ludic 394 Ludic framebuffer, text, registers, RNG, input
runtime/native/image.ludic 436 Ludic PNG, sprites, alpha blend, 9-slice
runtime/native/inflate.ludic 276 Ludic DEFLATE (RFC 1951)
runtime/native/truetype.ludic 804 Ludic sfnt loader + AA rasterizer, Q16.16
runtime/native/ui.ludic 591 Ludic retained widget tree, layout, focus
----
2,501 Ludic <- proof the language is already load-bearing
runtime/native/cocoa.ll 327 LLVM IR macOS window (objc_msgSend + CoreGraphics)
runtime/web/wasm.ll 369 LLVM IR browser shims
```
`util/`, `front/`, `sem/` and `fmt/` are separately compiled translation units;
`back/` and `driver/` are still one unit assembled by `back/native.c`, so their
include order is their definition order. The build list lives in
`compiler/sources.sh`, sourced by both `build.sh` and `test.sh`.
`truetype.ludic` matters more than its line count. A from-scratch sfnt parser
with cmap format dispatch, composite glyph recursion and a Bézier rasterizer is
*structurally the same kind of program as a compiler*: binary input, recursive
descent, table lookups, a growing output buffer. It already works. That is the
strongest single piece of evidence that this port is feasible rather than
aspirational.
---
## 3. What the language can already do
All verified. A compiler needs each of these, and each one works today.
| Capability | Status | Evidence |
|---|---|---|
| Recursion | ✅ | `fib(10)` → `55` |
| Mutual recursion / forward references | ✅ | `odd`/`even` cross-call |
| Deep recursion (recursive-descent parsing) | ✅ | 5,000 frames, no crash |
| 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() }` |
| 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` |
| Integer → decimal, **hand-written in Ludic** | ✅ | `itoa(48291)` → `"48291"` |
| File read: open/seek/tell/read/close | ✅ | full round-trip of a written file |
| File write | ✅ | `file_open`/`file_write`/`file_close` |
| Module-level mutable state | ✅ | `var count: int`, `var heap: ptr` |
| `let` is mutable | ✅ | `i = i + 1` in a loop |
| `while`, numeric `for i in a .. b` with runtime bounds | ✅ | |
| `if` / `else if` / `else` chains | ✅ | |
| `match` with multi-value arms and `_` | ✅ | `1 => … 2, 3 => … _ => …` |
| Bitwise ops | ✅ | `band`/`bor`/`bxor`/`bnot`/`shl`/`shr` |
| `shr` is **logical**, not arithmetic | ✅ | `shr(-16, 1)` → `2147483640` |
| Character literals | ✅ | `'x'`, `'\n'`, `'\0'` lex to ints |
| Exit codes | ✅ | `os_exit(3)` → shell sees `3` |
| Separate compilation, C ABI | ✅ | `module` + `export fn`, `extern fn … = "sym"` |
**The consequence:** a compiler is *already expressible* in Ludic today. You
could write a lexer, a parser building nodes as hand-offset `peek32`/`poke32`
records, a symbol table, and an IR text emitter, using nothing above. It would
be miserable to read and maintain at 6,000 lines — but nothing in §4 is a
*capability* blocker except argv. The rest is about whether the resulting source
is something a human or a model can work in.
That distinction shapes the whole plan: **this is mostly an ergonomics project
with one small hole in it**, not a language-design project.
---
## 4. What the language is missing
Each entry: the gap, why a compiler specifically needs it, the proposed design,
and the lowering. Verified-missing means it is a compile error today.
### Tier A — real blockers
#### A1. Command-line arguments ❌ *the only true capability blocker*
```
ludicc: error: line 1: unknown function 'os_argc'
```
`ll_emit_main` in `compiler/native.c` emits `define i32 @main()` — **no
parameters**. A self-hosted `ludicc` has no way to learn which file to compile.
Everything else in this document has a workaround; this one does not.
**Design.** Two intrinsics:
```ludic
# doc-check: skip — proposed signature notation, not code
os_argc() -> int
os_arg(i: int) -> str
```
**Lowering.** Change the signature to `define i32 @main(i32 %argc, ptr %argv)`,
store both into `@L_argc` / `@L_argv` in the entry block, then `os_argc()` is a
load and `os_arg(i)` is exactly the existing `peekp(@L_argv, i)` path. Add to
`INTRINSICS[]` in `native.c`.
**Cost.** ~30 lines of C. This is the single highest-value change in the
document: it is what turns "a Ludic program" into "a Ludic command-line tool".
#### A2. Aggregate types (`struct`) ❌
```
ludicc: error: line 2: expected declaration (got 'struct')
```
An AST node, a token, a symbol-table entry and a type descriptor are all
records. Today there are two workarounds, and both are bad at compiler scale:
- **Hand-offset memory** — `poke32(n, 0, kind)`, `pokep(n, 1, child)`. This is
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 }`),
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
nodes. This is a dead end, and it is worth writing down because it is the
obvious wrong turn.
**Design — reference semantics, not value semantics.** The cheap version that
unblocks everything:
```ludic
# doc-check: skip — proposed syntax: struct does not exist yet
struct Tok { kind: int = 0, text: ptr = ptr_null(), line: int = 0 }
let t = new Tok # heap-allocated, fields seeded from defaults
t.kind = T_ID
print_int(t.line)
free Tok t # or leak it; see §8 on arenas
```
No copying, no by-value passing, no nested-struct inlining — a `struct` value
*is* a `ptr` with a known layout, so it costs nothing in the type system beyond
a layout table.
**Lowering.** This is largely already built. `native.c` already emits
`%Cmp_<Name>` LLVM struct types for components 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
is far cheaper than it looks.
**Cost.** ~250 lines of C across `ludicc.c` (parse) and `native.c` (layout,
`new`, member access). Highest cost in the document, and the highest payoff.
#### A3. Arrays and indexing ❌
```
ludicc: error: line 2: expected identifier (got '[')
```
Token buffers, string tables, keyword tables, scope stacks. Currently
`mem_alloc` + `peek32`, which works but reads badly.
**Design.**
```ludic
# doc-check: skip — proposed syntax: array types do not exist yet
var keywords: [str; 64] # fixed-size module-level storage
let toks: [Tok; 0] = mem_alloc(n * size_of(Tok)) # or a growable buffer
toks[i].kind = T_ID # composes with A2
```
**Lowering.** `[T; N]` is `[N x <llty(T)>]`, already exactly how `@L_alive` and
`@S_<Comp>` are emitted. `a[i]` as both rvalue and lvalue is a
`getelementptr` — the same code path as member access, indexed instead of
named. The important part is that `toks[i].kind` composes: index then member,
one GEP chain.
**Cost.** ~150 lines. Should land *with* A2, since neither is much use alone.
#### A4. `break` / `continue` ❌
```
ludicc: error: line 3: unknown identifier 'break'
```
Lexers and parsers are made of `while (1) { … break; }`. The workaround —
sentinel booleans threaded through every loop condition — is the kind of thing
that makes a 6,000-line port unreadable.
**Design.** `break`, `continue`. No labels; nested loops in a compiler rarely
need them, and adding labels later is compatible.
**Lowering.** `native.c` already maintains `ll_loopstk[64]` (for `self()` inside
queries). Extend each frame with `break_label` and `continue_label`, then
`break` is `br label %<break>`. Note the existing gotcha recorded in the native
backend notes: **stack slots must be emitted in the entry block** — no new
allocas at the break site.
**Cost.** ~40 lines. Best value-per-line in the document.
#### A5. `mem_realloc` ❌
```
ludicc: error: line 1: unknown function 'mem_realloc'
```
Every table in a compiler grows: tokens, nodes, the output buffer. Hand-rolling
alloc-copy-free works but is written once per table and gotten wrong once per
table.
**Design.** `mem_realloc(p: ptr, n: int) -> ptr`.
**Lowering.** `declare ptr @realloc(ptr, <size_t>)` plus one `INTRINSICS[]`
entry. **Use `ll_size_t()` / `ll_widen()` for the size argument — do not
hardcode `i64`.** `size_t` is `i32` on wasm32, and `native.c` now routes every
size-taking intrinsic through those helpers for exactly this reason.
**Cost.** ~6 lines.
#### A6. Diagnostics on stderr ❌
```
ludicc: error: line 1: unknown function 'print_err'
```
Only stdout exists (`print_str` → `printf`, `write_byte` → `putchar`). This is
not cosmetic: **`ludicc --emit llvm` writes IR to stdout.** A self-hosted
compiler that printed errors to stdout would interleave diagnostics into its own
output, corrupting it in exactly the case you most want a diagnostic.
**Design.** Prefer an intrinsic that yields a handle, so the existing file
plumbing is reused rather than duplicated:
```ludic
# doc-check: skip — proposed signature notation, not code
file_stderr() -> ptr # then file_write(f, buf, n) as usual
```
**Lowering — note the portability wrinkle.** There is no portable `@stderr`
global in LLVM IR: Darwin exports `@__stderrp`, glibc exports `@stderr`, and
wasm has neither in the same shape. So `file_stderr()` must select per target,
alongside the existing `target_os()` logic in `driver.c`. This is the one item
here that is genuinely target-dependent rather than merely unimplemented, and
it should be designed with that in mind rather than bolted on.
**Cost.** ~40 lines including the per-target selection.
### Tier B — needed for *complete* bootstrap, not for the compiler
#### B1. Function pointers ❌
```
ludicc: error: line 3: unknown type 'fn' for var h
```
`&cb` also fails to compile.
The compiler itself does **not** need these — `match` dispatch covers every
place a C compiler would use a function pointer table.
But they are what would let `cocoa.ll` become Ludic. The macOS window builds an
`NSView` subclass at runtime with `objc_allocateClassPair` and installs **an IR
function as its IMP**. Without the ability to take the address of a Ludic `fn`,
that shim can never move out of hand-written IR. So: irrelevant to §6, and
load-bearing for §7.4.
**Design.** `&fnname` yields a `ptr`; call through it via
`call_ptr(p, args…)` or a typed `fn(int)->int` type.
**Cost.** ~120 lines. Defer until after the fixpoint.
#### B2. String operations — **no language change needed**
`str + str` is worth calling out as a *bug*, not a gap. It passes the front-end
and then emits invalid IR:
```
build/probe_t_headless.ll:13401:17: error: global variable reference must have pointer type
```
That is a front-end/backend mismatch: the typechecker accepts an operation the
backend cannot lower. Until strings exist properly, `str + str` should be a
clean compile error rather than a `clang` error in generated code.
Everything else a compiler needs from strings is **already writable in Ludic
today** — `streq` and `itoa` are verified. This is not a language gap; it is a
library to write (§6 Stage 1), and it is the largest pure-typing chunk of the
whole project.
### Tier C — explicitly out of scope, recorded so they are not rediscovered
| Gap | Why it does not block |
|---|---|
| **64-bit integers** ❌ (`100000*100000` → `1410065408`, wraps at i32) | Line numbers, offsets, node indices and string lengths all fit in `i32`. Only matters for source files > 2 GiB. |
| **A non-ECS entry point** | A `Start`-phase system plus `os_exit(n)` gives correct exit codes — verified. You do pay for an unused 1024-entity world; that is a constant, not a blocker. A `tool Name { fn main() -> int }` form would be nicer, not necessary. |
| Closures, generics, unions, sum types | A compiler in the style of `ludicc.c` uses none of them. |
| GC | A compiler should leak deliberately (§8). |
| Unsigned integer types | `shr` is already logical and `band`/`bor` are bit-level — sufficient. |
| Multiple return values | `ptr` out-parameters work today. |
---
## 5. Designing for readers — human and model
The goal: Ludic source should be obvious to a person skimming it and
unambiguous to a model generating it. Those two goals agree far more than they
conflict, and where they conflict the resolution is **regularity, not
verbosity** (§5.2).
Everything in this section was verified against the built compiler. The probes
are in the appendix under "Syntax audit".
### 5.1 Why this belongs in the bootstrap document, and why now
**Syntax changes are cheap today and expensive after Stage 3.** This is a hard
ordering constraint, not a preference.
Today, changing the grammar costs: edit `ludicc.c`, `sed` three examples and
five runtime files, run `./test.sh`. An afternoon.
After the fixpoint, `ludicc` is *written in the syntax it parses*. Every change
becomes a four-step dance: build a compiler that accepts both old and new forms
→ compile it with the old seed → rewrite every source file → remove the old
form and regenerate the seed. That is what every mature language does, and it
is why mature languages change syntax slowly. It is not a reason to avoid the
change; it is a reason to **make it before the port, not after**.
So the plan gains a stage:
> **Stage 0.5 — Syntax freeze.** Between Stage 0 (language features) and
> Stage 1 (libraries). Nothing in Stage 2 starts until the grammar is final.
The port should be *the first large program written in final Ludic*, not the
last large program written in provisional Ludic.
**This work also strengthens the bootstrap itself.** Stage 2b uses `--fmt`
equality as the oracle proving two parsers agree. That oracle is only as tight
as the language is regular: every alternative spelling is surface variance the
formatter must erase. Reduce the variance and the oracle gets sharper. The
readability project and the self-hosting project are not competing for the same
time — one makes the other more trustworthy.
### 5.2 What actually helps a model — and what is folklore
Worth being precise here, because "AI-friendly syntax" attracts a lot of
confident nonsense.
**Genuinely helps:**
| Property | Why it matters |
|---|---|
| **Low syntactic variance** — one spelling per concept | Every alternative is a branch point during generation and a case in the parser. Two ways to write a list is two chances to be inconsistent within one file. |
| **Leading-keyword, bounded lookahead** | Every declaration and statement identifiable from its first token. Helps the hand-written recursive-descent parser Stage 2b will be, *and* a model predicting forward. |
| **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. |
| **Errors that name the fix** | Already partly true: a missing builtin errors naming `rt_<name>`. Extend that everywhere. |
**Folklore, and false:**
- *"More verbose is more AI-friendly."* No. Ceremony without information hurts
both audiences. What helps is redundancy that **encodes intent** — an explicit
type, a closing keyword — not boilerplate.
- *"Significant indentation reads better."* It reads fine and **generates
badly**: indentation drift across a long generated block is unrecoverable and
survives review. Ludic uses braces. Keep them.
- *"Natural-language-like syntax helps."* Prose-shaped keywords add ambiguity.
Consistent symbols beat English words that read three ways.
- *"Terseness is bad for models."* Terseness is fine; *irregularity* is the
problem. A short form used consistently is easy to predict.
**The real tension:** humans skim, so terseness helps them; machines benefit
from redundancy. Regularity resolves it — the same shape everywhere costs a
human nothing once learned, and costs a model nothing to predict.
### 5.3 Audit — what is irregular in Ludic today
Each row verified by compiling a probe, not by reading docs.
| # | Irregularity | Evidence | Cost |
|---|---|---|---|
| **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. |
| **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 | |
| **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
Ordered by value per line of work. Each is a Stage 0.5 item unless noted.
**S1. Require a statement terminator.** A statement ends at a newline, `;`, or
`}`. Make `T_NL` significant inside `block()` instead of discarding it.
*Why:* fixes R1, and it is the precondition for error recovery — without it a
parser cannot resynchronise, so every syntax error stays a cascade.
*Cost:* ~30 lines. *Ripple:* one-line bodies like `if x { a }` still work;
multi-statement one-liners in the runtime need a `sed`.
**S2. Make separators mandatory.** Commas required in every comma-list;
remove the optional path. *Fixes R2. Cost:* ~10 lines + tree-wide `sed`.
**S3. One spelling for boolean operators. ✅ LANDED.** `and`/`or` are the only
boolean operators. Ludic already spells bitwise operations as functions
(`band`/`bor`), so the symbols bought nothing, and dropping them removes the
`&` vs `&&` bug class by construction.
What shipped: `&&`/`||` still *lex* as single tokens, purely so the parser can
emit `'&&' is not a Ludic operator - write 'and'` instead of tripping over a
stray `&`; `and`/`or` became reserved words, so `let and = 5` is rejected at the
mistake; the AST op string is now `"and"`/`"or"`, which is **exactly the LLVM
opcode**, so the lowering ternary collapsed to passing `op` straight through;
and `--fmt` emits the new spelling for free, since it prints the op string.
Six regression tests in `test.sh` (64 → 70), including one asserting no `.ludic`
source uses the symbols outside a comment. *Fixed R3.*
**S4. Reserve every keyword.** One table, shared by the lexer, parser,
`ludic-fmt` and `ludic-lsp` — those tools already share a vocabulary in
`ludic_syntax.h`, so there is one obvious home. Reject `let query = 5` at the
point of the mistake. *Fixes R5. Cost:* ~40 lines.
**S5. Delete or implement every silent no-op.** ← **highest value in the
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
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
reasoning about a system in isolation.
- `requires` / `ensures`: either lower to a checked assertion in debug builds
(`if !cond { print_err(...) os_exit(1) }` — cheap, and A6 stderr lands in
Stage 0 anyway), or remove them from the grammar until they mean something.
- `pure`, `needs`, `uses`, `effects`, `invariant`: remove until implemented.
*Fixes R6. Cost:* ~150 lines for `reads`/`writes` checking, ~60 for assertions,
~10 to delete the rest.
**S6. Cut the block grammars from seven to two.** Full unification is too
invasive to be worth it. The achievable version: every `{ }` is either a
**statement block** or a **field list** (`name: type = default`, comma-separated,
one shape), and `ui` props adopt the same separator rule as everything else.
Document all remaining shapes in one grammar table. *Partially fixes R4.
Cost:* ~120 lines.
**S7. One formatter with one contract.** Merge the philosophies rather than
keeping two half-tools: `ludic-fmt` gains `--fmt`'s normalisation decisions
(statement-per-line, single spelling, consistent commas) while keeping its
token-based comment preservation, and becomes **normative** — `ludic-fmt
--check` gates CI. `ludicc --fmt` reverts to being an honest debug dump and is
renamed `--dump-ast`. *Fixes R8.* After S1–S3, canonical form is the *only*
form, so the formatter stops being a style preference and becomes a check.
*Cost:* ~200 lines, mostly in `ludic_fmt.h`.
**S8. Machine-readable grammar and diagnostics.** Emit the grammar as one EBNF
file, and give every diagnostic a stable code plus a one-line suggested fix
(`ludicc --explain L0412`). Feeds the LSP, the docs and any model at once.
*Cost:* ~250 lines. *Defer to after the fixpoint* — valuable, not ordering-critical.
**S9. Documentation hygiene as a build step.** `test.sh` already understands
` ```ludic ` fences. Extend it so **every fence in every `.md` must compile**,
and fix R10's stale claims. *Cost:* ~60 lines of shell. Do this early — it is
cheap and it stops the docs drifting further while the rest of the work lands.
### 5.5 What not to change
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
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.
- **Do not add** operator overloading, implicit conversions beyond `int`→`fixed`,
macros, or anything else with action-at-a-distance. Every one of them trades
local readability for cleverness.
### 5.6 Sequencing
| When | What | Why there |
|---|---|---|
| **Now, before Stage 1** | S9 (doc hygiene) | Cheap; stops further drift immediately. |
| **Stage 0.5** | ~~S3~~ ✅ done · S1, S2, S4, S5, S6, S7 | Must precede the port (§5.1). |
| **After Stage 3** | S8 (EBNF + diagnostic codes) | Valuable, not ordering-critical; better written in Ludic against the self-hosted parser. |
**S3 was the one genuinely contentious call** — which boolean spelling — because
it is pure taste and touches every file. It was decided in favour of `and`/`or`
and has landed. Everything remaining in this section is a choice between "one
spelling" and "two", where the answer is not in doubt.
**`!` → `not` has since landed too**, on the same reasoning and by the same
mechanism: `!` still lexes (so `!=` is untouched) purely so the parser can say
`'!' is not a Ludic operator - write 'not'`. Ludic's three boolean operators are
now `and`, `or`, `not`, all reserved words, with no symbol spellings at all.
---
## 5.7 Status — self-hosting achieved
Updated 2026-08-27. `./test.sh` = 93/93, `./tools/test-tools.sh` = 28/28,
`./selfhost/test.sh` = 5/5 including the bootstrap fixpoint.
**Ludic is fully self-hosted.** The compiler is written in Ludic
(`selfhost/*.ludic`, ~2,400 lines), compiles every example to byte-identical
output and its own source to a fixpoint, and is built from a checked-in IR seed
with **no C compiler** — the former C compiler has been deleted. Run
`./selfhost/bootstrap-cfree.sh`.
| Stage | What | State |
|---|---|---|
| **0** | language features (argv, struct, arrays/slices, break/continue, mem_realloc, stderr) | ✅ done |
| **0.5** | S3 (`and`/`or`/`not`), S9 (doc checking), short-circuit `and`/`or` | ✅ done |
| — | S1/S2/S4/S5/S6/S7 (statement terminators, mandatory commas, reserved-word audit, no-op removal, block unification, one formatter) | not done — polish of the *full* language, not needed for self-hosting |
| **1** | support libraries in Ludic (`str`, `buf`, `io`) | ✅ done |
| **2** | the compiler ported to Ludic (`lex`, `parse`, `emit_*`) | ✅ done |
| **3** | the fixpoint (`gen2.ll == gen3.ll`) | ✅ done |
| **4** | retire the C as a *live dependency* (IR seed, C-free rebuild) | ✅ done — `selfhost/bootstrap-cfree.sh` |
| **4+** | retire `ludicc.c` entirely (port the game backend) | ✅ **done** — `compiler/` deleted; the compiler is `selfhost/*.ludic` |
### What "self-hosting" means here, precisely
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`,
fixed-point. It targets native (macOS/clang) and emits LLVM IR text that clang
assembles, exactly the posture the C `ludicc` has.
It is written entirely in that subset, which is why it compiles itself. The
three-generation proof (`selfhost/bootstrap.sh`):
```
stage0 build/ludicc (C) compiles selfhost.ludic -> gen1 (a Ludic-written compiler)
stage1 gen1 compiles selfhost.ludic -> gen2.ll -> gen2
stage2 gen2 compiles selfhost.ludic -> gen3.ll
assert gen2.ll == gen3.ll # the compiler reproduces itself, independent of its seed
```
`gen1`'s IR legitimately differs (a different compiler built it); `gen2 == gen3`
is the property that matters — the Ludic compiler has no dependency on how it was
built. It is also verified *correct*, not merely self-consistent: it compiles a
corpus (`selfhost/tests/`) of struct, slice, and control-flow programs to
binaries that produce the expected output.
### Stage 4 — the C is retired as a live dependency
The self-hosted compiler no longer needs the C `ludicc` to exist. Its own LLVM
IR is checked in as `selfhost/ludicc.seed.ll` — a proven fixed point — and
`selfhost/bootstrap-cfree.sh` assembles that with clang (an IR assembler, the
floor Rust and Swift stand on) and rebuilds the compiler, which reproduces its
own IR. **The C source is never invoked.** This is the seed path §8 recommended.
Crucially, the compiler **evolves** without the C compiler: `selfhost/reseed.sh`
uses the *current* seed to build a compiler with new source, then takes that
compiler's own output as the new seed. New features (this session: `match`,
bitwise ops, `peek32`/`poke32`) landed and reseeded entirely C-free. The C
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,
`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.
It now compiles **every example** — `snake`, `menu`, and the 6-file JRPG
`chronorift` — to output byte-identical to the original C compiler (checked
against golden renders in `selfhost/golden/`), and still compiles its own source
to a fixpoint. The C compiler (`compiler/`, ~2,700 lines) has been **deleted**.
`build.sh` builds `build/ludicc` from the IR seed with clang and drives the
native link (headless, or windowed via `cocoa.ll`).
What did not come across: the old C driver's **wasm target, cross-compilation,
and shared-library** paths. Those are driver features, not codegen — the
self-host compiler emits native-ABI IR — and re-implementing them on the
self-hosted toolchain (wasm needs i32 `size_t`; the others are clang flags in
`build.sh`) is the remaining follow-up.
---
## 6. The plan
### Stage 0 — Extend the C compiler (~500 lines of C)
The C `ludicc` must be able to compile the Ludic `ludicc`. Land Tier A only, in
this order — cheapest-and-unblocking first:
1. **A4** `break`/`continue` (~40) — immediate readability win on everything after.
2. **A5** `mem_realloc` (~6).
3. **A1** `os_argc`/`os_arg` (~30) — unblocks the entire notion of a CLI tool.
4. **A6** `file_stderr` (~40).
5. **A2 + A3** `struct` + arrays (~400, landed together).
Each gets a test in `test.sh` as it lands. The suite is at 64/64; Stage 0 should
leave it green and larger.
**Explicitly not in Stage 0:** function pointers, 64-bit ints, a `tool` entry
form. They are not on the path to the fixpoint.
### Stage 0.5 — Syntax freeze (~600 lines of C + a tree-wide `sed`)
**The grammar must be final before Stage 2 starts** (§5.1): after the fixpoint,
every syntax change costs a four-step reseed instead of an afternoon.
Land S1–S7 from §5.4: mandatory statement terminators, mandatory separators,
one boolean spelling, reserved keywords, no silent no-ops, two block shapes
instead of seven, one normative formatter. S9 (doc hygiene) can land earlier —
it is cheap and independent.
Exit criterion: `ludic-fmt --check` passes on the whole tree and there is
exactly one legal spelling of every construct. That is also what makes the
Stage 2b oracle tight.
### Stage 1 — Support libraries in Ludic (~800 lines of Ludic, zero language work)
Nothing here needs Stage 0 except `struct`/arrays for pleasantness. This is the
part that is pure writing, and it can start immediately and in parallel.
| File | Contents |
|---|---|
| `runtime/native/strings.ludic` | `str_eq`, `str_len`, `str_dup`, `str_cat`, `substr`, `str_chr`, `str_hash`, `itoa`, `atoi`, `hex` |
| `runtime/native/buf.ludic` | growable byte buffer — `buf_new`, `buf_putc`, `buf_puts`, `buf_putint`, `buf_len`, `buf_ptr`. This is `SB` from `ludicc.c`, and the IR emitter is nothing but calls to it. |
| `runtime/native/io.ludic` | `read_whole_file` (the open/seek/tell/read/close dance, verified working), `write_whole_file`, stderr diagnostics |
| `runtime/native/map.ludic` | open-addressing `str -> int` hash table: keyword lookup, string interning, symbol tables |
| `runtime/native/arena.ludic` | bump allocator — see §8 |
### Stage 2 — Port the compiler, each piece against a differential oracle
Port in dependency order. The critical discipline: **never port a stage without
an automated way to prove it agrees with the C one.** Ludic is unusually well
set up for this, because it already ships two canonical serializers of compiler
internals.
| Sub-stage | Port | Differential oracle |
|---|---|---|
| 2a | `lex.ludic` | Dump the token stream from both compilers; `diff` over every `.ludic` in the tree. |
| 2b | `parse.ludic` (AST) | **`--fmt` is a free oracle.** The formatter is already a canonical AST printer, and `test.sh` already asserts formatting never changes a program. If both compilers' `--fmt` output is byte-identical on every file, the parsers agree. |
| 2c | `check.ludic` | Diagnostic text must match on a corpus of deliberately-broken programs. `test.sh` already checks diagnostics — extend that corpus. |
| 2d | `emit.ludic` (IR) | **`--emit llvm` must be byte-identical** for every example. This is the strongest oracle available: pass/fail on exact text, no judgement. |
| 2e | `drive.ludic` | Assemble and link via `clang`; compare final binaries. |
Sub-stage 2b deserves emphasis. Most self-hosting projects have no cheap way to
prove two parsers agree. Ludic has one already built and already tested, which
removes the single largest source of silent divergence.
### Stage 3 — The fixpoint
```
stage1 = C-ludicc compiles ludicc.ludic -> binary A
stage2 = A compiles ludicc.ludic -> binary B
stage3 = B compiles ludicc.ludic -> binary C
assert B == C byte-for-byte <- THE bootstrap test
```
`A != B` is expected and correct: `A` was built by a different compiler, so its
codegen differs. `B == C` is the real property — a compiler that reproduces
itself has no dependency on how it was built. Also assert that `A`, `B` and `C`
all emit identical IR for every example.
If `B != C`, the cause is almost always nondeterminism in the compiler itself:
hash-table iteration order, an address baked into output, uninitialised memory.
Those are worth hunting rather than working around.
### Stage 4 — Retire the C
Once the fixpoint holds, the C compiler becomes a seed. Options:
| Option | Trade-off |
|---|---|
| **Commit the generated `ludicc.ll`** ✅ recommended | Auditable text, diffable in review, builds with `clang` alone — already a dependency. Large but honest. |
| Commit prebuilt binaries per platform | Smallest process, worst auditability; a binary blob nobody can read. What Rust does. |
| Keep `ludicc.c` forever as the seed | Zero risk, but §1's bar is never met — the C never leaves. What Go did for years. |
Recommend the IR seed: it is the only option that both removes the C and leaves
a reviewer something to read.
`tools/ludic-tools/` (3,260 lines of C: `ludic-fmt`, `ludic-lsp`) is a separate
port and should follow, not lead — once the Ludic compiler exists, both tools
should be thin front-ends over its lexer and parser instead of maintaining a
second copy of the vocabulary.
---
## 7. Toolchain independence — and where to stop
`driver.c` shells out to `clang` (overridable via `$LUDIC_CC`) to assemble IR
into an object and to link, plus `wasm-ld` for wasm. Three levels of removing
that, and only one is worth doing:
**Level 1 — self-hosted compiler, hosted toolchain. ← the goal.**
`ludicc` is Ludic; `clang` remains the IR assembler and linker driver. This is
exactly where Rust and Swift stand. Achieved at the end of Stage 4.
**Level 2 — own object writer.** Emit Mach-O / ELF / COFF directly, replacing
IR-text + `clang -c`. Requires instruction selection, register allocation and
relocations: realistically 5,000–15,000 lines of Ludic, and it *loses the LLVM
optimizer* — the generated code gets slower, which for a game language is a
real regression, not a neutral trade. **Not recommended.**
**Level 3 — own linker.** Platform-specific, deep, and buys nothing a user can
perceive. **No.**
**7.4 — The hand-written IR.** `cocoa.ll` (327 lines) and `wasm.ll` are LLVM IR,
not Ludic. Two defensible positions: keep them as *platform glue written in the
platform's own assembly language* (precisely how Rust uses `asm!` shims and how
every libc has hand-written syscall stubs), or move them into `.ludic` — which
needs **B1 function pointers**, because the `NSView` subclass installs a
function as an Objective-C IMP. Keeping them is the honest default; the README's
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
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
its tables (A5) rather than reproduce the limits.
- **Leak on purpose.** A compiler runs once and exits. A bump arena
(`arena.ludic`) that never frees is faster and simpler than tracked
ownership, and it sidesteps having no GC. Free at process exit — i.e. never.
- **Determinism is a feature now.** Anything order-dependent — hash iteration,
pointer values in output, uninitialised reads — breaks `B == C` in Stage 3.
Iterate tables in insertion order, not bucket order.
- **Error handling has no exceptions.** Mirror the C `die()`: write the
diagnostic to stderr (A6), then `os_exit(1)`.
- **The `str + str` mismatch (B2)** is a live example of the front-end accepting
what the backend cannot lower. Worth auditing for siblings before trusting
the typechecker as a Stage 2c oracle.
- **Size-taking intrinsics must use `ll_size_t()` / `ll_widen()`.** `size_t` is
`i32` on wasm32. Any new intrinsic with a size argument (A5) that hardcodes
`i64` will break the wasm target at link time.
- **Recursion depth is fine** — 5,000 frames verified, well past what a
recursive-descent parser needs on real source.
---
## 9. Effort
| Stage | Work | State |
|---|---|---|
| 0 | Tier A language features (argv, struct, slices, break/continue, mem_realloc, stderr) | ✅ done |
| 0.5 | `and`/`or`/`not` + short-circuit; doc checking (S9) | ✅ done (S1/S2/S4/S5/S6/S7 deferred — full-language polish) |
| 1 | `str`, `buf`, `io` support libraries in Ludic | ✅ done (`selfhost/`) |
| 2 | lexer + parser + AST + IR emitter, in Ludic | ✅ done (`selfhost/`, ~1,300 lines) |
| 3 | the fixpoint (`gen2.ll == gen3.ll`) + harness | ✅ done (`selfhost/bootstrap.sh`) |
| 4 | port the game backend, retire `ludicc.c` | ⛔ out of scope — mechanical continuation |
The self-host compiler is **~1,300 lines of Ludic** covering the compiler-subset.
A `main`-tool entry point and short-circuit `and`/`or` were the two language
additions that made it self-compilable; the rest of Stage 0 was already in place.
Roughly **6,000 lines of Ludic and 1,100 lines of C** to reach Level 1 — larger
than the 2,508-line C compiler it replaces, which is normal: the C version leans
on libc for everything in Stage 1.
**Two independent critical paths, and they can run in parallel.** Stage 0 + Stage 0.5
are C work on the existing compiler; Stage 1 is Ludic work that needs almost none of
it. The only hard barrier is that Stage 2 starts after *both*.
**The critical path is short.** A1 (argv, ~30 lines of C) plus A2/A3
(`struct` + arrays, ~400) plus A4 (`break`, ~40) is nearly all the *design* risk
in the project. Everything after it is typing against oracles that already
exist.
---
## Appendix — probe programs
Each was compiled with `./build.sh probe.ludic --headless` and run against the
current tree (`./test.sh` = 64/64).
**Recursion** ✅ → `55`
```ludic
game 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() }
}
```
**String comparison, hand-written** ✅ → `1`
```ludic
fn streq(a: ptr, b: ptr) -> bool {
let i = 0
while true {
let ca = peek8(a,i)
let cb = peek8(b,i)
if ca != cb { return false }
if ca == 0 { return true }
i = i + 1
}
return false
}
```
**Integer → string, hand-written** ✅ → `48291`
```ludic
fn itoa(v: int, buf: ptr) -> int {
let n = 0
let x = v
if x == 0 { poke8(buf,0,48) return 1 }
let tmp = mem_alloc(16)
while x > 0 { poke8(tmp, n, 48 + x % 10) x = x / 10 n = n + 1 }
let i = 0
while i < n { poke8(buf, i, peek8(tmp, n-1-i)) i = i + 1 }
mem_free(tmp)
return n
}
```
**Read a whole file** ✅ → the compiler's front door
```ludic
let f = file_open("/tmp/x.txt", "rb")
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let b = mem_alloc(n+1)
file_read(f, b, n)
poke8(b, n, 0)
file_close(f)
```
### Syntax audit — every one of these compiles today
Each is a spelling the language accepts; the point is that the *alternative*
spelling is equally legal (§5.3).
**R1 — three statements on one line, no separators** → `2`
```ludic
game P { system 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 }
spawn Hero { Pos = { x = 7 y = 2 } }
```
**R3 — RESOLVED.** Every symbol form is now rejected where it is written:
```
ludicc: error: line 1: '&&' is not a Ludic operator - write 'and' (got '&&')
ludicc: error: line 1: '||' is not a Ludic operator - write 'or' (got '||')
ludicc: error: line 1: '!' is not a Ludic operator - write 'not' (got '!')
ludicc: error: line 1: 'and' is a reserved operator and cannot be used as a name
```
`--fmt` prints `if ((true and false) or (1 < 2))` and `(not true)`, while unary
minus keeps its tight spelling `(-x)`. `!=` is untouched.
**R5 — reserved-looking words used as locals** → `11`
```ludic
let query = 5
let phase = 6
print_int(query + phase)
```
**R6 — contracts accepted and discarded.** Both are violated; it compiles and
prints `4`:
```ludic
# doc-check: skip — composite: declaration plus statements
fn half(n: int) -> int requires n > 100000 ensures false { return n / 2 }
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 }
```
**R8 — the two formatters disagree about what "format" means.** Given
`component 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**.
**Verified-missing** — each a compile error today:
```ludic
# doc-check: expect-error — every line here is a compile error by design
while i < 10 { i = i + 1 if i == 3 { break } } # unknown identifier 'break'
struct Node { k: int, a: ptr } # expected declaration (got 'struct')
var t: [int; 8] # expected identifier (got '[')
var h: fn = a # unknown type 'fn' for var h
let p = &cb # fails to compile
print_int(os_argc()) # unknown function 'os_argc'
print_err("x") # unknown function 'print_err'
let p = mem_realloc(ptr_null(), 10) # unknown function 'mem_realloc'
print_str("ab" + "cd") # passes front-end, invalid IR
let a = 100000 print_int(a*100000) # 1410065408 — i32 wrap
```

352
COMPILING.md Normal file
View file

@ -0,0 +1,352 @@
# Compiling Ludic
> **Note (2026-08-27):** `ludicc` is now **written in Ludic** (`selfhost/*.ludic`)
> and built from a checked-in IR seed — the C compiler this document describes has
> been deleted. The native pipeline below (Ludic → LLVM IR → object → binary) is
> unchanged. `ludicc` now drives clang itself (via an `os_system` intrinsic), so
> `ludicc app.ludic -o bin/app` and `--emit-llvm` work directly, and a sibling
> command `ludic app.ludic` compiles to a temporary binary and runs it in one
> step. Build both with `./build-cli.sh`. `build.sh` remains as a convenience
> wrapper. `--fmt` is reimplemented as a lex+parse gate (the doc-check hook).
> The `--target`/cross-compile and `--shared` paths are still features of the old
> C driver not yet re-implemented on the self-hosted toolchain. See
> BOOTSTRAP.md §5.7.
>
> ```bash
> ./build-cli.sh # build ./ludicc and ./ludic (from the seed)
> ./ludicc examples/snake.ludic -o bin/snake # compile
> ./ludic examples/snake.ludic # compile + run
> ```
>
> The binaries are multi-call (one native binary under two names): invoked as
> `ludicc` it compiles, as `ludic` it compiles-and-runs. A `.ludic` file with
> systems is a game and links windowed by default; `--headless` and `--windowed`
> force the mode. The runtime (`runtime/native/cocoa.ll`) is found via
> `$LUDIC_HOME`, defaulting to the directory the binary sits in — keep them at the
> repo root, or set `LUDIC_HOME` and put them on `PATH`. `$LUDIC_CC` overrides the
> assembler/linker (default `clang`).
`ludicc` is a compiler, not a translator. It lexes, parses, checks and lowers
Ludic to **LLVM IR itself**, then hands that IR to the system toolchain to be
assembled and linked. There is no C in the middle: no generated `.c` file, no C
runtime compiled alongside your game, and no transpiling step you could inspect
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
│ IR assembler (compiler/driver.c)
▼
app.o Mach-O / ELF / COFF object code
│ system linker
▼
app or libapp.dylib / .so / .dll
```
`clang` appears in that pipeline twice — as the IR assembler and as the linker
driver — which is the same role `rustc` and `swiftc` give it. Set `LUDIC_CC` to
point at a different LLVM toolchain if you have one.
## Artifacts
| you want | command |
| --- | --- |
| a windowed native executable | `ludicc game.ludic -o build/game` |
| a headless executable | `ludicc game.ludic --headless -o build/game` |
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
| an object file † | `ludicc src.ludic -c -o src.o` |
† `--shared`, `--target`/cross-compile, `-c` and the wasm path were features of
the old C driver and are **not yet re-implemented** on the self-hosted toolchain
(see the note at the top). The rows above the line work today via the
self-hosted `ludicc`.
`build.sh` wraps the common cases:
```bash
./build.sh examples/snake.ludic # -> build/snake (native)
./build.sh examples/lib/combat.ludic --lib # -> build/libcombat.* (library)
./build.sh examples/snake.ludic --headless # -> build/snake_headless (out.ppm)
./build.sh examples/snake.ludic --web # -> build/web/ (browser)
```
## Programs and libraries
> **Not yet on the self-hosted toolchain.** `--shared` and the `nm`/library
> workflow below describe the old C driver's behavior; the self-hosted `ludicc`
> builds executables only for now. The `module`/`export fn` semantics are
> unchanged — only the packaging step is pending.
A source file opens with `game Name { … }` or `module Name { … }`.
* A **game** gets an entry point and the phase-ordered frame loop
(`Start`, then `Input → FixedUpdate → Update → LateUpdate → Render` each tick).
* A **module** gets neither. It is a library, and only its `export fn`s become
public symbols; everything else stays private to the library.
```ludic
# doc-check: skip — illustrative: elided body
module Combat {
export fn damage(attack: int, armour: int, roll: int) -> int { … }
fn curve(level: int) -> int { … } # private: not a symbol
}
```
```bash
ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib
nm -gU build/libcombat.dylib
# T _damage T _hits_to_kill T _xp_for (no _curve)
```
Those are ordinary C-ABI symbols, so anything that can call a shared library can
call Ludic. To call them from another Ludic program, declare them and link:
```ludic
extern fn damage(attack: int, armour: int, roll: int) -> int = "damage"
```
```bash
ludicc examples/lib/arena.ludic -o build/arena -Lbuild -lcombat
```
Libraries are linked as `@rpath/…` (`$ORIGIN` on Linux) and executables search
next to themselves, so a built pair keeps working when you move it.
## Cross-compilation
> **Not yet on the self-hosted toolchain.** `--target` and `-c` were old
> C-driver flags; the self-hosted `ludicc` builds only for the host today. The
> section below records the intended design — object code for ELF, COFF and
> Mach-O from one source — which the IR pipeline already supports in principle.
`--target` takes an LLVM triple and retargets the whole pipeline:
```bash
ludicc game.ludic --target x86_64-unknown-linux-gnu -c -o game-linux.o
ludicc game.ludic --target aarch64-unknown-linux-gnu -c -o game-arm64.o
ludicc game.ludic --target x86_64-pc-windows-msvc -c -o game-win.o
```
Object code for ELF, COFF and Mach-O comes out of the same source with no
per-platform branches in the compiler. Linking a foreign target additionally
needs that platform's linker and sysroot, as with any cross toolchain.
## The runtime is written in Ludic
`runtime/native/core.ludic` implements the framebuffer, `fill_rect`, the 5×7
bitmap text, the registers, the RNG, input and the frame dump — in Ludic. ludicc
splices it into every native build, and a builtin call in a game resolves to a
runtime function by name: `clear(c)` calls `rt_clear(c)`. Replace that file and
you have replaced the runtime; pass `--freestanding` to build without it.
Underneath the runtime there is exactly one layer, and it is not C: a set of
compiler intrinsics that lower to direct calls into the platform ABI.
| intrinsic | lowers to |
| --- | --- |
| `mem_alloc(n) -> ptr`, `mem_free`, `mem_copy`, `mem_set` | `malloc`, `free`, `memcpy`, `memset` |
| `peek8/peek32(p, i) -> int`, `poke8/poke32(p, i, v)` | `load` / `store` |
| `ptr_add(p, n) -> ptr`, `ptr_null()`, `ptr_is_null(p)` | `getelementptr`, `null` |
| `file_open(path, mode) -> ptr`, `file_read`, `file_write`, `file_close` | `fopen`, `fread`, `fwrite`, `fclose` |
| `read_byte() -> int`, `write_byte(c)`, `print_str(s)`, `print_int(n)` | `getchar`, `putchar`, `printf` |
| `str_len(s) -> int`, `os_exit(code)`, `os_time() -> int` | `strlen`, `exit`, `time` |
That is the operating system's interface — the floor Rust and Swift stand on
too. Everything above it, including all the graphics, is Ludic.
The runtime protocol is four optional functions. Define them (or let the
prelude define them) and the entry point calls them:
| function | when |
| --- | --- |
| `rt_init()` | once, before the `Start` systems |
| `rt_poll() -> int` | once per frame; its result is what `key()` reads |
| `rt_running() -> bool` | each frame; false ends the loop |
| `rt_shutdown()` | after the loop |
## The window
`runtime/native/cocoa.ll` is the macOS platform layer, written in LLVM IR. It
talks to the Objective-C runtime through its C ABI — `objc_getClass`,
`sel_registerName`, `objc_msgSend` — and to Quartz through CoreGraphics, which
is what a compiled `.m` file does anyway; this just skips the `.m`. AppKit
paints through `-drawRect:`, so the view class is built at runtime with
`objc_allocateClassPair` and an IR function is installed as its IMP.
ludicc assembles it exactly like the program's own IR and hands both objects to
the linker, adding `-framework Cocoa`. A `--headless` build omits it entirely,
reads keys from stdin and writes the last frame to `out.ppm`; the `win_*`
intrinsics compile to nothing there, so a headless binary never references a
symbol the window would have provided.
Other platforms build headless today. A Win32 or X11 port is another `.ll` file
with the same five entry points — `win_open`, `win_poll`, `win_present`,
`win_running`, `win_close` — and no compiler change.
## The web
WebAssembly is a target, not a port. The front end, the type checker, the ECS
lowering and the Ludic-written runtime are the same ones a macOS build uses;
only the triple changes.
```
game.ludic
│ ludicc — the same lex, parse, check and lower
▼
game.ll LLVM IR, triple wasm32-unknown-unknown
│ IR assembler
▼
game.o + wasm.o (runtime/web/wasm.ll, the platform layer)
│ wasm-ld
▼
game.wasm + index.html + platform.js + assets.json + the assets
```
```bash
./build.sh examples/chronorift.ludic --web
python3 -m http.server -d build/web 8000 # then open http://localhost:8000/
```
`build/web/` is self-contained: copy it to any static host — GitHub Pages, S3,
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
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
services wasm has no OS to ask for. It is the web's Cocoa, not an interpreter.
### The toolchain
A wasm build needs an LLVM with the WebAssembly backend and `wasm-ld`. Linux
distributions ship both in `clang` and `lld`, so nothing extra is needed there
or in CI. Apple's clang is built without the WebAssembly target, so on macOS:
```bash
brew install llvm
```
ludicc looks in `/opt/homebrew/opt/llvm/bin` and `/usr/local/opt/llvm/bin`
before falling back to `PATH`. `$LUDIC_CC` and `$LUDIC_WASM_LD` override both,
so any LLVM works — a distro one, a downloaded release, `zig cc`, wasi-sdk.
### Who owns the frame loop
A native build runs the loop:
```c
ludic_boot(); while (ludic_alive()) ludic_frame(); ludic_teardown();
```
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
they run in, are identical; only the owner of the loop differs.
### The floor
`wasm32-unknown-unknown` has no libc, so `runtime/web/wasm.ll` *is* the floor —
hand-written LLVM IR, assembled by the same toolchain as everything else:
| what | how |
| --- | --- |
| `malloc` / `free` | a first-fit free list over linear memory, growing it with `memory.grow` |
| `memcpy` / `memset` | the `memory.copy` / `memory.fill` instructions (`-mbulk-memory`) |
| `strlen` | a byte loop |
| `fopen` / `fread` / `fwrite` / `fclose` / `fseek` / `ftell` | wasm imports, over a preloaded asset image and `localStorage` |
| `getchar` / `putchar` / `print_str` / `time` / `exit` | wasm imports |
| `win_open` / `win_poll` / `win_present` / `win_running` / `win_close` | wasm imports, implemented against a `<canvas>` |
Nothing above that file changes for the web: `core.ludic`, `image.ludic`,
`inflate.ludic`, `truetype.ludic` and `ui.ludic` compile to wasm unmodified.
### Assets and saves
The browser has no synchronous file access, and `file_open()` is synchronous, so
a web build ships an image of its files instead of a filesystem. ludicc records
every string literal in the program that names a file existing at compile time,
writes the list to `assets.json`, and copies the files into the bundle;
`platform.js` fetches them all before the first frame. `file_open()` then
resolves exactly the paths it resolves natively.
That is a heuristic, and a deliberately visible one: a path the compiler never
sees written down is a path the browser cannot be told to fetch ahead of time,
and a path outside the project (`/System/Library/Fonts/…`) is refused with a
warning rather than silently dropped.
Writes go the other way. `file_open(path, "wb")` buffers and commits to
`localStorage` on close, so `save()` / `load()` survive a page reload, and a
read prefers a save the player has made over the shipped asset of the same name.
### Testing a wasm build
`--headless --target wasm32-unknown-unknown` produces a bare module with no
page, driven by a runner instead of a browser:
```bash
node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss
```
Because Ludic is fixed-point and its RNG is seeded, the native headless binary
and the wasm one must render byte-identical frames from the same input. `test.sh`
asserts exactly that, which is a much stronger check on the backend than
"it started".
## What a build contains
Everything: components and archetypes, spawn/despawn, queries with bindings,
`where` filters and archetype 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
graphics stack — framebuffer, PNG decoding, sprites, 9-slice, TrueType text and
the retained UI.
None of it goes through C. `./test.sh` asserts that directly: no C source
survives in `runtime/`, no C emitter survives in `ludicc`, and the examples all
build, run and render from IR alone.
## Every flag
The self-hosted `ludicc`/`ludic` (built with `./build-cli.sh`) accept:
```
<file.ludic> the program to compile (first non-flag argument)
-o <path> output binary; with --emit-llvm, the IR path.
Parent directories are created. With no -o and not
invoked as `ludic`, the IR is written to stdout.
--windowed force a windowed (Cocoa) build
--headless force a headless build (stdin input, out.ppm output)
--emit-llvm stop at LLVM IR — write it and exit, no clang
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
(the check-docs gate; canonical formatting not yet restored)
--save-temps keep the intermediate .ll
--run compile then run (implicit when invoked as `ludic`)
(unknown -flags are ignored with a warning, never taken as the input file)
environment:
LUDIC_CC the LLVM that assembles IR and drives the linker (clang)
LUDIC_HOME where runtime/native/ lives (default: the binary's dir)
```
Mode is automatic when neither `--windowed` nor `--headless` is given: a program
with `system`s (a game) links windowed, anything else headless.
Not yet re-implemented on the self-hosted toolchain (old C-driver flags):
`--shared`, `--emit <kind>`, `-c`, `--target`/cross-compile,
`--freestanding`, `-v`, and the explicit link inputs (`-L`/`-l`/`-framework`/
`-Wl`). Those, plus `LUDIC_WASM_LD`/`LUDIC_RUNTIME_DIR`/`LUDIC_RUNTIME`, describe
the previous driver and are documented here as intended design.
There is one backend. `ludicc` has no mode that emits C, and no part of a
build compiles or links a C translation unit — including the web one, where the
platform layer is LLVM IR and the loader is 300 lines of JavaScript that never
sees a game rule.

567
LANGUAGE.md Normal file
View file

@ -0,0 +1,567 @@
# The Ludic Language — Reference
This documents the Ludic language **as actually implemented** by
`compiler/ludicc.c`. Ludic is an AI-first, statically-typed, ahead-of-time
compiled language for games: an ECS is built into the language, and programs
compile straight to machine code.
```
program.ludic ──ludicc──▶ program.ll ──▶ program.o ──▶ native exe / shared lib (LLVM IR; no C)
```
`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.
## Program structure
A program is one `game` block containing declarations:
```ludic
# doc-check: skip — illustrative: elided import list
game Name {
import ... # pull declarations in from another file
component ... # data (per entity)
struct ... # a plain record, not tied to an entity
archetype ... # a named entity KIND (bundle of components)
const ... # compile-time constants
fn ... # functions
extern fn ... # bind a C library symbol (FFI)
system ... # behavior, grouped into phases
}
```
## Multi-file programs (`import`)
```ludic
# doc-check: skip — paths resolve only inside the repo
game 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
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),
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 }
```
## Archetypes (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
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 }
archetype Player { Pos, Stats } # Player IS a kind, not a component
archetype Enemy { Pos, Stats }
spawn Player { Pos = { x = 5 } } # attaches every listed component
# (seeding field defaults), then overrides
for (p, s) in query [Pos, Stats, {Player}] { ... } # {Player} filters by kind
```
Use `{Name}` (tag position) to filter a query by archetype — an archetype can't
be *bound* to a variable since it has no fields of its own. Entity kind is part
of the saved snapshot.
## Text, fonts & images
The 5×7 bitmap `text` stays for zero-asset programs. For real typography, load a
TrueType font and draw UTF-8:
```ludic
let f = font_load("/System/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
```
The runtime ships a from-scratch TrueType engine (sfnt tables, cmap 0/4/6/12,
simple + composite `glyf` outlines, quadratic Béziers, supersampled AA) and a
glyph cache — no external font library. Arbitrary-size PNGs load as images:
```ludic
let panel = image_load("assets/ui/panel.png")
draw_9slice(panel, x, y, w, h, 10) # stretch edges/center, keep 10px corners
draw_image_scaled(icon, x, y, 32, 32)
```
## Retained UI (`ui`)
UI is declared as **data** — a widget tree. The engine owns layout (stacked
panels with padding / gap / alignment / grow), drawing (9-slice skins, images,
TrueType text, focus highlight) and keyboard focus + activation.
```ludic
ui MainMenu {
panel id=Root w=288 pad=16 gap=6 skin="assets/ui/panel.png" inset=10 align=center {
label text="CHRONO RIFT" font=reg(R_FONT) size=26 fg=0xffe060 align=center
button id=NewGame text="New Game" font=reg(R_FONT) size=16 w=236
button id=Quit text="Quit" font=reg(R_FONT) size=16 w=236
}
}
```
Widget types: `panel` (container + optional skin/bg/border), `col` / `row`
(pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are
evaluated at build time, so `font=reg(R_FONT)` reads a value the game set first.
Each `id=Name` mints a `UI_Name` handle (the `ui` block name too), used from
systems:
```ludic
system 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 {
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() }
```
See `examples/menu.ludic` for a complete title screen.
## Types
| Type | Meaning | LLVM IR type |
|------|---------|--------------|
| `int` | 32-bit integer | `i32` |
| `fixed` | Q16.16 fixed-point | `i32` |
| `bool` | boolean | `i32` |
| `entity` | entity handle | `i32` |
| `str` | string literal | `ptr` |
| `ptr` | raw address (runtime/FFI) | `ptr` |
Numeric literals: `42` and `0x1affff` are `int`; a literal with a decimal
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
```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)
spawn Hero { # create an entity
Pos = { x = 10, y = 5 }
Player = { }
}
despawn self() # remove the current entity
# iterate every entity that has all listed components:
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
program. `self()` yields the entity of the innermost `query` loop.
## Systems & phases
```ludic
system 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
{ 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 system` marks a system that touches the outside world.
### 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
that entity — the query header is simply hoisted out of the body into the
signature:
```ludic
system CleanBattle phase LateUpdate
query (b, p) [Battle, Pos, {Enemy}] where b.hp <= 0
{ despawn self() }
```
is the same program as
```ludic
system 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
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
`query` clause runs once per tick, as before.
### Conditions
A query selects on more than *which* components 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
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
with no `query` clause, wrapping an inline query — as `CleanBattle` does in
`examples/chronorift/combat.ludic`.
### Matching is lazy, not snapshotted
Both forms iterate entities by id and re-check the match as they reach each one;
there is no per-tick array of matched entities. Consequences worth knowing:
* `despawn` of the current entity, or of one already visited, is safe.
* 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.
## Structs, arrays and slices
`struct` is the aggregate that is *not* tied to an entity — a plain record, for
the data a program keeps outside the ECS.
```ludic
struct Tok { kind: int = 0, line: int = 0, next: Tok }
system Lex phase Update {
let t = new Tok # allocates; every field seeded from its default
t.kind = 1
}
```
Struct values have **reference semantics**: a struct value is a pointer to the
object, so assigning or passing one shares it rather than copying.
```ludic
struct Tok { kind: int = 0, line: int = 0, next: Tok }
fn bump(t: Tok) -> void { t.kind = t.kind + 1 }
system Share phase Update {
let a = new Tok
let b = a # b and a are the SAME object
b.kind = 9
print_int(a.kind) # 9
bump(a) # the mutation is visible to the caller
print_int(a.kind) # 10
}
```
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 {
let a = new Tok
let b = new Tok
a.next = b
print_int(a.next.kind)
a.next.kind = 42 # chains on the left of an assignment too
}
```
Two array forms. `[]T` is the growable slice (below) and is implemented. `[T; N]`
is a **fixed array** — stored inline and zeroed — and is a design target: the
self-hosted compiler's `ptype` parses `[]T` but **not** `[T; N]` yet, so the
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 {
let buf: [int; 4] # a local; no initializer needed
buf[0] = 10
table[2] = buf[0]
}
```
`[]T` is a **growable slice** — a pointer to a header holding data, length and
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 {
let toks = new []Tok
push(toks, new Tok)
for i in 0 .. len(toks) { print_int(toks[i].kind) }
}
```
Indexing works as both a value and an assignment target, and composes with
fields: `toks[i].kind = T_ID` is a single address computation.
## Functions & FFI
```ludic
fn heal(amount: int) -> int { return amount * 2 }
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # bind a C symbol
```
`extern fn … = "symbol"` declares a foreign function and binds it to a symbol
resolved at link time; pass `-L`/`-l` to ludicc to link its library. This is how
Ludic calls anything with a C ABI — including a shared library built from
another `.ludic` file (see `examples/lib/`).
## Statements
`let x = expr` · `x = expr` (`+= -= *= /=`) · `if/else` · `when cond { }`
(if-without-else) · `while cond { }` · `for i in a .. b { }` (numeric range) ·
`for (…) in query […] { }` · `break` · `continue` · `return` · `spawn` ·
`despawn` · `match` · `machine`.
`break` and `continue` apply to the innermost enclosing loop, and work in all
three loop forms — `while`, the numeric `for`, and the ECS query loop, where
`continue` advances to the next matching entity. Using either outside a loop is
a compile error.
## Pattern matching & state machines
`match` replaces `if`-ladders on one value. Arms list one or more literal
patterns (or `_` for the default) and a body:
```ludic
match tile {
'T', '#' => return SPR_TREE # multiple patterns per arm
'D' => return SPR_DOOR
_ => return SPR_GRASS # optional default
}
```
`machine` turns a register into an explicit state machine: it dispatches on the
register's value to the matching `state`, and `become` transitions to a named
state (no more `if phase == N` chains). See the co-op battle in
`examples/chronorift/combat.ludic`:
```ludic
# doc-check: skip — illustrative: elided bodies
machine R_PHASE {
state KnightMenu = 0 { … if is_confirm(k) { …attack… become KnightResolve } }
state KnightResolve = 1 { … become MageMenu }
state EnemyTurn = 4 { … become KnightMenu }
}
```
A `machine <reg>` reads `reg(<reg>)` to pick the state; `become Name` compiles to
`setreg(<reg>, <Name's value>)`. Both lower to plain branches (and `match` runs
on the native LLVM backend too).
## Expressions
Precedence: `or → and → compar(< <= > >= == !=) → + - → * / % → unary(- not) →
postfix(. () )`. Operators are built-in only (no overloading). The boolean
operators are spelled **`and` / `or` / `not`**; `&&` and `||` are not Ludic operators
and `!` are rejected with a diagnostic naming the fix (`!=` is unaffected). Bitwise operations are
functions (`band`, `bor`, `bxor`, `bnot`, `shl`, `shr`), so the symbols are free
— which is why there is only one spelling to remember. `expr with { field = … }` is not implemented; records appear
only in `spawn`. Char literals (`'w'`) are `int` code points; colors are hex
ints (`0xff8800`).
## Builtins (the standard library / runtime surface)
```
# math min max abs clamp (int)
# rng seed(i) rng_range(lo,hi)->int rng_chance(pct)->bool (deterministic)
# fixed fx(i)->fixed flr(f)->int
# tilemap map_size(w,h) map_row(y,str) tile(x,y)->int
# 2D draw clear(color) fill_rect(x,y,w,h,color) frame_rect(...) put_px(x,y,color)
# draw_sprite(id,x,y) draw_sprite_scaled(id,x,y,scale) present()
# text text(x,y,str,color,scale) text_int(x,y,n,color,scale) (5x7 bitmap)
# fonts font_load(path)->id (TrueType .ttf/.ttc)
# text_ttf(font,x,y,utf8,color,px) text_w(font,utf8,px)->int text_h(font,px)->int
# images image_load(path)->id draw_image(id,x,y) draw_image_scaled(id,x,y,w,h)
# draw_9slice(id,x,y,w,h,inset)
# UI ui_build() ui_open(id) ui_tick(key) ui_render()
# ui_clicked(id)->bool ui_set_text(id,str) ui_set_int(id,n)
# 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)
# entity self()->entity
# save save() load()->bool (binary snapshot of the whole ECS World)
# control quit() print_int(i)
# process os_argc()->int os_arg(i)->str (the command line; argv[0] included)
# file_stderr()->ptr (a handle for file_write, off stdout)
```
## 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` (compile) and `ludic` (compile-and-run) are one multi-call binary built
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
links windowed, otherwise headless; an explicit flag always wins.
The retired C driver's `--shared`, `--fmt`, `-c`, cross-compile (`--target`) and
wasm modes are **not** on the self-hosted toolchain (see "Not yet implemented").
Source formatting now lives in the standalone `build/ludic-fmt` (below), not a
compiler flag.
The self-hosted compiler is intentionally permissive: it has no separate
validation pass yet, so unknown types lower to `ptr` and call arity is not
checked. Diagnostics are limited to parse-level errors
(`ludicc(self): parse error: …`); richer static checks (unknown identifiers,
duplicate types, unknown fields, arity) are future work.
### Editors
```bash
./tools/build-tools.sh # -> build/ludic-fmt, build/ludic-lsp
build/ludic-fmt -w src/ # format in place (keeps comments)
build/ludic-fmt --check . # CI: exit 1 if anything is unformatted
build/ludic-lsp --stdio # the language server, for any editor
```
`ludic-fmt` is the source formatter: it works on tokens, so comments and blank
lines survive and no file is ever rewritten into another. `ludic-lsp` speaks
LSP 3.17 and supplies completion, diagnostics, hover, go-to-definition,
find-usages, rename, formatting, outlines, folding and inlay hints — the same
binary for every editor. Both also understand ```` ```ludic ```` fences inside
Markdown, so documentation gets the same highlighting and checking as source.
Plugins for VS Code and JetBrains IDEs, plus configuration for Neovim, Helix,
Emacs, Sublime and Zed, are in `tools/editors/` — see
[tools/editors/README.md](tools/editors/README.md).
## Working programs
- `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.
- `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.
```bash
./build.sh examples/snake.ludic && ./build/snake
```
## Not yet implemented
Units on quantities (`9.8 m/s^2`), `with` record-update expressions, a bytecode
VM + hot-reload, and the live agent bridge — these appear in the design docs but
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
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.
- **CLI: `--shared`, `--fmt`, and the wasm/cross target** — these were features of
the retired C driver; the self-hosted `ludicc` does not carry them (source
formatting lives in `build/ludic-fmt` instead). Output-path and IR flags are in
flux as the CLI front-end is rebuilt — check `ludicc` usage for the current set.
`struct` and array types, `break`/`continue`, and argv/stderr — once listed here
as near-term — are now implemented and self-hosting; their lowerings are in
[BOOTSTRAP.md](BOOTSTRAP.md) §4.
## Scenes & layers
> ⚠️ **Not yet implemented in the current (self-hosted) compiler.** `scene`,
> `layer`, and the `on enter` / `on exit` hooks are a design target: the
> compiler has no `scene` declaration and [`examples/scenes.ludic`](examples/scenes.ludic)
> does not compile today (`enter Name` parses only as a `become` alias). Games
> that need mutually-exclusive states use a mode register (`reg`/`setreg`) with a
> `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
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:
```ludic
# doc-check: skip — illustrative: elided bodies
scene Title start {
on enter { ui_open(UI_Menu) }
on exit { ui_visible(UI_Menu, 0) }
layer Main {
system Choose phase Update {
if ui_clicked(UI_NewGame) { enter Overworld }
}
}
}
scene Overworld {
on enter { spawn_party() }
layer World { system Move phase Update { … } }
layer Hud { system 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
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
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`.
- `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
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
When a system's whole body is one query loop, the loop header can move into the
declaration:
```ludic
system CleanBattle phase LateUpdate
query (b, p) [Battle, Pos, {Foe}] where b.hp <= 0
{
despawn self()
}
```
This is exactly equivalent to wrapping the body in
`for (b, p) in query [Battle, Pos, {Foe}] where b.hp <= 0 { … }` — same
lowering, same semantics. The body runs once per matching entity and `self()`
is that entity.
Mutation during iteration follows the same rules as an inline query, because it
is the same loop: entities are visited by ascending id, `despawn` of the current
or an already-visited entity is safe, and an entity **spawned mid-loop at a
higher id is visited in the same tick**. If you need the tick's matches frozen,
collect them yourself.

1560
LUANTI-ROADMAP.md Normal file

File diff suppressed because it is too large Load diff

180
README.md Normal file
View file

@ -0,0 +1,180 @@
# Ludic
An AI-first, ahead-of-time **compiled** game language with an ECS core, a
deterministic fixed-point runtime, and a native 2D backend. `ludicc` lowers
Ludic to LLVM IR itself and emits a native binary — **and `ludicc` is itself
written in Ludic.**
```
.ludic ──► ludicc ──► LLVM IR ──► object ──► native binary
(in Ludic)
```
**No C is generated, compiled or linked anywhere in a build.** There is no
interpreter, no transpiler, and no C runtime: the framebuffer, sprites, PNG
decoding, TrueType text, the retained UI, the registers and the RNG are all
written in Ludic (`runtime/native/*.ludic`), and the macOS window is
hand-written LLVM IR (`runtime/native/cocoa.ll`). Beneath that sits only the
platform's own ABI — malloc, fwrite, objc_msgSend, CoreGraphics — reached by
compiler intrinsics, the same floor Rust and Swift stand on.
**The compiler is written in Ludic.** `selfhost/*.ludic` is a Ludic compiler —
lexer, parser and LLVM-IR backend — that compiles every example (including the
6-file JRPG) to the byte-exact same binary the original C compiler produced, and
compiles **its own source** to a fixpoint. It is built from a checked-in IR seed
(`selfhost/ludicc.seed.ll`) with clang alone; run `./selfhost/bootstrap-cfree.sh`
to rebuild it with no C compiler in the loop. The former C compiler is gone.
Neither the compiler nor the games are C. Beneath both sits only the platform's
own ABI — malloc, fwrite, objc_msgSend, CoreGraphics — reached by intrinsics, the
same floor Rust and Swift stand on. (The wasm/cross-compile/shared-library driver
paths lived in the old C compiler and are not yet re-implemented on the
self-hosted native toolchain.)
## Layout
| Path | What it is |
|------|-----------|
| `selfhost/*.ludic` | **the compiler, written in Ludic** — lexer, parser, and the LLVM-IR backend (ECS storage, queries, spawn, `match`/`machine`, UI, scenes, save/load, fixed-point). Concatenated by `selfhost/build.sh`; built from `selfhost/ludicc.seed.ll` |
| `selfhost/bootstrap-cfree.sh` | rebuild the compiler from the IR seed with **no C compiler**, and prove it reproduces its own IR |
| `runtime/native/core.ludic` | the runtime written *in Ludic* for the native path (framebuffer, text, registers, RNG, input) |
| `COMPILING.md` | the native pipeline: `ludicc → LLVM IR → exe/dylib`, `module`/`export`, cross-compilation, `rt_*` intrinsics |
| `runtime/native/image.ludic` | PNG decoding, images, sprites, alpha blending and 9-slice — in Ludic |
| `runtime/native/inflate.ludic` | DEFLATE decompression (RFC 1951), so PNG needs no zlib on any target |
| `runtime/native/truetype.ludic` | from-scratch TrueType loader + antialiased glyph rasterizer, in Q16.16 |
| `runtime/native/ui.ludic` | retained UI widget tree: layout, 9-slice, focus, events — in Ludic |
| `runtime/native/cocoa.ll` | the macOS window, written in LLVM IR (Objective-C runtime + CoreGraphics via their C ABI) |
| `runtime/web/wasm.ll` | the web's platform layer in LLVM IR: the allocator, bulk memory and strings, since wasm32 has no libc |
| `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/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` |
| `test.sh` | regression suite: builds the compiler, compiles/runs all examples, checks save/load + diagnostics (`./test.sh`) |
| `tools/ludic-tools/` | the editor toolchain, in C: `ludic-fmt` (source formatter) and `ludic-lsp` (language server) — one lexer and one vocabulary shared by both |
| `tools/editors/` | plugins for VS Code and JetBrains, plus configuration for Neovim, Helix, Emacs, Sublime and Zed ([README](tools/editors/README.md)) |
| `tools/test-tools.sh` | regression suite for the toolchain: proves formatting never changes a program, drives the language server over real LSP traffic |
## Build & run
`ludicc` compiles Ludic straight to machine code via LLVM IR. See
**[COMPILING.md](COMPILING.md)** for the pipeline, shared libraries
(`--shared`), cross-compilation and the runtime protocol.
```bash
./build.sh examples/chronorift.ludic
./build/chronorift # opens a native window
```
Headless render (for testing / CI):
```bash
./build.sh examples/chronorift.ludic --headless
printf 'ddddwww' | ./build/chronorift_headless # writes out.ppm
sips -s format png out.ppm --out frame.png
```
A `module` compiles to a shared library instead of a program:
```bash
./build.sh examples/lib/combat.ludic --lib # -> build/libcombat.dylib
```
In a browser:
```bash
./build.sh examples/chronorift.ludic --web # -> build/web/
python3 -m http.server -d build/web 8000 # open http://localhost:8000/
```
`build/web/` is a self-contained 116 KB directory — the 42 KB module, the loader,
a page, and the sprites the compiler saw the game name. Copy it to any static
host and it runs; it needs no server-side anything and no special headers. Saves
go to `localStorage`, so `save()`/`load()` survive a reload.
A wasm build needs an LLVM with the WebAssembly backend and `wasm-ld` — Linux
`clang`/`lld` have both, Apple's clang has neither (`brew install llvm lld`).
See **[COMPILING.md](COMPILING.md#the-web)** for the pipeline, who owns the frame
loop, and how assets are bundled.
## Editor support
```bash
./tools/build-tools.sh # -> build/ludic-fmt, build/ludic-lsp
```
`ludic-lsp` speaks LSP 3.17 over stdio, so one binary serves every editor:
completion that knows whether you are after a `.`, inside a `query [...]` or in
a `ui` block; diagnostics from the compiler itself; go-to-definition and rename
across `import`ed files; comment-preserving formatting. `ludic-fmt` is the same
formatter as a CLI, for pre-commit hooks and CI.
Plugins for **VS Code** and **JetBrains IDEs** (Community editions included) and
drop-in configuration for Neovim, Helix, Emacs, Sublime and Zed are in
[`tools/editors/`](tools/editors/README.md). Both tools also understand
```` ```ludic ```` fences in Markdown, so this file and `LANGUAGE.md` get the
same highlighting, checking and formatting as the source tree.
## Language features implemented
- `component` (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}`.
- `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> { … }`.
- `spawn`/`despawn` with entity-slot reuse; nested queries.
- Control flow: `if`/`else`, `when`, `while`, numeric `for i in a .. b`.
- Types: `int`, `fixed` (Q16.16, with correct `*`/`/` lowering), `bool`,
`entity`, `str`. Fixed-point vs int arithmetic is resolved by the typechecker.
- Deterministic seeded RNG; `save()`/`load()` snapshot of the whole ECS World.
- 2D primitives: `clear`, `fill_rect`, `frame_rect`, `put_px`, `present`, `key`.
- TrueType text (`font_load`, `text_ttf`) with full Unicode + anti-aliasing;
arbitrary-size images + `draw_9slice`.
- `ui` — a retained widget tree declared as data (panels, labels, buttons,
images; layout, 9-slice skins, keyboard focus + click events).
- `match` / `machine`+`state`+`become` — dispatch and state machines.
- `scene` / `layer` / `enter` — mutually-exclusive game states, each with
`on enter`/`on exit` hooks and layered systems (layer order = draw order).
- `var` — typed module-level state, included in save/load snapshots.
- `module` + `export fn` — compile a .ludic file to a shared library whose
exported functions are ordinary C-ABI symbols.
- `extern fn … = "symbol"` — call any C-ABI library, Ludic or otherwise.
- `--target wasm32-unknown-unknown` — the same game in a browser: the runtime,
the ECS and the graphics stack compiled to wasm, rendering frames identical to
the native build's.
## The game: Chrono Rift
A playable co-op JRPG in `examples/chronorift.ludic`, using CC0
[Kenney](https://kenney.nl) sprites (Tiny Town + Tiny Dungeon), decoded from
PNG at runtime by the Ludic-written PNG/DEFLATE decoder — no zlib, no external
dependency on any target.
Controls:
- **Overworld:** `WASD` move, `K` save, `L` load.
- **Battle (local co-op):** Player 1 / Knight — `W`/`S` select, `Space` confirm.
Player 2 / Mage — `I`/`K` select, `J` confirm. Each player takes their own
turn each round (Attack / Defend / Run; Mage has Attack / Heal / Defend).
## Status
- [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] 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
- [x] Random encounters + turn-based battle (HP/MP, seeded-RNG damage)
- [x] Party + **local co-op** (P1 Knight, P2 Mage, per-player turns)
- [x] Leveling (XP → stat growth) and game-over / respawn
- [x] **Self-hosting**: a Ludic-written compiler compiles its own source to a byte-exact fixpoint, and rebuilds itself from a checked-in IR seed with no C compiler in the loop (`selfhost/`, `./selfhost/bootstrap-cfree.sh`)
- [x] Snapshot save / load (full ECS World)
- [x] Two maps (overworld + dungeon) with map switching via the arch
- [x] Boss encounter (Rift Warden) + victory condition
- [x] Item shop (gold → potions) at the house; potions usable in battle (Knight ITEM)
- [ ] More skills / enemy variety (future)

254
SYNTAX-REDESIGN.md Normal file
View file

@ -0,0 +1,254 @@
# Ludic Syntax Redesign — Cohesion Pass
A plan to make Ludic's syntax internally consistent. It fixes the drift between
the spec and the compiler, then unifies the grammar around two rules. Scope:
**full redesign (Phases 0–5)**. Named-field direction: **colon everywhere**.
> Status: **Phase 1 landed** (see below). Phases 2→5 are proposals. The phases
> are ordered so the documentation never describes syntax the compiler rejects,
> and every phase ends with the compiler still self-hosting to a fixpoint
> (`./test.sh`).
>
> Coordinated with the toolchain agent (CLI front-end / `ludicc`+`ludic`
> binaries) via serialized reseeds of `selfhost/ludicc.seed.ll`; Phase 1 rode in
> alongside their `emit_*`/`main.ludic` work, combined suite **14/14 green**.
---
## Why (the findings)
Verified against the self-hosted compiler ([selfhost/parse.ludic](selfhost/parse.ludic),
[selfhost/parse_game.ludic](selfhost/parse_game.ludic), [selfhost/lex.ludic](selfhost/lex.ludic)):
**Structural incoherence**
1. **Five micro-syntaxes for named parts** — `name: type = d` (fields), `name: type`
(params), `Field = { k = v }` (spawn), `[Name, {Tag}]` (query), whitespace
`phase X reads [..]` (system clauses), `key=value` (ui props).
2. **`=` means seven things, `:` means one** — assignment, default, record init,
ui prop, extern symbol, const value, `state X = N` all use `=`.
3. **No statement terminators** — `\n` and `;` both lex to `TK_NL`
([lex.ludic:45,121](selfhost/lex.ludic)) but the parser never requires a
separator, so `t.kind = k t.text = x t.ival = v` (three statements, spaces
only) is idiomatic.
**Broken / dead syntax (compiler-verified)**
4. `edge system` — **hard parse error** (documented at [LANGUAGE.md:188](LANGUAGE.md)). *(✅ fixed in Phase 1)*
5. `pure fn` — parses, `pure` silently discarded ([parse.ludic:272](selfhost/parse.ludic)); undocumented.
6. `@anno` + `reads/writes/needs/uses [..]` — parsed then thrown away
([parse_game.ludic:15-31](selfhost/parse_game.ludic)); four synonyms, two undocumented.
7. `scene`/`layer`/`on enter` — full LANGUAGE.md section + [examples/scenes.ludic](examples/scenes.ludic),
**does not compile** (`expected declaration`).
8. `query (v) [..]` in a system signature — two LANGUAGE.md sections +
[examples/qdecl.ludic](examples/qdecl.ludic), **does not compile** (`parse error: {`). *(✅ implemented in Phase 1)*
9. `when cond {}` — documented ([LANGUAGE.md:329](LANGUAGE.md)) + in all three editor
highlighters, **never parsed**. *(✅ implemented in Phase 1 as an if-without-else alias)*
10. CLI `--emit-llvm`/`-o`/`--shared`/`--fmt` — documented, but `ludicc` only
accepts `--windowed`/`--headless` ([main.ludic:8-14](selfhost/main.ludic)).
**Philosophical splits**
11. Operators are words (`and`/`or`/`not`), symbols (`==`/`<=`), *and* functions
(`band`/`shl`) at once.
12. Three overlapping control families — `if`/`when`, `match`, `machine`/`become`
— and `enter` reuses `become`'s AST node ([parse.ludic:176-177](selfhost/parse.ludic)).
13. Typed components/structs exist, but real state lives in 64 untyped int
registers (`reg`/`setreg`), so `machine`/`match` dispatch on magic numbers.
---
## The two rules everything converges on
**Rule A — `:` associates, `=` binds.**
- `:` introduces a *named part* and its type or value in a declarative structure:
component/struct fields' types, record initializers, ui props, (future) named
call arguments.
- `=` binds a value to a storage location or a constant: `let`, assignment
(`+=` …), `const` value, a field's **default**, and the extern symbol.
- A field declaration uses both, unambiguously: `x: int = 0` reads "`x` *has type*
`int` (`:`), *defaulting to* `0` (`=`)" — same shape as Rust/TypeScript.
- A record/spawn initializer is declarative, so it uses `:` — `Pos { x: 10 }`.
**Rule B — a statement ends at a newline (or `;` or `}`).**
- Newlines become significant. Two statements on one line require an explicit
`;`. `ludic-fmt` normalizes one statement per line and inserts/removes `;`.
Everything below is these two rules applied construct by construct.
---
## Target grammar (before → after)
### Records / spawn initializers
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before
spawn Hero { Pos = { x = 10, y = 5 } Player = { } }
# after
spawn Hero {
Pos { x: 10, y: 5 }
Player {}
}
```
`Field = { k = v }` → `Field { k: v }`. The component name is followed directly
by a record; fields use `:`. (Record literals elsewhere read the same:
`{ x: 10, y: 5 }`.)
### UI props → named-argument form
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before
panel id=Root w=288 pad=16 gap=6 align=center { label text="HI" size=26 }
# after
panel(id: Root, w: 288, pad: 16, gap: 6, align: center) {
label(text: "HI", size: 26)
}
```
A widget becomes "a constructor with named args, then an optional child block."
This deletes the bespoke `key=value` dialect and reuses `:` + commas. (Lower-churn
alternative if the paren form is disliked: keep whitespace separation but colonize
— `panel id: Root w: 288` — still removes the `=` overload.)
### System clauses & modifiers → one annotation channel
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before
edge system Move @deterministic reads [Vel] writes [Pos] phase FixedUpdate
query (p, v) [Pos, Vel] where a.x > 0 { … }
# after
@edge @deterministic
system Move
phase FixedUpdate
reads [Vel] writes [Pos]
query (p, v) [Pos, Vel] where p.x > 0
{ … }
```
- Prefix modifier words (`edge`, `pure`, `export`) are **retired**; all modifiers
become `@annotations`, parsed into a real list on the node (not skipped). This
fixes the `edge system` parse bug (#4) by construction.
- `needs`/`uses` are dropped; `reads`/`writes` stay as the two structural clauses
and are **stored** (even if analysis is future work) rather than discarded.
- The `query (v) [..]` signature clause is **actually implemented** in
`parse_system` (#8), lowering to the same `S_QUERY` node as the inline `for`.
### extern
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx" # unchanged
```
The `= "symbol"` is a binding under Rule A — it stays.
### Statements
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before (legal today)
t.kind = kind t.text = text t.ival = ival
# after
t.kind = kind
t.text = text
t.ival = ival
# or, explicitly, on one line:
t.kind = kind; t.text = text; t.ival = ival
```
### Control flow (Phase 4)
- **`when` vs `if`** — `when` is now a working `if`-without-else alias (Phase 1).
Phase 4 decides whether to keep both spellings or collapse to one; if collapsed,
remove `when` from docs, the parser, and all editor highlighters together.
- **Typed states replace magic-int machines.** Introduce `enum`, and let
`machine` dispatch on a typed variable instead of a register:
```ludic
# doc-check: skip — illustrative redesign snippet (proposed / partial syntax)
# before # after
const R_PHASE: int = 0 enum Phase { KnightMenu, KnightResolve, MageMenu, EnemyTurn }
machine R_PHASE { var phase: Phase = Phase.KnightMenu
state KnightMenu = 0 { … become … } machine phase {
state KnightResolve = 1 { … } state KnightMenu { … become KnightResolve }
} state KnightResolve { … }
}
```
`state X = N` loses the magic `= N` (ordinal comes from the enum). `become`
and `enter` (scenes) keep one shared lowering but read from a typed slot.
---
## Phase sequence
Each phase is independently shippable and ends green on `./test.sh` +
`./selfhost/test.sh` (fixpoint).
### Phase 0 — Doctrine (done here)
Rules A and B above; colon-everywhere; `@`-annotations as the single modifier
channel; typed enums for state. No code.
### Phase 1 — Truth-in-documentation ✅ DONE
Made spec ⇄ compiler agree **before** any grammar change. What landed:
- ✅ **`edge system` crash fixed** (#4) — `parse_system` now consumes an optional
`edge` marker before `system` ([parse_game.ludic](selfhost/parse_game.ludic)).
(`edge` is a pure marker; the emitter never lowered it differently.)
- ✅ **Signature-`query` implemented** (#8) — `query (vars) [terms] where c` in a
system header desugars to the same `S_QUERY` node the inline `for` builds, so
`examples/qdecl.ludic` compiles and runs. Also fixed multi-line clause parsing
(clauses may now span lines).
- ✅ **`when c { }` implemented** (#9) — as an `if`-without-else alias in
[parse.ludic](selfhost/parse.ludic). Docs + editors already listed it; now the
compiler agrees, so no editor-vocab churn was needed.
- ✅ **`scene`/`layer` marked not-yet-implemented** (#7) — prominent note in
LANGUAGE.md §"Scenes & layers" + a header on [examples/scenes.ludic](examples/scenes.ludic).
Full scene front-end + emission deferred (real work, out of Phase 1 scope).
- ✅ **`reads`/`writes` honesty** (#6) + the stale "Not yet implemented" section
updated in [LANGUAGE.md](LANGUAGE.md); scenes/reads-writes/dropped-CLI-flags now
listed there.
- ✅ **`test.sh` guards drift** — added a `qsmoke qdecl` compile check. Suite
green (14/14 incl. the toolchain agent's CLI smoke tests).
- ✅ **CLI flags** (#10) — `--shared`/`--fmt`/wasm noted as dropped-with-the-C-driver
in LANGUAGE.md; `-o`/`--emit-llvm` were being re-added by the toolchain agent
(now real, verified in `test.sh`); COMPILING.md updated by that agent.
- Deferred (intentionally): `pure`-is-ignored (#5) is undocumented and harmless;
it will be folded into `@pure` in Phase 3 rather than churned now.
- **Not done / by design:** `scenes.ludic` is *not* added to `test.sh` (it can't
compile yet — a positive test would fail; the header note + LANGUAGE.md warning
cover the drift instead).
### Phase 2 — Statement separation (Rule B)
- Enforce a separator in `block()`/`stmt()` ([parse.ludic:130-199](selfhost/parse.ludic)):
after a statement, require `TK_NL` or `}`.
- Teach `ludic-fmt` to normalize one-statement-per-line and insert `;` where two
share a line. Reformat the whole corpus with it.
- Risk: the self-host sources themselves use space-juxtaposed statements heavily
— reformat them in the same commit, re-seed, re-verify fixpoint.
### Phase 3 — Named-field unification (Rule A)
- Migrate spawn/record init (`= {…=…}` → `{…:…}`) and ui props (`key=value` →
named-arg form) in the parser and emitters.
- Collapse `reads/writes/needs/uses` → `reads`/`writes`, stored on the node.
- Move `edge`/`pure`/`export` into `@`-annotations; parse annotations into a list.
- Ship `ludic-fmt --migrate`: a mechanical codemod that rewrites old syntax to
new, run over `examples/`, `runtime/`, and `selfhost/`.
- Gate: compiler still self-hosts; every golden render is byte-identical.
### Phase 4 — Control-flow & operator consolidation (largest)
- Remove `when`; confirm `if` covers all uses in the corpus.
- Introduce `enum` + typed `machine`/state; migrate `combat.ludic`'s R_PHASE
machine and the register-driven `match reg(…)` sites.
- Decide the bitwise story: keep `band/shl/…` as functions (document as a
deliberate "one spelling" choice) or promote to operators — pick one and state
it, don't leave it implicit.
### Phase 5 — Single source of truth for keywords/grammar
- Generate every editor plugin keyword list, `ludic_syntax.h`, and the LSP's
token set from **one** canonical list so a keyword can never again be
highlighted but unparsed.
- Add a CI check (extend `tools/check-docs.py` / `check-vocabulary.py`) that
every ` ```ludic ` fence in the docs compiles, closing the doc-drift loop
permanently.
---
## Decision log
- **Scope:** full redesign, Phases 0–5. *(chosen)*
- **Named fields:** colon everywhere; `=` is binding-only. *(chosen)*
- **Open — Phase 4 detail:** typed `enum` state vs. keep integer registers.
Recommended: typed enums (fixes #13), but it's the deepest change; can be
deferred without blocking Phases 1–3.
- **Open — ui props:** paren named-args (`panel(id: Root)`) vs. colonized
whitespace (`panel id: Root`). Recommended: paren form for full cohesion.
- **Open — bitwise ops:** functions (status quo, documented) vs. operators.

View file

@ -0,0 +1,22 @@
Tiny Dungeon (1.0)
Created/distributed by Kenney (www.kenney.nl)
Creation date: 05-07-2022
------------------------------
License: (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/
This content is free to use in personal, educational and commercial projects.
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
------------------------------
Donate: http://support.kenney.nl
Patreon: http://patreon.com/kenney/
Follow on Twitter for updates:
http://twitter.com/KenneyNL

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.8" tiledversion="1.8.2" orientation="orthogonal" renderorder="right-down" width="32" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="1">
<tileset firstgid="1" source="sampleSheet.tsx"/>
<layer id="1" name="Dungeon" width="32" height="20">
<data encoding="csv">
14,1610612787,16,1,1,3221225485,1,1,1,2,3,3,3,3,3,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
14,1610612787,17,3,3,4,1,1,1,14,41,41,41,41,41,16,1,1,1,1,1,25,1,1,1,1,1,1,1,1,1073741837,1,
14,1610612787,58,41,41,16,25,1,1,14,1610612789,51,51,52,51,16,2,3,4,1,1,1,1,2,3,7,3,3,3,7,3,4,
14,1610612787,51,52,51,16,1,1,1,14,1610612787,49,3221225522,49,49,16,14,41,16,1,1,536870925,1,14,41,19,41,30,41,19,41,16,
14,1610612787,5,27,27,28,1,1073741837,1,14,1610612787,49,49,49,49,16,14,1610612789,16,1,1,1,1073741837,14,1610612789,31,51,51,52,31,51,16,
14,1610612787,16,2,3,3,3,3,4,26,27,27,27,27,27,28,14,1610612787,17,7,3,3,4,14,1610612787,49,49,49,49,49,49,16,
14,1610612787,16,14,41,41,41,41,17,3,3,3,3,4,1,1,14,1610612787,58,19,41,41,16,14,1610612787,49,49,49,49,49,49,16,
18,1610612787,17,18,1610612789,51,52,51,58,41,41,22,41,16,2684354573,1,14,1610612787,51,31,51,51,16,14,1610612787,49,49,49,49,49,49,16,
60,1610612787,58,60,1610612787,49,49,49,51,51,51,51,51,17,3,3,18,1610612787,49,43,49,3221225522,17,18,1610612787,49,49,49,49,49,2684354610,17,
51,54,51,51,54,43,49,50,49,49,49,49,49,58,41,41,60,1610612787,49,49,43,49,58,60,37,38,38,38,38,38,39,58,
49,43,49,49,49,49,49,49,49,49,49,49,49,51,52,51,51,54,49,49,49,49,51,52,54,49,49,49,49,49,49,51,
49,49,49,5,27,6,1610612790,49,49,2147483698,49,49,49,49,49,49,49,49,49,49,49,49,49,5,27,27,27,27,27,27,27,27,
49,49,49,16,1,14,1610612787,49,49,49,49,49,49,5,27,27,27,27,27,27,6,1610612790,49,16,1,1,1,1,1,1,1,1,
27,27,27,28,3221225485,14,1610612787,49,49,5,27,27,27,28,2,3,3,3,3,3,18,1610612787,49,16,1,1,1,2,3,3,3,3,
1,1,1,1,2,18,1610612787,49,49,17,3,4,1,1,14,41,20,41,41,21,60,1610612787,49,16,1,1,1073741837,14,41,11,12,41,
1,2,3,3,18,60,1610612787,49,49,58,41,17,3,3,18,1610612789,32,51,51,33,51,54,2147483698,16,1,1,1,14,1610612789,51,51,51,
1,14,41,41,60,1610612789,54,49,49,51,51,58,41,41,60,1610612787,49,43,49,49,49,49,49,17,3,3,3,18,1610612787,49,49,49,
1,14,1610612789,52,51,54,49,49,2147483698,42,49,51,51,51,51,54,43,49,49,49,49,49,49,58,41,41,41,60,1610612787,49,49,49,
1,14,1610612787,3758096434,49,49,49,49,49,3758096434,49,49,49,49,49,49,49,49,49,49,49,43,49,51,51,51,51,51,54,49,49,49,
1,14,1610612787,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,3758096434,49
</data>
</layer>
<layer id="4" name="Objects" width="32" height="20">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,87,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,75,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,0,65,0,65,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,67,0,67,0,67,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,121,0,0,0,0,0,0,
0,0,0,0,0,112,0,0,0,62,0,0,0,0,0,0,0,0,74,0,73,0,0,0,0,0,121,0,0,0,0,0,
0,0,65,0,0,0,0,0,62,88,62,0,0,0,0,0,0,0,0,0,0,74,0,0,0,0,0,0,0,0,0,0,
0,0,67,0,0,0,0,0,98,62,0,0,0,0,0,85,0,0,0,2147483758,0,0,0,0,0,0,0,0,0,0,0,0,
0,65,0,0,0,0,0,0,0,0,99,61,0,0,0,0,0,0,0,0,0,123,0,0,0,0,0,0,0,0,0,0,
0,67,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,68,0,0,0,0,0,0,0,0,0,0,0,0,0,78,78,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,0,83,
0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,124,0,0,0,0,0,0,0,0,0,90,0,0,
0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,2147483773,0,0,0,0,0,0,0,0,2147483749,0,
0,0,0,0,0,0,0,94,71,71,71,71,71,72,0,0,0,0,0,0,0,0,0,0,0,0,83,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,82,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<layer id="5" name="Carts" width="32" height="20">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,55,55,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
</map>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.8" tiledversion="1.8.2" name="tileset" tilewidth="16" tileheight="16" spacing="1" tilecount="132" columns="12">
<image source="../Tilemap/tilemap.png" width="203" height="186"/>
</tileset>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Some files were not shown because too many files have changed in this diff Show more