chore(release): v0.2.0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 16s
ci / build-and-test (push) Successful in 1m28s
commit-lint / conventional-commits (push) Successful in 2s

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 03:38:09 +03:00
parent 50ecb8472f
commit 237e13c95e
25 changed files with 53 additions and 198 deletions

View file

@ -1,12 +0,0 @@
bump: minor
type: feat
**Animation ergonomics (#48).** An ergonomic layer over the engine-owned
SpriteAnim/Motion systems: `Anim.clip` registers named spritesheet clips and
`Anim.play(entity, "run")` plays one (or `Anim.play(entity, fps, frames, mode)`
directly); `Anim.on_frame` / `Anim.fired` arm and read frame events (the engine
flags the tick a clip lands on a frame, gameplay reacts); `Motion.to` starts a
value tween over the Motion component in one call. New fluent, engine-advanced
`Tween` handles — `Tween.to` / `Tween.chain` / `Tween.delay` build a sequence,
`Tween.value` / `Tween.done` / `Tween.parallel` / `Tween.stop` read and control
it — advanced each Update tick by an engine-owned system. All integer and
deterministic, so animation and motion reproduce exactly under replay/lockstep.

View file

@ -1,15 +0,0 @@
bump: minor
type: feat
**Audio standard library (#22).** `Audio.*` — sound effects and music over a new
platform audio backend (`runtime/native/audio.ll`, AVAudioPlayer on macOS, spliced
and linked with AVFoundation only when a windowed build uses it). `Audio.load`
loads a sound file into a handle; `Audio.play` / `play_sound` fire it one-shot,
`play_music` loops it on a single music channel; `Audio.stop` / `stop_music` /
`stop_all` stop them; `Audio.volume` sets the master volume and `Audio.pitch` the
playback rate, both across every loaded sound; `Audio.is_playing` reports state.
Playback is out-of-band — the audio device is real-time, explicitly not part of
the deterministic simulation — but every trigger is an ordinary frame-driven
call, so a recorded/replayed run fires the same sounds at the same frames.
Headless builds carry the whole API as no-ops (the native `snd_*` calls are
`is_windowed()`-guarded and dead-stripped), so the same game code runs under the
test harness with no audio device.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Line coverage for the testing framework — compile with `--coverage` and the compiler instruments each statement with a per-source-line hit counter, dumped at exit (via an `atexit` hook) to the file named by `$LUDIC_COVERAGE` (default `ludic.cov`) as a `FILE <name>` header plus one `<line> <hits>` row per instrumented line. The instrumentation is flag-gated and additive, so an ordinary build — and the compiler's own self-compile — stays byte-identical and the C-free bootstrap fixpoint is untouched. `bin/x test --coverage` compiles the test specs this way, runs them, and aggregates the dumps into a per-file line-coverage report that names the unreached lines. Closes the last open acceptance item of the testing framework (#12).

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Engine-owned systems over user components (#43) — the ECS hook that runs a system automatically each frame over a component a game merely declares and carries, no `handler` wired. Declaring the well-known `SpriteAnim { ticks, fps, frames, mode, frame }` gives sprite-sheet frame animation (loop / once / ping-pong) that advances `frame` for free; `Motion { ticks, dur, from, to, ease, value, done }` gives value tweening (linear / in / out / in-out) that advances `value`. The systems stand on the reflection ABI, resolving fields by name, so they no-op cleanly when a component or field is absent and cost nothing in a game that declares neither — that build is byte-for-byte unchanged.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Error handling for games — `panic(msg)` prints `file:line: panic: <msg>` to stderr and aborts cleanly (exit 1) instead of crashing, and `assert(cond, msg)` does the same, guarded, only when an invariant is false (`file:line: assertion failed: <msg>`). Non-experts get a clear, located message for a broken invariant or an impossible branch, never a raw segfault or a silent wrong result. The source location is baked in at compile time; the message is any string. Recoverable failures as `try`/`else` values are sequenced after the tagged-union type system (#1); this ships the "programmer bug = loud, located panic" half now.

View file

@ -1,11 +0,0 @@
bump: patch
type: fix
**`const` of a non-int type is no longer miscompiled.** A `const` reference used
to lower to its initializer's raw integer bits hardcoded as type `int`, so a
`const X: fixed = 10.0` compared and computed as the raw Q16.16 value `655360`
instead of `10.0` — silently corrupting fixed-point math (and, in one case, spinning
an infinite loop). A const reference now emits its initializer expression with that
expression's real type, so `fixed` / `bool` / `string` consts behave correctly (and
computed const initializers work too). Every existing const is an `int` literal, for
which the lowering is byte-identical, so the bootstrap fixpoint and all golden
renders are unchanged. Covered by selfhost/tests/const.ludic.

View file

@ -1,20 +0,0 @@
bump: minor
type: feat
**HTTP client standard library (#6).** `Http.*` — a poll-based HTTP/HTTPS client
for out-of-band data (leaderboards, cloud saves, remote config, telemetry,
downloads). `Http.get` / `Http.post` / `Http.request` start a request (or
`Http.open` + `Http.set` headers + `Http.body` + `Http.send` to build one up);
`Http.poll` returns `-1` while pending, `0` on error, else the status code, so
the frame never blocks; `Http.status` / `ok` / `text` / `body_len` / `header`
read the reply, and `Http.free` releases it. TLS is the system's, on by default
with certificate verification — an `https://` URL just works. Pairs with the
`Json.*` companion (already shipped in #44): `Json.parse(Http.text(h))`.
The transport (`runtime/native/http.ll`) drives NSURLConnection through the objc
runtime's C ABI on a detached pthread — same hand-written-IR, no-ObjC/no-C style
as cocoa.ll — and is spliced with Foundation only when a program uses `Http.*`
(macOS for now). The response parser (`Http.parse`, header lookup) is pure Ludic
and portable, so the suite exercises it offline with no network. HTTP depends on
the network and wall clock and is explicitly out-of-band: it must never feed the
deterministic lockstep/replay simulation, like `Net.*` and `Time.now`. Also adds
the `\r` string/char escape the protocol needs.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Input action maps + deterministic record/replay (#7) — gameplay now reads named actions instead of physical keys, so keys are rebindable and a control scheme is data. `Input.bind(action, key)` binds a key to an action, `Input.down`/`Input.pressed` read it (held vs one-shot edge), and `Input.rebind(action, from, to)` remaps it at runtime for an options screen. `Input.poll` is the single per-frame input read, which makes deterministic replay fall out for free: `Input.record` captures the polled key each frame and `Input.replay` feeds the tape back so a run reproduces exactly — the seed of lockstep netcode. All integer and deterministic over the single-key poll every target provides; the multi-key/gamepad/touch/analog device layer is tracked as a follow-up.

View file

@ -1,14 +0,0 @@
bump: minor
type: feat
**Input device layer (#50).** Beyond one key per frame, gameplay can now read
multiple simultaneous held keys (`Input.key_down` / `key_pressed` /
`key_released`), analog `Input.axis` and a normalized `Input.vector`, the mouse
(`Input.mouse_x/y`, `mouse_dx/dy`, `mouse_down`, `wheel`), gamepads
(`Input.pad_button` / `pad_axis` / `pad_connected`) and touch
(`Input.touch_count` / `touch_x/y`). The held set is fed by the window when
windowed — cocoa.ll now tracks keyDown/keyUp into a held-key bitset and the mouse
buttons/wheel — and by the `Input.press` / `Input.set_mouse` / `Input.set_pad` /
`Input.set_touch` injection on every target (Godot-style action injection, for
replays, AI, and network-fed input). All integer and deterministic, and
`Input.record` / `replay` snapshot the full per-frame device state, extending
#7's single-key tape.

View file

@ -1,14 +0,0 @@
bump: minor
type: feat
**Native input hardware bindings (#51).** The #50 device layer's platform side
is now wired on macOS, feeding the same state buffers the read APIs already use —
no API changes. cocoa.ll reads the live cursor position (`mouseLocationOutsideOfEventStream`,
converted to framebuffer pixels and y-flipped) so windowed games get
`Input.mouse_x/y` without injection; polls GameController.framework each frame
(`GCController.controllers` → extended-gamepad buttons/sticks) into
`Input.pad_button` / `pad_axis` / `pad_connected`, mapping to SDL_GameControllerButton
order; and routes the view's NSTouch phase handlers into `Input.touch_count` /
`touch_x/y`. A windowed build now loads GameController (via `-needed_framework`,
since its classes are reached by name); the whole binding is DCE'd out of a
headless build. `Input.set_pad` / `set_touch` injection still works on every
target, so replays, AI, and network-fed input are unchanged.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Jobs, Promises & opt-in Sync — a layered concurrency library (#14). The safe default is `Job.*` / `Promise.*`: a `Job` is a future — start background work with `Job.run(kind, arg)` (a cooperative compute that advances a little each `Job.pump(budget)` and finishes after enough frames, so heavy work spreads out instead of hitching one), or drive one by hand with `Job.defer` + `Job.fulfill` / `Job.fail` / `Job.cancel`; poll it with `done`/`ok`/`failed`/`cancelled`, read `result`/`error`, and count outstanding work with `Job.pending`. `Promise.all`/`Promise.race` combine handle lists into a group job resolved on the main thread, and `Promise.count_done`/`Promise.all_done` give a loading bar its numerator and its ready check. The advanced, opt-in `Sync.*` tier — `mutex`/`lock`/`unlock`/`try_lock`, an atomic counter (`atomic`/`get`/`set`/`add`/`cas`) and a bounded int `channel` (`send`/`recv`/`can_recv`/`len`), plus `cpu_count` — is the "here be dragons" surface for engine-level message passing. The whole thing is a deterministic cooperative scheduler: results are collected on the main thread at a point you choose and a Job never touches the ECS world directly, so lockstep networking and replays stay bit-exact — the same jobs and the same budget reproduce byte-for-byte on every target, and a preemptive OS-thread backend can slot behind this same API later. Ludic has no first-class functions, so a Job carries a compute kind + int argument (or a hand-driven `defer`) rather than a closure, and Promise progress is polled rather than chained through a `then`. Written in Ludic and spliced on demand (like Regex/Dict/Numeric), so a program that never mentions `Job.*`/`Promise.*`/`Sync.*` compiles byte-identically and the C-free bootstrap fixpoint is untouched.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
ECS-native 2D lighting (#47) — a torch is now just an entity carrying `Light2D { x, y, radius, color, intensity }`, a wall an entity carrying `Occluder { x, y, w, h }`, and one `Ambient { color }` entity sets the night tint. The engine runs the whole deterministic light pass (ambient modulate → carve occluder shadows → accumulate each additive radial light) at the end of the Render phase and presents — no `Light.*` calls wired by hand. A `Light2D`/`Occluder` takes its position from a `Position { x, y }` component when the entity carries one, else from its own `x`/`y`. Built on the engine-owned-system hook, so it costs nothing in a game that declares none of these components.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
2D lighting — the `Light.*` namespace: a deterministic software light-accumulation pass over the framebuffer. `Light.ambient` modulates the scene toward a tint (night/cave mood), `Light.point` adds 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, so a scene lights identically every run and in a headless render.

View file

@ -1,12 +0,0 @@
bump: minor
type: feat
**Lighting render-quality tiers (#49).** The 2D light pass gains the tiers 3-4
from the original proposal, all on the same deterministic accumulation core:
`Light.spot` cone/flashlight lights (direction + spread), a `Light.falloff`
exponent (linear → quadratic → …), `Light.soft` soft shadows (occluder edges
fade through a penumbra), `Light.gel` colour cookies (centre → rim tint),
normal-mapped surfaces via `Light.normal` + `Light.height` (surfaces shade by
facing, N·L), and a `Light.time_of_day` day/night ambient ramp. The engine
lighting system consumes matching optional `Light2D` fields
(`direction`/`spread`/`falloff`/`softness`/`gel`) too. Every tier is integer +
Q16.16 fixed, so scenes light identically on every run and headless.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Recoverable failures as values (#46) — a fallible function returns a `result` (a new value type), built with `ok(payload)` on success or `err(message)` on failure, and 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 the stack. `is_ok(r)` / `is_err(r)` classify a result without unwrapping it. Payloads are any i32-width scalar (int/fixed/bool/entity); the whole feature is additive and only kicks in when `ok`/`err`/`try` are used, so programs that don't touch it compile byte-identically. Complements `panic`/`assert` from #8, which handle the unrecoverable programmer-bug half. The optional top-level frame `recover` remains deferred (it needs a frame-abort/unwind mechanism); the full tagged-union/`any` generalization is tracked in #1.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Tagged-union enums (#56) — an `enum` variant may now carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`), turning the enum into a sum type. Variants are constructed by name (`Door(3)`, bare `Empty`) and boxed as a tag plus payload slots; `match` destructures them, binding each payload in the arm's scope (`Door(n) => …`, `Portal(x, y) => …`). A tagged `match` is checked for exhaustiveness — it must cover every variant or end in `_`, and constructor/pattern arities are checked too — so adding a variant surfaces every site that must handle it. Plain (all-bare) enums keep their zero-cost compile-time-ordinal `Name.Variant` representation, byte-for-byte unchanged.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Built-in testing framework — a `test "name" { … }` block, discovered and run automatically by a synthetic runner (no `entry` to write). Inside a test, `expect(cond)`, `expect_eq(a, b)` and `expect_near(a, b, tol)` assert; on failure they print `file:line: … failed (got …, want …)` and mark the test failed without aborting, 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 and accumulated-integer game math need. Coverage instrumentation is a follow-up.

View file

@ -1,13 +0,0 @@
bump: minor
type: feat
**2D value types — `IVec2` and `Rect` (#1, phase 1).** Two by-value spatial
types that lower to packed integers, so they copy like scalars and never
allocate. `IVec2` is an integer 2D vector (a pair of `int` packed into one
`i64`) for tile and grid coordinates — `IVec2.make/zero/x/y/add/sub/scale/dot`,
the grid distance `IVec2.manhattan`, `IVec2.equal`, and `IVec2.to_vector` to
widen into the fixed-point `Vector`. `Rect` is an axis-aligned rectangle (four
Q16.16 `fixed` components packed into one `i128`) for HUD boxes and hitboxes —
`Rect.make/x/y/w/h`, the derived `Rect.right/bottom/center`, and the
`Rect.contains` (point) and `Rect.intersects` (overlap) tests. Both are exact
and deterministic, bit-identical on every platform, and complement the existing
`Vector` and `Color` types that already cover phase 1's other 2D primitives.

View file

@ -1,13 +0,0 @@
bump: minor
type: feat
**Exact numbers for game economies — `BigInt` + `Decimal` (#52, types phase 2).**
The thing a tycoon or idle game must never round wrong. `BigInt.*` is an
arbitrary-precision integer (sign-magnitude, base-1e9 limbs) with `from`/`parse`,
`add`/`sub`/`mul`/`pow`, integer `div`/`mod`, `cmp`/`eq`/`is_zero`, `to_int` and
`str` — for idle counters and exact huge currencies that overflow a 32/64-bit
int. `Decimal.*` is an exact base-10 fixed-point number (a `BigInt` mantissa
plus a decimal scale) with `from`/`parse`, exact `add`/`sub`/`mul`, `cmp`/`eq`,
`scale`/`rescale` and `str` — so money like `0.10 + 0.20` is exactly `0.30`, no
binary rounding. Both are exact and therefore deterministic; no `f32`/`f64`. The
engine (runtime/native/bignum.ludic) is spliced in on demand, so it costs
nothing when unused and works in plain tools as well as games.

View file

@ -1,13 +0,0 @@
bump: minor
type: feat
**Containers — `Dict` + `Set` (#54, types phase 4).** Everyday string-keyed
lookups backed by one open-addressing hash table (FNV-1a, linear probing,
tombstone deletes, grows at load factor 0.7), spliced in on demand.
`Dict.*` maps string keys to `int` values — `new`/`set`/`get`/`get_or`/`has`/
`remove`/`size`/`clear`/`keys` — for resource counts and id/name registries.
`Set.*` is a set of string members — `new`/`add`/`has`/`remove`/`size`/`clear`/
`members` — for tags, unlocked achievements and visited tiles. O(1) average
lookup instead of a linear list scan. Values are `int` (also holds an `entity`
or any small id); the `Value.*` tree already covers richer/heterogeneous maps,
and `[T; N]` inline fixed arrays remain future work (typed buffers `words` /
`fixeds` / `pointers` and `[]T` slices cover heap-backed arrays today).

View file

@ -1,15 +0,0 @@
bump: minor
type: feat
**Polish numeric types — `Huge` + `Angle` + `Percent` (#55, types phase 5).**
`Huge.*` is an idle/incremental big number (normalized mantissa x 10^exponent)
reaching far past the integer range — `from`/`add`/`sub`/`mul`/`neg`/`cmp`/
`sign`/`mantissa`/`exp`/`str` (scientific `1.23e45`); it is display-scale, not
lockstep-exact (use BigInt/Decimal for exactness). `Angle.*` is an auto-wrapping
radian angle — `from_degrees`/`to_degrees`/`wrap`/`sin`/`cos`/`add`/`diff`
(shortest signed rotation)/`lerp` (shortest arc) — over the deterministic
`Math.*` trig. `Percent.*` is a value clamped to [0,1] — `clamp`/`of`/`lerp`/
`apply` — for health fractions, volumes and interpolation `t`. All spliced in on
demand. Remaining phase-5 items are already covered or need front-end work:
`duration` = `Duration.*`, `rune`/`char` = `Unicode.*`, `i64` = `long`; and the
nominal-safety wrappers `handle` / typed `name` plus the other sized ints need a
type-checking pass, tracked for later.

View file

@ -1,11 +0,0 @@
bump: minor
type: feat
**`option` — a maybe-a-value type (#53, types phase 3).** The companion to
`result` (#46): `some(v)` wraps a present value (any i32-width scalar), `none()`
is the empty case — a missing value with no magic `-1` sentinel — and
`is_some`/`is_none`/`unwrap_or(o, fallback)` test and read it. A heap
`%Option = { i32 present, i32 value }`, additive and gated, so a program that
does not use it compiles byte-identically. With `result`/`ok`/`err`/`try`
already shipped, the concrete option/result safety types of phase 3 are done;
general tagged-union enums (variant payloads + binding `match` + exhaustiveness)
are tracked separately in #56.

View file

@ -1,3 +0,0 @@
bump: minor
type: feat
Generic value tree + reflection serialize/deserialize + JSON bridge (#44) — a self-describing `Value` node (null/int/fixed/bool/str/list/object) with constructors, builders (`Value.add`/`Value.put`) and accessors (`Value.get`/`Value.at`/`Value.count`/`Value.kind`/`Value.as_int`/`Value.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; the model id rides along under `"@kind"`. `Json.encode`/`Json.parse` bridge the tree to and from compact, stable, diffable JSON text, with `fixed` written as an exact terminating decimal that parses back bit-for-bit. Together they are a one-call save/load for entities and the backbone of data-driven tooling. Written in Ludic and spliced on demand (like Query/Light), so a program that doesn't touch `Value.*`/`Json.*`/`Reflect.serialize` compiles byte-identically and the C-free bootstrap fixpoint is untouched. The general tagged-union/`any` language type remains tracked in #1; this delivers the concrete value tree the serializer needs.