ludic/tools/ci/linux_stdio_shim.ll
Orkuncakilkaya 1c1192e7c0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 49s
ci: add build + test + bootstrap-cfree workflows for the Forgejo runner
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

33 lines
1.3 KiB
LLVM

; linux_stdio_shim.ll — build-time bridge so the Ludic toolchain links on Linux.
;
; The self-hosted compiler emits the macOS/BSD libc standard-stream globals
; @__stdoutp / @__stderrp (that is how <stdio.h> spells `stdout`/`stderr` on
; Darwin). glibc instead exports `stdout`/`stderr` directly, so a Linux link of
; any Ludic binary fails with "undefined reference to __stderrp".
;
; This shim defines the two Darwin-named globals and, in a startup constructor,
; points them at glibc's real FILE* streams. It is pure LLVM IR — no C compiler
; and no .c source — so it keeps the toolchain's C-free guarantee intact. It is
; injected on Linux only, via LUDIC_CC (see .forgejo/workflows/ci.yml); macOS
; never links it (libc already provides __stdoutp/__stderrp).
;
; Kept deliberately triple-free: clang stamps the host target when it assembles
; it, so the same file works on linux/arm64 and linux/amd64 runners.
@stdout = external global ptr
@stderr = external global ptr
@__stdoutp = global ptr null
@__stderrp = global ptr null
@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }]
[{ i32, ptr, ptr } { i32 65535, ptr @__ludic_bind_std_streams, ptr null }]
define internal void @__ludic_bind_std_streams() {
entry:
%o = load ptr, ptr @stdout
store ptr %o, ptr @__stdoutp
%e = load ptr, ptr @stderr
store ptr %e, ptr @__stderrp
ret void
}