- LANGUAGE.md: real diagnostic format, list literals, Font.load / Ui.* (no `reg`/`set_reg`, no `/Handler/Library` typo), `extern function`, existing example links, Input.key(); builtins table trimmed to what exists - COMPILING.md: pipeline names selfhost/ (not compiler/*.c), `program` instead of game/module, all thirteen win_* entry points by group - README.md: the window seam is not "five" functions - CONTRIBUTING.md: docs are checked with `x docs-gen` / `x docs-check` - docs/language: kw-ui and fn-ui_build use the namespaced API; @ClearColor documents constant expressions; examples/README lists operators.ludic Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7.2 KiB
Contributing to Ludic
Thanks for your interest in Ludic — an AoT-compiled game language with an ECS core, a deterministic fixed-point runtime, and a native 2D backend. This guide covers the unusual bit: Ludic is self-hosted, so the compiler, the runtime, and the tooling are all written in Ludic and built by Ludic.
Prerequisites
clang(or another C compiler) — used once to assemble the checked-in LLVM-IR seed into the firstludicc, and thereafter only to assemble IR and link. No C is generated in a build.- LLVM (for the web/wasm target:
brew install llvm lld). - macOS for the windowed Cocoa backend; headless PPM rendering works anywhere.
First build (bootstrap)
From a clean checkout, one line lifts the toolchain off the seed:
clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/x/main.ludic -o bin/x
That gives you bin/x, the Ludic task runner that replaces every build/test
shell script in the repo. From then on it builds everything — including itself:
bin/x build # rebuild the whole toolchain into bin/ (ludicc, ludic, x, ludic-fmt, ludic-lsp)
bin/x help # list every command
Always run x from the repository root, so assets/ and selfhost/ resolve.
The development loop
When you change the compiler or runtime, prove the self-hosting fixpoint still holds before you push:
bin/x reseed # regenerate selfhost/ludicc.seed.ll after a compiler change
bin/x bootstrap-cfree # rebuild the compiler from the seed with NO C compiler in the loop
bin/x test # the full regression suite
Other useful targets:
bin/x app <file.ludic> [--headless] # compile a program to a native app in build/
bin/x selfhost-test # correctness + bootstrap fixpoints
bin/x test-tools # the editor-toolchain suite (ludic-fmt, ludic-lsp)
bin/x clean # remove build/, out.ppm and stray artifacts
Adding to the standard library
The stdlib lives in the runtime (runtime/) and is surfaced as namespaces
(Math.*, Crypto.*, DateTime.*, Screen.*, …). When you add a symbol:
- Implement it in the runtime / emitter as appropriate.
- Document it: add one Markdown file per symbol under
docs/language/<namespace>/and register its id intools/docgen/inventory.json. Each documented namespace gets exactly one directory (the docs check enforces this). - Add or extend an example under
examples/and a case in the test suite. - Run
bin/x docs-gen --out build/pages && bin/x docs-check build/pages— the check fails if any inventory symbol lacks a page or is still seed text. - Add a changeset for the user-facing change: a small file under
changes/with abump:level and a one-line summary. The next release folds it intoCHANGELOG.md.
Versioning & releases
The toolchain is versioned with SemVer; VERSION is the
single source of truth and ludicc --version (or x version) reports it.
Releases are changeset-driven. Every user-facing change ships with a changeset (step 5 above). To cut a release:
x release [major|minor|patch] # omit the level to derive it from the changesets
That aggregates the pending changesets into a new CHANGELOG.md section, bumps
VERSION, commits chore(release): vX.Y.Z, and tags it. Add --publish (with
FORGEJO_TOKEN set) to also push and create the Forgejo release with source and
toolchain tarballs. The tag doubles as the reproducible bootstrap point: the
source archive plus its checked-in seed rebuild that exact toolchain.
Conventions
-
Commits: Conventional Commits —
type(scope): summary, with an optional!before the colon for a breaking change. Keep the summary imperative and under ~72 chars. The types in use:type for feata new user-facing capability (a stdlib namespace, a language feature) fixa bug fix refactora change that neither fixes a bug nor adds a feature perfa performance improvement docsdocumentation only ( docs/, README, comments)testtests only buildthe build/bootstrap machinery (seed, bin/x, linking)ciCI workflows under .forgejo/styleformatting/whitespace, no behaviour change choreroutine housekeeping with no other bucket revertreverts a previous commit Common scopes:
stdlib,lang,emit,runtime,tooling,docs,repo. Reference the issue you close with aCloses #NNtrailer. -
Enforcement: the hooks in
tools/git-hooks/enforce this locally, and thecommit-lintCI job is the backstop. Turn the hooks on once, per clone:git config core.hooksPath tools/git-hooksThat activates the
commit-msghook (rejects a non-conforming summary) and thepre-commithook (rejects unformatted Ludic). Both read the same rules CI does, so a green local commit is a green CI run. -
Formatting:
ludic-fmtis the source of truth (2-space indent, LF, UTF-8); the repo.editorconfigmirrors it. Runbin/ludic-fmt -won files you touch. The contract CI enforces is idempotence —ludic-fmtre-run on its own output is a no-op — which leaves deliberate hand alignment in place; it is not a blanketfmt(x) == x. -
Code structure: one job per file. Split large files by concern into subfolders rather than growing a single 500+-line module (see how
selfhost/andruntime/are organised). -
Language of the toolchain: new runtime and tooling are written in Ludic, not C, Python, or JS. The only non-Ludic pieces are the LLVM-IR seed (
selfhost/ludicc.seed.ll), the hand-writtenruntime/native/cocoa.ll/runtime/web/wasm.llshims, and the legacy Python docgen (being ported).
Git history
The log has two eras: the pre-self-hosting Phase Nx: … / Merge Phase Nx: …
commits, and the Conventional Commits used since. Decision: the old Phase
history stays as-is. Rewriting already-pushed history (filter-repo/rebase) is
destructive and non-reversible for anyone who has cloned, and it buys little — so
we do not rewrite it. The convention is enforced going forward by the hook and
the commit-lint job (both skip merge commits, so the old merges never trip it).
When the versioning work lands (issue #33), the first release tag doubles as a
clean v0 baseline that brackets the Phase-era prefix — the safe,
non-destructive version of "tidy the history" without touching a single commit.
Pull requests
- Base your branch on
main. - Ensure
bin/x test(andbin/x bootstrap-cfreefor compiler/runtime changes) pass, and thatludic-fmtleaves your files unchanged. - Fill in the PR template checklist. Reference the issue you close with
Closes #NNin the description or a commit message.
Reporting issues
Use the templates under .forgejo/issue_template/:
a bug report, a proposal (new stdlib/language surface), or a
cleanup/DX task. Choose the one that fits and fill in the sections.
By participating you agree to the Code of Conduct.