commit 91a660f11818a3d87a1e8510360b843f670efa01
parent 58f31a5e6f4252d8eb52be87c6e1d226f3ff6e17
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 11 May 2026 12:17:03 -0700
STAGE2.md plan update
Diffstat:
| M | doc/STAGE2.md | | | 248 | ++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- |
1 file changed, 163 insertions(+), 85 deletions(-)
diff --git a/doc/STAGE2.md b/doc/STAGE2.md
@@ -1,15 +1,12 @@
# Stage-2 self-host
What's missing to make `make self` produce a stage-2 `cfree` built by stage-1
-cfree itself. Companion to `DESIGN.md`. Snapshot taken by compiling every
-`src/**/*.c` and `driver/*.c` individually with stage-1 cfree-cc.
+cfree itself. Companion to `DESIGN.md`.
-Result at snapshot: **60 of 104 files compile clean. 44 fail.** The failures
-collapse into ~10 root causes, listed below in roughly the order a fix would
-unblock the most files.
-
-B1–B6 have landed; re-run the audit recipe at the bottom of this file to
-refresh the count.
+Latest snapshot: **104 / 106 files compile clean** (92/92 `src/**/*.c`,
+12/14 `driver/*.c`). The two remaining driver failures (`env.c`, `ld.c`)
+are both blocked by A2 — system-header ingest. Everything in `src/` builds
+under stage 1.
## Build configuration
@@ -20,107 +17,159 @@ cfree-stage1 cc -isystem rt/include -isystem rt/include/libc -Iinclude -Isrc
```
`-isystem rt/include/libc` is required so the hosted libc headers are
-visible (top-level `rt/include/` only ships the freestanding set). The SDK
-include path is deliberately *not* on the search path — stage 2 should
-resolve everything through `rt/include` + `rt/include/libc`.
+visible (top-level `rt/include/` only ships the freestanding set).
-`DEPFLAGS` is empty for stage 2 until B0 lands.
+`DEPFLAGS` is empty for stage 2 today; B0 has landed but the recipe has
+not been switched back on.
## Checklist
### Preprocessor / lexer
-- [ ] **A1.** `#include "x.h"` doesn't search the source file's directory.
- C99 §6.10.2 requires quoted includes to look in the including file's
- directory first, then fall back to the bracketed-include search list. Today
- cfree's pp jumps straight to the search list. Repro:
- `echo '#include "foo.h"' > /tmp/dir/use.c && cfree cc -c /tmp/dir/use.c`
- fails to find `/tmp/dir/foo.h`. _Blocks all 13 `driver/*.c` files._
-- [ ] **A2.** Expand `rt/include/libc/` to cover the POSIX/Mach surface the
- driver uses. Missing today: `sys/stat.h`, `sys/mman.h`, `sys/syscall.h`,
- `fcntl.h`, `unistd.h`, `signal.h`, `pthread.h`, `dlfcn.h`, `mach/mach.h`,
- `mach/mach_vm.h`, `mach/vm_map.h`. Scope question (vs. dropping the
- dependency in the driver source); not a compiler bug. _Blocks most of
- `driver/`._
+- [x] **A1.** Quoted `#include "x.h"` now searches the includer's
+ directory first per C99 §6.10.2 (commit c9baaf8). Was blocking every
+ `driver/*.c` file.
+- [ ] **A2.** System-header ingest. The driver pulls a POSIX/Mach surface
+ (`sys/stat.h`, `sys/mman.h`, `sys/syscall.h`, `fcntl.h`, `unistd.h`,
+ `signal.h`, `pthread.h`, `dlfcn.h`, `mach/mach.h`, `mach/mach_vm.h`,
+ `mach/vm_map.h`) that today's `rt/include/libc/` doesn't ship.
+ **Direction: ingest the real SDK headers** rather than growing
+ `rt/include/libc/`. With `-isystem $SDK/usr/include` and the right host
+ predefines, the SDK parses up to a small set of constructs cfree
+ doesn't yet handle. Each sub-item below is the minimal feature needed.
+
+ - [ ] **A2-S1.** Asm-label on function declarators:
+ `T fn(args) __asm__("name");`. GCC asm-label rename extension; what
+ `__DARWIN_ALIAS` / `__DARWIN_ALIAS_C` / `__DARWIN_INODE64` /
+ `__DARWIN_EXTSN` expand to. Blocks `sys/stat.h`, `sys/mman.h`,
+ `unistd.h`, `_string.h`, `_stdio.h`.
+ - [ ] **A2-S2.** Asm-label on global variables:
+ `extern T name __asm__("name");`. Same extension, declarator position
+ differs from S1. Blocks `_time.h` (→ `<time.h>`, `<signal.h>`,
+ `<pthread.h>`).
+ - [ ] **A2-S3.** Unknown `#pragma` accepted as no-op (full semantics
+ not required for ingest). Today fatal "expected declaration". Blocks
+ `sys/fcntl.h`, `mach/vm_types.h`. Same root cause as R2 below.
+ - [ ] **A2-S4.** `__has_include`, `__has_feature`, `__has_extension`
+ as preprocessor builtins inside `#if`. (`__has_attribute` already
+ works.) Blocks `Availability.h` and the `__enum_decl` feature-detect
+ branch.
+ - [ ] **A2-S5.** `__uint128_t` declared type. Declare-only is enough
+ to parse `mach/arm/_structs.h` (signal.h, ucontext); full codegen
+ is a bigger lift.
+ - [ ] **A2-S6.** `#warning` accepted as non-fatal. Today cfree errors
+ on the directive itself; `sys/cdefs.h`'s
+ `#warning "Unsupported compiler"` aborts any SDK ingest unless
+ `-D__GNUC__` is also passed.
+ - [ ] **A2-S7.** Predefine macOS-host macros (`__APPLE__`, `__MACH__`,
+ `__arm64__`/`__aarch64__`, `__LITTLE_ENDIAN__`, `__GNUC__`,
+ `__GNUC_MINOR__`) automatically when targeting macOS, so callers
+ don't need to hand-pass `-D`.
+
+ After S6+S7+S1+S2+S3+S4, both blocked driver files should ingest the
+ SDK without any growth of `rt/include/libc/`. S5 only needed for
+ signal.h/ucontext paths.
### Driver — dep emission
-- [ ] **B0.** Implement `cfree_dep_iter_new` / `_next` (today both stubs in
- `src/api/stubs.c:106-115`). PP needs to record header-include edges so the
- iterator can drain them. Until then, stage 2 strips `-MMD -MP` via
- `DEPFLAGS=''`. Also: change the failure path in `driver/cc.c:1264` so a
- NULL iter doesn't surface as `"out of memory"` — that error message hid
- this for the whole first investigation. _Quality-of-life; stage 2 builds
- fine without dep files for now._
+- [x] **B0.** `cfree_dep_iter_new` / `_next` implemented over
+ SourceManager (commit 8919185). Stage 2 can re-enable `-MMD -MP`
+ whenever the recipe drops `DEPFLAGS=''`.
### Parser / sema
-- [x] **B1.** Recognize `__alignof__` as an alias for `_Alignof`. Routed
- through `ident_kw`/`is_kw`; every `KW_ALIGNOF` consumer accepts both
- spellings.
-- [x] **B2.** Implement `__builtin_ctz`. Added `cg_intrinsic_unary_to_int`
- wrapper; lowers via the existing `INTRIN_CTZ` path (already implemented in
- all three backends — aa64 `rbit; clz`, x64 `bsf`, rv64 `ctz`).
-- [x] **B3.** No actual fix required — `parse_array_bound` (parse.c:3948-3958)
- already routes `SEK_ENUM_CST` through `eval_const_int`. The STAGE2 repro
- failed because it combined this with B4 (string literal in static init);
- fixing B4 unblocked the example. Regression case added.
-- [x] **B4.** `try_parse_addr_const` now accepts `TOK_STR`: it mints a
- rodata symbol via `emit_string_to_rodata` and emits a reloc against the
- pointer slot.
-- [x] **B5.** `try_parse_addr_const` admits `SEK_FUNC` identifiers (same
- `v.sym` shape as `SEK_GLOBAL`). Diagnostic reworded — the old message
- "static initializer requires object with static storage" was misleading
- for functions, which do have static storage duration.
-- [x] **B6.** The brace-tracker was a red herring. Root cause: file-scope
- `T name[] = {...}` skipped `complete_incomplete_array`, so the static-init
- walker saw `arr.count=0` and tripped "too many initializers" on the
- first element. Block-scope path already handled this; file-scope path now
- mirrors it. Affects every file-scope incomplete-array-of-aggregate init,
- not just the inner-array-field shape called out in the original repro.
+- [x] **B1.** `__alignof__` aliased to `_Alignof` (type-name form).
+- [x] **B2.** `__builtin_ctz` lowered through `INTRIN_CTZ`.
+- [x] **B3.** `parse_array_bound` already routed `SEK_ENUM_CST` through
+ `eval_const_int` — original repro was actually B4. Regression case
+ added.
+- [x] **B4.** `try_parse_addr_const` accepts string literals via
+ `emit_string_to_rodata`.
+- [x] **B5.** `try_parse_addr_const` admits `SEK_FUNC` identifiers.
+- [x] **B6.** File-scope `T name[] = {...}` now calls
+ `complete_incomplete_array` to match the block-scope path.
+- [x] **B7.** `__alignof__` accepts a **unary-expression** operand
+ (`__alignof__(*ptr)`), not just a type-name. Required by the
+ `VEC_GROW` macro in `src/core/vec.h`; previously blocked 8 files in
+ `src/debug/` and `src/link/`.
+- [x] **B8.** `sizeof` accepts the no-parens **unary-expression** form
+ in constant-expression contexts (e.g. file-scope initializers). C99
+ §6.5.3.4 standard, not an extension. Blocked `src/arch/aa64_isa.c`
+ and `src/arch/aa64_regs.c`.
+- [x] **B9.** Block-scope `static T name[] = {...}` now completes the
+ incomplete array, mirroring B6's file-scope fix. Was blocking
+ `src/pp/pp.c`.
### Codegen — aarch64 backend
-- [ ] **C1.** Argument lowering: handle `OPK_INDIRECT` source operands in
- both the INT and FP paths at `src/arch/aarch64.c:2073-2129`. Today only
- `OPK_IMM` / `OPK_REG` / `OPK_LOCAL` are wired; an indirect source (e.g.,
- passing `ptr->field` by value, or `arr[i].field` where the addressing was
- lowered to a base+offset load) panics with
- `aarch64 call: arg storage kind 4 unsupported`. The fix mirrors the
- existing `OPK_LOCAL` case but loads from `[base + part->src_offset]`
- instead of `[fp - slot_off + src_offset]`. _6 files: `src/arch/mc.c`,
- `src/cg/cg.c`, `src/decl/{decl,decl_attrs}.c`, `src/opt/opt.c`,
- `src/pp/pp.c`._
-- [ ] **C2.** Same `OPK_INDIRECT` gap in the indirect-return path
- (separate panic string: `aarch64 ret indirect: storage kind 4
- unsupported`). _`src/api/pipeline.c`, `src/parse/parse_asm.c`._
+- [x] **C1.** `OPK_INDIRECT` source operands handled in INT and FP arg
+ paths (commit f2d3e01).
+- [x] **C2.** `OPK_INDIRECT` on the indirect-return path (commit
+ f2d3e01).
+- [x] **C0.** Stage-1 regalloc "no spillable victim (class 0)" panic
+ fixed — was choking on the complex functions in `src/arch/aarch64.c`,
+ `src/arch/rv64.c`, `src/cg/cg.c`, and `src/opt/opt.c`. Not a feature
+ gap; a regalloc bug surfaced by self-host pressure.
### Codegen — x64 backend
-- [ ] **C3.** Mirror C1/C2 on x64. The same panics exist at
- `src/arch/x64.c:1761,1798,1817,1827,1904`. Doesn't block aarch64
- self-host but blocks x64 self-host once that's attempted.
+- [ ] **C3.** Mirror C1/C2 on x64
+ (`src/arch/x64.c:1761,1798,1817,1827,1904`). Doesn't block aarch64
+ self-host; blocks x64 self-host when that's attempted.
### Linker
-- [ ] **D1.** Stage 2 currently relies on `$(CC) -o $@ ... $(LIB_AR)` to do
- the final link — for stage 2 that's `cfree-stage1 cc`, which in turn
- shells out to the host linker. Once stage 2 builds, the `$(BIN)` recipe
- should be reviewed to confirm the produced binary is genuinely a
- stage-1-emitted object linked through cfree's own ld path, not falling
- back to clang/ld silently.
+- [ ] **D1.** Stage 2 currently relies on `$(CC) -o $@ ... $(LIB_AR)`
+ for the final link — for stage 2 that's `cfree-stage1 cc`, which in
+ turn shells out to the host linker. Once stage 2 builds, verify the
+ produced binary is genuinely a stage-1-emitted object linked through
+ cfree's own ld path, not falling back to clang/ld silently.
### Hosted libc shim
-- [ ] **E1.** Today `libcfree_hosted_macos.a` is built but not threaded into
- the `$(BIN)` link. For a "self-host on rt libc" milestone (separate from
- this checklist's primary goal of "stage 2 builds at all"), the `$(BIN)`
- rule on macOS should consume the hosted shim and route libc calls through
- it instead of clang's default `-lSystem` glue.
-
-## How to re-run the audit
-
-After landing any fix, regenerate the failure list with:
+- [ ] **E1.** `libcfree_hosted_macos.a` is built but not threaded into
+ the `$(BIN)` link. For a "self-host on rt libc" milestone (separate
+ from "stage 2 builds at all"), the macOS `$(BIN)` rule should consume
+ the hosted shim and route libc calls through it instead of clang's
+ default `-lSystem` glue.
+
+## Runtime — `rt/lib/*` ingest
+
+Separate from stage-2 self-host: can cfree compile `libcfree_rt.a`?
+Probed on the `aarch64-apple-darwin` variant — 8 sources, freestanding,
+no system headers. Result: **2 / 8 clean** today. Flags must drop
+`-std=c11 -Wpedantic -Wall -Wextra -Werror -ffreestanding -fno-builtin` —
+cfree rejects all of these. (`-fno-builtin` is the only one not already
+on the stage-2 drop list.)
+
+- [ ] **R1.** Accept `__inline` and `__inline__` as keyword aliases for
+ `inline`. One-line lexer/keyword-table change. Blocks `int/int.c`,
+ `fp/fp.c`, `int64/int64.c` via `rt/lib/include/lp64_le/int_lib.h:83`
+ (`static __inline …`).
+- [ ] **R2.** Unknown `#pragma` accepted as no-op. Same root cause as
+ A2-S3 — one fix, two payoffs. Blocks
+ `atomic/atomic_freestanding.c` (`#pragma clang diagnostic …`).
+- [ ] **R3.** Fold `__builtin_offsetof(T, m)` as a constant expression.
+ cfree already computes struct layout; this is plumbing for the
+ constant-evaluator. Blocks `coro/aarch64.c` (`offsetof` inside
+ `_Static_assert`).
+- [ ] **R4.** `_Alignas(N)` on a **struct member** must raise the
+ containing aggregate's alignment (C11 §6.7.5). cfree honors
+ `__attribute__((aligned(N)))` on the struct itself, but member-level
+ `_Alignas` doesn't propagate. Blocks `coro/coro.c` whose
+ `_Alignof(coro_t)` assertion silently evaluates to less than 16.
+- [ ] **R5.** `__int128` keyword. Latent — `int_lib.h` declares the
+ type via `__attribute__((mode(TI)))` which parses, but full codegen
+ correctness on `__int128` operations hasn't been exercised. Will
+ matter for ABI variants that hit the int128 paths.
+
+After R1+R2+R3+R4, all 8 sources of the `aarch64-apple-darwin` variant
+should compile. The same fixes apply to the linux variants (same
+`int_lib.h`, same `coro.c`, same Apache-2.0 atomic shim).
+
+## How to re-run the audits
+
+Stage-2 audit (src + driver):
```sh
make && cp build/cfree build/cfree-stage1
@@ -135,4 +184,33 @@ for f in $(find driver -name '*.c' | sort); do
done
```
+System-header ingest probe (after A2 work):
+
+```sh
+SDK=$(xcrun --show-sdk-path)
+DEFS="-D__GNUC__=4 -D__GNUC_MINOR__=2 -D__arm64__=1 -D__aarch64__=1 \
+ -D__LITTLE_ENDIAN__=1 -D__APPLE__=1 -D__MACH__=1"
+for h in sys/stat.h sys/mman.h sys/syscall.h fcntl.h unistd.h signal.h \
+ pthread.h dlfcn.h mach/mach.h mach/mach_vm.h mach/vm_map.h \
+ stdio.h stdlib.h string.h; do
+ echo "#include <$h>" > /tmp/h.c
+ $BIN cc $DEFS -isystem rt/include -isystem "$SDK/usr/include" \
+ -c /tmp/h.c -o /tmp/h.o 2>&1 | head -1 | sed "s|^|$h: |"
+done
+```
+
+rt ingest probe (`aarch64-apple-darwin` variant):
+
+```sh
+SRCS="lib/int/int.c lib/fp/fp.c lib/mem/mem.c \
+ lib/atomic/atomic_freestanding.c lib/cfree/ifunc_init.c \
+ lib/int64/int64.c lib/coro/aarch64.c lib/coro/coro.c"
+FLAGS="-target aarch64-apple-darwin -DHAS_INT128=1 \
+ -Irt/lib/include/common -Irt/lib/impl \
+ -Irt/lib/include/lp64_le -Irt/include"
+for f in $SRCS; do
+ $BIN cc $FLAGS -c "rt/$f" -o /dev/null 2>&1 | head -1 | sed "s|^|rt/$f: |"
+done
+```
+
Then `make self` to confirm a clean stage-2 build end-to-end.