`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the
platform gl3.h with every GL_* constant, generated by `ludic-dev glgen`
with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the
existing window at Retina resolution; headless builds render into an
offscreen CGL context, so a program that uses Gl.* renders and
screenshots identically under the test harness. It links gl.ll, the
thunks and OpenGL.framework only when used; every other build stays
byte-identical.
packages/ludic.render3d is a physically based renderer written on that
surface: HDRI image-based lighting, GPU-generated terrain with scanned
PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced
vegetation with impostors, procedural grass, water, SSAO, and an HDR
pipeline with bloom, auto-exposure and ACES.
It also carries this session's work on it: the terrain at half its cost
(10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer
you played, a resize that emptied the world, and the packaging that lets
a game use the renderer from its own repository — `ludic assets`, the
material manifest shipping with the package, and shader lookup falling
back to the install root. See changes/ for each, with its numbers.
The camping game that drove all of it has moved out to its own
repository, Maroon Lake; examples/rendering/smooth.ludic stays as the
renderer's example here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ludic help` ended with a section titled "contributing to the toolchain itself",
listing bootstrap, reseed, docs-gen and release tasks. None of that is available
to someone who installed the language — those tasks need the repository — so the
shipped tool was advertising work its user cannot do, in a namespace they have to
read past to find `new` and `run`.
The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a
checkout and excluded from every release artifact. `ludic` keeps the project and
package commands and nothing else; `ludic dev …` now explains where the tasks
went instead of failing as an unknown command.
What this shook out: the two programs share prelude/build/project/pkg, so the
helpers each had accreted in whichever file first needed them — cc(),
ensure_ludicc, the string functions, title_case, cmd_version — moved to where
both can see them. The argument-shift indirection added for the `dev` namespace
is gone with the namespace, so commands read argv directly again.
`ludic-dev test` asserts the split rather than trusting it: the staged install
must build a project, and `ludic dev build` there must fail while naming
ludic-dev. install.sh keeps building older tags, whose bootstrap goes through
main.ludic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Getting started meant cloning the repository, bootstrapping a compiler and
learning a task runner called `x`. That is a contributor's workflow handed to
everyone who wants to try the language.
Installing is now one command:
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
install.sh puts a complete toolchain — compiler, CLI, engine runtime, bundled
ludic.* packages, formatter, language server — in ~/.ludic and adds it to PATH.
Prebuilt artifacts are checksum-verified; where a platform has none, or the
release predates this layout, it bootstraps from the compiler's own IR seed with
clang. The docs site publishes the script beside the pages that quote it, so the
page and the script can never come from different releases.
`x` becomes `ludic`, and the surface splits by audience. A user of the language
sees `new`, `run`, `build`, `test`, `add`, `fmt`, `lsp`, `doctor`, `upgrade`;
`ludic new` scaffolds a project that builds and plays as it stands. Everything
the toolchain repo needs moved under `ludic dev` — build, test, reseed,
bootstrap-cfree, docs-gen, release — unchanged apart from the namespace. Those
tasks read arguments one position further along, so dispatch_dev sets a shift
and commands use arg_n()/arg_total() rather than each knowing its own depth.
Release artifacts become complete install roots (bin/ beside runtime/, packages/
and VERSION) rather than bare binaries, which is what the installer unpacks.
`ludic dev test` asserts the whole shape: it stages an install, puts it on PATH
with no LUDIC_HOME, and runs new -> build -> test through it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- LANGUAGE.md: real diagnostic format, list literals, Font.load / Ui.*
(no `reg`/`set_reg`, no `/Handler/Library` typo), `extern function`,
existing example links, Input.key(); builtins table trimmed to what exists
- COMPILING.md: pipeline names selfhost/ (not compiler/*.c), `program`
instead of game/module, all thirteen win_* entry points by group
- README.md: the window seam is not "five" functions
- CONTRIBUTING.md: docs are checked with `x docs-gen` / `x docs-check`
- docs/language: kw-ui and fn-ui_build use the namespaced API;
@ClearColor documents constant expressions; examples/README lists
operators.ludic
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes the two open issues and lands the pending unreleased batch:
- #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex
(scale/flip/tint), so cell / cell_span / strip ids of any size draw
through the engine sprite-render system. examples/library/sprite_atlas
is the pixel-readback regression.
- #91: `become` from an @On(Event) listener / global handler / plain
function no longer segfaults the compiler; it emits @L_scene_leave()
(a dispatch on the live scene id) so the leaving scene's on-exit runs.
UI_* handles are readable from any code (widget table built on first
use). examples/library/scene_menus covers it.
- fix: a windowed `ludicc -o` build that reaches the audio runtime only
through the atlas/Assets preload import now links audio.ll +
AVFoundation (the audio backend link was gated on a game-level
Audio.* call, so any windowed game declaring Sprite failed to link).
- the hand-written "Unreleased" CHANGELOG section is converted to
changesets under changes/ so `x release` generates it.
- plus the batch: engine-driven retained UI + UiClicked event, Overlay
phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File
namespaces, Sprite.strip, prefabs, managers, countdown fields,
enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon
packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3)
fix, shooter centre-aim fix, reserved-word function diagnostic.
Verified: x test (124/124), x test-tools, check-impl, check-vocabulary,
check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Extend `enum` from named int constants to a tagged union: a variant may
carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`).
Such enums box to a heap record (an i32 tag at offset 0, then one 8-byte
slot per payload position); an all-bare enum keeps its zero-cost compile-
time-ordinal representation, byte-for-byte unchanged (every golden render
and the bootstrap fixpoint still hold).
- Parser: variant payload declarations, stored as N_PARAM kids on the
variant node.
- Construction: by name — `Door(3)`, `Portal(x, y)`, bare `Empty` — resolved
ahead of the function-call fallback and boxed with the payloads coerced to
their declared types.
- match: destructures a tagged scrutinee, switching on the tag and binding
each arm's payload names in a scoped local frame.
- Checking pass: a tagged `match` must be exhaustive (cover every variant or
end in `_`), and constructor/pattern arities and binding forms are checked
— all reported where the scrutinee's type is known.
Adds selfhost/tests/enums.ludic to the regression suite and documents the
feature in LANGUAGE.md and the enum/match pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The raw device layer the input proposal sketched, over the action maps +
record/replay of #7. Beyond one key per frame, gameplay can read:
- Multiple simultaneous held keys: Input.key_down / key_pressed / key_released,
with clean rising/falling edges (hold left AND jump).
- Analog from keys: Input.axis(neg, pos) and a normalized Input.vector(l,r,u,d)
(diagonals scaled by 1/sqrt(2)), plus Input.strength(action).
- Mouse: Input.mouse_x/y, mouse_dx/dy (per-frame delta), mouse_down(btn), wheel.
- Gamepads: Input.pad_connected/pad_button/pad_axis (SDL-order buttons, -1..1
sticks); touch: Input.touch_count/touch_x/touch_y.
The held set is fed by the platform when windowed — cocoa.ll now tracks
keyDown/keyUp into a 256-bit held-key bitset (win_held) and the mouse
buttons/wheel (win_mouse), gated so headless builds DCE the native calls — and
by the Input.press / Input.set_mouse / Input.set_pad / Input.set_touch injection
on every target (Godot-style action injection: replays, AI, network-fed input).
Input.record / replay now snapshot the full per-frame device state (held set +
mouse), extending #7's single-key tape.
Everything is integer and deterministic, so the same inputs reproduce the same
frame on every run and headless. The gamepad/touch native hardware bindings
(GameController.framework / NSTouch) feed the same injected state and are the one
remaining platform-glue follow-up; the software layer, semantics and replay are
complete and driven deterministically today.
Worked example + regression: examples/library/input_device.ludic
(1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1, injection-driven headless). 23 new
docs/language/input pages. Full suite 78 passed, self-host C-free fixpoint
intact, no golden drift; cocoa.ll assembles and a windowed build links.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ergonomic layer over the engine-owned SpriteAnim/Motion systems (#43):
- Named clips: Anim.clip("run", frames, fps, mode) registers a clip by name and
Anim.play(entity, "run") plays it; Anim.play(entity, fps, frames, mode) sets
the clip directly. A name-keyed registry in systems.ludic.
- Frame events: Anim.on_frame(entity, frame) arms optional SpriteAnim
event_frame/event_fired fields; the engine flags the tick the clip first lands
on that frame, and Anim.fired(entity) reads it — the game reacts, so it stays
inside the no-runtime-dispatch event model.
- Motion.to(entity, from, to, dur, ease) starts a value tween over the Motion
component in one call (reflection-ABI writes, resetting the timer).
- Fluent Tween handles (runtime/native/tween.ludic): Tween.to / Tween.chain /
Tween.delay build a sequenced, disposable handle advanced by a new engine-owned
system (esys_tween, run each Update tick); Tween.value / Tween.done /
Tween.parallel / Tween.stop read and control it. The 1-arg Tween.done(handle)
is disambiguated from the 2-arg pure Tween.done(timer, dur).
Splicing: g_uses_anim_rt pulls in systems.ludic; g_uses_tween_rt pulls in
tween.ludic and inserts esys_tween into the Update phase. All integer and
deterministic, so animation and motion reproduce exactly under replay/lockstep.
Worked example + regression: examples/library/anim_sugar.ludic
(4 8 2 1 0 100 100 0 0 1 20 20 30 0 1). Twelve new docs pages (Anim, the new
Motion namespace, Tween handles). Full suite 77 passed, self-host C-free fixpoint
intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The remaining lighting tiers from the original proposal, all extending the
deterministic accumulation core (light.ludic) — no new ECS plumbing:
- Light.spot: cone / flashlight lights (direction + spread degrees), with a
self-contained integer atan2-in-degrees and a feathered edge.
- Light.falloff: a brightness-ramp exponent (1 linear, 2 quadratic, …) via
repeated fixed multiply.
- Light.soft: soft shadows — an area-sampled light so an occluder edge fades
through a penumbra instead of a hard cut.
- Light.gel + Light.clear_gel: colour cookies — a light gels from its centre
colour to a rim colour.
- Light.normal + Light.clear_normals + Light.height: a normal G-buffer so
surfaces shade by facing (N·L), not distance alone (tier 3).
- Light.time_of_day: a day/night ambient ramp from a single 0..1 value.
The engine lighting system (systems_light.ludic) consumes matching optional
Light2D fields — direction/spread/falloff/softness/gel — each defaulting off so
an older five-field Light2D lights exactly as before. Every tier is integer +
Q16.16 fixed, so scenes light identically on every run and headless.
Worked example + regression: examples/library/light_tiers.ludic (1 1 1 1 1 1 1 1 1).
Nine new docs/language/light pages. Full suite 76 passed, self-host C-free
fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two ideas the input revamp leads with, built in Ludic over the
single-key poll every target already provides:
- Action maps: gameplay reads named actions, not physical keys, so keys
are rebindable and a scheme is data. Input.bind(action, key),
Input.down/pressed(action), Input.rebind(action, from, to).
- Deterministic record/replay: Input.poll() is the one per-frame input
read; Input.record() captures the key each frame and Input.replay()
feeds the tape back, so a run reproduces exactly — the seed of lockstep
netcode. "Read input" and "read a recorded snapshot" are the same call.
runtime/native/input.ludic (spliced when the new Input.* methods are used;
pulls in core.ludic for rt_poll). emit_ns_call routes the methods to the
@fn_input_* runtime; parse.ludic gates the splice. Seven docs/language
pages; worked example + regression examples/library/input_actions.ludic
(1 0 1 1 0 1 0). Full suite 75 passed, self-host fixpoint intact, no golden
drift. The device layer (multi-key held, gamepads, touch, analog) needs a
platform key-state backend and is tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ECS-native shape the 2D lighting follow-up asked for, built on the
engine-owned-system hook from #43. A torch is just an entity carrying
Light2D, a wall an entity carrying Occluder, and one Ambient entity sets
the night tint — the engine runs the whole deterministic light pass at the
end of the Render phase (ambient modulate -> carve occluder shadows ->
accumulate additive radial lights) and presents. No Light.* calls wired.
- runtime/native/systems_light.ludic: esys_light2d, consuming the components
through the by-name reflection ABI and reusing the #4 accumulation core
(light_ambient / light_occlude / light_point). Reads position from a
Position component when present, else the light's own x/y.
- Split from systems.ludic so a SpriteAnim/Motion-only game never links the
light pass; spliced (with light.ludic) only when Light2D/Occluder declared.
- emit_main now boots rt_init like the auto-loop/test runner, so an
entry-driven game that renders has its framebuffer allocated (headless:
allocate only, no window, byte-identical stdout for non-rendering games).
Worked example + regression: examples/library/light_ecs.ludic (32 1 32 1).
Full suite 74 passed, self-host C-free fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the ECS hook issues #43 and #47 named as their real dependency: a
system the *engine* owns, inserted into the frame loop over a component a
game merely declares and carries — no `handler` wired.
- runtime/native/systems.ludic: esys_spriteanim (SpriteAnim frame advance:
loop/once/pingpong) and esys_motion (Motion value tween: linear/in/out/
in-out), both on the by-name reflection ABI, integer + deterministic.
- backend: emit_engine_systems_for_phase inserts the calls after every user
handler in a phase (auto-loop and the drivable tick helpers alike);
uses_engine_systems() drives the systems.ludic splice, the world-table
force-emit, and makes a component-only game count as a systems game.
- A game that declares neither component is byte-for-byte unchanged.
Worked example + regression: examples/library/anim_ecs.ludic. Full suite
73 passed, self-host C-free bootstrap fixpoint intact.
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>
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>
De-abbreviate the two bare fixed-point conversion builtins:
flr(f) -> int -> floor(f) -> int (fixed -> int, flooring)
fx(i) -> fixed -> fixed(i) -> fixed (int -> fixed; mirrors how the
stringify builtin is `string`)
Updates the compiler dispatch, all call sites, the grammars/LSP/JetBrains
tokens, and the docs (fn-flr -> fn-floor, fn-fx -> fn-fixed). 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>
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>
Groups B and C of the leftover-primitive cleanup — renames, not new machinery,
and deliberately NO unsafe_ prefix (a __-prefix is itself a C convention, and an
`unsafe` marker carries no signal in a fully-manual-memory language with no safe
subset to contrast against).
memory: mem_free -> free mem_realloc -> resize mem_set -> fill
ptr_add -> offset
process: os_argc -> arg_count os_arg -> arg os_exit -> exit
os_system -> run os_getenv -> getenv read_byte -> read_char
dead: mem_copy, os_time, write_byte (0 uses) — deleted
Two reseeds: accept both old and new names in the intrinsic dispatch, then
migrate every call site and drop the old names. file_open/read/write/seek/tell/
close are left as-is — they're the domain-prefixed syscall layer wrapped by
read_file, not the argc/argv-style C-ness the audit targeted; a `File` type is a
separate, larger design if wanted.
test.sh's CLI smoke updated (os_exit -> exit); check-vocabulary's grammar marker
moved off the deleted names. Reseeded (22243 lines); C-free fixpoint holds;
goldens identical; 18/18; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last peek/poke pairs were the same "typed buffer access wearing a C name" as
peek8/peek32, so they become indexing too:
peekf(sc_x, i) -> sc_x[i] (a `fixeds` buffer -> a fixed)
pokep(gc_bmp, s,b) -> gc_bmp[s] = b (a `ptrs` buffer -> a pointer)
str_len(s) -> len(s) (len is polymorphic since 7f; str_len was dead)
Two new element-typed buffers join words: `fixeds` (32-bit fixed) and `ptrs`
(pointer, 8-byte stride). emit_index_addr dispatches on the base type; IR is
byte-identical to the old intrinsics.
The peekf/peekp buffers (ed_x0/ed_x1/ol_x/sc_x fixed coords; gc_bmp/img_px/
tt_data/ui_text pointer arrays) were retyped scope-aware, then the 33 sites
migrated to indexing. Two bugs found via a menu golden diff / a link-time type
error and fixed: the buffer-name regex truncated digit suffixes (ed_x0 -> ed_x),
and the char-literal brace-miscount skipped a few module declarations (same class
as 7j).
Reseeded (22371 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab (fixeds/ptrs types in, 5 intrinsics out) + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allocation reads as intent, not malloc:
mem_alloc(64) -> bytes(64) (64 bytes -> a byte buffer)
mem_alloc(w * h * 4) -> words(w * h) (w*h 32-bit words)
bytes(n) mallocs n bytes and returns a plain pointer (byte-indexed); words(n)
mallocs n*4 bytes and returns a `words` pointer (int-indexed). Since words(X) and
mem_alloc(X*4) allocate the identical number of bytes, the migration cannot change
any allocation size — the `* 4` factor just moves from the argument into the
allocator name, pairing naturally with the Phase-7j `words` retyping
(`var fb: words = words(w * h)`).
Migrated 102 sites (mem_alloc(E*4) -> words(E), else bytes(E)); deleted the
mem_alloc intrinsic. mem_realloc/free/copy/set stay as the low-level
reallocation/free family. Reseeded (22565 lines); C-free fixpoint holds; goldens
byte-identical; 18/18; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
32-bit word access is indexing now, not peek32/poke32:
peek32(ui_rx, i) -> ui_rx[i] (reads an int)
poke32(rt_fb, i, c) -> rt_fb[i] = c (writes an int)
A buffer typed `words` (a pointer whose elements are i32) indexes with `w[i]`
as a full int; a plain `ptr`/`str` keeps byte indexing. emit_index_addr picks the
element type from the base's type — byte-identical IR to the old intrinsics, so
the migration reproduces the compiler and every golden render exactly.
The ~60 word buffers (rt_fb, tt_*/gc_* font tables, png_px/spr_px pixels, ui_*
layout arrays) were retyped from `ptr` to `words` scope-aware (per-function, so
the s/out/p byte-vs-word name collisions across functions stay correct), then the
254 peek32/poke32 sites migrated to indexing. A scope-analysis miss left 12
buffers (gc_*, sc_d, ui_rx/ui_ry) un-retyped — caught as a menu golden diff and
fixed. No true mixed byte+word access exists on any one variable, so a per-buffer
element type is sound.
Reseeded (22527 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab (byte/words types in, peek32/poke32 out) + doc-fences clean.
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>
`len` is now polymorphic — a slice's element count OR a string's byte length —
so the C-style `slen` (strlen) is gone: `slen(name)` -> `len(name)`. emit_len
branches on the operand type (slice header vs @strlen). Two reseeds: add the
string branch, then migrate the 19 slen calls and delete slen.
Reseeded (22565 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean. (String slicing s[a..b] to replace substr is deferred to its
own phase.)
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>
`null` is now a real pointer literal and null-tests are comparisons, instead of
`ptr_null()` and `ptr_is_null(x)`:
ptr_null() -> null
ptr_is_null(x) -> (x == null)
not ptr_is_null(x) -> (x != null)
Mechanics: a new E_NULL primary (`null`, like true/false) lowers to the `null`
pointer; emit_bin's comparison path now picks `ptr` vs `i32` from operand type
(via llty), so `==`/`!=` work on any pointer/record/slice. The two intrinsics are
deleted.
Two reseeds: (A) add the literal + ptr comparison keeping the intrinsics; (B)
migrate all 182 call sites (compiler + runtime, via a balanced-paren script that
skips string-literal args and rewrites `not ptr_is_null` to `!= null`) and delete
the intrinsics. Node/Val/Buf/Tok field defaults now read `ptr = null`.
Vocabulary drops the two from LUDIC_INTRINSICS; `null` joins true/false as a
language constant (grammar + ludic_syntax.h). LANGUAGE.md notes the literal.
Reseeded (21664 lines); C-free fixpoint holds; goldens identical; 17/17; vocab +
doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First step of the "stop feeling like C" pass. Bitwise ops were functions
(`band(x, MASK)`, `shl(a, 3)`); they are now real operators:
band -> & bor -> | bxor -> ^ shl -> << shr -> >> bnot -> ~
Precedence is Go-style so the C footgun is gone: `<<`/`>>`/`&` bind like `*`,
`|`/`^` like `+`, both tighter than comparison — `flags & MASK == 0` parses as
`(flags & MASK) == 0`. `>>` is logical (lshr), matching the old `shr`.
Mechanics: lexer tokenizes `<< >> & | ^ ~`; p_mul takes `<< >> &`, p_add takes
`| ^`, p_unary takes `~`; emit_bin routes them through the existing int arith
path (arith_code gains and/or/xor/shl/lshr) and E_UN handles `~`. The six
intrinsics are deleted.
Delivered as two reseeds: (A) add the operators keeping the intrinsics, (B)
migrate every call site to operator form (85 lines across compiler + runtime,
via a balanced-paren call->operator script; two multi-line big-endian reads in
image/truetype done by hand) and delete the intrinsics. Vocabulary drops the six
from LUDIC_INTRINSICS (ludic_syntax.h, grammar, LudicTokens.kt) and the grammar
gains a bitwise-operator rule. LANGUAGE.md precedence table rewritten.
Reseeded (21724 lines); C-free fixpoint holds; goldens byte-identical (the PNG
and TrueType decoders lean on these ops); 17/17; 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>
`struct` and `property` had identical syntax and differed only in semantics, so
they are now one keyword: `property`. How a property is stored follows from how
it is used —
- listed in a `model` or attached by `spawn` -> an ECS component, kept in the
engine's per-entity @S_/@H_ arrays and bound in queries (as before);
- constructed with `new` -> a heap record with reference
semantics (what `struct` used to be).
A program that declares only `property` records and functions — no `model`, no
`handler` — is not an ECS program: it gets record layouts and `new`, but no
entity storage, allocator, snapshot, or runtime splice. This is exactly the
shape of the Ludic compiler itself, whose Node/Tok/Buf/Val are now `property`.
Mechanics:
- record layout (%Cmp_) now always emitted in the header (emit_head), so `new`
works with or without the ECS; the per-entity arrays stay in
emit_ecs_storage. %Str_ is gone — one layout prefix.
- has_ecs() is now `has_systems() or has_models()`, not "any component"; a
property alone no longer drags in the ECS runtime. Added has_models().
- emit_new / member access / layout_ty / layout_node collapse onto find_comp.
Dropped struct keyword, parse_struct, find_struct, is_struct_ty, N_STRUCT
emission (the const stays at kind 0, the default node kind).
Migration done as two reseeds (the old compiler treats any component as ECS, so
it cannot see `property` records in the compiler source until has_ecs is fixed):
A) teach the compiler property-as-record + fix has_ecs, keeping `struct`;
B) migrate the compiler's own records to `property` and remove `struct`.
selfhost/tests/structs.ludic migrated (still prints 7 9 109 2 42). Vocabulary
drops `struct` from DECL (ludic_syntax.h, grammar, LudicTokens.kt). LANGUAGE.md
"Records" section rewritten. Reseeded (21613 lines); C-free fixpoint holds;
goldens identical; 17/17; vocab + doc-fences clean.
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>