RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.
panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.
Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.
- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
runs to the end); a panic_case in the suite covers the failure path (non-zero
exit + the located stderr message). x test is now 69 checks.
Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A software light pass over the framebuffer, run in a render phase after drawing
the scene: Light.ambient multiplies the scene toward a tint (night/cave mood),
Light.point additively accumulates a radial glow with linear falloff clamped per
channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking
a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout,
so a scene lights identically every run and in a headless render (diffable).
Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the
regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1
(ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites,
soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder
components are follow-ups (the auto-system hook is tracked by #43).
- runtime/native/light.ludic: the light-accumulation engine (isqrt falloff,
segment/occluder shadow test, ambient modulate)
- examples/library/lighting.ludic: 14 pixel-readback assertions
- docs/language/light/: Light.ambient/point/occlude/clear_occluders
- tools/x/test.ludic: lighting.ludic in the regression suite
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the transform/state-based rendering #23 tracked as blocked on new
renderer state. All of it threads through the two framebuffer chokepoints every
draw primitive already funnels through (rt_put_px / rt_fill_rect), so one place
gives the whole draw API a camera, a clip rect, and a blend mode. Defaults are
neutral — camera (0,0), clip = full screen, blend = replace — so every existing
golden render is byte-identical (the 60+ render tests still pass unchanged).
New renderer state (runtime/native/core.ludic):
- Screen.camera(x, y) / Camera.set(x, y) world-space draw offset; a world
point draws at (wx-x, wy-y). Moves
everything — reset to (0,0) for a HUD.
- Camera.follow(x, y, lerp) ease the offset toward centring a
target (fixed lerp 0..1)
- Camera.shake(amount) +/- amount jitter from the seeded RNG
(replay shakes identically); 0 clears
- Screen.clip(x,y,w,h) / clip_reset() screen-space clip rectangle
- Screen.blend_mode(m) 0 = replace, 1 = additive (clamped)
New primitives:
- Screen.oval(x, y, rx, ry, color) axis-aligned ellipse outline (midpoint)
- Screen.measure_text(text) -> int advance width in the 5x7 font
- Screen.pixel(x, y) -> int read a framebuffer pixel (0x00RRGGBB)
Everything stays integer and deterministic (the camera, shake, and blend all
reproduce exactly under identical inputs), so headless renders remain diffable.
Camera.follow interpolates in the fixed domain (fixed*fixed then floor) to avoid
the int*fixed coercion trap.
Screen.pixel makes the whole surface testable by reading rendered pixels back:
examples/library/render.ludic asserts 18 cases — pixel round-trip, camera and
Camera.set/follow offsets, clip in/out + reset, additive blend with 255 clamp,
oval extremes vs hollow centre, and text measurement — all verified against the
actual framebuffer, not just that the call compiled. Wired into x test (now 65
passed). Docs: 7 new Screen pages + a Camera section with 3 pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime type reflection: enumerate properties and fields by index, resolve ids
by name, read a field's type, and get/set/has an entity's fields generically —
the foundation the issue calls out for auto-serialization, data-driven tools,
and debug/inspector overlays. Built on the existing EV2 reflection ABI plus a
new EV8 metadata-enumeration layer, all generated at compile time (a table walk,
no heavy runtime introspection), so a binary that never reflects pays nothing.
Surface (Reflect.*, aliased in emit_call.ludic over the world_* reflection ABI):
- Reflect.prop(name) / field(prop,name) resolve ids by name (-1 = none)
- Reflect.prop_count() / prop_name(i) enumerate properties
- Reflect.field_count(prop) / field_name(prop,i) / field_type(prop,i)
enumerate a component's fields
- Reflect.get / set / has (entity, prop, ...) read/write/test a field by id
- Reflect.kind(entity) / model(name) an entity's model, by id/name
New codegen (emit_world.ludic, EV8): ludic_prop_count / prop_name /
field_count / field_name / field_type, generated the same way as ludic_prop_id
— a switch over the compile-time property/field metadata, falling through to the
mod-registered (dynamic) registries. Field names/types come straight from the
AST, so field_type reports the declared type ("int"/"fixed"/…). A program that
uses Reflect.* force-emits the reflection ABI (g_uses_reflect) so it needs no
@events of its own, exactly like Query.* (#42).
examples/library/reflect.ludic asserts 20 cases including a generic inspector
that sums every field of every component an entity has while naming none of them
— the auto-save / debug-overlay pattern end to end. Wired into x test (now 64
passed). Docs: a Reflect section + 12 per-symbol pages (positioned as an
advanced/tooling surface), inventory/coverage green. Seed reseeded; the C-free
bootstrap fixpoint holds.
Scope: this lands the reflection core and a real consumer (the generic
inspector). The generic value-tree `serialize` the proposal also sketches wants
a tagged-union/any value type from the #1 type-system work, so it is tracked as
a follow-up rather than forced in here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):
- Query.count(prop) -> int how many live entities carry prop
- Query.first(prop) -> int the lowest-id bearer, or -1
- Query.nearest(prop, pos, xf, yf, x, y) the bearer closest to (x,y), or -1
- Query.within(prop, pos, x, y, r, xf, yf) -> []int every bearer within r
prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.
The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).
examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ECS-native, deterministic namespaces for 2D motion, driven off the fixed
frame clock so replays and lockstep netcode reproduce every frame and every
eased value exactly. Both are pure computed-inline Q16.16 / integer math (no new
runtime, no heap) — the game stores a timer on a component and calls these each
frame, exactly the way Collision.* / Grid.* are used.
Anim.* — spritesheet frame animation:
- Anim.frame(timer,fps,count) -> int looping frame index
- Anim.once(timer,fps,count) -> int one-shot, clamps on the last frame
- Anim.pingpong(timer,fps,count) -> int bounce 0..count-1..0
- Anim.finished(timer,fps,count) -> bool has a one-shot run past its end?
- Anim.duration(fps,count) -> fixed seconds for one cycle
- Anim.cell_x/cell_y(frame,cols,cell) -> int source rect on a grid sheet
Tween.* — value interpolation over a timeline:
- Tween.progress/loop/yoyo(timer,duration) -> fixed normalized amount
- Tween.done(timer,duration) -> bool
- Tween.ease(t, mode) -> fixed shape by a literal curve 0..6,
the same curves as Ease.* (now
factored into a shared ease_eval)
- Tween.number/round/point/tint(from,to,t) blend a fixed / int / Vector / color
The typed blends reuse the existing fixed / Vector / color helpers, and
Tween.ease shares Ease.*'s exact formulas via the new ease_eval(mode,t) — one
source of truth for every easing curve in the engine.
examples/library/anim.ludic asserts 34 cases (frame math, clamping, ping-pong,
cell geometry, timeline clamp/loop/yoyo, rounding, color/vector blends, and
Ease.in == Tween.ease(.,1)); wired into x test (now 62 passed). Docs: Anim +
Tween sections with 16 per-symbol pages, inventory/coverage green. Seed
reseeded; the C-free bootstrap fixpoint holds.
The stateful sugar the proposal sketches (named clips, Anim.play, fluent
Tween.chain/parallel handles, and an auto-injected advance system) is deliberately
left as a follow-up — this lands the deterministic math core both halves stand on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grid.* operates on the Map tilemap (Map.size/Map.row): a cell is passable unless
it is out of bounds or holds the caller's `wall` tile (a char code, e.g. '#'), so
any impassable glyph works. Everything is integer and deterministic.
- Grid.line(x0,y0,x1,y1) -> []Cell Bresenham line cells (LOS/raycast base)
- Grid.blocked(x,y,wall) -> bool the shared passability test
- Grid.line_of_sight(x0,y0,x1,y1,wall) unobstructed straight line?
- Grid.flood(x,y,wall) -> []Cell 4-connected reachable region (BFS)
- Grid.a_star(x0,y0,x1,y1,wall) -> []Cell shortest 4-connected path (A*,
Manhattan heuristic), empty if unreachable
The engine (runtime/native/grid.ludic, ~150 lines of Ludic, C-free) is spliced
into a game via core.ludic since it reads the tilemap runtime; returned Cell
slices are ordinary Ludic slices (`len` / `[i]`; each cell has `.x` `.y`).
Pathfinding lives under Grid rather than a `Path` namespace — that name is
already the filesystem-paths library (#10).
Verified against Python references: a 1500-case fuzzer over random maps agrees
exactly on A* path length (optimal, == BFS), flood-fill count, and line-of-sight.
examples/library/grid.ludic asserts the behaviour and is wired into `x test`
(now 61 passed); docs: a Grid section + 5 per-symbol pages, inventory/coverage
green. Seed reseeded; the C-free bootstrap fixpoint holds.
Scope: this lands the Grid.*/pathfinding half of #24. The ECS Query.* helpers
(count/first, and nearest/within which want a runtime spatial index) remain the
tracked follow-up the issue calls out as blocked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A regular-expression library with PCRE/PECL-compatible syntax, implemented as a
Thompson NFA / Pike VM so a bad pattern from a modder can NEVER cause
catastrophic backtracking — matching is O(n·m), never exponential. `(a+)+$` on
40 non-matching chars, `(a*)*b`, `(.*a){20}b` all run in microseconds; a 50 KB
input scans in ~7 ms.
The engine (runtime/native/regex.ludic + regex_vm.ludic, ~700 lines of Ludic, no
C) parses a pattern to a small bytecode program — an unanchored lazy `.*?` prefix
makes a plain search match anywhere — and the VM runs every alive thread in
lockstep per input byte, deduped by program counter and carrying capture slots
(save/restore, leftmost-greedy priority). Supported: literals, `.`, classes
`[...]` (ranges, negation, `\d \w \s` and their negations), anchors `^ $`,
alternation `|`, capturing and `(?:…)` groups, and `* + ? {n} {n,} {n,m}` in
greedy or lazy form, plus the common escapes; numbered capture groups. Errors are
values — an invalid pattern compiles to null, never a crash. Backreferences and
look-around are out of scope for a linear engine, and on the degenerate case of a
nullable subpattern under an unbounded quantifier positions may differ from a
backtracking engine (the price of the linear-time guarantee) — documented.
Surface (Regex.*, aliased in emit_call.ludic to the regex_* functions):
compile / valid / matches / test / find / exec / next / replace / group /
group_count / start / end / ok.
The runtime is spliced on demand: the parser sets a flag when it sees `Regex.`
and maybe_splice_runtime imports the engine — so it costs nothing in a program
that doesn't use it and works in a plain tool (not just an ECS game).
Verified against Python's `re` as an oracle: a 20k-case grammar fuzzer agrees
100% on realistic patterns (0 / 15000 with capture groups) and 99.8% on group-0
spans across the full pathological grammar, the residual being the documented
nullable-quantifier case. examples/library/regex.ludic asserts the behaviour
(wired into `x test`, now 60 passed); docs: a Regex section + 13 per-symbol
pages, inventory + coverage green. Seed reseeded; the C-free bootstrap fixpoint
holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repository root carried 11 large Markdown files (~330 KB); most were
long-lived design records rather than things a newcomer needs on first
contact, which buried the README and mixed "how to use Ludic" with "how we
decided to build it."
Move the design/roadmap docs to the Forgejo wiki (now enabled and
populated): Events, Networking, Scenes, Lifecycle, Mobile and
Syntax-redesign design records, the Bootstrap deep-dive and the Luanti
roadmap, under a Home index + sidebar. Each page had its selfhost/ source
links corrected for the #29 reorg and every repo-relative link rewritten to
an absolute URL on main so it resolves from the wiki.
All eight were current, actively-maintained records, so none were dropped.
The root now holds README.md plus the two user-facing references,
LANGUAGE.md and COMPILING.md; the README links to the wiki, and the
remaining references in LANGUAGE.md / COMPILING.md / examples/README.md and
the emit_net.ludic header comment point at the wiki pages. The emit_net.ludic
change is a comment only — the seed stays byte-identical and bootstrap-cfree
+ the full suite (56) stay green.
Closes#26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A cohesive filesystem & IO library — the foundation for saves, config, mods, and
asset loading — wrapping the bare file_* builtins into one safe, ergonomic API a
non-expert can use without touching a file descriptor or a byte buffer.
Path.* join / dir / base / ext / stem / normalize (pure lexical string ops)
Fs.* exists / is_dir / read_text / write_text / append_text / remove /
size / mkdir / copy / list
Mime.* of (extension table) / sniff (magic bytes: PNG/JPEG/GIF/PDF)
Pure string IR for Path.*; libc (fopen/access/mkdir/rename/opendir…) for Fs.*;
C-free, emitted on demand (g_uses_fsrt). Safety and determinism baked in:
- write_text and copy are atomic (write a temp file, then rename over the target)
so a crash mid-write never corrupts the previous file;
- mkdir creates parents (mkdir -p);
- list is sorted for a stable, reproducible directory walk;
- fallible calls return values (null / false / -1), never crashes — ready for a
first-class try/else when the error-handling work lands.
Complements Os.* (#21): Os supplies per-user locations, Fs the operations. v1
targets the native macOS/BSD filesystem with "/" separators; Windows separators,
a sandboxed wasm virtual FS, recursive directory copy, and richer magic-byte
sniffing are documented follow-ups.
- examples/library/fs.ludic: 32 assertions across pure Path ops (incl. normalize
resolving ./ .. and duplicate slashes), a real create/read/append/copy/list/
remove cycle under build/, and Mime by-extension + by-magic (GIF signature vs a
.bin extension). Wired into `x test` (now 56 passed).
- docs: new Path, Fs, and Mime sections + 18 per-symbol pages; inventory updated;
every fence passes check-docs; site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#10
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make Ludic text correct-by-default over UTF-8, so player names, translated UI,
and chat behave for every language instead of counting bytes and splitting
characters in half. The byte-oriented Text.* stays for speed; Unicode.* is the
layer that understands code points and (approximately) grapheme clusters.
- len / byte_len code points vs bytes — the two lengths, kept distinct
- is_valid_utf8 strict validation of untrusted input
- char_at / chars code-point access by index; chars() -> []int
- upper / lower case mapping (ASCII + Latin-1)
- truncate first n code points, never a half-character
- grapheme_len user-perceived characters (approx UAX#29)
Pure integer/byte IR over NUL-terminated buffers; C-free, no data-table blob.
Decoding and validation cover the full UTF-8 range (overlong/surrogate/>10FFFF
rejected). grapheme_len collapses combining marks, variation selectors, ZWJ
sequences (family emoji), and regional-indicator flag pairs. Documented v1
scope: wider-script/locale case rules (Latin-Extended, Greek, Cyrillic, Turkish
i, German ß) and NFC normalization are follow-ups.
- examples/library/unicode.ludic: asserts the invariants across ASCII, Latin-1
(é round-trips through upper/lower), a decomposed "café" (5 code points, 4
graphemes), a ZWJ family emoji (5 code points, 1 grapheme), and a flag (2
regional indicators, 1 grapheme). Wired into `x test` (now 55 passed).
- docs: a new Unicode section + 9 per-symbol pages clarifying byte vs code point
vs grapheme; inventory updated; every fence passes check-docs; site builds.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#13
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An Os.* namespace, Go-flavored and game-scoped, for the environment *around*
the game: the command line, environment variables, standard streams, process
exit, the host platform, and the per-user known folders a game writes into.
Rounds the bare System.* builtins (arg/getenv/exit) into one coherent surface.
- args / arg_count / arg the argument vector (args() -> []string)
- env / env_or / has_env read env vars (null-safe via env_or)
- set_env / unset_env mutate this process's environment
- exit(code) terminate with a status code
- platform() / arch() host facts (uname sysname/machine)
- stdout_write / stderr_write raw writes to the standard streams
- save_dir / config_dir / cache_dir / temp_dir per-user known folders
Pure libc over NUL-terminated strings; C-free, no new runtime. arg_count/arg/
exit stay light (no prelude) as thin aliases of the existing intrinsics; the
rest share one Os runtime prelude emitted on demand (g_uses_osrt). platform()
is portable (uname system name is field 0 on every Unix); arch() and the
known-folder layout follow the macOS/BSD conventions — the fully supported
native target today. Linux/Windows/wasm folder resolution and a target-aware
arch() are documented follow-ups.
- examples/library/os.ludic: asserts the invariants that hold regardless of
host — env round-trip, env_or fallback, unset, args()==arg_count(), non-empty
platform/arch and known dirs. Wired into `x test` (now 54 passed).
- docs: a new Os section + 17 per-symbol pages; inventory updated; every fence
passes check-docs (--fmt) and the site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#21
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Log.* namespace: five levels (trace/debug/info/warn/error), a runtime
threshold, and structured key=value fields, so games get something better than
scattered print calls and release builds can go quiet without touching call
sites.
- Log.trace/debug/info/warn/error(msg, [k, v]...) -> stderr, "[LEVEL] msg k=v"
- Log.set_level(n) show only level >= n (0 = all default, 5 silences all)
- Log.level() read the current threshold
Fields accept strings, ints, and longs (numbers formatted automatically); the
level tag is chosen at compile time so a filtered-out level costs only a
comparison. Writes to stderr, never touching the simulation — no effect on
determinism/replays. v1 is the console sink; rotating-file and in-engine overlay
sinks are noted as follow-ups.
- examples/library/logging.ludic: asserts the set_level/level threshold
round-trip and that every level (with mixed-type fields) runs without faulting;
the stderr gating itself was verified by hand (warn/error emit, lower levels
suppressed). Wired into `x test` (now 53 passed).
- docs: a new Log section + per-symbol pages; inventory and coverage pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Noise.* namespace for procedural generation, implemented entirely in Q16.16
fixed point over an integer permutation hash so a seed reproduces the exact same
field on every platform and run (native/headless/wasm) — the determinism edge
over float noise that drifts across CPUs.
- value2 / perlin2 / simplex2 — value, gradient, and simplex noise -> [-1,1]
- fbm2(x,y,seed,octaves) — fractal Brownian motion (octaves of simplex)
- cellular2 / cellular2_id — Worley F1 distance + nearest-cell id
- unit(n) — remap [-1,1] -> [0,1]
Covers issue phases 1–2 fully plus cellular from phase 3; domain warp, ridged/
billow, and sample1/sample3 remain as follow-ups. Pure integer IR, C-free;
cellular/fbm reuse the math prelude's fx_sqrt.
- examples/library/noise.ludic: asserts the invariants a fixed-point generator
must hold (Perlin == 0 at lattice points, every sampler within [-1,1],
reproducibility, seed sensitivity, non-negative cellular distance). Wired into
`x test` (now 52 passed).
- docs: a new Noise section + per-symbol pages; inventory and coverage pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crypto (#19): add the OS cryptographically-secure random surface
(random_bytes/random_hex/random_u32, reading /dev/urandom) and a standard
base64 encoder, completing the library alongside the existing SHA-256/
HMAC-SHA256/verify_hmac/hex/ct_equal. All pure integer IR, C-free.
Uuid (#16): a new namespace for stable, collision-free IDs — v4 (random) and
v7 (time-ordered) generation, plus parse/is_valid/to_text/equals/nil. UUIDs are
canonical lowercase 36-char strings; v4 and v7's random tail draw from the
crypto CSPRNG, so both carry the documented determinism caveat (mint at the
edges, never inside lockstep simulation). Reuses the crypto prelude's
fn_secure_bytes / fn_hex_encode.
- examples/library/{crypto,uuid}.ludic: known-answer vectors (SHA-256, HMAC,
base64 per RFC 4231/4648) and structural invariants (uuid version/variant
bits, parse/equals), wired into `x test` (now 51 passed).
- docs: per-symbol pages for every new method + a new Uuid section; inventory
and impl-vs-docs coverage check pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repository-cleanup / DX pass folding three tracker items into one coherent
change, verified green end to end (`bin/x test` 49/0, `bin/x selfhost-test`
29/0, `bin/x test-tools` 29/0).
#28 — curate & categorise examples/
- 42 flat entries regrouped into intent-revealing subdirs: games/, rendering/,
ecs/, events/, networking/, lang/, library/ (was lib/).
- chronorift dir-vs-file duplication resolved: the entry file and its import
modules now live together under games/chronorift(.ludic).
- Every path reference updated repo-wide (test runner, editor-tool drivers,
docs/site, design docs).
- New examples/README.md indexes the whole set with run commands.
- Showcase examples without a self-asserting entry (hello, events, net_rt) now
get a compile-only rot guard in `bin/x test`, so nothing here rots silently.
#30 — text-diffable golden baseline
- The 4 binary selfhost/golden/*.ppm blobs are replaced by a single
selfhost/golden/renders.sha256 manifest (SHA-256 per render). Hashes are
byte-identical to the old PPMs, so the baseline is unchanged — only its form.
- game_case now compares framebuffer hashes; a regression shows as a changed
hex line in review, not "binary files differ".
- New `bin/x golden` regenerates the manifest deliberately (review with
`git diff selfhost/golden/renders.sha256`).
#27 — PPM & asset handling
- Headless renders now write build/out.ppm, never the repo root; `x app`,
`x clean`, messaging and .gitignore updated to match. Nothing is written to
the working root any more.
- Redundant local Kenney .zip archives removed (the art ships extracted;
.gitignore already excludes *.zip). CC0 License.txt files retained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finish issue #9 by adding the two remaining acceptance items on top of the
calendar/clock core, still pure-integer and deterministic:
DateTime.format(dt, pattern) -> string render an instant via a token
pattern (YYYY/YY/MM/DD/HH/mm/ss;
other chars pass through)
DateTime.parse(text, pattern) -> int read an instant back; -1 on a
non-digit where one is expected
Clock.now/set/advance/reset a game-controlled simulated clock
(the @L_clock global) that never
touches the wall clock, so gameplay
reading Clock.now() is replay-safe
format/parse take a string-LITERAL pattern and are expanded at compile time
(field offsets are then constant), folding @fn_str_concat over literal runs and
two small runtime helpers: @fn_dt_pad0 (zero-padded field) and @fn_dt_rd
(fixed-width digit reader that stops at the terminator and flags malformed
input). Clock is a universal i32 global declared in emit_head, so it works in
entry and game programs alike.
Adds examples/offline_rewards.ludic — the issue's worked "you were away N hours"
example, driven from its own entry and asserted in the regression suite — plus
selfhost/tests/datetime2.ludic (format/parse round-trip, parse failure, clock),
docs (Clock section + 4 pages, DateTime.format/parse pages), inventory and LSP
hover. Reseeded; C-free fixpoint holds; all suites green (27 self-host / 46
regression / 29 tools); check.py (366 symbols), check-impl.py (218 ns-methods)
and validate.py OK.
With this, #9's scope is fully delivered: DateTime/Date/Duration + core ops,
format/parse, a deterministic simulated clock, docs + offline-rewards example,
and tests. (v1 stays UTC-only, no leap seconds, i32 epoch valid through 2038.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the abbreviated pointer types to full words on the language surface:
ptr -> pointer (a raw address / FFI handle)
ptrs -> pointers (a buffer of pointers)
The Ludic type name is distinct from LLVM's own `ptr` spelling: llty() maps
`pointer`/`pointers` to LLVM `ptr`, and the emitted IR keeps `ptr`, so only
the Ludic-level surface changes. Rewrites type annotations across all
sources, the 8 hardcoded pointer type-tags, the `pointers`-buffer indexing
in emit_addr, the grammars/LSP/JetBrains tokens, and the docs
(type-ptr -> type-pointer, type-ptrs -> type-pointers). int/bool keep their
conventional short spelling (like Math).
Reseeded; C-free fixpoint holds; all suites green (45/24/29); site + check.py OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the abbreviated string type and its conversion builtin to the full
word everywhere:
str -> string (the immutable-string type)
str(x) -> str -> string(x) -> string (the stringify builtin;
what `{…}` interpolation calls)
Types are recognized by identifier, and llty maps both spellings to LLVM
`ptr`, so this is an atomic source rewrite: type annotations, the Ludic
type tags, the builtin name/dispatch, and the interpolation desugar, plus
the grammars, LSP, docs (type-str -> type-string, fn-str -> fn-string), and
inventory. int/bool stay (universally accepted, like Math).
Reseeded; C-free fixpoint holds; all suites green (45/24/29); site + check.py OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the function-declaration keyword to the full word across the whole
language and toolchain:
fn name(...) -> T { ... } -> function name(...) -> T { ... }
Done as a self-hosting migration: teach the parser both spellings, reseed,
rewrite every .ludic definition to `function`, then drop `fn`. The compiler
now rejects `fn`. Touches the parser, all selfhost/tools/runtime/example/test
sources, the grammars (TextMate shared+vscode, ludic_syntax.h, JetBrains
LudicTokens.kt), the LSP and formatter, the Python doc/vocab tools
(check-impl, check-docs, validate, palette, test-lsp), and the docs
(fences, prose, kw-fn -> kw-function).
Reseeded; C-free bootstrap fixpoint holds. All suites green (45 regression,
24 self-host, 29 tool); the docs site generates and check.py passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).
Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.
Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
discovered from the docs, no hardcoded list); new check-impl.py enforces
that every implemented namespace method / keyword / type / phase has a
doc page, wired into `x test-tools`. Document the previously-undocumented
keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.
Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An opinionated, game-facing surface for the language:
- Color.<Name>: 221 named colors resolved to 0xRRGGBB at compile time
(selfhost/emit_color.ludic).
- Namespaced builtins Screen.* / Input.key / Random.* / Map.*, so calls read as
subject.action; present() -> Screen.show(), clear -> Screen.clear,
fill_rect -> Screen.fill_rectangle, etc.
- Named arguments, e.g. Screen.fill_rectangle(x:, y:, width:, height:, color:),
reordered to the callee's parameters at emit time.
- machine/become can run over a named program-scope var, not just a register.
- Migrate examples off numeric registers to named vars; snake/menu/chronorift
render byte-identical to the goldens and the bootstrap fixpoint is preserved.
- Regenerate the checked-in seed (selfhost/ludicc.seed.ll).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.
Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().
C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.
Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`s[a..b]` is a fresh substring of the bytes [a, b) — the modern, end-based form
of the C-style `substr(s, start, count)`:
substr(src, start, i - start) -> src[start..i]
substr(t, 2, len(t) - 2) -> t[2..len(t)]
substr(src, i, 2) -> src[i..i + 2]
Mechanics: a new E_SLICE postfix (`base[lo..hi]`, distinct from `base[i]`
indexing) lowers to a @fn_str_slice prelude (malloc + copy + terminate), emitted
once into any program that slices. Two reseeds: add the syntax + prelude, then
migrate the 22 substr calls and delete substr. The migrator recognises the
common `count == end - start` shape and emits the clean `s[start..end]` rather
than `s[start..start + (end - start)]`.
examples/strings.ludic gains slicing (now prints 1..9). Reseeded (22673 lines);
C-free fixpoint holds; goldens identical; 18/18; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One `print` instead of two C-style names: `print(x)` writes an int OR a string
followed by a newline, dispatching on the operand type (int -> %d, string ->
%s). `print(int)` emits byte-identically to the old print_int, so every existing
call and every smoke-test output is unchanged.
print_str was only ever the raw IR-to-stdout dump in ir_flush (no newline), which
is not "printing a line" — so it now uses file_write to a new file_stdout()
stream, keeping the emitted IR byte-for-byte identical. That frees `print` to
have consistent always-newline semantics.
Two reseeds: (A) add print + str + file_stdout keeping the intrinsics; (B)
migrate the 61 print_int calls to print, ir_flush to file_write(file_stdout()),
and delete print_int/print_str (+ the now-dead @.fmt_str). str(x) (the
interpolation converter from 7d) is now also a documented standalone builtin.
Vocabulary: print/str/file_stdout in, print_int/print_str out (ludic_syntax.h,
grammar, LudicTokens.kt). LANGUAGE.md updated. Reseeded (22551 lines); C-free
fixpoint holds; goldens identical; 18/18; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The readable way to build strings, as you noted `{a} {b}` beats `a + " " + b`.
A backtick string embeds any expression in `{…}` and desugars to the Phase-7c
`+` chain, wrapping each hole in `str(...)`:
`hello {name}, n={count + 1}`
== "hello " + name + ", n=" + str(count + 1)
- Lexer: a backtick captures its content raw as TK_INTERP.
- Parser: parse_interp splits literal runs from `{…}` holes (brace-depth aware,
`{{`/`}}` escape to literal braces), re-lexes each hole as a full expression
(save/restore toks/pi like an import), and folds it all into E_BIN(+) nodes —
so no new AST or runtime beyond the existing concat.
- str(x): a string passes through; int/bool/fixed convert via a small emitted
@fn_int_str prelude (digits from the end of a buffer, '-' for negatives),
emitted once into any program that uses it.
examples/strings.ludic gains interpolation cases (now prints 1..7); the smoke
covers it. Grammar + ludic_syntax.h tokenize backtick strings (holes highlighted
as embedded code). LANGUAGE.md documents it as the preferred form.
Reseeded (22530 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strings are values now: `a + b` concatenates and `a == b` / `a != b` compare by
content, replacing the 605 `sconcat(...)` / `streq(...)` calls that made the
compiler read like C.
streq(name, "let") -> name == "let"
sconcat("load ", reg) -> "load " + reg
sconcat(a, sconcat(b, c)) -> a + b + c
Implementation: emit_bin gains a string path. Strings are pointer-typed, so any
`+` with a pointer operand concatenates and `==`/`!=` between pointers compares
content — except when one side is the `null` literal, which stays a pointer
identity test (the only two kinds of pointer `==` in the codebase). Both call a
small hand-written IR prelude, @fn_str_eq / @fn_str_concat, emitted once into any
program that uses string ops (so it works for tools, games and the compiler with
no runtime-splice dependency and no duplicate symbols).
Delivered as two reseeds: (A) add the operators + prelude with the full
pointer-aware dispatch, keeping streq/sconcat; (B) migrate every call site
(354 lines, via a string-literal-safe balanced-paren script that leaves the
function definitions alone) and delete streq/sconcat. examples/strings.ludic +
a test.sh smoke (prints 1 2 3 4 5) guard it.
Reseeded (21890 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bindings now signal mutability the way Rust/Swift do, instead of `let` meaning
"local" and `var` meaning "module-level":
- `let x = e` -> immutable binding; a later `x = …` is a compile error
(`cannot assign to immutable 'x' … use var`).
- `var x = e` -> mutable binding, at local OR module scope (position decides
scope; the keyword decides mutability).
- `const` -> unchanged (compile-time).
Immutability is of the *binding*, not the object: `let n = new Node; n.kind = 1`
is fine (mutation through the reference); only rebinding `n` is rejected. The
check lives in emit_assign — a direct `name =` whose target is a `let` local
(loc_mut == 0) errors; field/index targets and `var`/param/loop bindings are
unaffected.
Delivered as three reseeds so the self-hosting compiler never had to compile
source its own rules would reject:
A) add `var` as a local statement + per-local mutability tracking (loc_mut),
no enforcement;
B) migrate every reassigned `let` -> `var` across the compiler, runtime and
examples (337 declarations), driven by a per-function, string/comment-aware
scan (binding targets only, never `x.f =` / `x[i] =`);
C) turn on the check. bootstrap-cfree (compiler vs its own source) and every
golden build (which splices the runtime) then proved zero reassigned `let`
was missed anywhere.
Also folded in: removed leftover debug instrumentation in block() (a `cur=` /
print_int(777…) trace on the separator-error path) and fixed parse.ludic's stale
header comment (no more `struct`). Reseeded (21711 lines); C-free fixpoint holds;
goldens identical; 17/17; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixed the naming inconsistencies the cohesion audit flagged, converging on the
domain-first style the bulk of the surface already uses (ui_*, text_*, rng_*,
font_load, image_load):
- load_png -> png_load (asset loaders were split: font_load/image_load
- load_sprites -> sprites_load were domain-first, load_* were verb-first)
- setreg -> set_reg (missing underscore vs ui_set_int/ui_set_text)
Game builtins resolve to their `rt_` runtime function, so the renames are in
runtime/native (rt_png_load, rt_sprites_load, rt_set_reg) plus the ~100 example
call sites; `become` lowering in emit_machine now emits @fn_rt_set_reg. Purely a
surface rename — every renamed call maps to the same runtime symbol, so behavior
and golden renders are byte-identical.
Vocabulary + docs updated (ludic_syntax.h, grammar, LudicTokens.kt, LANGUAGE.md,
README, SYNTAX-REDESIGN). Reseeded; C-free fixpoint holds; goldens identical;
17/17; vocab clean.
Left as-is: os_argc/os_arg (compiler-internal intrinsics, already namespaced and
consistent with each other; renaming would need a bootstrap dance for little
gain). reg/set_reg keep the getter-bare/setter-set_ shape ui_focused/ui_set_int
already use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The handler-signature `query (vars) [terms] where …` clause and the
`reads`/`writes` clauses overlapped the `@Queries` annotation (and each other):
two ways to attach a query to a handler. Consolidated on the decorator.
- parse_system no longer parses `query`/`reads`/`writes` clauses; it keeps the
postfix `@anno(...)` channel and `phase`. A handler's query is the prefix
`@Queries(these: [...], on: Model)` annotation. Data-access hints are now
`@Reads(...)`/`@Writes(...)` — absorbed by the generic annotation skipper,
same parse-and-reserve status the old clauses had.
- The inline `for (…) in query […] where …` statement is unchanged and still
covers cross-property constraints / multiple kind filters. `query` stays a
keyword there (now dispatched via is_id so the vocabulary check sees it).
examples/hello.ludic and examples/qdecl.ludic migrated to `@Queries` (qdecl now
demonstrates a per-property constraint + `on:` tag); outputs unchanged
(4 4 10 3 / 0 3 -2). LANGUAGE.md handler sections rewritten. Vocabulary: drop
`reads`/`writes` from CLAUSE (ludic_syntax.h, grammar, LudicTokens.kt).
Reseeded (21931 lines); C-free fixpoint holds; goldens identical; 17/17;
vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cohesion audit found two keywords that are pure duplicates:
- `when c { }` produced a byte-identical S_IF to an else-less `if c { }`
(no distinguishing flag) — so it was a second spelling of the same node.
- `enter Name` produced identical codegen to `become Name`: it set an
`ival` flag that emit_become never reads.
Both removed from the parser. `if` already parses with an optional `else`, so
nothing is lost. examples/scenes.ludic (an uncompiled design sketch) and the
LANGUAGE.md scenes section now use `become Name` for scene transitions.
Vocabulary synced (ludic_syntax.h, TextMate grammar, LudicTokens.kt);
check-vocabulary clean. Reseeded (22148 lines); C-free fixpoint holds;
goldens identical; 17/17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three enable/disable statement scopes, each a reversible flag flip:
- `disable P on e` / `enable P on e` — one property on one entity. Clears
the has-flag so queries stop matching; field data persists in storage, so
enable restores it untouched. @OnDisable(P)/@OnEnable(P) handler hooks run
at the toggle point with the property bound by name.
- `disable Model` / `enable Model` — @ME_<Model> global flag; the model's
entities drop out of every query while disabled.
- `disable Handler` / `enable Handler` — @HE_<Handler> global flag; the
handler stops being called each phase while disabled.
Nothing is copied or freed — each toggle is one global store or one has-flag
store. Reduces entirely to existing ECS machinery (has-flags, kind filter,
per-phase call guards), so the data-oriented model is untouched.
New AST node S_TOGGLE; emit_toggle lowers it. Query {Model} filter now ANDs
@ME_; phase calls now guard on @HE_. Parser gains enable/disable statements
and @OnEnable/@OnDisable annotations.
Vocabulary: `on` promoted from RESERVED to CLAUSE (parser now dispatches on
it); enable/disable added as STMT keywords — synced across ludic_syntax.h,
the TextMate grammar, and LudicTokens.kt (check-vocabulary.py clean).
examples/toggle.ludic demonstrates all three scopes (prints 6 0 7 1 0);
test.sh smoke asserts it. Reseeded; C-free fixpoint holds; goldens identical.
Also: stop tracking tools/.idea/ (gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the rest of the game/entity/property lifecycle as @-hooks, each firing at
one timeline moment and reducing to ordinary code:
- @OnStart / @OnQuit (program): @OnStart aliases the Start phase; @OnQuit runs in
the frame loop's done: block, after the loop stops and before teardown.
- @OnDespawn(Model) (entity): a destructor. Despawn doesn't statically know the
entity's model, so hooks compile to @on_despawn_<Model>(entity) functions and
emit_despawn dispatches on @L_kind. Symmetric with @OnSpawn.
- @OnAttach(Property): fires in emit_init_component once a property is attached
and seeded, with the property bound by name.
Registries + parsing mirror @OnSpawn. examples/lifecycle.ludic narrates the whole
timeline (output 1 700 50 950 2). Behaviour-preserving (goldens byte-identical,
despawn users unaffected when no hooks registered); test.sh 16/16, fixpoint holds.
Deferred: @OnDetach (per-property runtime dispatch) and scene @OnEnter/@OnExit
(need scene support in the compiler).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two annotations that give properties/models a scriptable feel WITHOUT
reattaching behavior to data — both reduce to code the data-oriented model
already emits:
- @Computed field on a property: a derived value that is NOT stored; x.field
expands inline to its expression (bare names read as fields of x) at each use.
Zero storage, zero runtime dispatch. Reuses qualify_fields (now non-destructive,
base-node based); a g_computed registry keeps derived fields out of the layout.
- @OnSpawn(Model) on a handler: a constructor that runs at each spawn of Model
with the model's properties bound by name. Spawn statically knows the model, so
no runtime dispatch; emit_spawn binds the properties and inlines the hook body.
examples/annotations.ludic now exercises @Handles/@Queries/@Computed/@OnSpawn
(output 3 25 0 0); test.sh 15/15, fixpoint holds, goldens byte-identical.
Deferred (need more machinery, by design): @OnDespawn (despawn doesn't statically
know the entity's model) and @OnChange (needs change-tracking).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Queries(these: [Prop{constraint}, ...], on: Model) on a handler desugars to the
existing S_QUERY loop: each property binds by its own name, a Prop{...} block
qualifies its bare fields to that property, and on: adds a {Model} tag filter.
Implemented via parse_queries_anno + qualify_fields (parse_game.ludic), wired
into parse_one_decl; @Handles parses on the program (documentation).
examples/annotations.ludic demonstrates it (output 3 1 0 0); test.sh 15/15.
Docs: added the Annotations section to LANGUAGE.md and did the vocabulary prose
pass (component->property, archetype->model, system->handler, game->program)
across the docs. check-docs + test-tools green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
enum Name { A, B, C } declares named int constants; Enum.Variant is a
compile-time int (index by declaration order), usable in match patterns,
comparisons, setreg. New: parse_enum + dispatch, enum_ordinal resolver
(emit_core), Enum.Variant handling (emit_expr). Scoped to a naming layer over
int (no enum-typed variables), so register/save semantics are untouched.
combat.ludic battle menus dispatch on KnightAct/MageAct instead of 0..3;
chronorift golden byte-identical. Editor vocab (ludic_syntax.h, JetBrains,
TextMate, emacs) gained enum, dropped retired edge/export/pure decl keywords;
check-vocabulary + test-tools 28/0. when/if kept by choice; bitwise stays
functions. Reseeded, fixpoint holds, test.sh 14/14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
state KnightMenu = 0 { } -> state KnightMenu { }. Machine states now take their
value from declaration order (0,1,2,...); an explicit 'state Name = expr' still
works for the rare non-ordinal case. Removes the magic constants from state
machines (finding #13). Migrated combat.ludic's battle machine; chronorift golden
byte-identical (the ordinals match the old explicit 0-4). Reseeded, fixpoint
holds, test.sh 14/14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
edge/pure/export prefix keywords retired; modifiers are now leading @annotations
(@export, @edge, @pure, @deterministic). parse_one_decl collects a leading @anno
run and @export sets the fn export flag; dead edge-dispatch removed from
parse_system. Migrated examples/lib/combat.ludic; old prefixes now rejected.
Behavior-identical: the export flag is parse-only in the self-hosted emitter
(emits @fn_<name> for every fn, never reads the flag), so IR is byte-identical.
Reseeded, fixpoint holds, test.sh 14/14. Docs updated. Phase 3 complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3b: widget props go from key=value to key: value (parse_widget). emit_ui and
ludic-fmt unchanged (AST-identical; ':' formats correctly by default). Chose the
colonized form over parenthesized named-args — minimal churn, no new grammar.
Migration: tools/ludic-tools/migrate_ui.c. menu golden byte-identical.
3c: removed the undocumented, unused 'needs'/'uses' clause synonyms from
parse_system; reads/writes stay.
Reseeded, fixpoint holds, test.sh 14/14, check-docs + check-vocabulary green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comp = { f = v } -> Comp { f: v }. record() requires ':' and parse_spawn()
drops the '=' before the record; '=' is now assignment/binding only. Records
appear only in games, so the compiler seed is unaffected.
Migration: tools/ludic-tools/migrate_records.c (spawn-context aware). Verified
every golden byte-identical, old '=' form rejected, reseeded, test.sh 14/14.
Docs updated (LANGUAGE.md, BOOTSTRAP.md R2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The parser now requires a newline or ';' between statements (block() in
selfhost/parse.ludic); two statements may no longer sit adjacent with only
spaces. Also fixed if-without-else swallowing its trailing separator.
Migration: tools/ludic-tools/migrate_separators.c inserts ';' at statement
boundaries corpus-wide (examples, runtime, 25 self-host fragments, ~1100
boundaries). Verified semantically identical — the migrated compiler compiles
itself to IR byte-identical to the pre-migration seed, and every golden game
renders identically. Reseeded to the strict compiler; C-free fixpoint holds;
test.sh 14/14.
Docs: Rule B documented in LANGUAGE.md; BOOTSTRAP.md R1 + stale fences updated;
check-docs green across all docs. Fixed a multi-line string literal in
emit_expr.ludic (byte-identical \n escape) that the C toolchain lexer mis-lexes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>