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.
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>
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>
`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>
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>
- `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>
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>
--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>
`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.
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>
`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>
`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>
- `[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>
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>
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>
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>
`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>
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>
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>
#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>
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>
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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
A self-describing Value node (null/int/fixed/bool/str/list/object) with
constructors, builders (Value.add/put) and accessors (get/at/count/kind/
as_int/as_str/…). Reflect.serialize(entity) walks an entity's whole component
set into a value tree — one member per component, each a sub-object of its
fields — and Reflect.apply(entity, value) writes one back; a fixed field
becomes a fixed node, everything else an int node, so the round-trip is
bit-exact, with the model id under "@kind". Json.encode/parse bridge the tree
to and from compact, stable, diffable text, with fixed written as an exact
terminating decimal that parses back bit-for-bit (verified across the raw
Q16.16 range). Together: a one-call, bit-exact save/load for entities.
Written in Ludic and spliced on demand (runtime/native/value.ludic +
reflect_io.ludic, like Query/Light), so a program that doesn't touch
Value.*/Json.*/Reflect.serialize compiles byte-identically and the C-free
bootstrap fixpoint holds (verified). The general tagged-union/any language type
stays tracked in #1; this ships the concrete value tree the serializer needs.
Adds 21 namespace-method docs pages + Value/Json sections,
examples/library/serialize.ludic, and a regression case. Whole CI set green:
x test 72/72, x test-tools 30/30, check-impl/vocabulary/docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.
Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.
Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.
panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.
Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.
- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
runs to the end); a panic_case in the suite covers the failure path (non-zero
exit + the located stderr message). x test is now 69 checks.
Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A software light pass over the framebuffer, run in a render phase after drawing
the scene: Light.ambient multiplies the scene toward a tint (night/cave mood),
Light.point additively accumulates a radial glow with linear falloff clamped per
channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking
a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout,
so a scene lights identically every run and in a headless render (diffable).
Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the
regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1
(ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites,
soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder
components are follow-ups (the auto-system hook is tracked by #43).
- runtime/native/light.ludic: the light-accumulation engine (isqrt falloff,
segment/occluder shadow test, ambient modulate)
- examples/library/lighting.ludic: 14 pixel-readback assertions
- docs/language/light/: Light.ambient/point/occlude/clear_occluders
- tools/x/test.ludic: lighting.ludic in the regression suite
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>