Each clang invocation the task runner makes emitted
warning: overriding the module target triple with arm64-apple-macosx15.5.0
four times per `x build`, with nothing for anyone to act on. `cc()` now passes
-Wno-override-module, the same flag ludicc already passes for its own link.
The comment in selfhost/main.ludic explaining that flag had it backwards — it
claimed "our IR carries an explicit target triple", when the emitted IR names
no triple at all, which is precisely why clang substitutes the host's and
warns. Corrected. No IR changes, so the seed is untouched.
Also adds .claude/launch.json entry for previewing the generated docs locally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- every scratch file lives in `$TMPDIR/x_<pid>/` (tmp_dir/tmp_path in
prelude.ludic), removed by main's new dispatch() → tmp_cleanup();
X_KEEP_TMP=1 keeps it. `x test` and `x check-*` may now run together.
- `x test-lsp` (tools/x/lsp_test.ludic) replaces tools/test-lsp.py: the
whole request stream is framed into one stdin file, the server runs to
`exit`, and the response stream is parsed back by request id; adds a
check that ludicc's own error is published on save
- tools/x/forgejo.ludic replaces tools/ci/forgejo_release.py (curl with a
0600 header file; the token is no longer on the command line;
LUDIC_FORGEJO_API for forks)
- `x docs-palette --check` regenerates into scratch and compares, so the
drift guard judges the working tree rather than git HEAD; the generator
no longer emits a trailing blank line the formatter rejects
- `x test-tools`: exit 2 from test-lsp / test-grammar.js is a visible
skip, never a pass; the widget-prop test uses the `id: Root` syntax
- tools/test-grammar.js: current vocabulary (property/model/handler/
prefab/scene/event/become/@Queries), LUDIC_NODE_MODULES, exit 2 on skip
- tools/atlas.ludic rewritten in the current language (it did not compile)
- operators.ludic / os.ludic registered in the suite
- json.ludic: j_quote() writer helper
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>
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>
Two ECS-native, deterministic namespaces for 2D motion, driven off the fixed
frame clock so replays and lockstep netcode reproduce every frame and every
eased value exactly. Both are pure computed-inline Q16.16 / integer math (no new
runtime, no heap) — the game stores a timer on a component and calls these each
frame, exactly the way Collision.* / Grid.* are used.
Anim.* — spritesheet frame animation:
- Anim.frame(timer,fps,count) -> int looping frame index
- Anim.once(timer,fps,count) -> int one-shot, clamps on the last frame
- Anim.pingpong(timer,fps,count) -> int bounce 0..count-1..0
- Anim.finished(timer,fps,count) -> bool has a one-shot run past its end?
- Anim.duration(fps,count) -> fixed seconds for one cycle
- Anim.cell_x/cell_y(frame,cols,cell) -> int source rect on a grid sheet
Tween.* — value interpolation over a timeline:
- Tween.progress/loop/yoyo(timer,duration) -> fixed normalized amount
- Tween.done(timer,duration) -> bool
- Tween.ease(t, mode) -> fixed shape by a literal curve 0..6,
the same curves as Ease.* (now
factored into a shared ease_eval)
- Tween.number/round/point/tint(from,to,t) blend a fixed / int / Vector / color
The typed blends reuse the existing fixed / Vector / color helpers, and
Tween.ease shares Ease.*'s exact formulas via the new ease_eval(mode,t) — one
source of truth for every easing curve in the engine.
examples/library/anim.ludic asserts 34 cases (frame math, clamping, ping-pong,
cell geometry, timeline clamp/loop/yoyo, rounding, color/vector blends, and
Ease.in == Tween.ease(.,1)); wired into x test (now 62 passed). Docs: Anim +
Tween sections with 16 per-symbol pages, inventory/coverage green. Seed
reseeded; the C-free bootstrap fixpoint holds.
The stateful sugar the proposal sketches (named clips, Anim.play, fluent
Tween.chain/parallel handles, and an auto-injected advance system) is deliberately
left as a follow-up — this lands the deterministic math core both halves stand on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes#29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A cohesive filesystem & IO library — the foundation for saves, config, mods, and
asset loading — wrapping the bare file_* builtins into one safe, ergonomic API a
non-expert can use without touching a file descriptor or a byte buffer.
Path.* join / dir / base / ext / stem / normalize (pure lexical string ops)
Fs.* exists / is_dir / read_text / write_text / append_text / remove /
size / mkdir / copy / list
Mime.* of (extension table) / sniff (magic bytes: PNG/JPEG/GIF/PDF)
Pure string IR for Path.*; libc (fopen/access/mkdir/rename/opendir…) for Fs.*;
C-free, emitted on demand (g_uses_fsrt). Safety and determinism baked in:
- write_text and copy are atomic (write a temp file, then rename over the target)
so a crash mid-write never corrupts the previous file;
- mkdir creates parents (mkdir -p);
- list is sorted for a stable, reproducible directory walk;
- fallible calls return values (null / false / -1), never crashes — ready for a
first-class try/else when the error-handling work lands.
Complements Os.* (#21): Os supplies per-user locations, Fs the operations. v1
targets the native macOS/BSD filesystem with "/" separators; Windows separators,
a sandboxed wasm virtual FS, recursive directory copy, and richer magic-byte
sniffing are documented follow-ups.
- examples/library/fs.ludic: 32 assertions across pure Path ops (incl. normalize
resolving ./ .. and duplicate slashes), a real create/read/append/copy/list/
remove cycle under build/, and Mime by-extension + by-magic (GIF signature vs a
.bin extension). Wired into `x test` (now 56 passed).
- docs: new Path, Fs, and Mime sections + 18 per-symbol pages; inventory updated;
every fence passes check-docs; site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#10
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make Ludic text correct-by-default over UTF-8, so player names, translated UI,
and chat behave for every language instead of counting bytes and splitting
characters in half. The byte-oriented Text.* stays for speed; Unicode.* is the
layer that understands code points and (approximately) grapheme clusters.
- len / byte_len code points vs bytes — the two lengths, kept distinct
- is_valid_utf8 strict validation of untrusted input
- char_at / chars code-point access by index; chars() -> []int
- upper / lower case mapping (ASCII + Latin-1)
- truncate first n code points, never a half-character
- grapheme_len user-perceived characters (approx UAX#29)
Pure integer/byte IR over NUL-terminated buffers; C-free, no data-table blob.
Decoding and validation cover the full UTF-8 range (overlong/surrogate/>10FFFF
rejected). grapheme_len collapses combining marks, variation selectors, ZWJ
sequences (family emoji), and regional-indicator flag pairs. Documented v1
scope: wider-script/locale case rules (Latin-Extended, Greek, Cyrillic, Turkish
i, German ß) and NFC normalization are follow-ups.
- examples/library/unicode.ludic: asserts the invariants across ASCII, Latin-1
(é round-trips through upper/lower), a decomposed "café" (5 code points, 4
graphemes), a ZWJ family emoji (5 code points, 1 grapheme), and a flag (2
regional indicators, 1 grapheme). Wired into `x test` (now 55 passed).
- docs: a new Unicode section + 9 per-symbol pages clarifying byte vs code point
vs grapheme; inventory updated; every fence passes check-docs; site builds.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#13
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An Os.* namespace, Go-flavored and game-scoped, for the environment *around*
the game: the command line, environment variables, standard streams, process
exit, the host platform, and the per-user known folders a game writes into.
Rounds the bare System.* builtins (arg/getenv/exit) into one coherent surface.
- args / arg_count / arg the argument vector (args() -> []string)
- env / env_or / has_env read env vars (null-safe via env_or)
- set_env / unset_env mutate this process's environment
- exit(code) terminate with a status code
- platform() / arch() host facts (uname sysname/machine)
- stdout_write / stderr_write raw writes to the standard streams
- save_dir / config_dir / cache_dir / temp_dir per-user known folders
Pure libc over NUL-terminated strings; C-free, no new runtime. arg_count/arg/
exit stay light (no prelude) as thin aliases of the existing intrinsics; the
rest share one Os runtime prelude emitted on demand (g_uses_osrt). platform()
is portable (uname system name is field 0 on every Unix); arch() and the
known-folder layout follow the macOS/BSD conventions — the fully supported
native target today. Linux/Windows/wasm folder resolution and a target-aware
arch() are documented follow-ups.
- examples/library/os.ludic: asserts the invariants that hold regardless of
host — env round-trip, env_or fallback, unset, args()==arg_count(), non-empty
platform/arch and known dirs. Wired into `x test` (now 54 passed).
- docs: a new Os section + 17 per-symbol pages; inventory updated; every fence
passes check-docs (--fmt) and the site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes#21
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Log.* namespace: five levels (trace/debug/info/warn/error), a runtime
threshold, and structured key=value fields, so games get something better than
scattered print calls and release builds can go quiet without touching call
sites.
- Log.trace/debug/info/warn/error(msg, [k, v]...) -> stderr, "[LEVEL] msg k=v"
- Log.set_level(n) show only level >= n (0 = all default, 5 silences all)
- Log.level() read the current threshold
Fields accept strings, ints, and longs (numbers formatted automatically); the
level tag is chosen at compile time so a filtered-out level costs only a
comparison. Writes to stderr, never touching the simulation — no effect on
determinism/replays. v1 is the console sink; rotating-file and in-engine overlay
sinks are noted as follow-ups.
- examples/library/logging.ludic: asserts the set_level/level threshold
round-trip and that every level (with mixed-type fields) runs without faulting;
the stderr gating itself was verified by hand (warn/error emit, lower levels
suppressed). Wired into `x test` (now 53 passed).
- docs: a new Log section + per-symbol pages; inventory and coverage pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Noise.* namespace for procedural generation, implemented entirely in Q16.16
fixed point over an integer permutation hash so a seed reproduces the exact same
field on every platform and run (native/headless/wasm) — the determinism edge
over float noise that drifts across CPUs.
- value2 / perlin2 / simplex2 — value, gradient, and simplex noise -> [-1,1]
- fbm2(x,y,seed,octaves) — fractal Brownian motion (octaves of simplex)
- cellular2 / cellular2_id — Worley F1 distance + nearest-cell id
- unit(n) — remap [-1,1] -> [0,1]
Covers issue phases 1–2 fully plus cellular from phase 3; domain warp, ridged/
billow, and sample1/sample3 remain as follow-ups. Pure integer IR, C-free;
cellular/fbm reuse the math prelude's fx_sqrt.
- examples/library/noise.ludic: asserts the invariants a fixed-point generator
must hold (Perlin == 0 at lattice points, every sampler within [-1,1],
reproducibility, seed sensitivity, non-negative cellular distance). Wired into
`x test` (now 52 passed).
- docs: a new Noise section + per-symbol pages; inventory and coverage pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crypto (#19): add the OS cryptographically-secure random surface
(random_bytes/random_hex/random_u32, reading /dev/urandom) and a standard
base64 encoder, completing the library alongside the existing SHA-256/
HMAC-SHA256/verify_hmac/hex/ct_equal. All pure integer IR, C-free.
Uuid (#16): a new namespace for stable, collision-free IDs — v4 (random) and
v7 (time-ordered) generation, plus parse/is_valid/to_text/equals/nil. UUIDs are
canonical lowercase 36-char strings; v4 and v7's random tail draw from the
crypto CSPRNG, so both carry the documented determinism caveat (mint at the
edges, never inside lockstep simulation). Reuses the crypto prelude's
fn_secure_bytes / fn_hex_encode.
- examples/library/{crypto,uuid}.ludic: known-answer vectors (SHA-256, HMAC,
base64 per RFC 4231/4648) and structural invariants (uuid version/variant
bits, parse/equals), wired into `x test` (now 51 passed).
- docs: per-symbol pages for every new method + a new Uuid section; inventory
and impl-vs-docs coverage check pass.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The security-sensitive counterpart to the fast, non-cryptographic Hash.*
library: standard, test-vector-backed hashing for signed saves and message
integrity, kept in its own namespace so nobody reaches for the wrong tool.
Crypto.sha256(s) SHA-256 -> 64-char lowercase hex
Crypto.hmac_sha256(key, msg) HMAC-SHA256 -> 64-char hex
Crypto.verify_hmac(key, msg, mac) recompute + constant-time compare -> bool
Crypto.hex(s) lowercase hex of a string's bytes
Crypto.ct_equal(a, b) constant-time string equality
The primitives are implemented from scratch in plain integer LLVM IR
(FIPS 180-4 / RFC 2104): no libc crypto, no data-dependent branches in the
compression rounds, so a given input hashes to the same 32 bytes on every
platform and run. Digests are returned as hex strings, not raw bytes, because
a `str` is null-terminated and a raw digest can contain a NUL. MAC checks use
a non-short-circuiting compare so timing does not leak how much of a forged tag
was correct.
Emitted on demand via g_uses_cryptort, mirroring the emit_hash prelude gate.
Scoped to the deterministic, known-answer-testable core; OS-backed
random_bytes (the one piece that can't be validated by test vectors) is left
for a follow-up.
Tested against published SHA-256 vectors (empty/"abc"/fox + 55/56/64-byte
multi-block padding) and HMAC-SHA256 vectors; wired into the self-host suite as
`crypto`. Docs: a new Crypto section with honest "what this protects / does
not" guidance, one page per method, all fences checked and in the inventory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the calendar/clock half of #9 as plain-i32 integer epochs — no
new type, no floating point (the issue's "integer epochs to avoid drift") —
so every operation is deterministic and bit-identical on every platform:
Duration — a span in whole seconds; seconds/minutes/hours/days build one,
as_seconds/as_minutes/as_hours/as_days read it back. Because a
duration is just an int, `+` and `>` work with no extra machinery
(Duration.minutes(5) + Duration.seconds(30), away > Duration.hours(3)).
Date — a civil day as days-since-1970 (UTC): new/year/month/day/weekday/
is_leap/days_in_month/to_epoch/add_days/diff_days.
DateTime — an instant as seconds-since-1970 (UTC, matching Time.now):
from/date/add/year/month/day/weekday/hour/minute/second.
Time.since(past) = now - past, for offline-progress / "time away" checks.
New selfhost/emit_datetime.ludic (is_/emit_ for the three namespaces, wired
into emit_ns_call + the frag list). The two civil<->epoch conversions are
Howard Hinnant's public-domain proleptic-Gregorian algorithms, emitted once
per program as the @fn_days_from_civil / @fn_civil_from_days prelude and gated
by g_uses_datert; days_in_month is next-month-day-0 (no lookup table). Time
gains `since`. Docs (Duration/Date/DateTime sections, 28 method pages +
time-since), inventory, and LSP hover kept in sync; a registered test checks
component math against hand-computed values. Reseeded; C-free fixpoint holds;
all suites green (26 self-host / 45 regression / 29 tools); check.py,
check-impl.py and validate.py OK.
format/parse, a game-controlled simulated clock, and timezones are tracked
follow-ups; v1 is UTC-only and, on the i32 epoch, valid through 2038.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the Vec.* half of #25 under the proper (de-abbreviated) name
Vector, unblocking it with a self-contained value type instead of waiting
on the full #1 type system.
A Vector is two Q16.16 fixed components (x, y) packed into one i64 — a true
by-value type that lives in a register and never allocates (reuses the new
`long`/i64 support; llty maps `Vector` to i64). Fifteen operations, all
deterministic fixed-point reusing fx_mul/fx_div/fx_lerp and the @fn_fx_*
prelude: make/zero/x/y, add/sub/scale/dot, length/distance/normalize/lerp,
rotate/angle/from_angle.
New selfhost/emit_vector.ludic (wired into emit_ns_call + the frag list),
the `Vector` primitive type in llty and the grammars/LSP/JetBrains tokens,
docs (type-vector + 15 Vector.* pages + section), and a registered test.
Reseeded; C-free fixpoint holds; all suites green (45/25/29); site + check.py OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the abbreviated pointer types to full words on the language surface:
ptr -> pointer (a raw address / FFI handle)
ptrs -> pointers (a buffer of pointers)
The Ludic type name is distinct from LLVM's own `ptr` spelling: llty() maps
`pointer`/`pointers` to LLVM `ptr`, and the emitted IR keeps `ptr`, so only
the Ludic-level surface changes. Rewrites type annotations across all
sources, the 8 hardcoded pointer type-tags, the `pointers`-buffer indexing
in emit_addr, the grammars/LSP/JetBrains tokens, and the docs
(type-ptr -> type-pointer, type-ptrs -> type-pointers). int/bool keep their
conventional short spelling (like Math).
Reseeded; C-free fixpoint holds; all suites green (45/24/29); site + check.py OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand the function-declaration keyword to the full word across the whole
language and toolchain:
fn name(...) -> T { ... } -> function name(...) -> T { ... }
Done as a self-hosting migration: teach the parser both spellings, reseed,
rewrite every .ludic definition to `function`, then drop `fn`. The compiler
now rejects `fn`. Touches the parser, all selfhost/tools/runtime/example/test
sources, the grammars (TextMate shared+vscode, ludic_syntax.h, JetBrains
LudicTokens.kt), the LSP and formatter, the Python doc/vocab tools
(check-impl, check-docs, validate, palette, test-lsp), and the docs
(fences, prose, kw-fn -> kw-function).
Reseeded; C-free bootstrap fixpoint holds. All suites green (45 regression,
24 self-host, 29 tool); the docs site generates and check.py passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).
Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.
Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
discovered from the docs, no hardcoded list); new check-impl.py enforces
that every implemented namespace method / keyword / type / phase has a
doc page, wired into `x test-tools`. Document the previously-undocumented
keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.
Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>