Commit graph

159 commits

Author SHA1 Message Date
9ed0070039 feat(tooling): port the docgen site generator to Ludic (no Python) (#41)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 16s
ci / build-and-test (push) Successful in 1m4s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 17s
Follow-up to #31: the doc/lint/grammar checks moved to Ludic there; this ports
the remaining docgen piece (gen.py / check.py / palette.py) so nothing in the
documentation pipeline is Python any more.

Three new `x` subcommands, all in Ludic and compiled by Ludic:

  - x docs-gen [--out DIR]  the static-site generator: parses docs/language/**
    front-matter + bodies (fences, Parameters:), builds the section/symbol
    model, reads the asset templates, and emits every page + ns/color/api pages
    + the landing page + ludic-highlight.js + symbols.json + .nojekyll.
  - x docs-check [DIR]      the coverage / integrity guard (required files, a
    page per inventory.json symbol, duplicate-token and one-dir-per-namespace
    guards, highlighter link targets).
  - x docs-palette          the named-colour source of truth: the palette table
    moved into tools/x/docgen.ludic, emitting emit_color.ludic (pointer, not
    ptr) + palette.json.

Verified against the Python oracle: `x docs-gen` reproduces all 466 output files
BYTE-FOR-BYTE (a Ludic json.dumps/html.escape/front-matter port — ordered dicts,
indent=2 vs compact, ensure_ascii \uXXXX, codepoint-aware truncation), and
`x docs-check` matches check.py's pass/fail output. Wired into `x test` as a
gate (docs-gen -> docs-check on a fresh site; docs-palette stays byte-identical).

CI swap: ci.yml and docs.yml call the Ludic generator; docs.yml drops the
python:3.12 container and bootstraps the toolchain from the IR seed instead.
tools/docgen/{gen,check,palette}.py deleted; only assets/ + inventory.json
remain. Completes #31's criterion 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 02:12:32 +03:00
109d8c5b4f fix(docgen): palette.py emitted ptr instead of pointer
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 13s
ci / build-and-test (push) Successful in 52s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s
palette.py generates selfhost/backend/stdlib/emit_color.ludic but its
template wrote `function color_lookup(name: ptr)`, while the committed,
correct source (and the rest of the compiler) uses `pointer` — so running
the generator rewrote the file to a drifted version. Emit `pointer`;
`python3 tools/docgen/palette.py` now leaves emit_color.ludic byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 01:20:06 +03:00
f5e9b5d6c2 feat(lang): new Type { field: value } record initialisers
`new` accepted only a bare `new T` (every field its declared default) or
`new []T`, but the docs (kw-new) document `new Record { field: value, … }`
as the way to construct a record with non-default fields — a documented,
intended form the parser never accepted (`let o = new Point { x: 3 }` failed
with "expected newline or ';'").

Parse an optional `{ … }` override record after the type in a `new`
expression (reusing the existing `record()` parser that `spawn` uses), and
seed each field in emit_new_struct from that record when present, else from
the field's declared default. `new []T` and bare `new T` are unchanged.

Also mark the illustrative kw-import fence `# doc-check: skip` (its imports
are example paths that can't resolve in isolation), which makes `x check-docs`
fully green (398 fences, 0 drifted) — so it is now wired as a gate in
`x test-tools` and CI, guarding against future doc/compiler drift.

Reseed is a clean fixpoint (x bootstrap-cfree holds); x test (56),
x selfhost-test (29, golden renders unchanged) and x test-tools (30) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 01:20:06 +03:00
e65e862244 feat(tooling): port the doc/lint/grammar checks to Ludic (no Python)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 13s
ci / build-and-test (push) Successful in 52s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s
Replace the Python doc/lint/vocabulary guards with Ludic equivalents that
run through the `x` task runner, so the checks need no Python interpreter:

  x check-docs         every ```ludic doc fence parses (or is marked)
  x check-impl         every implemented feature has a docs/language page
  x check-vocabulary   vocabulary in sync across grammar / lexer / header / parser
  x lint-asset <file>  validate one editor .json / .xml asset

New fragments: tools/x/json.ludic (a small JSON reader — objects/arrays/
strings with \uXXXX + surrogates/numbers/literals, used by the vocabulary
check's grammar navigation and the asset validator) and tools/x/checks.ludic
(the checks + string helpers + a minimal XML well-formedness validator).

`x test-tools` now runs the vocabulary + docs-coverage checks and the
JSON/XML asset validation through Ludic instead of python3; ci.yml's
docs-coverage step calls `x check-impl` / `x check-vocabulary`. Each port was
verified against its former Python script for exact verdict parity on the
clean tree and on injected drift (a removed keyword, a broken grammar
alternation, an undocumented method).

Deletes the superseded scripts: tools/check-vocabulary.py, tools/check-docs.py,
tools/docgen/check-impl.py, tools/docgen/validate.py. The docgen site
generator (gen.py/check.py/palette.py) and the LSP protocol driver
(test-lsp.py) remain and are tracked separately.

Toolchain unchanged (seed byte-identical); `x test` (56) and `x test-tools`
(29) stay green.

Part of #31

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 01:06:38 +03:00
23726afa90 refactor(selfhost): reorganise into concern-based subdirectories
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-31 00:26:02 +03:00
fed80f2152 feat(release): SemVer + ludicc --version, changesets, and x release
Some checks failed
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Successful in 13s
ci / build-and-test (push) Has been cancelled
The project had no versioning discipline: 0 tags, no CHANGELOG, no way for the
compiler to report a version. Add a lightweight, native release flow.

- Versioning: SemVer, with VERSION as the single source of truth. `ludicc
  --version` (and `ludic --version`) read it at runtime — so a bump touches one
  file and never reseeds the compiler. `x version` reports it too.
- Changesets: one small Markdown file per user-facing change under changes/
  (bump level + type + summary; see changes/README.md). This replaces "remember
  to edit the changelog" with a mergeable artifact, no Node changeset tool.
- `x release [major|minor|patch] [--publish]`: fold the pending changesets into a
  new CHANGELOG.md section (grouped by type), bump VERSION, commit, and tag
  vX.Y.Z. The level defaults to the highest changeset bump. `--publish` also
  pushes and creates the Forgejo release with source + toolchain tarballs;
  tools/ci/forgejo_release.py is the small stdlib-Python HTTP glue for the
  release API (a native Http client is issue #6).

Seed the initial changesets describing the shipped surface; the first `x release`
turns them into the v0.1.0 CHANGELOG. Reseeded for the --version flag; C-free
bootstrap fixpoint holds; suites 56 / 29 / 29 on macOS, 51 / 28 (+skips) on Linux
CI, bootstrap-cfree byte-identical on both.

Part of the repository-cleanup / DX pass (with #32, #34).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 23:46:59 +03:00
709465cdd8 build(git-hooks): enforce Conventional Commits via a hook + CI, record history decision
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 12s
ci / build-and-test (push) Successful in 50s
commit-lint / conventional-commits (push) Successful in 3s
The convention was documented in CONTRIBUTING.md but nothing enforced it, and no
decision was on record for the pre-self-hosting `Phase` history.

- tools/git-hooks/commit-msg — rejects a summary that is not a Conventional
  Commit. tools/git-hooks/lib.sh holds the single rule (types, scope, `!`, and
  the merge/revert/autosquash exemptions) so the hook and CI cannot drift.
- tools/git-hooks/lint-range.sh — lints a commit range with that same rule.
- .forgejo/workflows/commit-lint.yml — runs it over the new commits on every
  push and PR, as the backstop for contributors who have not enabled the hook.
- Fix the pre-commit hook, which pointed at the old build/ludic-fmt path (the
  toolchain moved to bin/) and so silently no-op'd; it now finds bin/ludic-fmt.
- CONTRIBUTING.md — full type/scope table, the one-line enable
  (`git config core.hooksPath tools/git-hooks`), and a **Git history** section
  recording the decision: leave the pushed `Phase`-era history as-is (a rewrite
  is destructive and non-reversible for anyone who cloned); enforce the
  convention going forward; let #33's first release tag double as the clean `v0`
  baseline that brackets the old prefix without touching a commit.

Verified: the hook accepts feat/fix/ci/refactor(!)/merge/revert and rejects
"added regex" / "Fix bug" / "WIP"; lint-range passes recent history and flags
the old `Phase 8b:` commit.

Closes #34

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 23:30:45 +03:00
1c1192e7c0 ci: add build + test + bootstrap-cfree workflows for the Forgejo runner
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 49s
Until now the only workflow was docs.yml — nothing gated a change on the
compiler even building, on `x test` / `x test-tools`, or on the headline C-free
self-rebuild reproducing the seed. Add two Forgejo Actions workflows on the same
`docker` runner the docs job uses.

The toolchain is macOS-first: the self-hosted compiler emits the Darwin libc
standard-stream globals (`__stdoutp`/`__stderrp`), the one thing that stops its
IR from linking on Linux. Everything else is portable — clang-16 assembles the
seed cleanly and the C-free bootstrap reproduces it byte-for-byte on Linux too.
So rather than require a macOS runner (none is registered), bridge that single
gap with a tiny **C-free LLVM-IR shim** (tools/ci/linux_stdio_shim.ll) that
defines the Darwin-named globals over glibc's stdout/stderr, injected into every
clang link via LUDIC_CC. The language keeps its no-C-compiler guarantee.

Workflows:
- ci.yml — bootstrap the toolchain from the seed, then `x test` + `x test-tools`
  + the docs-cover-the-implementation checks, on push to main and PRs.
- bootstrap.yml — `x bootstrap-cfree`: assert the seed rebuilds itself
  byte-for-byte (returns non-zero on drift).

Make the suites host-aware so a Linux run is green without hiding anything: a
new is_darwin()/skip() pair (tools/x/prelude.ludic) makes the cases that are
genuinely macOS-ABI bound — the Cocoa-windowed `ludicc -o` link, the golden
render hashes (blessed on macOS; text raster differs by a hair elsewhere), the
Os known-folder/uname surface, Fs.list and the LSP workspace walk (both read the
BSD dirent layout) — print a visible `skip` off Darwin instead of failing. On
macOS every one of them still runs: suites stay 56 / 29 / 29 green there, and
run 51 / 28 (+skips) on Linux, bootstrap-cfree byte-identical on both.

The formatting gate is ludic-fmt *idempotence* (already in `x test-tools`), not
`fmt(x) == x`: this codebase deliberately preserves hand alignment, so a strict
"already formatted" check would fight that contract.

A prebuilt CI image with clang-16 + python3 baked in is the obvious follow-up
speed-up (ties into the packaging work in #33).

Closes #32

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 23:23:23 +03:00
ac1e8d157d refactor(stdlib): consolidate System.* onto Os.*, retire duplicated members
All checks were successful
docs / build-and-deploy (push) Successful in 2s
`System.*` and the newer `Os.*` (added in #21) both covered the environment
around the game, with four members (`arg`, `arg_count`, `env`, `exit`) lowering
byte-for-byte identically and the standard streams overlapping in concern. That
is a user-facing ambiguity (`System.env` vs `Os.env` are indistinguishable) and
a drift hazard (two copy-pasted codegen paths).

Make `Os.*` the single canonical environment/process namespace and retire the
overlapping `System.*` members:

- Remove `System.{arg, arg_count, env, exit, stdout, stderr}` from the namespace
  dispatch (selfhost/emit_expr.ludic). Use `Os.arg`/`Os.arg_count`/`Os.env`/
  `Os.exit` and `Os.stdout_write`/`Os.stderr_write` instead.
- `System.*` now covers only its unique low-level surface: the raw file handles
  (`file_open/read/write/seek/tell/close`), `read_char`, and `run`.
- The bare `arg`/`exit`/`getenv`/`file_stdout`/`file_stderr` intrinsics stay —
  they are the primitive layer the self-hosted compiler itself uses; only the
  redundant *namespaced* sugar is gone.
- Docs: drop the six retired `docs/language/system/*` pages, retune the section
  blurb, update inventory.json and the LSP signature table.
- Secondary finding from the issue: cross-link `Text.upper`/`Text.lower`
  (ASCII-only) to `Unicode.upper`/`Unicode.lower` (full Unicode case mapping).

Reseeded; C-free bootstrap fixpoint holds. All suites green (56 test / 29
selfhost / 29 test-tools); docs cover every implemented feature (291 ns-methods).

Closes #40

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 22:54:28 +03:00
dd4f5a26ad feat(stdlib): add Fs.* / Path.* / Mime.* — filesystem, paths, content types (#10)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 22:43:50 +03:00
bad6cd1ac9 feat(stdlib): add Unicode.* — UTF-8 code points, graphemes, case mapping (#13)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 22:29:00 +03:00
b205dd8dfd feat(stdlib): add Os.* — the OS/environment interface (#21)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 22:19:11 +03:00
031b138f84 feat(stdlib): add Log.* — levelled, structured logging (#15)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 21:57:22 +03:00
a4f1494a04 feat(stdlib): add Noise.* — deterministic fixed-point procedural noise (#3)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 21:50:50 +03:00
2ddf830f0b feat(stdlib): finish Crypto (CSPRNG + base64) and add Uuid.* library (#19 #16)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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>
2026-08-30 21:40:51 +03:00
fb728bbefe chore(repo): DX cleanup — categorise examples, text-diffable golden, build/ output (#27 #28 #30)
Repository-cleanup / DX pass folding three tracker items into one coherent
change, verified green end to end (`bin/x test` 49/0, `bin/x selfhost-test`
29/0, `bin/x test-tools` 29/0).

#28 — curate & categorise examples/
- 42 flat entries regrouped into intent-revealing subdirs: games/, rendering/,
  ecs/, events/, networking/, lang/, library/ (was lib/).
- chronorift dir-vs-file duplication resolved: the entry file and its import
  modules now live together under games/chronorift(.ludic).
- Every path reference updated repo-wide (test runner, editor-tool drivers,
  docs/site, design docs).
- New examples/README.md indexes the whole set with run commands.
- Showcase examples without a self-asserting entry (hello, events, net_rt) now
  get a compile-only rot guard in `bin/x test`, so nothing here rots silently.

#30 — text-diffable golden baseline
- The 4 binary selfhost/golden/*.ppm blobs are replaced by a single
  selfhost/golden/renders.sha256 manifest (SHA-256 per render). Hashes are
  byte-identical to the old PPMs, so the baseline is unchanged — only its form.
- game_case now compares framebuffer hashes; a regression shows as a changed
  hex line in review, not "binary files differ".
- New `bin/x golden` regenerates the manifest deliberately (review with
  `git diff selfhost/golden/renders.sha256`).

#27 — PPM & asset handling
- Headless renders now write build/out.ppm, never the repo root; `x app`,
  `x clean`, messaging and .gitignore updated to match. Nothing is written to
  the working root any more.
- Redundant local Kenney .zip archives removed (the art ships extracted;
  .gitignore already excludes *.zip). CC0 License.txt files retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:54:21 +03:00
3bab2d2d4c docs: merge duplicate networking namespace dirs; guard against recurrence
Documentation namespace cleanup (issue #38).

Audit outcome:

- `date` vs `datetime` are NOT duplicates — `Date` is calendar days since the
  epoch, `DateTime` is instants (seconds); distinct runtime namespaces. Kept
  both.
- `network` vs `networking` WAS a real duplicate. Every other stdlib area
  documents only its namespace (`World.*`, `Screen.*`, …), never the bare
  builtins it lowers to. Networking alone also documented the low-level
  `net_*`/builtin forms under `networking/`, duplicating the `Network.*`
  pages under `network/`. Removed `networking/`; `network/` (the `Network`
  namespace, which the compiler and LSP both expose) is canonical. Folded the
  `@Sync`/`@Owned` framing into `network/_section.md` so no context is lost.
- Dropped the `networking` key from docgen inventory.json.

Guard (AC3): `tools/docgen/check.py` now fails if any `ns:` is documented from
more than one directory, or if two sections share an id or (case-folded)
title — so a duplicate-namespace split cannot silently reappear.

`gen.py` + `check.py` pass (34 sections, 365 symbols).

Closes #38

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 16:46:21 +03:00
1c9235a948 chore(repo): add .editorconfig, reconcile .gitignore, add x clean
Repository hygiene pass (issue #39):

- Add `.editorconfig` mirroring ludic-fmt: 2-space indent, LF, UTF-8, trim
  trailing whitespace and final newline by default; 4-space for Python
  tooling; keep trailing whitespace in Markdown (hard line breaks).
- Reconcile `.gitignore`: root-anchor `/build/` and `/out.ppm`, normalise the
  misleading `**.zip` glob to `*.zip`, and document that the Kenney art is
  tracked *extracted* while the download/plugin zips are local-only. `.idea`
  -> `.idea/`.
- Add a `bin/x clean` command that removes `build/`, the root `out.ppm`, and
  stray `bin/*.tmp`, keeping the toolchain binaries so the running `x`
  survives.

No generated artifacts land outside build/ or bin/, both of which are ignored.

Closes #39

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 16:46:10 +03:00
9ae69e64b4 feat(stdlib): Crypto.* — SHA-256 + HMAC-SHA256, constant-time verify (#19)
All checks were successful
docs / build-and-deploy (push) Successful in 3s
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>
2026-08-30 13:37:02 +03:00
4aa2012231 feat(stdlib): sorting toolkit — sort_by/sort_desc_by/sort_with + stable merge sort (#11)
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Grow List sorting from a numeric-only insertion sort into a small,
game-friendly toolkit that sorts records and query results by a key or a
full comparator, stably and in O(n log n).

- List.sort_by(s, keyfn)      ascending by a key (draw order, price)
- List.sort_desc_by(s, keyfn) descending (leaderboards)
- List.sort_with(s, cmpfn)    full cmp(a,b)->int comparator (multi-field)

Comparators/keys are passed as named top-level functions rather than
lambdas, so the toolkit ships without waiting on closures (#1).

Engine: a stable bottom-up merge sort. emit_takeright is the single
place stability is decided ("take the right run's head only on a strict
win" -> equal keys keep prior order). List.sort becomes a hybrid:
insertion sort for n<32, merge sort above; both stable, so output is
unchanged. Key functions must return an integer-ish type; record slices
hold pointer elements, so the key/comparator receives the record pointer.

Tests: selfhost/tests/sort.ludic (scalar large-n, sort_by, sort_desc_by,
stability, sort_with). Docs: list-sort_by/desc_by/with + updated
list-sort. All suites green (28 self-host / 46 test / 29 test-tools);
reseeded, C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 11:39:26 +03:00
b5455cd550 feat(stdlib): DateTime format/parse + simulated Clock — completes #9
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Finish issue #9 by adding the two remaining acceptance items on top of the
calendar/clock core, still pure-integer and deterministic:

  DateTime.format(dt, pattern) -> string   render an instant via a token
                                           pattern (YYYY/YY/MM/DD/HH/mm/ss;
                                           other chars pass through)
  DateTime.parse(text, pattern) -> int     read an instant back; -1 on a
                                           non-digit where one is expected
  Clock.now/set/advance/reset              a game-controlled simulated clock
                                           (the @L_clock global) that never
                                           touches the wall clock, so gameplay
                                           reading Clock.now() is replay-safe

format/parse take a string-LITERAL pattern and are expanded at compile time
(field offsets are then constant), folding @fn_str_concat over literal runs and
two small runtime helpers: @fn_dt_pad0 (zero-padded field) and @fn_dt_rd
(fixed-width digit reader that stops at the terminator and flags malformed
input). Clock is a universal i32 global declared in emit_head, so it works in
entry and game programs alike.

Adds examples/offline_rewards.ludic — the issue's worked "you were away N hours"
example, driven from its own entry and asserted in the regression suite — plus
selfhost/tests/datetime2.ludic (format/parse round-trip, parse failure, clock),
docs (Clock section + 4 pages, DateTime.format/parse pages), inventory and LSP
hover. Reseeded; C-free fixpoint holds; all suites green (27 self-host / 46
regression / 29 tools); check.py (366 symbols), check-impl.py (218 ns-methods)
and validate.py OK.

With this, #9's scope is fully delivered: DateTime/Date/Duration + core ops,
format/parse, a deterministic simulated clock, docs + offline-rewards example,
and tests. (v1 stays UTC-only, no leap seconds, i32 epoch valid through 2038.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 03:51:48 +03:00
1a2c6ec2c7 feat(stdlib): Time/Date/Duration calendar-clock core (issue #9)
Some checks are pending
docs / build-and-deploy (push) Waiting to run
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>
2026-08-30 03:37:38 +03:00
121053e179 feat(stdlib): 2D Vector type + Vector.* namespace (issue #25)
Some checks failed
docs / build-and-deploy (push) Has been cancelled
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>
2026-08-30 02:09:38 +03:00
2e0047514b refactor(lang): rename builtins flr->floor and fx->fixed
De-abbreviate the two bare fixed-point conversion builtins:
  flr(f) -> int     ->  floor(f) -> int    (fixed -> int, flooring)
  fx(i)  -> fixed   ->  fixed(i) -> fixed  (int -> fixed; mirrors how the
                                            stringify builtin is `string`)

Updates the compiler dispatch, all call sites, the grammars/LSP/JetBrains
tokens, and the docs (fn-flr -> fn-floor, fn-fx -> fn-fixed). 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>
2026-08-30 02:01:56 +03:00
effb3f637f refactor(lang): rename the ptr/ptrs types to pointer/pointers
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>
2026-08-30 01:58:54 +03:00
b7745a4600 refactor(lang): rename the str type (and stringify builtin) to string
Expand the abbreviated string type and its conversion builtin to the full
word everywhere:
  str            ->  string        (the immutable-string type)
  str(x) -> str  ->  string(x) -> string   (the stringify builtin;
                                            what `{…}` interpolation calls)

Types are recognized by identifier, and llty maps both spellings to LLVM
`ptr`, so this is an atomic source rewrite: type annotations, the Ludic
type tags, the builtin name/dispatch, and the interpolation desugar, plus
the grammars, LSP, docs (type-str -> type-string, fn-str -> fn-string), and
inventory. int/bool stay (universally accepted, 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>
2026-08-30 01:51:11 +03:00
4c48077d68 refactor(lang): rename the fn keyword to function
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>
2026-08-30 01:43:22 +03:00
2f19c8d8e2 refactor(stdlib): de-abbreviate namespaces (Mem/Sys/Net/Collide)
Expand the abbreviated public namespaces to full words, part of the
language-wide de-abbreviation pass:
  Mem -> Memory, Sys -> System, Net -> Network, Collide -> Collision

Math stays (universally accepted, like int/bool). Renames the dispatch
strings, LSP signatures, docs (dirs, files, frontmatter), and the
inventory manifest; behavior is byte-identical (the bare rt_ targets are
unchanged). Reseeded; C-free bootstrap fixpoint holds; all suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 01:30:47 +03:00
6e6467b515 feat(stdlib): Math.exp/log/pow + Ease.elastic transcendentals (issue #25)
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Finish the unblocked "Math / Ease" half of #25: the fixed-point
transcendentals deferred from #2. Vec.* stays blocked on the vec2 type
in #1.

- Math.exp, Math.log (natural), Math.pow — deterministic Q16.16 via two
  new prelude fns in emit_math_prelude: @fn_fx_exp2 (range-reduced 5th-order
  Taylor 2^f, then a clamped shift by the integer part) and @fn_fx_log2
  (llvm.ctlz for the exponent + an atanh series on (m-1)/(m+1) for the
  mantissa). exp=2^(x·log2 e), log=log2(x)·ln2, pow=2^(b·log2 a).
- Ease.elastic — ease-out elastic 2^(-10t)·sin((10t-0.75)·2pi/3)+1.
- Pure integer IR, so bit-identical on every platform. Results must fit the
  Q16.16 range (|x| < 32768); larger magnitudes saturate (documented).

Test selfhost/tests/transcend.ludic (registered in the self-host suite) +
docs for all four. Reseeded; the C-free bootstrap fixpoint holds. All suites
green (24 self-host, 45 regression, 29 tool).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 01:19:44 +03:00
2002e977d9 feat(lang,stdlib): 64-bit long type + 64-bit Hash variants (issue #17)
Some checks are pending
docs / build-and-deploy (push) Waiting to run
Add `long`, a 64-bit signed integer primitive (i64), threaded through
codegen: llty; int<->long coercion (coerce_code/to_long) at let/assign/
return/call-args; i64 arithmetic + comparison promotion in emit_bin;
unary negate/~; print via %lld and str()/interpolation via @fn_long_str.
Editor vocabulary (syntax header, TextMate grammar, formatter, LSP)
synced; check-vocabulary green. Numeric literals stay i32 — build large
values by widening (documented on the type page).

Complete the Hash.* namespace (issue #17) with both 32- and 64-bit
algorithms: Hash.of/fnv1a/crc32/mix/combine (32-bit) and
Hash.of64/fnv1a_64/mix64 (64-bit, returning long). Deterministic and
C-free; CRC-32 (poly 0xEDB88320) and FNV vectors verified against
reference implementations.

Tests: selfhost/tests/{hash,long}.ludic. Docs: docs/language/hash/*,
type-long.md. Seed reseeded; C-free bootstrap fixpoint holds; 23
selfhost + 45 regression + 29 tooling checks green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 01:01:06 +03:00
a38195128f feat(stdlib): namespaced standard library (issue #2)
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>
2026-08-30 00:26:19 +03:00
ff15c4e01d docs(types): document the fixeds and ptrs typed buffers
All checks were successful
docs / build-and-deploy (push) Successful in 2s
The type table lists fixeds (buffer of fixed values) and ptrs (buffer of
pointers) alongside words, but they had no reference pages. Add both, with
compilable examples; coverage inventory updated (types: 10 -> 12).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 18:50:15 +03:00
31f1019a69 docs(site): vertical pipeline on mobile, click tooltips, consistent chrome
All checks were successful
docs / build-and-deploy (push) Successful in 2s
- Pipeline diagram stacks vertically with downward arrows on mobile (row on
  >=720px) instead of the awkward wrap.
- Tooltip cards now open on CLICK (works on touch too), the card itself is
  clickable and opens the symbol's page in a NEW TAB, and an outside click or
  Escape closes it. Replaces the hover-only behavior.
- Reference pages get their own nav — Home / API Reference / Source ↗ — instead
  of the landing-only Features/Examples/Get started anchors, and now include the
  Source link.
- Unify the container width (1120px) across the landing and all reference pages
  so the header and content align between them.
- Reword extern/@export FFI docs from "C-ABI" to "native" for consistency with
  the site's no-C wording.

Verified in-browser at 375px and 1280px: vertical pipeline, click→card→new-tab,
outside-click close, correct reference nav, aligned container; 158 examples still
compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 18:42:40 +03:00
c93eca80af docs(site): mobile-first revamp of all pages
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Rewrite both stylesheets mobile-first (base = phone; @media min-width
progressively enhances) and add a responsive nav.

- Hamburger menu on small screens: links collapse into a clean, uniform
  dropdown list (plain rows + separators, no out-of-place boxed buttons);
  full inline nav with boxed CTA returns at >=720px. Auto-wired for every page.
- Fix horizontal-overflow root causes: min-width:0 on grid/flex code containers,
  pre/sig scroll internally, long names wrap (overflow-wrap). 0 page overflow on
  every page type at 375px.
- Stack layouts on mobile: single-column hero/features/steps/index grid,
  column parameter cards, 2-col color swatches; multi-column returns on wider
  screens. Reduced hero/section padding; fluid clamp() headings.
- Hover cards gated to hover-capable devices so a tap just navigates.
- Desktop layout preserved (verified at 1280px: inline nav, 2-col hero, 3-col
  features, no regression).

Verified in-browser at 375px and 1280px across landing, API index, item
(method/keyword/annotation/phase), namespace overview, and color pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 18:22:55 +03:00
3c7ec9b016 docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Rebuild the API Reference around one page per symbol and richer, verified content.

Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
  instead of a single scrolling page; namespace overview pages (ns-screen …
  ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
  never hidden, plus a flash highlight on the scrolled-to target.

Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
  `Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).

Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
  the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
  page), all 22 annotations, namespace methods with parameter docs, builtins,
  the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
  descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
  delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
  world_query_next(property, cursor) arg order, event fields bind by name; dropped
  `when` and `module` (not in the self-hosted parser).

Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
  token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 17:53:22 +03:00
51ddfa3ce9 docs: automated documentation pipeline (per-symbol source → pages)
Some checks failed
docs / build-and-deploy (push) Failing after 38s
Replace the hardcoded landing page and minimal reference with a generated
documentation site driven by a single source of truth.

- docs/language/**: one file per symbol (93 keywords/types/builtins/namespace
  methods/operators/annotations), each with front-matter (id, kind, tokens,
  sig, tip) + description + a ```ludic example. Seeded by exploding the former
  inline SECTIONS list; these files are now the source of truth.
- docs/site/: site.json (editable hero/features/showcase/messaging, not
  hardcoded) + snippets/*.ludic (real programs shown on the landing page).
- tools/docgen/gen.py: generates index.html, api.html, ludic-highlight.js and
  symbols.json. The highlighter's symbol tables, hover tips and jump anchors
  are GENERATED from the per-symbol files — add a symbol and it is recognized,
  tipped and linked in every snippet automatically. Python stdlib only.
- tools/docgen/check.py: verifies the pages contract + that no snippet token
  links to a missing reference anchor.
- .forgejo/workflows/docs.yml: rebuilds and publishes to the pages branch on
  every push to main touching the docs sources.

Consumes the new Screen.*/Color.*/named-arg API and the 221-color palette.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 16:25:54 +03:00
bca8f126fc Networking N2–N6, and a fully C-free toolchain
Implement the rest of NETWORKING-DESIGN.md (N2–N6) and eliminate every
`.c` file from the repo. clang remains only the LLVM-IR assembler; no C
is compiled anywhere.

Networking (selfhost/emit_net.ludic + parser/emit changes):
- N2 @Sync: per-model serialize/apply + by-kind dispatchers; POD-scalar
  compile error and empty-participation warning; selective replication.
- N3 @Owned: @L_owner array + owner/set_owner/is_owner; owners snapshot.
- N4 @ToServer/@ToClients remote events: framed net_send + net_pump re-emit.
- N5 @Server/@Predicted role guards + drivable sim (tick_fixed/tick_render,
  entry-owns-the-loop).
- Built-in loopback transport so multiplayer runs with zero foreign code;
  extern fn net_send/net_poll still overrides it for a real socket.
- N6 blessed runtime (examples/net_rt.ludic) + end-to-end demo (net_demo).
- Fix: llty("entity") is now i32 (entities are i32 handles), so let e = self().

C elimination:
- Networking + foreign-mod-ABI tests rewritten as self-contained pure-Ludic
  programs (examples/net_*, world_*, mod_events, scoped); tests/ removed.
- Reflection ABI exposed to Ludic as world_* builtins (Ludic-to-Ludic modding).
- Formatter rewritten C→Ludic: tools/ludic-tools/fmt.ludic.
- Language server rewritten C→Ludic: tools/ludic-tools/lsp.ludic (lexer, index
  parser, cross-file workspace resolver, JSON, all LSP handlers).
- Obsolete migrate_*.c codemods deleted; ludic_syntax.h kept as vocabulary data.

Suites: ./test.sh 44/44, ./tools/test-tools.sh 28/28 (LSP 42/42), fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 15:08:23 +03:00
d3301684f1 Phase 8b: modern names for the raw-memory and OS/IO primitives
Groups B and C of the leftover-primitive cleanup — renames, not new machinery,
and deliberately NO unsafe_ prefix (a __-prefix is itself a C convention, and an
`unsafe` marker carries no signal in a fully-manual-memory language with no safe
subset to contrast against).

  memory:  mem_free -> free   mem_realloc -> resize   mem_set -> fill
           ptr_add -> offset
  process: os_argc -> arg_count   os_arg -> arg   os_exit -> exit
           os_system -> run   os_getenv -> getenv   read_byte -> read_char
  dead:    mem_copy, os_time, write_byte (0 uses) — deleted

Two reseeds: accept both old and new names in the intrinsic dispatch, then
migrate every call site and drop the old names. file_open/read/write/seek/tell/
close are left as-is — they're the domain-prefixed syscall layer wrapped by
read_file, not the argc/argv-style C-ness the audit targeted; a `File` type is a
separate, larger design if wanted.

test.sh's CLI smoke updated (os_exit -> exit); check-vocabulary's grammar marker
moved off the deleted names. Reseeded (22243 lines); C-free fixpoint holds;
goldens identical; 18/18; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:55:24 +03:00
5a9e8cb2da Phase 8a: finish typed indexing — fixeds/ptrs buffers (retire peekf/pokef/peekp/pokep/str_len)
The last peek/poke pairs were the same "typed buffer access wearing a C name" as
peek8/peek32, so they become indexing too:

  peekf(sc_x, i)     -> sc_x[i]        (a `fixeds` buffer -> a fixed)
  pokep(gc_bmp, s,b) -> gc_bmp[s] = b  (a `ptrs` buffer -> a pointer)
  str_len(s)         -> len(s)         (len is polymorphic since 7f; str_len was dead)

Two new element-typed buffers join words: `fixeds` (32-bit fixed) and `ptrs`
(pointer, 8-byte stride). emit_index_addr dispatches on the base type; IR is
byte-identical to the old intrinsics.

The peekf/peekp buffers (ed_x0/ed_x1/ol_x/sc_x fixed coords; gc_bmp/img_px/
tt_data/ui_text pointer arrays) were retyped scope-aware, then the 33 sites
migrated to indexing. Two bugs found via a menu golden diff / a link-time type
error and fixed: the buffer-name regex truncated digit suffixes (ed_x0 -> ed_x),
and the char-literal brace-miscount skipped a few module declarations (same class
as 7j).

Reseeded (22371 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab (fixeds/ptrs types in, 5 intrinsics out) + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:49:40 +03:00
86948d2b19 Phase 7k: bytes(n) / words(n) allocators (retire mem_alloc)
Allocation reads as intent, not malloc:

  mem_alloc(64)          -> bytes(64)              (64 bytes -> a byte buffer)
  mem_alloc(w * h * 4)   -> words(w * h)           (w*h 32-bit words)

bytes(n) mallocs n bytes and returns a plain pointer (byte-indexed); words(n)
mallocs n*4 bytes and returns a `words` pointer (int-indexed). Since words(X) and
mem_alloc(X*4) allocate the identical number of bytes, the migration cannot change
any allocation size — the `* 4` factor just moves from the argument into the
allocator name, pairing naturally with the Phase-7j `words` retyping
(`var fb: words = words(w * h)`).

Migrated 102 sites (mem_alloc(E*4) -> words(E), else bytes(E)); deleted the
mem_alloc intrinsic. mem_realloc/free/copy/set stay as the low-level
reallocation/free family. Reseeded (22565 lines); C-free fixpoint holds; goldens
byte-identical; 18/18; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:26:15 +03:00
ae0bae0457 Phase 7j: words buffers + w[i] word indexing (retire peek32/poke32)
32-bit word access is indexing now, not peek32/poke32:

  peek32(ui_rx, i)       -> ui_rx[i]        (reads an int)
  poke32(rt_fb, i, c)    -> rt_fb[i] = c    (writes an int)

A buffer typed `words` (a pointer whose elements are i32) indexes with `w[i]`
as a full int; a plain `ptr`/`str` keeps byte indexing. emit_index_addr picks the
element type from the base's type — byte-identical IR to the old intrinsics, so
the migration reproduces the compiler and every golden render exactly.

The ~60 word buffers (rt_fb, tt_*/gc_* font tables, png_px/spr_px pixels, ui_*
layout arrays) were retyped from `ptr` to `words` scope-aware (per-function, so
the s/out/p byte-vs-word name collisions across functions stay correct), then the
254 peek32/poke32 sites migrated to indexing. A scope-analysis miss left 12
buffers (gc_*, sc_d, ui_rx/ui_ry) un-retyped — caught as a menu golden diff and
fixed. No true mixed byte+word access exists on any one variable, so a per-buffer
element type is sound.

Reseeded (22527 lines); C-free fixpoint holds; goldens byte-identical; 18/18;
vocab (byte/words types in, peek32/poke32 out) + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:20:50 +03:00
feb3a71e56 Phase 7i: byte indexing p[i] / p[i] = v (retire peek8/poke8)
Raw byte access is now indexing, not C-style peek/poke:

  peek8(src, i)          -> src[i]        (reads a byte, widened to int)
  poke8(out, j, r)       -> out[j] = r    (narrows the int to a byte)

E_INDEX on a non-slice pointer/string lowers to a `getelementptr i8` + load/zext
(read) or trunc/store (write) — byte-identical to the old peek8/poke8, so the
migration reproduces the compiler exactly. A "byte" element type (llty i8) drives
the widen/narrow. The compiler's own byte work now reads naturally, e.g.
`is_slice_ty` is `t[0] == 91 and t[1] == 93`.

Subtlety fixed on the way: g_addr_ty (the out-param carrying the indexed element
type) must be set AFTER evaluating the index expression, since a member/index in
the index would otherwise clobber it — doing it early made a byte read load a
full pointer from a byte address and crash the self-compile.

Two reseeds: add byte-index support keeping peek8/poke8, then migrate 148 call
sites and delete the intrinsics (+ the now-dead emit_gep_i8). Vocabulary drops
peek8/poke8. Reseeded (22604 lines); C-free fixpoint holds; goldens identical;
18/18; vocab clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:01:59 +03:00
e9c15cc620 Phase 7e: polymorphic print(x) + str(x) (retire print_int/print_str)
One `print` instead of two C-style names: `print(x)` writes an int OR a string
followed by a newline, dispatching on the operand type (int -> %d, string ->
%s). `print(int)` emits byte-identically to the old print_int, so every existing
call and every smoke-test output is unchanged.

print_str was only ever the raw IR-to-stdout dump in ir_flush (no newline), which
is not "printing a line" — so it now uses file_write to a new file_stdout()
stream, keeping the emitted IR byte-for-byte identical. That frees `print` to
have consistent always-newline semantics.

Two reseeds: (A) add print + str + file_stdout keeping the intrinsics; (B)
migrate the 61 print_int calls to print, ir_flush to file_write(file_stdout()),
and delete print_int/print_str (+ the now-dead @.fmt_str). str(x) (the
interpolation converter from 7d) is now also a documented standalone builtin.

Vocabulary: print/str/file_stdout in, print_int/print_str out (ludic_syntax.h,
grammar, LudicTokens.kt). LANGUAGE.md updated. Reseeded (22551 lines); C-free
fixpoint holds; goldens identical; 18/18; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 01:22:38 +03:00
ad63f09d53 Phase 7d: string interpolation text {expr} text
The readable way to build strings, as you noted `{a} {b}` beats `a + " " + b`.
A backtick string embeds any expression in `{…}` and desugars to the Phase-7c
`+` chain, wrapping each hole in `str(...)`:

  `hello {name}, n={count + 1}`
    ==  "hello " + name + ", n=" + str(count + 1)

- Lexer: a backtick captures its content raw as TK_INTERP.
- Parser: parse_interp splits literal runs from `{…}` holes (brace-depth aware,
  `{{`/`}}` escape to literal braces), re-lexes each hole as a full expression
  (save/restore toks/pi like an import), and folds it all into E_BIN(+) nodes —
  so no new AST or runtime beyond the existing concat.
- str(x): a string passes through; int/bool/fixed convert via a small emitted
  @fn_int_str prelude (digits from the end of a buffer, '-' for negatives),
  emitted once into any program that uses it.

examples/strings.ludic gains interpolation cases (now prints 1..7); the smoke
covers it. Grammar + ludic_syntax.h tokenize backtick strings (holes highlighted
as embedded code). LANGUAGE.md documents it as the preferred form.

Reseeded (22530 lines); C-free fixpoint holds; goldens identical; 18/18; vocab +
doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 01:12:07 +03:00
70ab79a4e9 Phase 7b: null literal + x == null (retire ptr_null/ptr_is_null)
`null` is now a real pointer literal and null-tests are comparisons, instead of
`ptr_null()` and `ptr_is_null(x)`:

  ptr_null()          -> null
  ptr_is_null(x)      -> (x == null)
  not ptr_is_null(x)  -> (x != null)

Mechanics: a new E_NULL primary (`null`, like true/false) lowers to the `null`
pointer; emit_bin's comparison path now picks `ptr` vs `i32` from operand type
(via llty), so `==`/`!=` work on any pointer/record/slice. The two intrinsics are
deleted.

Two reseeds: (A) add the literal + ptr comparison keeping the intrinsics; (B)
migrate all 182 call sites (compiler + runtime, via a balanced-paren script that
skips string-literal args and rewrites `not ptr_is_null` to `!= null`) and delete
the intrinsics. Node/Val/Buf/Tok field defaults now read `ptr = null`.

Vocabulary drops the two from LUDIC_INTRINSICS; `null` joins true/false as a
language constant (grammar + ludic_syntax.h). LANGUAGE.md notes the literal.
Reseeded (21664 lines); C-free fixpoint holds; goldens identical; 17/17; vocab +
doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 00:50:21 +03:00
fcada23eba Phase 7a: bitwise operators & | ^ << >> ~ (retire band/bor/shl/…)
First step of the "stop feeling like C" pass. Bitwise ops were functions
(`band(x, MASK)`, `shl(a, 3)`); they are now real operators:

  band -> &   bor -> |   bxor -> ^   shl -> <<   shr -> >>   bnot -> ~

Precedence is Go-style so the C footgun is gone: `<<`/`>>`/`&` bind like `*`,
`|`/`^` like `+`, both tighter than comparison — `flags & MASK == 0` parses as
`(flags & MASK) == 0`. `>>` is logical (lshr), matching the old `shr`.

Mechanics: lexer tokenizes `<< >> & | ^ ~`; p_mul takes `<< >> &`, p_add takes
`| ^`, p_unary takes `~`; emit_bin routes them through the existing int arith
path (arith_code gains and/or/xor/shl/lshr) and E_UN handles `~`. The six
intrinsics are deleted.

Delivered as two reseeds: (A) add the operators keeping the intrinsics, (B)
migrate every call site to operator form (85 lines across compiler + runtime,
via a balanced-paren call->operator script; two multi-line big-endian reads in
image/truetype done by hand) and delete the intrinsics. Vocabulary drops the six
from LUDIC_INTRINSICS (ludic_syntax.h, grammar, LudicTokens.kt) and the grammar
gains a bitwise-operator rule. LANGUAGE.md precedence table rewritten.

Reseeded (21724 lines); C-free fixpoint holds; goldens byte-identical (the PNG
and TrueType decoders lean on these ops); 17/17; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 00:45:10 +03:00
cc701013f5 Phase 6e: unify builtin naming (loaders domain-first, set_reg)
Fixed the naming inconsistencies the cohesion audit flagged, converging on the
domain-first style the bulk of the surface already uses (ui_*, text_*, rng_*,
font_load, image_load):

  - load_png    -> png_load       (asset loaders were split: font_load/image_load
  - load_sprites -> sprites_load   were domain-first, load_* were verb-first)
  - setreg      -> set_reg        (missing underscore vs ui_set_int/ui_set_text)

Game builtins resolve to their `rt_` runtime function, so the renames are in
runtime/native (rt_png_load, rt_sprites_load, rt_set_reg) plus the ~100 example
call sites; `become` lowering in emit_machine now emits @fn_rt_set_reg. Purely a
surface rename — every renamed call maps to the same runtime symbol, so behavior
and golden renders are byte-identical.

Vocabulary + docs updated (ludic_syntax.h, grammar, LudicTokens.kt, LANGUAGE.md,
README, SYNTAX-REDESIGN). Reseeded; C-free fixpoint holds; goldens identical;
17/17; vocab clean.

Left as-is: os_argc/os_arg (compiler-internal intrinsics, already namespaced and
consistent with each other; renaming would need a bootstrap dance for little
gain). reg/set_reg keep the getter-bare/setter-set_ shape ui_focused/ui_set_int
already use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 23:38:10 +03:00
dc475b17c2 Phase 6d: merge struct into property; storage follows use
`struct` and `property` had identical syntax and differed only in semantics, so
they are now one keyword: `property`. How a property is stored follows from how
it is used —

  - listed in a `model` or attached by `spawn`  -> an ECS component, kept in the
    engine's per-entity @S_/@H_ arrays and bound in queries (as before);
  - constructed with `new`                       -> a heap record with reference
    semantics (what `struct` used to be).

A program that declares only `property` records and functions — no `model`, no
`handler` — is not an ECS program: it gets record layouts and `new`, but no
entity storage, allocator, snapshot, or runtime splice. This is exactly the
shape of the Ludic compiler itself, whose Node/Tok/Buf/Val are now `property`.

Mechanics:
  - record layout (%Cmp_) now always emitted in the header (emit_head), so `new`
    works with or without the ECS; the per-entity arrays stay in
    emit_ecs_storage. %Str_ is gone — one layout prefix.
  - has_ecs() is now `has_systems() or has_models()`, not "any component"; a
    property alone no longer drags in the ECS runtime. Added has_models().
  - emit_new / member access / layout_ty / layout_node collapse onto find_comp.
    Dropped struct keyword, parse_struct, find_struct, is_struct_ty, N_STRUCT
    emission (the const stays at kind 0, the default node kind).

Migration done as two reseeds (the old compiler treats any component as ECS, so
it cannot see `property` records in the compiler source until has_ecs is fixed):
  A) teach the compiler property-as-record + fix has_ecs, keeping `struct`;
  B) migrate the compiler's own records to `property` and remove `struct`.

selfhost/tests/structs.ludic migrated (still prints 7 9 109 2 42). Vocabulary
drops `struct` from DECL (ludic_syntax.h, grammar, LudicTokens.kt). LANGUAGE.md
"Records" section rewritten. Reseeded (21613 lines); C-free fixpoint holds;
goldens identical; 17/17; vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 23:32:05 +03:00
3fd599ce47 Phase 6b: a handler's query is @Queries only; drop the signature clauses
The handler-signature `query (vars) [terms] where …` clause and the
`reads`/`writes` clauses overlapped the `@Queries` annotation (and each other):
two ways to attach a query to a handler. Consolidated on the decorator.

  - parse_system no longer parses `query`/`reads`/`writes` clauses; it keeps the
    postfix `@anno(...)` channel and `phase`. A handler's query is the prefix
    `@Queries(these: [...], on: Model)` annotation. Data-access hints are now
    `@Reads(...)`/`@Writes(...)` — absorbed by the generic annotation skipper,
    same parse-and-reserve status the old clauses had.
  - The inline `for (…) in query […] where …` statement is unchanged and still
    covers cross-property constraints / multiple kind filters. `query` stays a
    keyword there (now dispatched via is_id so the vocabulary check sees it).

examples/hello.ludic and examples/qdecl.ludic migrated to `@Queries` (qdecl now
demonstrates a per-property constraint + `on:` tag); outputs unchanged
(4 4 10 3 / 0 3 -2). LANGUAGE.md handler sections rewritten. Vocabulary: drop
`reads`/`writes` from CLAUSE (ludic_syntax.h, grammar, LudicTokens.kt).

Reseeded (21931 lines); C-free fixpoint holds; goldens identical; 17/17;
vocab + doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 23:20:09 +03:00
920d476a15 Phase 6a: remove the redundant when and enter keywords
Cohesion audit found two keywords that are pure duplicates:
  - `when c { }` produced a byte-identical S_IF to an else-less `if c { }`
    (no distinguishing flag) — so it was a second spelling of the same node.
  - `enter Name` produced identical codegen to `become Name`: it set an
    `ival` flag that emit_become never reads.

Both removed from the parser. `if` already parses with an optional `else`, so
nothing is lost. examples/scenes.ludic (an uncompiled design sketch) and the
LANGUAGE.md scenes section now use `become Name` for scene transitions.

Vocabulary synced (ludic_syntax.h, TextMate grammar, LudicTokens.kt);
check-vocabulary clean. Reseeded (22148 lines); C-free fixpoint holds;
goldens identical; 17/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 23:14:32 +03:00