Commit graph

15 commits

Author SHA1 Message Date
0d1b09e4f0 feat(errors): recoverable failures as values — try/else over ok/err results (#46)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m14s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 15:04:14 +03:00
498593311f feat(testing): line coverage via --coverage + bin/x test --coverage (#45)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
Instrument each emitted statement with a per-source-line hit counter, gated
behind a new --coverage flag (default off) so ordinary builds — and the
compiler's own self-compile — stay byte-identical and the C-free bootstrap
fixpoint is untouched. A static line table plus a parallel hit-counter array
are dumped at exit through an atexit hook to $LUDIC_COVERAGE (default
ludic.cov) as a `FILE <name>` header and `<line> <hits>` rows.

bin/x test --coverage compiles the test specs with instrumentation, runs them
into per-file dumps, and aggregates a clean per-file line-coverage report that
names the unreached lines. Adds examples/library/coverage.ludic (a spec whose
tests deliberately miss one branch) and docs. Closes the last open acceptance
item of the testing framework (#12).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 14:51:14 +03:00
c7c8e2779c feat(errors): panic(msg) + assert(cond, msg) with file:line — no raw crashes (#8)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 14:33:37 +03:00
ea2c6ab246 feat(testing): built-in test blocks + expect assertions, auto-run with pass/fail (#12)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 14:27:45 +03:00
a71279a7b9 feat(rendering): add Light.* — deterministic 2D light accumulation with hard shadows (#4)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 14:07:19 +03:00
31cfbc2465 feat(rendering): add Screen.camera/clip/blend_mode/oval + Camera.* + Screen.pixel (#23)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m12s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 13:43:01 +03:00
12f2dbe958 feat(types): add Reflect.* — runtime reflection over the world schema (#20)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m12s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 13:28:51 +03:00
b25dc328a2 feat(stdlib): add Query.* — ECS spatial queries over the reflection ABI (#42)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m11s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):

  - Query.count(prop) -> int                 how many live entities carry prop
  - Query.first(prop) -> int                 the lowest-id bearer, or -1
  - Query.nearest(prop, pos, xf, yf, x, y)   the bearer closest to (x,y), or -1
  - Query.within(prop, pos, x, y, r, xf, yf) -> []int   every bearer within r

prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.

The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).

examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 13:19:25 +03:00
e4d1e95dcb feat(stdlib): add Anim.* + Tween.* — deterministic 2D animation & tweening (#5)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 16s
ci / build-and-test (push) Successful in 1m9s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 13:05:06 +03:00
07e5a20c0e feat(stdlib): add Grid.* — tile geometry + A* pathfinding over the tilemap (#24)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 16s
ci / build-and-test (push) Successful in 1m9s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 12:36:12 +03:00
b798e3024e feat(stdlib): add Regex.* — a linear-time regular-expression engine (#18)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 26s
ci / build-and-test (push) Successful in 1m6s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
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>
2026-08-31 12:23:19 +03:00
9ed0070039 feat(tooling): port the docgen site generator to Ludic (no Python) (#41)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 16s
ci / build-and-test (push) Successful in 1m4s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 17s
Follow-up to #31: the doc/lint/grammar checks moved to Ludic there; this ports
the remaining docgen piece (gen.py / check.py / palette.py) so nothing in the
documentation pipeline is Python any more.

Three new `x` subcommands, all in Ludic and compiled by Ludic:

  - x docs-gen [--out DIR]  the static-site generator: parses docs/language/**
    front-matter + bodies (fences, Parameters:), builds the section/symbol
    model, reads the asset templates, and emits every page + ns/color/api pages
    + the landing page + ludic-highlight.js + symbols.json + .nojekyll.
  - x docs-check [DIR]      the coverage / integrity guard (required files, a
    page per inventory.json symbol, duplicate-token and one-dir-per-namespace
    guards, highlighter link targets).
  - x docs-palette          the named-colour source of truth: the palette table
    moved into tools/x/docgen.ludic, emitting emit_color.ludic (pointer, not
    ptr) + palette.json.

Verified against the Python oracle: `x docs-gen` reproduces all 466 output files
BYTE-FOR-BYTE (a Ludic json.dumps/html.escape/front-matter port — ordered dicts,
indent=2 vs compact, ensure_ascii \uXXXX, codepoint-aware truncation), and
`x docs-check` matches check.py's pass/fail output. Wired into `x test` as a
gate (docs-gen -> docs-check on a fresh site; docs-palette stays byte-identical).

CI swap: ci.yml and docs.yml call the Ludic generator; docs.yml drops the
python:3.12 container and bootstraps the toolchain from the IR seed instead.
tools/docgen/{gen,check,palette}.py deleted; only assets/ + inventory.json
remain. Completes #31's criterion 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 02:12:32 +03:00
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>
2026-08-31 01:20:06 +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
23726afa90 refactor(selfhost): reorganise into concern-based subdirectories
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
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-31 00:26:02 +03:00