Commit graph

58 commits

Author SHA1 Message Date
e6f565a6c4 carry: the runtime and render3d work Maroon Lake builds against
Input.text, App.monitor_count / window_to_monitor / window_fixed,
gl_sleep_us, and the grass and collide changes, uncommitted on main and
depended on by the game; carried here so the language work starts from
what the game actually uses. main's working tree is untouched.
2026-09-23 15:55:02 +03:00
43d5eb5b88 feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show
Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.

Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.

App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:32:23 +03:00
ebb1a00352 feat(udp): Udp.* - polled IPv4 datagrams on macOS and Windows
Udp.open/port/send/recv/from_ip/from_port/close/resolve/local_ip/ip/ip_text, native
BSD sockets (udp.ll) and Winsock (udp_win.ll, -lws2_32), linked only when a program
uses Udp.*; example, docs and tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:16:14 +03:00
e010c2cecc feat(lang): float and double types with ordinary operators
`float` (32-bit) and `double` (64-bit) with + - * / %, comparisons and unary minus.
Decimal literals take their type from context and stay `fixed` elsewhere; int and long
promote implicitly (LUDIC_WARN_FLOAT_PROMOTE=1 lists every promotion). float(), double(),
int(), long() and fixed() convert; floats(n)/doubles(n) buffers; float fields, globals,
constants and parameters; Math.* computes in float for float arguments; string/print
write the shortest round-tripping decimal; float_bits/float_from_bits expose the bits.
@deterministic code may not use floats. The f_* runtime helpers stay as they are.

Editors know the new type words; the JetBrains plugin is 1.5.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:13:31 +03:00
619476ffed fix(input): keys are physical positions on every layout; Input.key_label names them
The Windows runtime keyed the held set by the character MapVirtualKeyA gave a
virtual key, so a game's WASD belonged to whatever the active layout put there,
and with an input method on every letter arrived as VK_PROCESSKEY. The typing
block is now read from the scancode (the arrows, numpad and F-keys still by
virtual key); macOS reads keyCode the same way. Input.key_label(key) names a key
in the player's own layout (Windows) or as its US character elsewhere.

Verified on Windows with SendInput into a live window: VK_Z carrying W's scancode
holds 'w', VK_PROCESSKEY carrying A's holds 'a', Caps Lock changes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:26:53 +03:00
c10abd9f9f feat(jobs): real OS threads - Job.parallel_for, fn name, thread-safe Sync
- `fn name` names a top-level function as a value (E_FNREF, lowers to @fn_<name>); the worker
  entry point for Job.parallel_for, which checks it takes (int, pointer-like) and returns void.
- runtime/native/threads.ll (pthreads) and threads_win.ll (Win32 SRWLOCK/CONDITION_VARIABLE): a
  pool of one worker per core but one, parked between batches; every thread claims chunks by
  compare-and-swap. Linked only into programs that use Job/Promise/Sync, by `ludicc -o`,
  `ludic build` and the test suite's build helper.
- Sync.* is real: native mutexes, atomics as cmpxchg retry loops (neither clang takes atomicrw,
  the PC's rejects seq_consistent), mutex-guarded channels, Sync.cpu_count from the OS.
- spawn/despawn on a pool thread stop the program with a located panic.
- examples/library/threads.ludic and its test; docs for fn, Job.parallel_for, Job.is_worker.
- Reseeded (bootstrap-cfree: out.ll == seed.ll). 141/141 on macOS; jobs, threads and the guard
  pass on Windows from the reseeded Windows seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:52:22 +03:00
208cad7ca1 feat(vk): Vk.* - Vulkan 1.0-1.4 generated from the registry, loaded at run time
ludic-dev vkgen reads vk.xml into runtime/native/vk_api.ludic (constants, every struct's
<Struct>_sizeof and <Struct>_<field> offsets, one extern per command) and vk_thunks.ll.
Every size and offset was compiled against the SDK's C headers; `ludic-dev test` checks the
tracked files against the registry wherever the Vulkan SDK is installed.

vk_win.ll (vulkan-1.dll) and vk_mac.ll (libvulkan.1.dylib, MoltenVK) open the loader at run
time, so a program built with Vk.* starts on a machine without Vulkan. ludicc and ludic
build link both for any program that uses Vk.*. The seeds are regenerated for the new
compiler.

vk_probe reports what a machine's Vulkan can do; vk_compute dispatches a Slang compute
shader and reads the picture back, clean under the validation layer on an RTX 3070 Ti and
on an M4 Pro through MoltenVK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:52:12 +03:00
5801005fca feat(windows): a Windows target for ludicc, and the compiler builds itself there
--target <triple> (default: the host, from OS=Windows_NT) selects the Windows
runtime. emit_win.ludic defines the POSIX names the backend already calls over
the UCRT and Win32 in IR, so rename replaces an existing file, ftell is 64-bit,
and fopen is binary. Known folders follow %APPDATA% / %LOCALAPPDATA% / %TEMP%,
and the driver speaks cmd.exe, writes .exe outputs and finds LLVM's clang.

Verified: macOS three-stage fixpoint, ludic-dev test 135/135, and on Windows
the Mac-emitted Windows IR and the Windows-built compiler's IR are identical
through two generations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:59:41 +03:00
38b6b81cd7 feat(app): App.set_icon, so a game without a bundle still has an icon
`ludic bundle` builds an AppIcon.icns and macOS reads it out of the .app, so a
shipped game has an icon. `ludic build` produces a bare executable: no bundle, no
CFBundleIconFile, and so no icon at all - macOS draws the generic green "exec"
tile. That is the build a developer runs every day, which is why "the game has no
icon" can be true for months while the bundle is perfect. It was here: the .app's
icns validated against iconutil, the plist was right, the signature was right,
and NSWorkspace rendered the artwork - and the binary beside it still had the
exec tile.

App.set_icon(path) takes the bytes through lp_pak_open, so a packed path and a
loose one both work and this does not repeat Audio.load's trick of taking a
filesystem path only. NSData copies them, so the buffer goes straight back. A
missing or undecodable image leaves the existing icon alone rather than clearing
it; a bundled app is unaffected; headless links no AppKit and compiles it away.

Verified the Cocoa sequence against an ObjC twin doing the same message sends:
before, a bare binary's applicationIconImage is the generic 128x128 tile; after,
it is the 1024x1024 artwork.

Backend change, so selfhost/ludicc.seed.ll is reseeded: the bootstrap fixpoint
and the C-free rebuild from the seed both pass.
2026-09-12 12:55:12 +03:00
3c27606747 feat: per-voice audio, outlines for what is not an actor, and a coast
Three things a game could not say, each of which had been worked around.

Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain,
pitch and stereo position. Audio.volume and Audio.pitch are global - they are
the options screen - so a game placing a sound in the world was fighting them,
and distance attenuation was simply not expressible. The backend already took
volume and rate per call; this adds setPan: alongside them and stops routing
through the master state.

ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around
something that is not an Actor. The outline pass walked the actor list and
stopped, so instanced scatter - a forest - could not be highlighted at all. It
is a queue flushed by the same pass, which is what gets the depth test right
when the caller does not control pass order.

And terrain_coast(cx, cz, margin, fall), the other way to make an island:
the sea around the survey's own edge rather than cut out of the middle of it.
terrain_island measures a radius from a centre, which drowns two thirds of a
real survey to make an island of the rest; this measures inward from the
boundary, so everything the data covers stays land and the coast is where the
data runs out. Both modes gained a strand - the last few metres of height
either side of the water line compressed, which stretches a cliff plunge out
into beach and shallows.

132 regression tests and the self-host fixpoints pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:25:46 +03:00
be74b4de6f feat(bundle): ship a game as a macOS .app, with a splash it controls
`ludic build` produces a program. Double-clicked it opens a Terminal window, it
wears the generic executable icon, it calls itself whatever the file is called,
and it carries none of its assets. `ludic bundle` produces an application.

Everything it needs is in package.ludic, so the command takes no arguments: an
Info.plist and PkgInfo from `app` lines, an .icns built by sips and iconutil at
all ten sizes macOS asks for from a single source PNG, the asset pack in
Contents/Resources, and an ad-hoc signature - which is not optional on Apple
silicon, where an unsigned binary is killed rather than warned about. The bundle
identifier falls back to the package path reversed, so a project that never
thinks about it still gets a defensible one instead of two apps sharing a key
Launch Services hangs the Dock, saved state and permissions off.

A bundled game is moved to ~/Library/Application Support/<name> before main,
because Finder starts a .app with its working directory at "/" where no save
could ever be written. Reads come out of the pack, writes land somewhere real
and per-user, and the game's save code needs no change and no platform
knowledge.

The splash is the other half of looking like an application. A game that loads
165 MB spends a visible moment doing it with nothing on screen, which from the
outside is indistinguishable from a launch that failed. splash_show puts a
borderless window up from the same constructor that mounts the pack - before
main, so it appears while the process is still starting rather than after the
slow part it exists to cover - and reads the artwork out of the pack like any
other asset. It turns the run loop enough times to be mapped and composited
there and then; once composited the backing store survives a busy main thread,
so it stays up for the whole load.

Nothing hides it automatically. Only the game knows when its first real frame is
ready, and a splash that vanishes before that leaves the same black gap it was
covering, so the game calls App.splash_hide(). Headless there is no splash and
the call lowers to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:57:27 +03:00
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
647dfec334 feat(compiler): list literals, typed compound assignment, file:line diagnostics
- `[a, b, c]` list literals (E_LIST → emit_list); static_type learns
  slice-element, `new T`, list, string and literal kinds
- `x op= y` lowers through the same path as `x = x op y` (emit_bin_vals):
  fixed `*=`/`/=` use the Q16.16 64-bit paths, string `+=` concatenates,
  int→long widens; unary `-` keeps a fixed operand's type (arith_ty)
- one `unescape()` table for "strings", 'chars' and `interpolation`;
  `'\''`, `'\\'`, `'\"'` no longer read as 0; unterminated char literals
  and unexpected characters are errors instead of silently skipped
- every diagnostic is `file:line: error: msg` (g_parse_file / g_err_file,
  Node.file + Node.line set by node()); tok_desc() in expectation errors;
  duplicate `function` names and unknown `phase` names are reported in
  source terms (phase_id used to default unknown phases to Overlay)
- interpolation holes skip braces inside string literals
- hand-IR preludes move from the user `@fn_` prefix to `@lp_` so a user
  `is_ws` / `str_eq` / `path_join` no longer collides at link time
- `@ClearColor(expr)` accepts any constant expression; `Os.pid()` added
  (docs page + inventory); `str_starts()` in support/str
- main.ludic: `else if` flag ladder, char literals, stale script comments
- examples/lang/operators.ludic covers all of the above; os.ludic covers
  Os.pid; docs pages for Os.pid and the Overlay phase; ten changesets
- reseeded: selfhost/ludicc.seed.ll is the new compiler's own fixpoint

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:16 +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
347352cc4c feat(ecs): #80 entity-pool stats (Pool.live/free/reserved/capacity)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m27s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
Ludic's ECS is already pool-based — the allocator recycles freed entity slots
through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
storage is fixed per-entity arrays, so spawn/despawn churn (bullet-hell/horde)
does no per-spawn heap allocation and cannot fragment. Expose that with a Pool.*
namespace so a game can watch reuse: Pool.live (alive now), Pool.free (recycled
slots waiting), Pool.reserved (high-water — stays flat across a steady
spawn/despawn loop, proving reuse not reallocation), Pool.capacity (the fixed
cap). Zero-cost inline reads of the existing counters.

Example pool.ludic proves the key property: after despawn+respawn,
Pool.reserved() stays 3 (freed slot reused) — prints 0 0 3 3 0 2 1 3 0 3 1.
4 docs pages. Full suite 118/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:23:58 +03:00
f2cb3cd7e8 feat(assets): #82 incremental asset preloading + loading-scene pattern
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m27s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
Assets loaded synchronously in Boot stalled the first frame(s). Adds an
Assets.* preload queue over the #81 atlas: Assets.enqueue(name, path) queues a
named image without loading it, Assets.pump(max) loads up to max per frame
(returns how many), and Assets.total/loaded/ready/progress (0..100) drive a
progress bar. A loading scene pumps a few per frame, draws Assets.progress(),
and becomes the play scene once Assets.ready() — the deterministic, no-threads
form of async preloading (work spread across frames; same enqueue+pump order
loads identically every run). Loaded assets are reachable by name via
Assets.get / Sprite.named.

Example preload (enqueue 3, pump incrementally 0->33->66->100, ready flips, get
by name) prints 3 0 0 0 1 33 66 1 100 1. 6 docs pages. Full suite 117/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:19:19 +03:00
23e232e380 feat(lang): #76 namespace block form with export/internal visibility
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
`namespace Name { export function foo(...) ... internal function bar(...) ... }`
declares a Name.* namespace once and controls its public surface declaratively,
instead of annotating every function with @Namespace(Name). Inside the block
each `function short(...)` is emitted as `namelower_short`; export (the default)
makes it callable as Name.short(...), internal keeps it a private helper
(emitted, callable by short name from siblings — calls are rewritten — but
Name.internalOne() is a compile error). Block sugar for the per-function
@Namespace annotation; a package's public API reads at a glance. Namespaces
declared the old way are unchanged (gameplay_foundation still passes).

namespace added to LUDIC_KW_DECL + JetBrains/TextMate + docs page + inventory
(vocab/impl/docs checks green). Example namespace_block (export + internal +
sibling calls + internal-visibility compile error verified). Full suite 116/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:14:54 +03:00
3eb5447f74 feat(input): #89 cursor capture — Input.cursor_mode (hide/lock/confine)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
A windowed action game can hide the OS cursor and lock/confine the mouse to the
window. Input.cursor_mode(mode): 0 normal, 1 hidden (draw your own reticle),
2 locked (hidden + dissociated — the mouse feeds relative motion via
Input.mouse_dx/dy and Input.mouse_x/y is a clamped virtual cursor, FPS/twin-stick
aim), 3 confined (dissociated but visible; the mouse can't leave the window).
The platform auto-releases (shows + reconnects) while the window is not key
(Cmd-Tab) and on close, so the cursor is never left captured. Headless it is a
no-op (DCE'd).

Native macOS impl in cocoa.ll: [NSCursor hide]/[unhide] (ref-counted, toggled
only on change so the count stays balanced across focus changes),
CGAssociateMouseAndMouseCursorPosition, and CGGetLastMouseDelta for the relative
virtual cursor, behind a new win_cursor_mode intrinsic. Windowed-only behaviour
(not in the headless golden suite); example compiles headless and links
windowed. Full suite 115/0, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:06:34 +03:00
f3f1336882 feat(assets): #81 namespaced spritesheet/atlas API (Sprite.* / Assets.*)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 29s
Sprite loading was a bare png_load — one file per 16x16 sprite, no way to load
one sheet and address a cell by grid coords or name. Adds a Sprite.*/Assets.*
runtime (atlas.ludic) over the variable-size image loader, so a cell is a
sub-rect of the kept image and is NOT restricted to the 16x16 sprite table:
Sprite.sheet(path,cw,ch), Sprite.cell(sheet,col,row),
Sprite.cell_span(sheet,col,row,cols,rows) (a sprite may span >1 cell),
Sprite.define/named (name + lookup), Sprite.draw/draw_scaled (through
camera/zoom/clip like Screen.sprite), Sprite.width/height, and
Assets.image/load/get. Spliced on demand (Sprite.sheet/… or Assets.*), so a
program using neither is byte-identical.

Example examples/library/atlas.ludic (verified against a real 12x11 Kenney
sheet, incl. a 2x3 multi-cell span and named lookup). 14 docs pages. Full
suite 114/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:58:13 +03:00
ad3be0c53c feat(input,ecs): #79 Input.axis_i directional int + #84 world bounds
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m23s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
#79 — Input.axis_i(neg,pos) -> int returns a -1/0/1 movement intent from the
multi-key device set, so WASD-to-movement needs no bool->int glue and feeds an
int mover directly (dx = Input.axis_i('a','d')).

#84 — a Bounds config entity (rect + policy, from ludic.core) drives the
engine-owned world-bounds system (LateUpdate): clamp / wrap / bounce (clamp +
flip Body velocity) / kill (despawn a body fully outside). Reads the Collider
size so the box stays inside; off by default, spliced only when Bounds is
declared (byte-identical otherwise). Adds World.despawn(e) — the by-id
reflective despawn (runs @OnDespawn + frees) via a new world_despawn intrinsic
whose @fn_world_despawn helper is emitted in emit_program's tail once a use is
seen (the g_uses_* prelude pattern), used by the kill policy and callable from
any system.

Examples input_movement + world_bounds. Full suite 112/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:44:41 +03:00
bccd26fb29 feat(input): #83 Input Manager + auto-commit the device layer in the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
Fixes the papercut: the generated frame loop called rt_poll() (feeding only
Input.key) but never input_poll(), so Input.active/key_down/mouse/pad read
empty unless the game called Input.poll() by hand. The loop now calls
input_poll() when the game uses any Input runtime method — committing the
held-key/mouse/gamepad state, and record/replay — and stores its return as the
frame key so Input.key still works. A game using no Input runtime keeps the
plain rt_poll path, byte-identical.

Adds the Input-Manager API: Input.action(name,key) ships a default binding
(kept if already bound, so a rebind/loaded map isn't clobbered),
Input.bind_pad(name,button) makes an action device-agnostic (keyboard OR pad),
and Input.active/just_pressed/just_released read the multi-key device layer
with clean on-press/on-release edges (deterministic, dispatch-free — a handler
polls the edge; a replay fires identically).

Examples input_manager + input_auto, 5 docs pages. Full suite 107/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:04:10 +03:00
6a83c28e05 feat(camera): #78 deterministic Camera.zoom (Q16.16 render-time zoom)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m15s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
The #78 investigation rejected hardware f32/f64 for the coordinate types
(they would desync lockstep/replay/save) and identified camera zoom as the
one genuinely-missing render feature. Ship it: Camera.zoom(scale) scales the
whole view about the screen centre by a Q16.16 factor, threaded through the
same two framebuffer chokepoints (rt_put_px/rt_fill_rect) that carry the
camera offset, so it composes with Camera.set/follow/shake. Gated by an
internal rt_cam_zoomed flag so a game that never zooms renders byte-for-byte
identically (golden renders unchanged); Camera.zoom(1.0) turns it back off.
The world coordinate types stay integer px + Q16.16 velocity, so it's a pure
render-time transform and itself deterministic.

Example examples/library/camera_zoom.ludic (pixel-readback verified),
docs page, RFC updated (docs/RFC-POSITION-TYPES.md). Full suite 105/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 06:55:37 +03:00
764a0296ce feat(stdlib): Tiled P6 — infinite/chunked maps, .world stitching, base64+zstd (#74)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m4s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
- Infinite/chunked maps: <chunk x y width height> (TMX) and JSON chunks[]
  (default 16x16) decode and flatten into the dense layer array, sized to the
  chunk union; the map's 0/0 header dimensions fall back to the flattened bounds.
- .world stitching: Tiled.world / Tiled.world_count / Tiled.world_map read a
  .world (JSON, reusing Json.parse) and list its member maps at their offsets.
- base64+zstd: a self-contained pure-Ludic Zstandard decompressor
  (runtime/native/zstd.ludic, RFC 8878) — the design's "largest single item,
  explicitly last". Frame header + raw/RLE/compressed blocks; raw/RLE and
  direct-weight Huffman literals; the full FSE sequence path (predefined,
  transcribed exactly from zstd's hardcoded tables since they're not rebuildable
  from the default distributions; RLE; FSE-described) with repeat offsets and
  execution. Decodes the low-entropy GID streams a tilemap produces; a high-
  entropy FSE-compressed-Huffman-weights block fails cleanly with -1 rather than
  emitting wrong bytes (documented scope).

Proven by library/tiled_p6.ludic (12 assertions): TMX + TMJ chunk flattening,
.world offsets, and a real zstd-compressed tile layer decoding byte-exactly to
its CSV baseline. x test: 97 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 16:07:10 +03:00
78a5719fae feat(engine): Tiled P5 — image/group layers, iso/hex/staggered coords, Wang (#73)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 1m59s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
- Image layers: <imagelayer> renders with parallax + optional repeatx/repeaty
  tiling across the view; the image loads relative to the map file. Group layers
  flatten at build (children render in file order); layer offset/opacity/tint are
  queryable (Tiled.layer_kind / layer_offsetx / layer_offsety / layer_opacity /
  layer_tint), a group's tint applying recursively.
- Orientation transforms: Tiled.cell_x / Tiled.cell_y compute a tile cell's
  screen position for orthogonal, isometric ((x-y)*tw/2, (x+y)*th/2), staggered,
  and hexagonal (rows step by (tileh+hexsidelength)/2, alternate rows shoved per
  staggerindex) — the tile draw now places cells through them.
- Wang: exported Wang-set GIDs are ordinary GIDs and resolve through the standard
  GID resolver; the terrain-corner authoring concept is editor-side (design cut).

Proven by library/tiled_p5.ludic (14 assertions) over the real
isometric_grass_and_water.tmx (iso + Wang) and hexagonal-mini.tmx, plus a
hand-authored image+group fixture. x test: 96 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:14:36 +03:00
b8c71d2a8e feat(stdlib): Tiled P4 — objects, custom props/types, templates, opt-in spawn (#72)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 1m57s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 27s
- Object layers: all shapes (rectangle/ellipse/point/polygon/polyline/text) and
  custom properties parse and are queryable — Tiled.object_count / Tiled.object /
  Tiled.object_shape, and Tiled.prop / prop_int / prop_type over any object /
  tile / layer / map.
- Custom types: Tiled.load_types reads an objecttypes.xml project custom-type
  table so a class property resolves its default; enum values resolve as strings.
- Templates: Tiled.template reads a .tx/.tj, and Tiled.load merges each object's
  `template` reference under the instance's overrides (field inheritance).
- Opt-in spawning: Tiled.spawn / Tiled.spawn_layer map an object's class +
  properties onto Ludic components through the reflection ABI (Position from x/y,
  each property to a like-named field). Off by default — no gameplay coupling in
  the core load. Split into tiled_spawn.ludic, spliced only for a Tiled *game*
  (the world table exists only under has_ecs), so a plain map-reading tool never
  links against the reflection ABI.

Proven by library/tiled_p4.ludic (18 assertions) incl. the design's named
orthogonal-outside.tmx object shapes. x test: 95 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:08:06 +03:00
58e1105f2d feat(engine): Tiled P3 — animated tiles (deterministic frame clock) + tile objects (#71)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 1m53s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 28s
- Animated tiles: a tileset <animation>'s {tileid, duration} frames advance as a
  pure function of the fixed 60/s engine frame counter (the same clock SpriteAnim
  rides), so an animated GID resolves at draw to the current frame's GID with its
  flip flags preserved — deterministic, frame-identical across runs, ticks for
  free. Tiled.frame_gid(map, gid, frame) / Tiled.animated(map, gid) expose it;
  Tiled.draw_anim(map, camx, camy, frame) draws a map with animations advanced.
- Tile objects: object-layer entries with a `gid` draw the tile image (with their
  own flip flags), bottom-anchored at the object position, as placeable sprites;
  tmap_draw_full now renders object layers alongside tile layers.

Proven by library/tiled_p3.ludic (14 assertions): frame advance at authored
durations + wrap, flip flags riding an animation swap, tile-object parse + draw +
flip mirroring, and the design's named rpg/beach_tileset.tsx (33 <animation>
blocks / 131 frames). x test: 94 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:58:21 +03:00
0cb57774d7 feat(stdlib): Tiled P2 — collision normalisation into the Solids feed (#70)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m51s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 28s
Normalise three collision sources into the byte tilemap esys_move / Grid.* /
Path.* read, in the design's priority order (§3.5):

- per-tile <objectgroup> hitboxes (Tiled.tile_shapes) — the precise source;
- the solid/oneway/trigger bool property convention (Tiled.collision_kind);
- the designated collision layer — any non-zero GID solid (Tiled.project),
  the fallback source, applied automatically on load.

Tiled.collide drives collision from a visual layer's per-tile metadata alone
(a tile with no collision metadata stays passable). tmap_collision_kind
classifies a GID (0 none / 1 solid / 2 one-way / 3 trigger) from its metadata;
the projection adds the collision-layer fallback.

Proven by library/tiled_p2.ludic (14 assertions): kind classification for each
source, the property convention and collision-layer fallback producing an
identical Solids feed, a per-tile-<objectgroup> floor blocking an esys_move
mover, and A* over a loaded map matching the hand-authored baseline. x test:
93 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:50:27 +03:00
bc301c8d17 feat(engine): Tiled P1 — rt_tmap model + GID resolver + render + projection (#69)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m48s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 27s
The heart of Tiled support: load a map and draw it.

- rt_tmap model (Tmap/TmLayer/TmTileset in tiled.ludic): map header + ordered
  layers (dense int32 GID arrays, heap-allocated to w*h, lifting the 96x64 cap)
  + tilesets. Built from the intermediate Value tree, so the TMX and TMJ paths
  both feed it.
- GID resolver (Tiled.resolve): gid -> (tileset, localId, flipH/V/D); the three
  flip flags masked off before the local-id lookup, returned alongside. gid==0
  is empty.
- Image-backed render (Tiled.draw): every visible tile layer in file order,
  blitting each tile from its tileset image with flips applied at draw.
- Compatibility projection (Tiled.project / auto on load): a designated
  collision layer projects to the legacy byte tilemap ('#' solid, '=' one-way
  via the oneway property, ' ' empty) so Grid.*/Path.*/esys_move are unchanged.
- Tiled.load resolves external tilesets + images relative to the map file and
  auto-projects a collision/solids/walls layer.
- The grid and physics_tiles demos now run off a loaded map (grid_maze.tmx /
  physics_map.tmx) instead of hand-authored Map.row strings, byte-identically.

Two compiler fixes fell out of this (see the changeset):
- emit_index_addr set g_addr_ty before evaluating the index, so slice[obj.field]
  came back mis-typed; set it last, like the raw-pointer branches.
- @strcmp was declared by both the world table and the fs prelude; centralise
  it in the head prelude so a game that uses Fs/Path links.

Proven by library/tiled_p1.ludic (14 assertions: loads+renders Kenney map
identically from .tmx and .tmj, flip mirroring) + the converted grid/
physics_tiles demos. x test: 92 passed; self-host bootstrap fixpoint intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:43:29 +03:00
071c268de7 feat(stdlib): Tiled P0.5 — TMX/TSX reader over the XML reader (#68)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m45s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 28s
Tiled.read / Tiled.read_tsx (runtime/native/tiled.ludic): map the native
XML formats (TMX/TSX/TX) onto the SAME intermediate the JSON path produces
— a Value tree in Tiled's JSON schema — so one format-independent core
(P1) consumes either reader.

- tmx_to_value walks a <map> into {orientation, width/height, tilewidth/
  height, tilesets[], layers[]}; every tile layer's <data> is decoded to a
  dense GID int list (CSV split, or base64 -> zlib/gzip inflate ->
  little-endian u32s), so a CSV .tmx and a base64 .tmj of the same map read
  structurally identically.
- External tilesets keep the {firstgid, source} reference (as TMJ does);
  embedded tilesets and standalone .tsx (tsx_to_value) inline full geometry
  + per-tile metadata (animation frames, collision objectgroup, class,
  properties) for later phases.
- Object layers, shapes (rect/ellipse/point/polygon/polyline/text),
  image/group layers and custom properties are parsed into the tree now so
  P2/P4/P5 just read it.
- tmj_normalize decodes base64 `data` strings in a parsed .tmj so the JSON
  path matches the XML path.

Proven by library/tiled_p05.ludic (30 assertions) incl. the in-repo Kenney
sampleMap.tmx + external sampleSheet.tsx and TMX-vs-TMJ structural
identity. Docs + inventory added; x test: 91 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:15:41 +03:00
79776d1f6a feat(stdlib): Tiled P0 — XML reader + base64 decode + gzip framing (#67)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m43s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 27s
The three parsing primitives the TMX path needs that were not already in
the tree (zlib inflate + the JSON reader already shipped):

- Xml.* — a minimal, deterministic pure-Ludic XML reader for the
  element/attribute/CDATA subset TMX/TSX/TX use, spliced on demand. Handles
  nested elements, single/double-quoted attributes, text + <![CDATA[…]]>,
  comments, the <?xml?> prolog and <!DOCTYPE>, the five predefined entities
  and numeric character references.
- Base64.* — standard base64 (RFC 4648) decode + a matching pure-Ludic
  encoder; the decoder ignores the whitespace Tiled wraps into <data>.
  Splicing Base64.* also pulls in inflate.ludic so a plain tool can run the
  full base64 -> zlib/gzip decode chain.
- z_gunzip — gzip framing (RFC 1952) over the existing DEFLATE inflater:
  skip the 10-byte header + optional fields, inflate the body, ignore the
  CRC32/ISIZE trailer.

Golden corpus vendored under assets/tiled-fixtures/ (mapeditor/tiled
examples, attributed, + hand-authored multi-encoding fixtures). New
example library/tiled_p0.ludic proves all three (21 assertions); docs
pages + inventory added so check-impl stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 14:04:49 +03:00
7cbd5d175c feat(compiler): package-declarable engine systems + namespaces (#62)
Registry-izes the two hooks that made stdlib namespaces and engine systems
compiler-hardcoded, so a package registers them with no compiler edit — the
Phase-1 prerequisite for shipping the controller libraries (#58–#61) as real
packages rather than in-repo stdlib.

- Engine systems are a data-driven registry (component, esys-fn, phase). The
  core three (SpriteAnim/Motion — Update, Light2D — Render) are seeded in that
  exact order, so uses_engine_systems / emit_engine_systems_for_phase are now
  registry-driven with byte-identical output (verified: anim_ecs, light_ecs,
  snake IR unchanged; 87/0 golden renders; C-free fixpoint holds). A package
  appends with `@EngineSystem(Component, Phase)` on its esys function.
- Namespaces are a registry too: a package marks a provider with
  `@Namespace(Foo)`, and emit_ns_call aliases an otherwise-unknown Foo.method to
  the bare foo_method (the same generic path the core namespaces use) — after
  every hardcoded core block, so core dispatch is untouched.
- Both annotations are keyword-free (like #64's @System), so no vocabulary /
  grammar churn.

Proven end-to-end (hermetic, source path, runs everywhere): a package registers
Score + esys_score via @EngineSystem and coach_bonus via @Namespace; a consumer
game imports it and prints "4 99" — the engine system ran each Update and
Coach.bonus() dispatched, with no compiler edit for the package. Package suite
18/0.

Core stdlib namespaces stay on their optimized hardcoded blocks by design
(byte-identity + determinism); the generic path is proven to carry a namespace
and packages ride it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 12:30:01 +03:00
50ecb8472f feat(stdlib): Jobs, Promises & opt-in Sync concurrency (#14)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 15s
ci / build-and-test (push) Successful in 1m28s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 23s
A layered concurrency library, safe by default. The recommended tier is
Job.* / Promise.*: a Job is a future — Job.run(kind, arg) starts a
cooperative background compute that advances each Job.pump(budget) and
finishes after enough frames (heavy work spreads out instead of hitching),
or Job.defer + Job.fulfill/fail/cancel drives one by hand. Poll with
done/ok/failed/cancelled, read result/error, count outstanding work with
Job.pending. Promise.all/race combine handle lists into a group job resolved
on the main thread; Promise.count_done/all_done power a loading bar.

The advanced, opt-in Sync.* tier (mutex/atomic/channel + 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 and a Job never touches the ECS world, so
lockstep and replays stay bit-exact — same jobs + 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 closures, so a Job carries a
compute kind + int arg (or a hand-driven defer) rather than fn()->…, and
Promise progress is polled rather than chained through then.

Written in Ludic and spliced on demand (like Regex/Dict/Numeric): a program
that never mentions Job.*/Promise.*/Sync.* compiles byte-identically and the
C-free bootstrap fixpoint is untouched. New: runtime/native/jobs.ludic,
emit_ns_call dispatch, parse-time splice, examples/library/jobs.ludic (31
self-asserting checks), 33 docs pages + inventory, changeset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 03:32:31 +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
7c91d24595 fix(compiler): preserve declared type of const references
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 15s
ci / build-and-test (push) Successful in 1m25s
commit-lint / conventional-commits (push) Successful in 1s
docs / build-and-deploy (push) Successful in 22s
A const reference lowered to `val(itoa(g.a.ival), "int")` in emit_call.ludic —
the initializer's raw integer bits, hardcoded as `int`. For a fixed const like
`const X: fixed = 10.0` that yielded the Q16.16 bits (655360) typed as int, so
every fixed comparison/arithmetic against it silently broke (it caused an
infinite loop in runtime/native/numeric.ludic, previously worked around with
inline literals).

Fix: a const reference now emits its initializer expression via emit_expr(g.a),
which carries the initializer's real type (E_FLOAT->fixed, E_BOOL->bool,
E_STR->string) and even handles computed initializers. Every existing const is
an int literal, for which this is byte-identical to the old immediate — the
C-free bootstrap fixpoint and all golden renders are unchanged.

- selfhost/tests/const.ludic + sh_case pin fixed/int/bool const behaviour.
- runtime/native/numeric.ludic restored to named fixed consts (HUGE_TEN etc.),
  which the workaround had inlined; the numeric example (20 assertions) still
  passes, validating the fix under runtime splice.

All suites green: x selfhost-test 31/31 (fixpoint holds, goldens byte-identical),
x test 85/85, x test-tools 30/30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 02:48:03 +03:00
790eda6f73 feat(types): option (some/none) safety type (#53)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m31s
commit-lint / conventional-commits (push) Successful in 6s
docs / build-and-deploy (push) Successful in 23s
Types phase 3 — the option/result safety pair. result/ok/err/try shipped in
#46; this adds its companion option:

- some(v)   -> option   (present value; any i32-width scalar)
- none()    -> option   (empty; no magic -1 sentinel)
- is_some / is_none -> bool
- unwrap_or(o, fallback) -> int

A heap %Option = { i32 present, i32 value }, bare builtins guarded by find_fn
(a user fn of the same name still wins), gated by g_uses_option so unused
programs compile byte-identically — same idiom as result.

Wired: emit_call codegen + %Option decl (emit_decl) + g_uses_option (emit_core),
reseeded seed, vocabulary sync (header/JetBrains/TextMate), builtin docs +
inventory, and a self-asserting example (examples/library/optionresult.ludic +
feat_case). All suites green incl. golden renders byte-identical and the
bootstrap fixpoint.

Tagged-union enums (variant payloads + binding match + exhaustiveness) are the
deep type-system feature, split out to #56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 19:11:27 +03:00
5dc8394f22 feat(types): Huge + Angle + Percent polish numeric types (#55)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m30s
commit-lint / conventional-commits (push) Successful in 6s
docs / build-and-deploy (push) Successful in 23s
Types phase 5 (polish). A splice-on-demand numeric runtime
(runtime/native/numeric.ludic, built on the inline Math.* trig) behind three
namespaces:

- Huge.* — idle big numbers (normalized mantissa x 10^exponent): from/add/
  sub/mul/neg/cmp/sign/mantissa/exp/str (scientific 1.23e45). Display-scale,
  not lockstep-exact (BigInt/Decimal for exactness).
- Angle.* — auto-wrapping radians: from_degrees/to_degrees/wrap/sin/cos/add/
  diff (shortest signed rotation)/lerp (shortest arc).
- Percent.* — clamped [0,1]: clamp/of/lerp/apply.

Remaining phase-5 items are already covered (duration=Duration.*,
rune=Unicode.*, i64=long) or need a type-checking pass (handle, typed name,
other sized ints) — tracked for later.

Wired: parser splice trigger (g_uses_numeric), emit_call dispatch, reseeded
seed, a self-asserting example (examples/library/numeric.ludic + feat_case),
per-symbol docs + inventory. All suites green incl. golden renders byte-
identical and the bootstrap fixpoint.

NOTE: fixed `const`s lower to raw-int-typed values (emit_call N_CONST), which
breaks fixed comparisons — the runtime uses inline fixed literals instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 19:04:37 +03:00
539f258d92 feat(types): Dict + Set string-keyed containers (#54)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m29s
commit-lint / conventional-commits (push) Successful in 6s
docs / build-and-deploy (push) Successful in 21s
Types phase 4 — containers. A splice-on-demand open-addressing hash table
(runtime/native/dict.ludic, FNV-1a, linear probing, tombstones, grow at 0.7)
behind two namespaces:

- Dict.* — string -> int map: new/set/get/get_or/has/remove/size/clear/keys.
  Resource counts, id/name registries. O(1) average vs a linear list scan.
- Set.* — set of strings: new/add/has/remove/size/clear/members. Tags,
  unlocked achievements, visited tiles. Shares the same table.

Values are int (also an entity handle / small id); Value.* covers richer
maps. [T; N] inline fixed arrays remain future work — typed buffers and []T
slices already cover heap-backed arrays.

Wired: parser splice trigger (g_uses_dict), emit_call dispatch, reseeded seed,
a self-asserting example (examples/library/containers.ludic + feat_case), and
per-symbol docs + inventory. All suites green incl. golden renders byte-
identical and the bootstrap fixpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 18:51:42 +03:00
bd6b12d2ea feat(types): BigInt + Decimal exact economy numbers (#52)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m28s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 22s
Types phase 2 — the "money problem". A splice-on-demand bignum engine
(runtime/native/bignum.ludic, self-contained, only intrinsics) exposed as
two namespaces:

- BigInt.* — arbitrary-precision integer (sign-magnitude, base-1e9 limbs):
  from/parse, add/sub/mul/pow, div/mod (by int), cmp/eq/is_zero, to_int, str.
  For idle counters and exact huge currencies that overflow a 32/64-bit int.
- Decimal.* — exact base-10 fixed point (BigInt mantissa + decimal scale):
  from/parse, exact add/sub/mul, cmp/eq, scale/rescale (truncate), str.
  So 0.10 + 0.20 is exactly 0.30 — no binary rounding.

Both exact => deterministic; no f32/f64. Wired: parser splice trigger
(g_uses_bignum), emit_call dispatch, reseeded seed, a self-asserting example
(examples/library/bignum.ludic + feat_case), and per-symbol docs + inventory.
All suites green incl. golden renders byte-identical and the bootstrap fixpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 18:45:14 +03:00
2c9f9ac549 feat(types): IVec2 + Rect 2D value types (#1)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 6s
docs / build-and-deploy (push) Successful in 21s
Phase 1 of the fuller type-system proposal: two by-value spatial types
that lower to packed integers (no heap, copy like scalars).

- IVec2 — integer 2D vector, a pair of int packed into one i64, for tile
  and grid coordinates: make/zero/x/y/add/sub/scale/dot, the grid distance
  manhattan, equal, and to_vector (widen into the fixed-point Vector).
- Rect — axis-aligned rectangle, four Q16.16 fixed components packed into
  one i128, for HUD boxes and hitboxes: make/x/y/w/h, the derived
  right/bottom/center, and the contains (point) / intersects (overlap) tests.

Both are exact and deterministic, bit-identical on every platform. Vector
and Color already cover phase 1's other 2D primitives.

Wired end to end: emit_core llty (IVec2->i64, Rect->i128), emit_call
dispatch, the FRAGS list + reseeded seed, a selfhost test (types2d),
per-symbol docs + type pages + inventory, and the vocabulary/editor sync
(header, JetBrains, TextMate, LSP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 18:29:41 +03:00
3df6640fa5 feat(http): Http.* poll-based HTTP/HTTPS client over a native NSURLConnection backend (#6)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 19s
ci / build-and-test (push) Successful in 1m26s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 21s
Adds the Http.* namespace and its transport, the HTTP client from #6. The JSON
companion the proposal called for already shipped as Json.* (#44).

- runtime/native/http.ll: the macOS transport — NSURLConnection driven through
  the objc runtime C ABI (no ObjC/C source), run on a detached pthread so the
  frame never blocks; TLS is the system's, on by default. A fixed slot pool holds
  each in-flight request; the worker publishes status/body/response behind an
  atomic done flag (release/acquire). Spliced + Foundation linked only when a
  program uses Http.*.
- runtime/native/http.ludic: the Http.* runtime — get/post/request, the
  open/set/body/send builder, poll/status/ok/text/body_len/header/free, plus a
  pure-Ludic response parser (Http.parse + case-insensitive header lookup) that
  is transport-independent and portable.
- compiler: Http.* dispatch, g_uses_http splice, hs_* transport intrinsics +
  declarations, the conditional Foundation link, and a new \r string/char escape
  the protocol needs.
- docs: a full docs/language/http section (16 pages); check-impl/check-docs green.
- test: examples/library/http.ludic self-asserts the parser offline (Darwin-gated
  build via the canonical path, since it links Foundation).

Verified end to end against real endpoints: HTTPS GET (200 + headers + body) and
POST (body + custom header). HTTP is out-of-band and never feeds the
deterministic sim. Reseeded; suites green (81 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 18:10:53 +03:00
4eef5ebbce feat(audio): Audio.* standard library over a native AVAudioPlayer backend (#22)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s
Adds the Audio.* namespace and its platform backend, the audio subsystem #22 was
blocked on.

- runtime/native/audio.ll: the macOS backend, AVAudioPlayer driven through the
  objc runtime C ABI (no ObjC/C source), same style as cocoa.ll — snd_load /
  play / stop / playing / set_volume / set_rate. Spliced and linked with
  AVFoundation only when a windowed build actually uses Audio.* (needed_framework,
  since AVAudioPlayer is reached by name).
- runtime/native/audio.ludic: the Audio.* runtime — a handle table, master
  volume/pitch, a single music channel. load/play/play_sound/play_music/stop/
  stop_music/stop_all/volume/pitch/is_playing. Every native call is
  is_windowed()-guarded, so a headless build carries the API as no-ops (load
  returns 0, is_playing false) and needs no audio device.
- compiler: Audio.* namespace dispatch, g_uses_audio splice, snd_* intrinsics +
  declarations, and the conditional AVFoundation link in both the canonical
  (main.ludic) and dev-runner (x app) paths.
- docs: a full docs/language/audio section (10 method pages); check-impl green.
- test: examples/library/audio.ludic self-asserts the headless no-op path.

Playback is out-of-band and never feeds the deterministic sim, but triggers are
frame-driven so replays fire the same sounds. Reseeded; suites green (80 + 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 17:52:31 +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
9452557f3c feat(reflect): generic value tree + Reflect.serialize/apply + JSON bridge (#44)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m15s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s
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>
2026-08-31 15:19:21 +03:00
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
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