67 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 53bb441f23 |
feat(input): raw device layer — multi-key held state, analog, mouse, gamepad, touch, full-state replay (#50)
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> |
|||
| 1f5e3c1c1a |
feat(anim): animation ergonomics — named clips, Anim.play/Motion.to, frame events, fluent Tween handles (#48)
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> |
|||
| 382826889f |
feat(light): render-quality tiers 3-4 — cones, falloff, soft shadows, gels, normals, day/night (#49)
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> |
|||
| 377b6d1186 |
feat(input): action maps + deterministic record/replay (#7)
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> |
|||
| 9452557f3c |
feat(reflect): generic value tree + Reflect.serialize/apply + JSON bridge (#44)
A self-describing Value node (null/int/fixed/bool/str/list/object) with constructors, builders (Value.add/put) and accessors (get/at/count/kind/ as_int/as_str/…). Reflect.serialize(entity) walks an entity's whole component set into a value tree — one member per component, each a sub-object of its fields — and Reflect.apply(entity, value) writes one back; a fixed field becomes a fixed node, everything else an int node, so the round-trip is bit-exact, with the model id under "@kind". Json.encode/parse bridge the tree to and from compact, stable, diffable text, with fixed written as an exact terminating decimal that parses back bit-for-bit (verified across the raw Q16.16 range). Together: a one-call, bit-exact save/load for entities. Written in Ludic and spliced on demand (runtime/native/value.ludic + reflect_io.ludic, like Query/Light), so a program that doesn't touch Value.*/Json.*/Reflect.serialize compiles byte-identically and the C-free bootstrap fixpoint holds (verified). The general tagged-union/any language type stays tracked in #1; this ships the concrete value tree the serializer needs. Adds 21 namespace-method docs pages + Value/Json sections, examples/library/serialize.ludic, and a regression case. Whole CI set green: x test 72/72, x test-tools 30/30, check-impl/vocabulary/docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| 0d1b09e4f0 |
feat(errors): recoverable failures as values — try/else over ok/err results (#46)
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.
Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.
Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| c7c8e2779c |
feat(errors): panic(msg) + assert(cond, msg) with file:line — no raw crashes (#8)
RFC decision (the split the issue recommended): programmer bugs abort loud and located; recoverable failures become values. This ships the first half. panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with exit code 1 — a clear, located error instead of a segfault or a silent wrong result. assert(cond, msg) is the guarded form: it aborts with `file:line: assertion failed: <msg>` only when cond is false, otherwise execution continues. The location is baked in at compile time (the call node carries its source line, g_src_name carries the file); the message is any string. Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail (assert branches on the condition first). @fprintf and the format constant are declared on demand (g_uses_panic), so a program that never panics is unchanged — and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds. - panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/) - examples/library/errors.ludic covers the success path (asserts hold, program runs to the end); a panic_case in the suite covers the failure path (non-zero exit + the located stderr message). x test is now 69 checks. Deferred: recoverable failures as `try`/`else` values (needs the tagged-union type system, #1) and a top-level `recover` for the dev game loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| ea2c6ab246 |
feat(testing): built-in test blocks + expect assertions, auto-run with pass/fail (#12)
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| a71279a7b9 |
feat(rendering): add Light.* — deterministic 2D light accumulation with hard shadows (#4)
A software light pass over the framebuffer, run in a render phase after drawing the scene: Light.ambient multiplies the scene toward a tint (night/cave mood), Light.point additively accumulates a radial glow with linear falloff clamped per channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout, so a scene lights identically every run and in a headless render (diffable). Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1 (ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites, soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder components are follow-ups (the auto-system hook is tracked by #43). - runtime/native/light.ludic: the light-accumulation engine (isqrt falloff, segment/occluder shadow test, ambient modulate) - examples/library/lighting.ludic: 14 pixel-readback assertions - docs/language/light/: Light.ambient/point/occlude/clear_occluders - tools/x/test.ludic: lighting.ludic in the regression suite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| 31cfbc2465 |
feat(rendering): add Screen.camera/clip/blend_mode/oval + Camera.* + Screen.pixel (#23)
Completes the transform/state-based rendering #23 tracked as blocked on new renderer state. All of it threads through the two framebuffer chokepoints every draw primitive already funnels through (rt_put_px / rt_fill_rect), so one place gives the whole draw API a camera, a clip rect, and a blend mode. Defaults are neutral — camera (0,0), clip = full screen, blend = replace — so every existing golden render is byte-identical (the 60+ render tests still pass unchanged). New renderer state (runtime/native/core.ludic): - Screen.camera(x, y) / Camera.set(x, y) world-space draw offset; a world point draws at (wx-x, wy-y). Moves everything — reset to (0,0) for a HUD. - Camera.follow(x, y, lerp) ease the offset toward centring a target (fixed lerp 0..1) - Camera.shake(amount) +/- amount jitter from the seeded RNG (replay shakes identically); 0 clears - Screen.clip(x,y,w,h) / clip_reset() screen-space clip rectangle - Screen.blend_mode(m) 0 = replace, 1 = additive (clamped) New primitives: - Screen.oval(x, y, rx, ry, color) axis-aligned ellipse outline (midpoint) - Screen.measure_text(text) -> int advance width in the 5x7 font - Screen.pixel(x, y) -> int read a framebuffer pixel (0x00RRGGBB) Everything stays integer and deterministic (the camera, shake, and blend all reproduce exactly under identical inputs), so headless renders remain diffable. Camera.follow interpolates in the fixed domain (fixed*fixed then floor) to avoid the int*fixed coercion trap. Screen.pixel makes the whole surface testable by reading rendered pixels back: examples/library/render.ludic asserts 18 cases — pixel round-trip, camera and Camera.set/follow offsets, clip in/out + reset, additive blend with 255 clamp, oval extremes vs hollow centre, and text measurement — all verified against the actual framebuffer, not just that the call compiled. Wired into x test (now 65 passed). Docs: 7 new Screen pages + a Camera section with 3 pages, inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| 12f2dbe958 |
feat(types): add Reflect.* — runtime reflection over the world schema (#20)
Runtime type reflection: enumerate properties and fields by index, resolve ids
by name, read a field's type, and get/set/has an entity's fields generically —
the foundation the issue calls out for auto-serialization, data-driven tools,
and debug/inspector overlays. Built on the existing EV2 reflection ABI plus a
new EV8 metadata-enumeration layer, all generated at compile time (a table walk,
no heavy runtime introspection), so a binary that never reflects pays nothing.
Surface (Reflect.*, aliased in emit_call.ludic over the world_* reflection ABI):
- Reflect.prop(name) / field(prop,name) resolve ids by name (-1 = none)
- Reflect.prop_count() / prop_name(i) enumerate properties
- Reflect.field_count(prop) / field_name(prop,i) / field_type(prop,i)
enumerate a component's fields
- Reflect.get / set / has (entity, prop, ...) read/write/test a field by id
- Reflect.kind(entity) / model(name) an entity's model, by id/name
New codegen (emit_world.ludic, EV8): ludic_prop_count / prop_name /
field_count / field_name / field_type, generated the same way as ludic_prop_id
— a switch over the compile-time property/field metadata, falling through to the
mod-registered (dynamic) registries. Field names/types come straight from the
AST, so field_type reports the declared type ("int"/"fixed"/…). A program that
uses Reflect.* force-emits the reflection ABI (g_uses_reflect) so it needs no
@events of its own, exactly like Query.* (#42).
examples/library/reflect.ludic asserts 20 cases including a generic inspector
that sums every field of every component an entity has while naming none of them
— the auto-save / debug-overlay pattern end to end. Wired into x test (now 64
passed). Docs: a Reflect section + 12 per-symbol pages (positioned as an
advanced/tooling surface), inventory/coverage green. Seed reseeded; the C-free
bootstrap fixpoint holds.
Scope: this lands the reflection core and a real consumer (the generic
inspector). The generic value-tree `serialize` the proposal also sketches wants
a tagged-union/any value type from the #1 type-system work, so it is tracked as
a follow-up rather than forced in here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| b25dc328a2 |
feat(stdlib): add Query.* — ECS spatial queries over the reflection ABI (#42)
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
|
|||
| e4d1e95dcb |
feat(stdlib): add Anim.* + Tween.* — deterministic 2D animation & tweening (#5)
Two ECS-native, deterministic namespaces for 2D motion, driven off the fixed
frame clock so replays and lockstep netcode reproduce every frame and every
eased value exactly. Both are pure computed-inline Q16.16 / integer math (no new
runtime, no heap) — the game stores a timer on a component and calls these each
frame, exactly the way Collision.* / Grid.* are used.
Anim.* — spritesheet frame animation:
- Anim.frame(timer,fps,count) -> int looping frame index
- Anim.once(timer,fps,count) -> int one-shot, clamps on the last frame
- Anim.pingpong(timer,fps,count) -> int bounce 0..count-1..0
- Anim.finished(timer,fps,count) -> bool has a one-shot run past its end?
- Anim.duration(fps,count) -> fixed seconds for one cycle
- Anim.cell_x/cell_y(frame,cols,cell) -> int source rect on a grid sheet
Tween.* — value interpolation over a timeline:
- Tween.progress/loop/yoyo(timer,duration) -> fixed normalized amount
- Tween.done(timer,duration) -> bool
- Tween.ease(t, mode) -> fixed shape by a literal curve 0..6,
the same curves as Ease.* (now
factored into a shared ease_eval)
- Tween.number/round/point/tint(from,to,t) blend a fixed / int / Vector / color
The typed blends reuse the existing fixed / Vector / color helpers, and
Tween.ease shares Ease.*'s exact formulas via the new ease_eval(mode,t) — one
source of truth for every easing curve in the engine.
examples/library/anim.ludic asserts 34 cases (frame math, clamping, ping-pong,
cell geometry, timeline clamp/loop/yoyo, rounding, color/vector blends, and
Ease.in == Tween.ease(.,1)); wired into x test (now 62 passed). Docs: Anim +
Tween sections with 16 per-symbol pages, inventory/coverage green. Seed
reseeded; the C-free bootstrap fixpoint holds.
The stateful sugar the proposal sketches (named clips, Anim.play, fluent
Tween.chain/parallel handles, and an auto-injected advance system) is deliberately
left as a follow-up — this lands the deterministic math core both halves stand on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| 07e5a20c0e |
feat(stdlib): add Grid.* — tile geometry + A* pathfinding over the tilemap (#24)
Grid.* operates on the Map tilemap (Map.size/Map.row): a cell is passable unless
it is out of bounds or holds the caller's `wall` tile (a char code, e.g. '#'), so
any impassable glyph works. Everything is integer and deterministic.
- Grid.line(x0,y0,x1,y1) -> []Cell Bresenham line cells (LOS/raycast base)
- Grid.blocked(x,y,wall) -> bool the shared passability test
- Grid.line_of_sight(x0,y0,x1,y1,wall) unobstructed straight line?
- Grid.flood(x,y,wall) -> []Cell 4-connected reachable region (BFS)
- Grid.a_star(x0,y0,x1,y1,wall) -> []Cell shortest 4-connected path (A*,
Manhattan heuristic), empty if unreachable
The engine (runtime/native/grid.ludic, ~150 lines of Ludic, C-free) is spliced
into a game via core.ludic since it reads the tilemap runtime; returned Cell
slices are ordinary Ludic slices (`len` / `[i]`; each cell has `.x` `.y`).
Pathfinding lives under Grid rather than a `Path` namespace — that name is
already the filesystem-paths library (#10).
Verified against Python references: a 1500-case fuzzer over random maps agrees
exactly on A* path length (optimal, == BFS), flood-fill count, and line-of-sight.
examples/library/grid.ludic asserts the behaviour and is wired into `x test`
(now 61 passed); docs: a Grid section + 5 per-symbol pages, inventory/coverage
green. Seed reseeded; the C-free bootstrap fixpoint holds.
Scope: this lands the Grid.*/pathfinding half of #24. The ECS Query.* helpers
(count/first, and nearest/within which want a runtime spatial index) remain the
tracked follow-up the issue calls out as blocked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| b798e3024e |
feat(stdlib): add Regex.* — a linear-time regular-expression engine (#18)
A regular-expression library with PCRE/PECL-compatible syntax, implemented as a
Thompson NFA / Pike VM so a bad pattern from a modder can NEVER cause
catastrophic backtracking — matching is O(n·m), never exponential. `(a+)+$` on
40 non-matching chars, `(a*)*b`, `(.*a){20}b` all run in microseconds; a 50 KB
input scans in ~7 ms.
The engine (runtime/native/regex.ludic + regex_vm.ludic, ~700 lines of Ludic, no
C) parses a pattern to a small bytecode program — an unanchored lazy `.*?` prefix
makes a plain search match anywhere — and the VM runs every alive thread in
lockstep per input byte, deduped by program counter and carrying capture slots
(save/restore, leftmost-greedy priority). Supported: literals, `.`, classes
`[...]` (ranges, negation, `\d \w \s` and their negations), anchors `^ $`,
alternation `|`, capturing and `(?:…)` groups, and `* + ? {n} {n,} {n,m}` in
greedy or lazy form, plus the common escapes; numbered capture groups. Errors are
values — an invalid pattern compiles to null, never a crash. Backreferences and
look-around are out of scope for a linear engine, and on the degenerate case of a
nullable subpattern under an unbounded quantifier positions may differ from a
backtracking engine (the price of the linear-time guarantee) — documented.
Surface (Regex.*, aliased in emit_call.ludic to the regex_* functions):
compile / valid / matches / test / find / exec / next / replace / group /
group_count / start / end / ok.
The runtime is spliced on demand: the parser sets a flag when it sees `Regex.`
and maybe_splice_runtime imports the engine — so it costs nothing in a program
that doesn't use it and works in a plain tool (not just an ECS game).
Verified against Python's `re` as an oracle: a 20k-case grammar fuzzer agrees
100% on realistic patterns (0 / 15000 with capture groups) and 99.8% on group-0
spans across the full pathological grammar, the residual being the documented
nullable-quantifier case. examples/library/regex.ludic asserts the behaviour
(wired into `x test`, now 60 passed); docs: a Regex section + 13 per-symbol
pages, inventory + coverage green. Seed reseeded; the C-free bootstrap fixpoint
holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| f5e9b5d6c2 |
feat(lang): new Type { field: value } record initialisers
`new` accepted only a bare `new T` (every field its declared default) or
`new []T`, but the docs (kw-new) document `new Record { field: value, … }`
as the way to construct a record with non-default fields — a documented,
intended form the parser never accepted (`let o = new Point { x: 3 }` failed
with "expected newline or ';'").
Parse an optional `{ … }` override record after the type in a `new`
expression (reusing the existing `record()` parser that `spawn` uses), and
seed each field in emit_new_struct from that record when present, else from
the field's declared default. `new []T` and bare `new T` are unchanged.
Also mark the illustrative kw-import fence `# doc-check: skip` (its imports
are example paths that can't resolve in isolation), which makes `x check-docs`
fully green (398 fences, 0 drifted) — so it is now wired as a gate in
`x test-tools` and CI, guarding against future doc/compiler drift.
Reseed is a clean fixpoint (x bootstrap-cfree holds); x test (56),
x selfhost-test (29, golden renders unchanged) and x test-tools (30) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| 23726afa90 |
refactor(selfhost): reorganise into concern-based subdirectories
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes #29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Renamed from selfhost/emit_expr.ludic (Browse further)