build_section piped every changeset body through `tr '\n' ' '`. A multi-line
changeset came out as one paragraph, so nested bullets rendered as inline
" - " runs and a whole release read as a single unbroken block — v0.3.0 was one
~4 KB bullet.
The renderer is now Ludic rather than a shell one-liner. A section is grouped
by conventional-commit type (Features, Fixes, Performance, ...), each changeset
is one bullet, and continuation lines are indented two spaces so nested lists
and paragraphs stay inside their item. Bullets are sorted within a group, so
cutting the same release twice produces the same text.
Also:
- `x release --dry-run` renders the pending section to stdout and touches
nothing, so a release can be read before it is cut.
- `x changelog-render` re-renders a section from a directory of changesets, and
`x changelog-section` prints one release's section back out of CHANGELOG.md.
- The v0.1.0 and v0.3.0 sections are re-rendered with the former, from the
changesets recovered at each tag's parent commit; the bullet counts (6 and
25) and word multisets are unchanged. v0.2.0 is left alone: it carries a
hand-written summary and topical subheadings, and regenerating it would have
replaced curation with raw changeset dumps. The file header now says that
a section may carry such a summary, since it previously claimed released
sections are never hand-edited.
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>
The package-manager half of prebuilt binary packages, on top of the compiler
foundation (dynamic system registration + --emit-module).
- x build-lib [module.ludic]: compile a package's module to a per-target native
dylib under lib/<target>/, with an @rpath install name so a consumer resolves
it from the content-addressed store.
- x link-flags: print the clang flags (the dylib, an rpath to its store dir,
-export_dynamic) so any build system links a project's prebuilt module dylibs;
x app splices them automatically for in-repo builds.
- kind prebuilt is resolved + linked like any dependency; a missing build target
stays a hard error.
Proven hermetically (macOS-gated, since dylibs are native): a module exporting a
component + an @System(Update) + a function is built with x build-lib, fetched
as a prebuilt dep, and linked into a consumer game that never saw its source —
the module's system mutates the shared world and its function is callable
("3 42"). Package suite 17/0; full suite 87/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the v1 direction decided in the RFC as a set of `x` subcommands
plus a small, contained compiler change.
* URL-as-identity, no registry — a dependency is named by its git import
path and a `git tag vX.Y.Z` publishes a version.
* Minimum Version Selection — a `require` is a minimum; the resolver picks
the greatest required minimum per module, then the reachable closure at
those versions. Deterministic, no SAT solver (tools/x/pkg.ludic).
* Content-addressed global store + per-project links — packages live once in
~/.ludic/store keyed by a content hash; each project links them under
ludic_modules/. package.ludic (manifest) + package.lock.ludic (lock).
* Namespace registration for source packages via a module-root import
fallback in the compiler: do_import resolves a non-local, non-absolute
import under $LUDIC_MODULES (default ludic_modules/), so a fetched
package's Ludic compiles into the consumer the way the built-in stdlib
does. Collisions and missing prebuilt targets are hard errors.
Commands: x add / x get / x update / x verify / x vendor. New hermetic suite
`x test-pkg` (stands up throwaway git repos, offline) is gated inside `x test`.
Existing programs compile byte-for-byte identically (the import fallback only
fires when the local path is absent); the C-free bootstrap fixpoint holds and
the seed is regenerated. Full suite: 87 passed, package suite: 12 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instrument each emitted statement with a per-source-line hit counter, gated
behind a new --coverage flag (default off) so ordinary builds — and the
compiler's own self-compile — stay byte-identical and the C-free bootstrap
fixpoint is untouched. A static line table plus a parallel hit-counter array
are dumped at exit through an atexit hook to $LUDIC_COVERAGE (default
ludic.cov) as a `FILE <name>` header and `<line> <hits>` rows.
bin/x test --coverage compiles the test specs with instrumentation, runs them
into per-file dumps, and aggregates a clean per-file line-coverage report that
names the unreached lines. Adds examples/library/coverage.ludic (a spec whose
tests deliberately miss one branch) and docs. Closes the last open acceptance
item of the testing framework (#12).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>