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>
11
.claude/launch.json
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
180
README.md
Normal 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
|
|
@ -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.
|
||||
22
assets/kenney/tiny-dungeon/License.txt
Normal 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
|
||||
BIN
assets/kenney/tiny-dungeon/Preview.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/kenney/tiny-dungeon/Sample.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
76
assets/kenney/tiny-dungeon/Tiled/sampleMap.tmx
Normal 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>
|
||||
4
assets/kenney/tiny-dungeon/Tiled/sampleSheet.tsx
Normal 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>
|
||||
BIN
assets/kenney/tiny-dungeon/Tilemap/tilemap.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/kenney/tiny-dungeon/Tilemap/tilemap_packed.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0000.png
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0001.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0002.png
Normal file
|
After Width: | Height: | Size: 150 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0003.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0004.png
Normal file
|
After Width: | Height: | Size: 178 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0005.png
Normal file
|
After Width: | Height: | Size: 181 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0006.png
Normal file
|
After Width: | Height: | Size: 164 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0007.png
Normal file
|
After Width: | Height: | Size: 180 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0008.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0009.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0010.png
Normal file
|
After Width: | Height: | Size: 159 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0011.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0012.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0013.png
Normal file
|
After Width: | Height: | Size: 110 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0014.png
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0015.png
Normal file
|
After Width: | Height: | Size: 110 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0016.png
Normal file
|
After Width: | Height: | Size: 147 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0017.png
Normal file
|
After Width: | Height: | Size: 152 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0018.png
Normal file
|
After Width: | Height: | Size: 176 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0019.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0020.png
Normal file
|
After Width: | Height: | Size: 211 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0021.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0022.png
Normal file
|
After Width: | Height: | Size: 173 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0023.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0024.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0025.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0026.png
Normal file
|
After Width: | Height: | Size: 185 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0027.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0028.png
Normal file
|
After Width: | Height: | Size: 149 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0029.png
Normal file
|
After Width: | Height: | Size: 208 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0030.png
Normal file
|
After Width: | Height: | Size: 138 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0031.png
Normal file
|
After Width: | Height: | Size: 171 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0032.png
Normal file
|
After Width: | Height: | Size: 195 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0033.png
Normal file
|
After Width: | Height: | Size: 202 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0034.png
Normal file
|
After Width: | Height: | Size: 196 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0035.png
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0036.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0037.png
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0038.png
Normal file
|
After Width: | Height: | Size: 145 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0039.png
Normal file
|
After Width: | Height: | Size: 140 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0040.png
Normal file
|
After Width: | Height: | Size: 128 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0041.png
Normal file
|
After Width: | Height: | Size: 178 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0042.png
Normal file
|
After Width: | Height: | Size: 142 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0043.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0044.png
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0045.png
Normal file
|
After Width: | Height: | Size: 191 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0046.png
Normal file
|
After Width: | Height: | Size: 180 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0047.png
Normal file
|
After Width: | Height: | Size: 178 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0048.png
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0049.png
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0050.png
Normal file
|
After Width: | Height: | Size: 109 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0051.png
Normal file
|
After Width: | Height: | Size: 133 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0052.png
Normal file
|
After Width: | Height: | Size: 122 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0053.png
Normal file
|
After Width: | Height: | Size: 110 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0054.png
Normal file
|
After Width: | Height: | Size: 184 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0055.png
Normal file
|
After Width: | Height: | Size: 182 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0056.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0057.png
Normal file
|
After Width: | Height: | Size: 135 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0058.png
Normal file
|
After Width: | Height: | Size: 140 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0059.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0060.png
Normal file
|
After Width: | Height: | Size: 140 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0061.png
Normal file
|
After Width: | Height: | Size: 149 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0062.png
Normal file
|
After Width: | Height: | Size: 152 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0063.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0064.png
Normal file
|
After Width: | Height: | Size: 176 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0065.png
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0066.png
Normal file
|
After Width: | Height: | Size: 192 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0067.png
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0068.png
Normal file
|
After Width: | Height: | Size: 181 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0069.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0070.png
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0071.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0072.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0073.png
Normal file
|
After Width: | Height: | Size: 161 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0074.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0075.png
Normal file
|
After Width: | Height: | Size: 128 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0076.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0077.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0078.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0079.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0080.png
Normal file
|
After Width: | Height: | Size: 180 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0081.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0082.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0083.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
assets/kenney/tiny-dungeon/Tiles/tile_0084.png
Normal file
|
After Width: | Height: | Size: 232 B |