The ECS-native shape the 2D lighting follow-up asked for, built on the
engine-owned-system hook from #43. A torch is just an entity carrying
Light2D, a wall an entity carrying Occluder, and one Ambient entity sets
the night tint — the engine runs the whole deterministic light pass at the
end of the Render phase (ambient modulate -> carve occluder shadows ->
accumulate additive radial lights) and presents. No Light.* calls wired.
- runtime/native/systems_light.ludic: esys_light2d, consuming the components
through the by-name reflection ABI and reusing the #4 accumulation core
(light_ambient / light_occlude / light_point). Reads position from a
Position component when present, else the light's own x/y.
- Split from systems.ludic so a SpriteAnim/Motion-only game never links the
light pass; spliced (with light.ludic) only when Light2D/Occluder declared.
- emit_main now boots rt_init like the auto-loop/test runner, so an
entry-driven game that renders has its framebuffer allocated (headless:
allocate only, no window, byte-identical stdout for non-rendering games).
Worked example + regression: examples/library/light_ecs.ludic (32 1 32 1).
Full suite 74 passed, self-host C-free fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the ECS hook issues #43 and #47 named as their real dependency: a
system the *engine* owns, inserted into the frame loop over a component a
game merely declares and carries — no `handler` wired.
- runtime/native/systems.ludic: esys_spriteanim (SpriteAnim frame advance:
loop/once/pingpong) and esys_motion (Motion value tween: linear/in/out/
in-out), both on the by-name reflection ABI, integer + deterministic.
- backend: emit_engine_systems_for_phase inserts the calls after every user
handler in a phase (auto-loop and the drivable tick helpers alike);
uses_engine_systems() drives the systems.ludic splice, the world-table
force-emit, and makes a component-only game count as a systems game.
- A game that declares neither component is byte-for-byte unchanged.
Worked example + regression: examples/library/anim_ecs.ludic. Full suite
73 passed, self-host C-free bootstrap fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.
panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.
Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.
- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
runs to the end); a panic_case in the suite covers the failure path (non-zero
exit + the located stderr message). x test is now 69 checks.
Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A software light pass over the framebuffer, run in a render phase after drawing
the scene: Light.ambient multiplies the scene toward a tint (night/cave mood),
Light.point additively accumulates a radial glow with linear falloff clamped per
channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking
a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout,
so a scene lights identically every run and in a headless render (diffable).
Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the
regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1
(ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites,
soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder
components are follow-ups (the auto-system hook is tracked by #43).
- runtime/native/light.ludic: the light-accumulation engine (isqrt falloff,
segment/occluder shadow test, ambient modulate)
- examples/library/lighting.ludic: 14 pixel-readback assertions
- docs/language/light/: Light.ambient/point/occlude/clear_occluders
- tools/x/test.ludic: lighting.ludic in the regression suite
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The project had no versioning discipline: 0 tags, no CHANGELOG, no way for the
compiler to report a version. Add a lightweight, native release flow.
- Versioning: SemVer, with VERSION as the single source of truth. `ludicc
--version` (and `ludic --version`) read it at runtime — so a bump touches one
file and never reseeds the compiler. `x version` reports it too.
- Changesets: one small Markdown file per user-facing change under changes/
(bump level + type + summary; see changes/README.md). This replaces "remember
to edit the changelog" with a mergeable artifact, no Node changeset tool.
- `x release [major|minor|patch] [--publish]`: fold the pending changesets into a
new CHANGELOG.md section (grouped by type), bump VERSION, commit, and tag
vX.Y.Z. The level defaults to the highest changeset bump. `--publish` also
pushes and creates the Forgejo release with source + toolchain tarballs;
tools/ci/forgejo_release.py is the small stdlib-Python HTTP glue for the
release API (a native Http client is issue #6).
Seed the initial changesets describing the shipped surface; the first `x release`
turns them into the v0.1.0 CHANGELOG. Reseeded for the --version flag; C-free
bootstrap fixpoint holds; suites 56 / 29 / 29 on macOS, 51 / 28 (+skips) on Linux
CI, bootstrap-cfree byte-identical on both.
Part of the repository-cleanup / DX pass (with #32, #34).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>