CI: add build + test + bootstrap-cfree workflows (currently only docs runs) #32

Closed
opened 2026-08-30 12:50:28 +02:00 by orkun · 1 comment
Owner

Problem

The repo has no CI that builds or tests the language. The only workflow is
.forgejo/workflows/docs.yml, which regenerates the docs site. That means:

  • PRs are not gated on bin/x test (the regression suite that builds the
    compiler, compiles/runs examples, checks save/load + diagnostics).
  • The headline guarantee — bin/x bootstrap-cfree rebuilds the compiler from
    the IR seed with no C compiler and reproduces its own IR byte-for-byte
    — is
    never verified automatically. A regression here is invisible until someone runs
    it locally.
  • No lint/format gate (ludic-fmt), no test-tools run.

Proposal

Add Forgejo Actions workflows (matching the existing runs-on: docker runner
convention already used by docs.yml):

  1. ci.yml on PRs + pushes to main:
    • build the toolchain (bootstrap from seed),
    • bin/x test (examples + save/load + diagnostics),
    • bin/x test-tools (fmt idempotence + LSP traffic),
    • ludic-fmt check (fail if formatting would change source).
  2. bootstrap.yml: run bin/x bootstrap-cfree and assert the rebuilt
    compiler reproduces selfhost/ludicc.seed.ll byte-for-byte (the C-free +
    fixpoint guarantee).
  3. Cache the LLVM/clang toolchain where possible to keep runs fast.

Acceptance criteria

  • PRs blocked unless build + test + test-tools pass.
  • Bootstrap-cfree byte-identity verified in CI.
  • Formatting enforced in CI.
  • Runner labels match the self-hosted Forgejo runner (docker).

Part of the repository-cleanup / DX pass.

## Problem The repo has **no CI that builds or tests the language**. The only workflow is `.forgejo/workflows/docs.yml`, which regenerates the docs site. That means: - PRs are not gated on `bin/x test` (the regression suite that builds the compiler, compiles/runs examples, checks save/load + diagnostics). - The headline guarantee — **`bin/x bootstrap-cfree` rebuilds the compiler from the IR seed with no C compiler and reproduces its own IR byte-for-byte** — is never verified automatically. A regression here is invisible until someone runs it locally. - No lint/format gate (`ludic-fmt`), no `test-tools` run. ## Proposal Add Forgejo Actions workflows (matching the existing `runs-on: docker` runner convention already used by `docs.yml`): 1. **`ci.yml`** on PRs + pushes to `main`: - build the toolchain (bootstrap from seed), - `bin/x test` (examples + save/load + diagnostics), - `bin/x test-tools` (fmt idempotence + LSP traffic), - `ludic-fmt` check (fail if formatting would change source). 2. **`bootstrap.yml`**: run `bin/x bootstrap-cfree` and assert the rebuilt compiler reproduces `selfhost/ludicc.seed.ll` byte-for-byte (the C-free + fixpoint guarantee). 3. Cache the LLVM/clang toolchain where possible to keep runs fast. ## Acceptance criteria - [ ] PRs blocked unless build + `test` + `test-tools` pass. - [ ] Bootstrap-cfree byte-identity verified in CI. - [ ] Formatting enforced in CI. - [ ] Runner labels match the self-hosted Forgejo runner (`docker`). Part of the repository-cleanup / DX pass.
orkun added the
priority:high
area:tooling
area:ci
labels 2026-08-30 12:50:28 +02:00
orkun closed this issue 2026-08-30 22:23:26 +02:00
Author
Owner

Done in 1c1192e — and verified on the live runner: both new workflows went green on the push (build-and-test ✓, cfree-fixpoint ✓).

The wrinkle worth recording: the toolchain is macOS-first. The self-hosted compiler emits the Darwin libc standard-stream globals @__stdoutp/@__stderrp, which is the single thing that stops its IR from linking on Linux — everything else (the seed, the whole self-compile, the byte-for-byte fixpoint) is host-neutral. The Forgejo runner is a Linux docker executor, and no macOS runner is registered, so rather than block on that I bridged the one gap with a C-free LLVM-IR shim (tools/ci/linux_stdio_shim.ll) that defines the Darwin-named globals over glibc's stdout/stderr and is injected into every clang link through LUDIC_CC. No C compiler enters the picture — the no-C guarantee holds.

Workflows (both on runs-on: docker, reusing the runner's node:20-bookworm base + clang-16/python3):

  • ci.yml — bootstrap the toolchain from the seed, then x test + x test-tools + the docs-cover-the-implementation checks, on pushes to main and PRs.
  • bootstrap.yml — x bootstrap-cfree: asserts the seed rebuilds itself byte-for-byte (non-zero exit on drift). This is the guarantee the issue flagged as "never verified automatically" — it now is, on every push and PR.

Acceptance criteria:

  • PRs/pushes gated on build + test + test-tools.
  • Bootstrap-cfree byte-identity verified in CI.
  • Formatting enforced — via ludic-fmt idempotence (fmt(fmt(x)) == fmt(x), already in x test-tools). Note: I deliberately did not add a strict fmt(x) == x gate — this codebase keeps hand alignment on purpose (there's even a "hand alignment preserved" fmt test), so --check would fight that contract and fail on current source.
  • Runner label matches the self-hosted runner (docker).

Honest scope note: to keep a Linux run green without hiding anything, the cases that are genuinely macOS-ABI bound now print a visible skip off Darwin rather than fail — 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, and Fs.list + the LSP workspace walk (both read the BSD dirent layout). On macOS every one still runs (suites stay 56 / 29 / 29 green); on Linux they run 51 / 28 (+skips), bootstrap-cfree byte-identical on both. Making those runtime pieces portable (a Linux dirent/utsname path, a non-Cocoa window backend) is a proper platform-port effort and deserves its own issue.

Follow-up already noted in the workflow comments: a prebuilt CI image with clang-16/python3 baked in would shave the per-run apt-get (ties into #33's packaging).

Done in 1c1192e — and verified on the live runner: both new workflows went green on the push (`build-and-test` ✓, `cfree-fixpoint` ✓). **The wrinkle worth recording:** the toolchain is macOS-first. The self-hosted compiler emits the Darwin libc standard-stream globals `@__stdoutp`/`@__stderrp`, which is the *single* thing that stops its IR from linking on Linux — everything else (the seed, the whole self-compile, the byte-for-byte fixpoint) is host-neutral. The Forgejo runner is a Linux `docker` executor, and no macOS runner is registered, so rather than block on that I bridged the one gap with a **C-free LLVM-IR shim** (`tools/ci/linux_stdio_shim.ll`) that defines the Darwin-named globals over glibc's `stdout`/`stderr` and is injected into every clang link through `LUDIC_CC`. No C compiler enters the picture — the no-C guarantee holds. **Workflows** (both on `runs-on: docker`, reusing the runner's `node:20-bookworm` base + `clang-16`/`python3`): - `ci.yml` — bootstrap the toolchain from the seed, then `x test` + `x test-tools` + the docs-cover-the-implementation checks, on pushes to `main` and PRs. - `bootstrap.yml` — `x bootstrap-cfree`: asserts the seed rebuilds itself byte-for-byte (non-zero exit on drift). This is the guarantee the issue flagged as "never verified automatically" — it now is, on every push and PR. **Acceptance criteria:** - [x] PRs/pushes gated on build + `test` + `test-tools`. - [x] Bootstrap-cfree byte-identity verified in CI. - [x] Formatting enforced — via `ludic-fmt` **idempotence** (`fmt(fmt(x)) == fmt(x)`, already in `x test-tools`). Note: I deliberately did *not* add a strict `fmt(x) == x` gate — this codebase keeps hand alignment on purpose (there's even a "hand alignment preserved" fmt test), so `--check` would fight that contract and fail on current source. - [x] Runner label matches the self-hosted runner (`docker`). **Honest scope note:** to keep a Linux run green *without hiding anything*, the cases that are genuinely macOS-ABI bound now print a visible `skip` off Darwin rather than fail — 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, and `Fs.list` + the LSP workspace walk (both read the BSD `dirent` layout). On macOS every one still runs (suites stay 56 / 29 / 29 green); on Linux they run 51 / 28 (+skips), bootstrap-cfree byte-identical on both. Making those runtime pieces portable (a Linux `dirent`/`utsname` path, a non-Cocoa window backend) is a proper platform-port effort and deserves its own issue. Follow-up already noted in the workflow comments: a prebuilt CI image with `clang-16`/`python3` baked in would shave the per-run `apt-get` (ties into #33's packaging).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#32
No description provided.