Commit graph

46 commits

Author SHA1 Message Date
f25289db20 feat(gl): OpenGL 4.1 and the ludic.render3d renderer
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
`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>
2026-09-10 03:31:12 +03:00
e175619543 refactor(cli)!: split the contributor tool out of the ludic CLI
`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>
2026-09-05 23:15:12 +03:00
aca263642d feat(cli): install in one command, and call the CLI ludic
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>
2026-09-05 22:01:52 +03:00
583783449a docs: correct stale references across LANGUAGE, README, COMPILING, CONTRIBUTING
- 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>
2026-09-05 01:12:26 +03:00
ad548840c7 feat(engine): #90 atlas-aware Sprite component, #91 become from listeners, 0.3.x ergonomics batch
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 32s
ci / build-and-test (push) Successful in 2m49s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 30s
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>
2026-09-04 01:36:08 +03:00
872f458cb2 feat(types): tagged-union enums — variant payloads + binding match + exhaustiveness (#56)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 25s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 23s
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>
2026-09-01 03:07:46 +03:00
53bb441f23 feat(input): raw device layer — multi-key held state, analog, mouse, gamepad, touch, full-state replay (#50)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
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>
2026-08-31 17:14:24 +03:00
1f5e3c1c1a feat(anim): animation ergonomics — named clips, Anim.play/Motion.to, frame events, fluent Tween handles (#48)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m21s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
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>
2026-08-31 16:53:16 +03:00
382826889f feat(light): render-quality tiers 3-4 — cones, falloff, soft shadows, gels, normals, day/night (#49)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m21s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 16:35:41 +03:00
377b6d1186 feat(input): action maps + deterministic record/replay (#7)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m19s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 20s
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>
2026-08-31 16:03:56 +03:00
3679ce1797 feat(ecs): Light2D/Occluder/Ambient as auto-consumed components (#47)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m18s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 15:49:50 +03:00
b0143337d9 feat(ecs): engine-owned systems auto-tick user components (#43)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m17s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 20s
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>
2026-08-31 15:41:44 +03:00
c040fff8c8 docs: move design/roadmap docs to the wiki, trim the repo root
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
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>
2026-08-31 00:36:19 +03:00
fb728bbefe chore(repo): DX cleanup — categorise examples, text-diffable golden, build/ output (#27 #28 #30)
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>
2026-08-30 18:54:21 +03:00
2e0047514b refactor(lang): rename builtins flr->floor and fx->fixed
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>
2026-08-30 02:01:56 +03:00
4c48077d68 refactor(lang): rename the fn keyword to function
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>
2026-08-30 01:43:22 +03:00
a38195128f feat(stdlib): namespaced standard library (issue #2)
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>
2026-08-30 00:26:19 +03:00
bca8f126fc Networking N2–N6, and a fully C-free toolchain
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>
2026-08-29 15:08:23 +03:00
d3301684f1 Phase 8b: modern names for the raw-memory and OS/IO primitives
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>
2026-08-28 02:55:24 +03:00
5a9e8cb2da Phase 8a: finish typed indexing — fixeds/ptrs buffers (retire peekf/pokef/peekp/pokep/str_len)
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>
2026-08-28 02:49:40 +03:00
86948d2b19 Phase 7k: bytes(n) / words(n) allocators (retire mem_alloc)
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>
2026-08-28 02:26:15 +03:00
ae0bae0457 Phase 7j: words buffers + w[i] word indexing (retire peek32/poke32)
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>
2026-08-28 02:20:50 +03:00
1b9789630a Phase 7h: string slicing s[a..b] (retire substr)
`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>
2026-08-28 01:35:24 +03:00
839a6bf0c0 Phase 7f: len(x) works on strings too (retire slen)
`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>
2026-08-28 01:25:57 +03:00
e9c15cc620 Phase 7e: polymorphic print(x) + str(x) (retire print_int/print_str)
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>
2026-08-28 01:22:38 +03:00
ad63f09d53 Phase 7d: string interpolation text {expr} text
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>
2026-08-28 01:12:07 +03:00
a021362cdb Phase 7c: string operators + and ==/!= (retire streq/sconcat)
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>
2026-08-28 01:05:36 +03:00
70ab79a4e9 Phase 7b: null literal + x == null (retire ptr_null/ptr_is_null)
`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>
2026-08-28 00:50:21 +03:00
fcada23eba Phase 7a: bitwise operators & | ^ << >> ~ (retire band/bor/shl/…)
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>
2026-08-28 00:45:10 +03:00
e1d7797e29 Phase 6f: let = immutable, var = mutable (binding-only)
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>
2026-08-27 23:52:59 +03:00
cc701013f5 Phase 6e: unify builtin naming (loaders domain-first, set_reg)
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>
2026-08-27 23:38:10 +03:00
dc475b17c2 Phase 6d: merge struct into property; storage follows use
`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>
2026-08-27 23:32:05 +03:00
3fd599ce47 Phase 6b: a handler's query is @Queries only; drop the signature clauses
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>
2026-08-27 23:20:09 +03:00
920d476a15 Phase 6a: remove the redundant when and enter keywords
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>
2026-08-27 23:14:32 +03:00
0bded528df Add enable/disable across ECS scopes + @OnEnable/@OnDisable hooks
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>
2026-08-27 23:05:47 +03:00
6e11212cf2 Lifecycle hooks: @OnStart/@OnQuit, @OnDespawn, @OnAttach (like @OnSpawn)
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>
2026-08-27 21:15:50 +03:00
1951af99e9 Scriptable properties (ECS-safe subset): @Computed and @OnSpawn
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>
2026-08-27 21:01:32 +03:00
dfc17398cf Phase 6b: annotation DSL (@Queries, @Handles) + docs prose pass
@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>
2026-08-27 18:49:24 +03:00
42f955fd22 Phase 6a: rename core vocabulary (game/main/component/archetype/system)
game/module -> program, main -> entry, component -> property,
archetype -> model, system -> handler. Done via a transitional self-hosting
bootstrap: parser accepts both -> reseed -> compiler source moved to new
keywords + parser tightened to new-only -> reseed (fixpoint holds). Old keywords
now rejected.

Token-safe corpus migration (tools/ludic-tools/rename_kw.c) leaves the LLVM
@main/entry: labels in emit strings untouched; goldens byte-identical. Editor
vocab (header/JetBrains/TextMate/emacs), check-docs wrapper, and doc fences
updated; error string 'unknown component' -> 'unknown property'. test.sh 14/14,
test-tools 28/0, check-vocabulary green. (Doc prose pass to follow.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 18:22:13 +03:00
f87964c5f7 Phase 4b: enum types name magic-int value spaces
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>
2026-08-27 16:29:02 +03:00
fd0b890acc Phase 4a: machine states auto-number by declaration order
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>
2026-08-27 16:20:34 +03:00
9a558d6b58 Phase 3d: declaration modifiers become @annotations (Rule A)
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>
2026-08-27 16:16:16 +03:00
b643037d8f Phase 3b+3c: ui props use ':'; drop dead needs/uses synonyms (Rule A)
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>
2026-08-27 16:03:35 +03:00
39bd430ce8 Phase 3a: spawn/record initializers use ':' (Rule A)
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>
2026-08-27 15:58:00 +03:00
eac8f8f335 Phase 2: enforce statement separators (Rule B)
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>
2026-08-27 15:41:29 +03:00
985f9ad8f2 Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling,
and docs. Phase 1 of the syntax-redesign cohesion pass has landed:
edge-system fix, signature-query, when-alias, and the documentation truth-pass.
Suite green (14/14), C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:15:35 +03:00