commit dca1d7920a5a3ee3c5fd68e4d277264c3603495c
parent bf8dcd15f094656d643c07382501d47d925b8831
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 29 Apr 2026 11:25:07 -0700
cc.scm: --lib mode for catm-style libc linking
Replace scripts/boot-libc-prepend.sh's awk transforms with a --lib=PFX
flag in cc.scm that suppresses the auto-emitted entry stub and trailing
:ELF_end and namespaces anonymous string labels (cc__str_N → PFX+...).
Library and executable TUs now compose by catm:
P1/entry-libc.P1pp build/$ARCH/libc.P1pp client.P1pp P1/elf-end.P1pp
boot-build-p1pp.sh accepts the chain as multiple sources; boot-build-cc.sh
gains a CC_LIB env var to drive the new flag. Three small fragments in
P1/ (entry-plain, entry-libc, elf-end) supply the parts that aren't
cc.scm's job.
cg-intern-string pads each string literal to 8B so the next label lands
at an aligned address (aarch64 BLR / 4-byte LDR fault otherwise). Padding
is gated on the trailing-NUL flag so non-string init pieces (4-byte int
slots inside struct initializers, etc.) keep their natural length and
struct field offsets don't shift.
Diffstat:
11 files changed, 279 insertions(+), 240 deletions(-)
diff --git a/Makefile b/Makefile
@@ -199,7 +199,7 @@ $(HELLO_BINS): build/%/hello: $(HELLO_SRC) $(P1_BUILD_DEPS)
$(call PODMAN,$*) sh scripts/boot-build-p1.sh $(HELLO_SRC) $@
$(SCHEME1_BINS): build/%/scheme1: $(SCHEME1_SRC) $(P1PP_BUILD_DEPS)
- $(call PODMAN,$*) sh scripts/boot-build-p1pp.sh $(SCHEME1_SRC) $@
+ $(call PODMAN,$*) sh scripts/boot-build-p1pp.sh $@ $(SCHEME1_SRC)
# cc.scm: catm prelude + cc.scm + main.scm entry into one source the
# scheme1 interpreter can run. Catm runs inside the per-arch container.
@@ -248,24 +248,31 @@ $(LIBC_FLATS): build/cc-bootstrap/%/libc.flat.c: \
scripts/libc-flatten.sh $(LIBC_VENDOR_SRCS)
sh scripts/libc-flatten.sh --arch $*
+# libc and tcc.flat are both compiled with --lib= so they omit the
+# entry stub and trailing :ELF_end (provided by P1/entry-libc.P1pp
+# and P1/elf-end.P1pp at link time). Distinct prefixes keep the
+# anonymous string labels (cc__str_N) from colliding when both TUs
+# end up in the same catm chain.
$(LIBC_P1PPS): build/%/libc.P1pp: \
build/cc-bootstrap/%/libc.flat.c \
build/%/scheme1 build/%/cc/cc.scm \
scripts/boot-build-cc.sh build/%/.image
- $(call PODMAN,$*) sh scripts/boot-build-cc.sh $< $@
+ $(call PODMAN,$*) env CC_LIB=libc__ sh scripts/boot-build-cc.sh $< $@
$(TCC_BOOT2_P1PPS): build/%/tcc-boot2/tcc.flat.P1pp: \
$(TCC_FLAT) build/%/scheme1 build/%/cc/cc.scm \
scripts/boot-build-cc.sh build/%/.image
- $(call PODMAN,$*) sh scripts/boot-build-cc.sh $(TCC_FLAT) $@
+ $(call PODMAN,$*) env CC_LIB=tcc__ sh scripts/boot-build-cc.sh $(TCC_FLAT) $@
+# tcc-boot2 link: pure catm chain — entry stub, libc, client TU,
+# elf terminator. boot-build-p1pp.sh concatenates them in order
+# ahead of the M1pp expander/M0/hex2 pipeline.
$(TCC_BOOT2_BINS): build/%/tcc-boot2/tcc-boot2: \
build/%/tcc-boot2/tcc.flat.P1pp build/%/libc.P1pp \
- scripts/boot-libc-prepend.sh \
+ P1/entry-libc.P1pp P1/elf-end.P1pp \
$(P1PP_BUILD_DEPS)
- $(call PODMAN,$*) sh scripts/boot-libc-prepend.sh \
- build/$*/libc.P1pp $< $(@D)/tcc-boot2.P1pp
- $(call PODMAN,$*) env P1PP_TRACE=1 sh scripts/boot-build-p1pp.sh $(@D)/tcc-boot2.P1pp $@
+ $(call PODMAN,$*) env P1PP_TRACE=1 sh scripts/boot-build-p1pp.sh $@ \
+ P1/entry-libc.P1pp build/$*/libc.P1pp $< P1/elf-end.P1pp
# --- Native tools (opt-in dev-loop helpers) -------------------------------
@@ -327,11 +334,12 @@ TEST_CC_UNIT_DEPS := $(foreach a,$(TEST_ARCHES), \
TEST_CC_DEPS := $(TEST_CC_UNIT_DEPS) \
$(foreach a,$(TEST_ARCHES),build/$(a)/cc/cc.scm)
-# cc-libc: cc deps + the pre-built libc.P1pp the suite prepends to every
-# fixture. Targeted red-green TDD on the cc.scm + libc combination.
+# cc-libc: cc deps + the pre-built libc.P1pp the suite catm's into every
+# fixture, plus the entry/elf-end fragments the catm chain depends on.
+# Targeted red-green TDD on the cc.scm + libc combination.
TEST_CC_LIBC_DEPS := $(TEST_CC_DEPS) \
$(foreach a,$(TEST_ARCHES),build/$(a)/libc.P1pp) \
- scripts/boot-libc-prepend.sh
+ P1/entry-libc.P1pp P1/elf-end.P1pp
test:
ifeq ($(SUITE),)
@@ -343,6 +351,7 @@ ifeq ($(SUITE),)
@$(MAKE) --no-print-directory test SUITE=cc-pp
@$(MAKE) --no-print-directory test SUITE=cc-cg
@$(MAKE) --no-print-directory test SUITE=cc
+ @$(MAKE) --no-print-directory test SUITE=cc-libc
else ifeq ($(SUITE),m1pp)
@$(MAKE) --no-print-directory $(TEST_M1PP_DEPS)
sh scripts/run-tests.sh --suite=m1pp $(if $(ARCH_FILTER),--arch=$(ARCH_FILTER))
diff --git a/P1/elf-end.P1pp b/P1/elf-end.P1pp
@@ -0,0 +1,8 @@
+# P1/elf-end.P1pp — terminator for catm-style links.
+#
+# vendor/seed/$ARCH/ELF.hex2 sizes p_filesz from the first :ELF_end
+# label it sees. Exactly one TU in a link must emit it; in exec mode
+# cc.scm appends it for you, but in --lib mode every TU suppresses
+# it so the chain can supply this fragment once at the tail.
+
+:ELF_end
diff --git a/P1/entry-libc.P1pp b/P1/entry-libc.P1pp
@@ -0,0 +1,19 @@
+# P1/entry-libc.P1pp — executable entry stub for links that include
+# our libc (vendor/mes-libc + lispcc-syscall.c).
+#
+# Same shape as entry-plain.P1pp but threads __libc_init in front of
+# main. __libc_init (lispcc-syscall.c) reads argv's NULL terminator
+# to populate `environ` so getenv()/etc don't dereference a NULL
+# environment pointer on the first call. argc/argv arrive in a0/a1
+# from the bootstrap _start; %call doesn't clobber them, so the
+# second %call delivers them to main unchanged.
+#
+# Cat this in exactly once at the head of a libc-using catm chain
+# and pair with P1/elf-end.P1pp at the tail. Library TUs (libc,
+# client) should be built with cc.scm --lib=PFX so they don't
+# also try to define :p1_main.
+
+%fn(p1_main, 0, {
+ %call(&__libc_init)
+ %call(&main)
+})
diff --git a/P1/entry-plain.P1pp b/P1/entry-plain.P1pp
@@ -0,0 +1,21 @@
+# P1/entry-plain.P1pp — executable entry stub for catm-style links.
+#
+# P1's program-entry contract (docs/P1.md §Program Entry) delivers
+# argc in a0 and argv in a1 at :p1_main. Under cc.scm's standard
+# convention :main has the same shape, so a bare %call(&main) forwards
+# both unchanged (%call doesn't clobber a0/a1). main builds its own
+# frame on top.
+#
+# %fn size = 0: this stub has no locals and no overflow outgoing args
+# (only 2 register-passed args). %enter on every arch independently
+# allocates a 16-byte slot for saved-lr / saved-old-sp on top of the
+# user-requested size, so a 0-size frame still has the saves it needs.
+#
+# Cat this fragment in exactly once at the head of a catm chain to
+# supply :p1_main, and pair with P1/elf-end.P1pp at the tail. Library
+# TUs (built with cc.scm --lib=PFX) suppress the auto-emitted entry
+# stub, which is what makes adding this fragment safe (no collision).
+
+%fn(p1_main, 0, {
+ %call(&main)
+})
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -453,8 +453,16 @@
;; cg-fn-meta: transient per-function state (fn-name, ret-slot, ret-type,
;; vararg-first-slot, indirect-slots, switch-case lists, ...). Reset on
;; cg-fn-begin/v; reads via %cg-fn-get / writes via %cg-fn-set!.
+;; lib? / str-prefix encode the --lib=PFX flag from cc-main:
+;; #f / "" — exec mode (default): cg-finish emits the
+;; p1_main entry stub and trailing :ELF_end, and
+;; cg-intern-string labels strings cc__str_N.
+;; #t / "<pfx>" — library mode: skip the stub and :ELF_end so the
+;; output catm's into a larger TU, and label strings
+;; <pfx>cc__str_N so two cc.scm outputs in the same
+;; link don't collide on cc__str_0..N.
(define-record-type cg
- (%cg text data bss vstack frame-hi label-ctr world fn-meta fn-buf prologue-buf max-outgoing in-fn?)
+ (%cg text data bss vstack frame-hi label-ctr world fn-meta fn-buf prologue-buf max-outgoing in-fn? lib? str-prefix)
cg?
(text cg-text)
(data cg-data)
@@ -467,7 +475,9 @@
(fn-buf cg-fn-buf)
(prologue-buf cg-prologue-buf)
(max-outgoing cg-max-outgoing cg-max-outgoing-set!)
- (in-fn? cg-in-fn? cg-in-fn?-set!))
+ (in-fn? cg-in-fn? cg-in-fn?-set!)
+ (lib? cg-lib?)
+ (str-prefix cg-str-prefix))
(define (cg-str-pool cg) (world-str-pool (cg-world cg)))
(define (cg-str-pool-set! cg v) (world-str-pool-set! (cg-world cg) v))
@@ -2816,7 +2826,12 @@
;; Lifecycle
;; --------------------------------------------------------------------
-(define (cg-init)
+;; cc-cg fixtures construct a cg directly via (cg-init) — they don't
+;; emit ELF and don't link against another TU, so library knobs are
+;; irrelevant. cc-main routes through cg-init/v with the parsed flag.
+(define (cg-init) (cg-init/v #f ""))
+
+(define (cg-init/v lib? str-prefix)
(%cg (make-buf/cap %BUF-CAP-TEXT) ; text
(make-buf/cap %BUF-CAP-DATA) ; data
(make-buf/cap %BUF-CAP-BSS) ; bss
@@ -2828,7 +2843,9 @@
(make-buf/cap %BUF-CAP-FN) ; fn-buf (reused per fn)
(make-buf/cap %BUF-CAP-PROLOGUE) ; prologue-buf (reused per fn)
0 ; max-outgoing
- #f)) ; in-fn?
+ #f ; in-fn?
+ lib? ; lib? (skip entry stub + :ELF_end)
+ str-prefix)) ; str-prefix (cc__str_N namespacing)
(define (cg-finish cg)
;; Tentative file-scope defs (`int x;` / `static int x;` with no
@@ -2840,17 +2857,24 @@
;; clobber a0/a1, so falling straight through to main forwards
;; them unchanged. The 16-byte frame is just enough for %enter's
;; saved-fp/lr to fit; main builds its own frame on top.
- (let ((tb (cg-text cg)))
- (buf-push! tb "# entry stub: forwards argc=a0, argv=a1 to main\n")
- (buf-push! tb "%fn(p1_main, 16, {\n")
- (buf-push! tb "%call(&main)\n")
- (buf-push! tb "})\n"))
- ;; Every P1pp translation unit must end with :ELF_end so the ELF
- ;; header can compute file-size and ph_memsz boundaries.
+ ;;
+ ;; In lib mode the stub and :ELF_end are suppressed: the catm chain
+ ;; supplies them once, from P1/entry-*.P1pp and P1/elf-end.P1pp, so
+ ;; library TUs don't fight the executable TU for ownership of
+ ;; :p1_main and don't truncate ELF p_filesz at the first inner
+ ;; :ELF_end (hex2 sizes off the first one it sees).
+ (cond
+ ((not (cg-lib? cg))
+ (let ((tb (cg-text cg)))
+ (buf-push! tb "# entry stub: forwards argc=a0, argv=a1 to main\n")
+ (buf-push! tb "%fn(p1_main, 16, {\n")
+ (buf-push! tb "%call(&main)\n")
+ (buf-push! tb "})\n"))))
(bv-cat (list (buf-flush (cg-text cg))
(buf-flush (cg-data cg))
(buf-flush (cg-bss cg))
- ":ELF_end\n")))
+ (cond ((cg-lib? cg) "")
+ (else ":ELF_end\n")))))
(define (cg-fn-begin cg name params return-type)
(cg-fn-begin/v cg name params return-type #f))
@@ -4026,7 +4050,8 @@
(p p)
(else
(let* ((n (length (cg-str-pool cg)))
- (lbl (bytevector-append "cc__str_" (%n n))))
+ (lbl (bytevector-append
+ (cg-str-prefix cg) "cc__str_" (%n n))))
(cg-str-pool-set! cg
(alist-set bv-content lbl (cg-str-pool cg)))
(buf-push! (cg-data cg)
@@ -4044,12 +4069,24 @@
;; is 256 bytes on amd64 and overflows otherwise.
;;
;; If TRAILING-NUL? is #t, an extra 0x00 byte is appended to terminate
-;; a C string. Returns a list of bytevectors ready for bv-cat.
+;; a C string AND total emitted bytes are rounded up to %CG-STR-ALIGN
+;; (8). The padding makes the next label after a string land at an
+;; 8-aligned address — aarch64 BLR / 4-byte LDR fault otherwise.
+;; Padding is gated on TRAILING-NUL? because the only other caller
+;; (%cg-init-piece->bv) emits arbitrary initializer bytes whose
+;; length is sized exactly to the C-visible field; padding a 4-byte
+;; int slot to 8 would shift every following struct field. The
+;; %cg-hex-line tail already emits 0x00 for indices past LEN, so
+;; padding costs only output bytes. Returns a list of bytevectors
+;; ready for bv-cat.
(define %CG-HEX-CHUNK-BYTES 64)
+(define %CG-STR-ALIGN 8)
(define (%cg-bv->hex-lines bv trailing-nul?)
- (let* ((len (bytevector-length bv))
- (total (cond (trailing-nul? (+ len 1)) (else len))))
+ (let* ((len (bytevector-length bv))
+ (logical (cond (trailing-nul? (+ len 1)) (else len)))
+ (total (cond (trailing-nul? (align-up logical %CG-STR-ALIGN))
+ (else logical))))
(cond
((= total 0) '())
(else
@@ -6653,31 +6690,57 @@
((bv= (car args) flag) (cdr args))
(else (cons (car args) (%cc-strip-flag (cdr args) flag)))))
+;; --lib=PFX selects library-mode codegen: cc.scm skips the p1_main
+;; entry stub and trailing :ELF_end (the catm chain supplies them
+;; from P1/entry-*.P1pp + P1/elf-end.P1pp once), and namespaces
+;; anonymous string labels as PFX+"cc__str_N" so two cc.scm outputs
+;; in the same link don't collide on cc__str_0..N. Returns
+;; (values prefix-bv rest-args). PREFIX = "" means exec mode (flag
+;; absent). PREFIX = "" with the flag present is rejected — silently
+;; falling back to exec mode would mask a typo'd Makefile rule.
+(define (%cc-take-lib args)
+ (let loop ((acc '()) (rest args) (pfx #f))
+ (cond
+ ((null? rest)
+ (values (cond (pfx pfx) (else "")) (reverse acc)))
+ ((bv-prefix? "--lib=" (car rest))
+ (cond (pfx (die #f "cc: --lib= specified twice")))
+ (let* ((arg (car rest))
+ (p (bv-slice arg 6 (bytevector-length arg))))
+ (cond ((= 0 (bytevector-length p))
+ (die #f "cc: --lib= requires a non-empty PREFIX")))
+ (loop acc (cdr rest) p)))
+ (else
+ (loop (cons (car rest) acc) (cdr rest) pfx)))))
+
(define (cc-main av)
(let* ((raw (cdr (cdr av)))
(dbg (%cc-flag? raw "--cc-debug"))
- (args (%cc-strip-flag raw "--cc-debug")))
+ (a1 (%cc-strip-flag raw "--cc-debug")))
(cond (dbg (debug-log-on!)))
- (cond
- ((or (null? args) (null? (cdr args)))
- (die #f "usage: cc [--cc-debug] <input.c> <output.P1pp>")))
- (let* ((in-path (car args))
- (out-path (car (cdr args))))
- (debug-log "phase=start" "heap" (heap-usage))
- ;; Streaming pipeline: lex → pp → parser → cg, all concurrent.
- ;; Each stage pulls one tok at a time from upstream. Steady-state
- ;; live data is bounded by parser/pp state, not source length.
- (let* ((src (%cc-slurp in-path))
- (_1 (debug-log "phase=slurp" "heap" (heap-usage)
- "src-bytes" (bytevector-length src)))
- (lex-iter (make-lex-iter src in-path))
- (pp-iter (make-pp-iter lex-iter '()))
- (cg (cg-init))
- (ps (make-pstate pp-iter cg)))
- (parse-translation-unit ps)
- (debug-log "phase=parse" "heap" (heap-usage))
- (let ((out (cg-finish cg)))
- (debug-log "phase=cg-finish" "heap" (heap-usage)
- "out-bytes" (bytevector-length out))
- (%cc-write out-path out))
- 0))))
+ (let-values (((lib-prefix args) (%cc-take-lib a1)))
+ (cond
+ ((or (null? args) (null? (cdr args)))
+ (die #f "usage: cc [--cc-debug] [--lib=PFX] <input.c> <output.P1pp>")))
+ (let* ((in-path (car args))
+ (out-path (car (cdr args)))
+ (lib? (cond ((= 0 (bytevector-length lib-prefix)) #f)
+ (else #t))))
+ (debug-log "phase=start" "heap" (heap-usage))
+ ;; Streaming pipeline: lex → pp → parser → cg, all concurrent.
+ ;; Each stage pulls one tok at a time from upstream. Steady-state
+ ;; live data is bounded by parser/pp state, not source length.
+ (let* ((src (%cc-slurp in-path))
+ (_1 (debug-log "phase=slurp" "heap" (heap-usage)
+ "src-bytes" (bytevector-length src)))
+ (lex-iter (make-lex-iter src in-path))
+ (pp-iter (make-pp-iter lex-iter '()))
+ (cg (cg-init/v lib? lib-prefix))
+ (ps (make-pstate pp-iter cg)))
+ (parse-translation-unit ps)
+ (debug-log "phase=parse" "heap" (heap-usage))
+ (let ((out (cg-finish cg)))
+ (debug-log "phase=cg-finish" "heap" (heap-usage)
+ "out-bytes" (bytevector-length out))
+ (%cc-write out-path out))
+ 0)))))
diff --git a/docs/LIBC.md b/docs/LIBC.md
@@ -49,8 +49,12 @@ vendor/mes-libc/
scripts/
├── libc-flatten.sh host (no boot- prefix); stage + patch + -E
-├── boot-build-cc.sh container; cc.scm → libc.P1pp
-└── boot-libc-prepend.sh container; link-time transforms (§Linking)
+└── boot-build-cc.sh container; cc.scm → libc.P1pp (CC_LIB=PFX
+ selects cc.scm --lib= mode; see §Linking)
+
+P1/
+├── entry-libc.P1pp :p1_main wrapper (calls __libc_init, main)
+└── elf-end.P1pp single :ELF_end terminator label
tests/cc-libc/ targeted fixtures for cc.scm + libc TDD
```
@@ -193,49 +197,49 @@ stack_t;`.
`libc.flat.c` to produce `build/$ARCH/libc.P1pp` (~520 KB,
~21 K lines).
-### Linking — `scripts/boot-libc-prepend.sh`
-
-cc.scm's output `libc.P1pp` is *almost* a P1pp library — but cc.scm
-emits the same standard executable tail for every TU. The link-time
-script transforms libc.P1pp:
-
-1. Drop cc.scm's auto-emitted exec tail (`# entry stub` comment plus
- the `%fn(p1_main, 16, { %call(&main) })` block). Library TUs must
- not own `:p1_main`.
-2. Drop the trailing `:ELF_end`. `ELF.hex2` sizes `p_filesz` from
- the first `:ELF_end` it sees; only the executable TU may emit it.
-3. Rename internal-linkage `cc__str_N` (anonymous string literals) to
- `libc__cc__str_N`. cc.scm restarts that counter at 0 per TU; hex2
- silently first-def-wins on duplicates and refs in the loser TU
- bind to the wrong bytes.
-4. Pad each `'<hex>'` literal to an 8-byte boundary. cc.scm emits
- strings at their natural length; if the byte count isn't a
- multiple of 4, every label that follows lands at a non-4-aligned
- address and aarch64 BLR SIGBUSes.
-
-Then it appends our own fixed `:p1_main` wrapper that calls
-`__libc_init` (sets `environ`) before forwarding to `main`, and
-applies the same auto-`:p1_main` strip to the executable TU so its
-copy doesn't shadow ours.
-
-The Makefile uses this script for both the tcc-boot2 link and the
-cc-libc test suite, so there's exactly one place to evolve the
-link-time invariants.
-
-All four numbered transforms work around cc.scm bugs; the right
-long-term fix is a `--library` mode in cc.scm that does (1, 2, 3)
-internally and a string-padding pass that handles (4). See
-[TCC-TODO.md §cc.scm-libc-issues](TCC-TODO.md).
+### Linking — catm chain
+
+cc.scm has a `--lib=PFX` flag that turns its output into a P1pp
+library: it suppresses the auto-emitted entry stub
+(`%fn(p1_main, 16, { %call(&main) })`) and the trailing `:ELF_end`,
+and namespaces anonymous string labels as `PFX+"cc__str_N"` so two
+cc.scm outputs in the same link don't collide on `cc__str_0..N`.
+String literals are 8-byte padded unconditionally (any TU, lib or
+exec) so labels following a string land at an aligned address —
+without it, aarch64 BLR / 4-byte LDR SIGBUS once a non-multiple-of-4
+string shows up in `.data`.
+
+Wired together, the link is just `catm`:
+
+```
+P1/entry-libc.P1pp # :p1_main → __libc_init → main
+build/$ARCH/libc.P1pp # cc.scm --lib=libc__ → libc__cc__str_*
+<client>.P1pp # cc.scm --lib=<pfx>__ → <pfx>__cc__str_*
+P1/elf-end.P1pp # :ELF_end
+```
+
+`scripts/boot-build-p1pp.sh` already cats its inputs in front of
+the M1pp expander, so the catm chain is just its source-list
+arguments. Both the tcc-boot2 link rule (Makefile) and the cc-libc
+test suite (boot-run-tests.sh) compose this way; the tcc-boot2
+client uses prefix `tcc__`, every cc-libc fixture uses `app__`.
+
+`__libc_init` (`vendor/mes-libc/lispcc-syscall.c`) walks argv's
+NULL terminator to populate `environ`; it must run before any libc
+function that reads the environment. That's why the entry fragment
+calls it ahead of `main`.
### Wiring
```
-make tcc-boot2 ARCH=aarch64 # builds libc.P1pp + tcc.flat.P1pp,
- # then libc-prepends and assembles to ELF
+make tcc-boot2 ARCH=aarch64 # builds libc.P1pp + tcc.flat.P1pp
+ # (both --lib= mode), then catms with
+ # entry-libc + elf-end into the ELF
```
-The tcc-boot2 link rule depends on `build/$ARCH/libc.P1pp` and
-`scripts/boot-libc-prepend.sh`; rebuilds when either changes.
+The tcc-boot2 link rule depends on `build/$ARCH/libc.P1pp`,
+`P1/entry-libc.P1pp`, and `P1/elf-end.P1pp`; rebuilds when any
+changes.
## Phase A status
@@ -282,16 +286,17 @@ make test SUITE=cc-libc ARCH=aarch64 -- 05-printf-int # one fixture
Per-fixture artefacts at `build/$ARCH/cc-libc/<name>/`:
-- `<name>.client.P1pp` — cc.scm output for the fixture
-- `<name>.P1pp` — merged (libc + client) input to boot-build-p1pp.sh
+- `<name>.client.P1pp` — cc.scm output for the fixture (lib mode,
+ prefix `app__`)
- `<name>` — final ELF
-- `cc.log` / `prepend.log` / `p1pp.log` — captured stdout+stderr from
- each pipeline stage; the suite handler dumps the relevant log under
- the FAIL row when a stage exits non-zero.
+- `cc.log` / `p1pp.log` — captured stdout+stderr from each pipeline
+ stage; the suite handler dumps the relevant log under the FAIL row
+ when a stage exits non-zero.
-When triaging a failure, the merged `.P1pp` is the artefact to grep
-for the symbol or sequence in question; cc.scm's output marks
-function regions clearly.
+When triaging a failure, the catm'd source the M1pp expander sees
+lives at `build/$ARCH/.work/<name>/combined.M1pp` (boot-build-p1pp.sh
+copies it there alongside the rest of the per-stage scratch outputs);
+grep that for the symbol or sequence in question.
## Phase B — build the on-disk archives tcc-boot2 needs
@@ -423,20 +428,12 @@ That's tracked in [TCC.md](TCC.md), not here.
### cc.scm bugs surfaced by Phase A
-All worked around at link time (boot-libc-prepend.sh) or via patches.
-Migrate into cc.scm as that becomes the cleanest place to fix them.
-
-- **String literal padding.** cc.scm emits `'<hex>'` literals at their
- natural byte length. If a string's count isn't a multiple of 4, every
- label that follows lands at a non-4-aligned address; aarch64 BLR
- SIGBUSes. Workaround: link-time pad to 8 bytes.
-- **Per-TU label namespacing.** cc.scm restarts `cc__str_N` at 0 per
- TU. Linking two cc.scm outputs gives duplicate `:cc__str_0..N`;
- hex2 first-def-wins silently. Workaround: rename library TU's
- `cc__str_` to `libc__cc__str_` at link time.
-- **Library mode missing.** cc.scm always emits `# entry stub` +
- `%fn(p1_main, 16, { %call(&main) })` + trailing `:ELF_end`. Library
- TUs must not own those. Workaround: strip them at link time.
+The four link-composition issues — string padding, per-TU label
+namespacing, missing library mode, missing :ELF_end suppression —
+are now fixed in cc.scm itself (string padding is unconditional in
+`%cg-bv->hex-lines`; the other three are gated on the `--lib=PFX`
+flag, see §Linking). Remaining issues:
+
- **Empty-arg-list redecl rejection.** mes headers' K&R-style `f();`
followed by a typed definition fails with `redecl: type mismatch`.
Workaround: vendored-header patches that prototype the offenders
diff --git a/scripts/boot-build-cc.sh b/scripts/boot-build-cc.sh
@@ -9,6 +9,15 @@
## Env: ARCH=aarch64|amd64|riscv64
## CC_DEBUG=1 (optional) — pass --cc-debug to cc.scm so it prints
## per-phase heap usage on stderr.
+## CC_LIB=PFX (optional) — compile in library mode (cc.scm
+## --lib=PFX). Skips cc.scm's auto-emitted entry
+## stub and trailing :ELF_end so the output catm's
+## into a larger link, and namespaces anonymous
+## string labels as PFX+"cc__str_N" to avoid
+## collisions with other cc.scm outputs in the
+## same chain. The catm caller then prepends
+## P1/entry-{plain,libc}.P1pp and appends
+## P1/elf-end.P1pp exactly once each.
## Usage: boot-build-cc.sh <src.c> <out.P1pp>
set -eu
@@ -28,8 +37,10 @@ CC_SRC=build/$ARCH/cc/cc.scm
mkdir -p "$(dirname "$OUT")"
-if [ "${CC_DEBUG:-0}" = "1" ]; then
- "$SCHEME1_BIN" "$CC_SRC" --cc-debug "$SRC" "$OUT"
-else
- "$SCHEME1_BIN" "$CC_SRC" "$SRC" "$OUT"
-fi
+# Build cc-flag list once. Order doesn't matter to cc-main but
+# stays stable for log readability.
+set --
+[ "${CC_DEBUG:-0}" = "1" ] && set -- "$@" --cc-debug
+[ -n "${CC_LIB:-}" ] && set -- "$@" "--lib=$CC_LIB"
+
+"$SCHEME1_BIN" "$CC_SRC" "$@" "$SRC" "$OUT"
diff --git a/scripts/boot-build-p1pp.sh b/scripts/boot-build-p1pp.sh
@@ -6,11 +6,11 @@
## ELF binary (build/$ARCH/m1pp, built by boot2.sh / boot-build-p1.sh).
##
## Pipeline:
-## cat <P1-$ARCH.M1pp> <P1.M1pp> <P1pp.P1pp> <src> -> /tmp/combined.M1pp
-## m1pp /tmp/combined.M1pp -> /tmp/expanded.M1
-## M0 /tmp/expanded.M1 -> /tmp/prog.hex2
-## catm /tmp/elf.hex2 /tmp/prog.hex2 -> /tmp/linked.hex2
-## hex2-0 /tmp/linked.hex2 -> $OUT
+## cat <P1-$ARCH.M1pp> <P1.M1pp> <P1pp.P1pp> <srcs...> -> /tmp/combined.M1pp
+## m1pp /tmp/combined.M1pp -> /tmp/expanded.M1
+## M0 /tmp/expanded.M1 -> /tmp/prog.hex2
+## catm /tmp/elf.hex2 /tmp/prog.hex2 -> /tmp/linked.hex2
+## hex2-0 /tmp/linked.hex2 -> $OUT
##
## libp1pp (P1/P1pp.P1pp) is concatenated unconditionally so portable
## sources can use %fn, the control-flow macros, and libp1pp routines
@@ -19,11 +19,19 @@
## programs that don't reference any libp1pp routine still pay a fixed
## code-size tax (~a few KB).
##
+## Multiple <srcs> are concatenated in the order given. This is how
+## libc-using executables compose: a typical chain is
+## P1/entry-libc.P1pp build/$ARCH/libc.P1pp client.P1pp P1/elf-end.P1pp
+## with libc.P1pp / client.P1pp produced by cc.scm --lib=PFX so they
+## omit the entry stub and trailing :ELF_end (those come from the
+## fixed fragments instead). For a single-TU exec, pass exactly one
+## source built without --lib= and the fragments are unnecessary.
+##
## Env: ARCH=aarch64|amd64|riscv64
## P1PP_TRACE=1 — print a one-line marker (phase, in/out path, size)
## before each pipeline stage so the failing tool is
## obvious. Off by default to keep test runs quiet.
-## Usage: boot-build-p1pp.sh <src> <out>
+## Usage: boot-build-p1pp.sh <out> <srcs...>
set -eu
@@ -35,10 +43,10 @@ trace() {
}
: "${ARCH:?ARCH must be set}"
-[ "$#" -eq 2 ] || { echo "usage: ARCH=<arch> $0 <src> <out>" >&2; exit 2; }
+[ "$#" -ge 2 ] || { echo "usage: ARCH=<arch> $0 <out> <srcs...>" >&2; exit 2; }
-SRC=$1
-OUT=$2
+OUT=$1
+shift
BACKEND=P1/P1-$ARCH.M1pp
FRONTEND=P1/P1.M1pp
@@ -46,11 +54,11 @@ LIBP1PP=P1/P1pp.P1pp
ELF_HDR=vendor/seed/$ARCH/ELF.hex2
TOOLS=build/$ARCH/tools
M1PP_BIN=build/$ARCH/m1pp
-NAME=$(basename "$SRC" .P1pp)
+NAME=$(basename "$OUT")
WORK=build/$ARCH/.work/$NAME
mkdir -p "$WORK" "$(dirname "$OUT")"
-cat "$BACKEND" "$FRONTEND" "$LIBP1PP" "$SRC" > /tmp/combined.M1pp
+cat "$BACKEND" "$FRONTEND" "$LIBP1PP" "$@" > /tmp/combined.M1pp
trace "cat: combined" /tmp/combined.M1pp
"$M1PP_BIN" /tmp/combined.M1pp /tmp/expanded.M1
trace "m1pp: expanded" /tmp/expanded.M1
diff --git a/scripts/boot-libc-prepend.sh b/scripts/boot-libc-prepend.sh
@@ -1,98 +0,0 @@
-#!/bin/sh
-## boot-libc-prepend.sh — produce a single .P1pp suitable for
-## scripts/boot-build-p1pp.sh by stripping cc.scm's standard
-## executable tail from libc.P1pp and prepending the result to a
-## client (executable) TU.
-##
-## Per the scripts/ convention (boot-*.sh always runs in the minimal
-## container) callers are: the cc-libc suite handler in
-## boot-run-tests.sh (already in container), and the tcc-boot2 link
-## rule in the Makefile (which wraps with `$(call PODMAN,…)`). The
-## transforms are pure awk + sh — they happen to work on the host too,
-## but the convention is what's load-bearing.
-##
-## Used by both call sites so the link-time invariants (which symbols
-## collide between two cc.scm outputs, what alignment cc.scm doesn't
-## enforce, what startup wiring our :_start expects) live in exactly
-## one place.
-##
-## Transforms applied to libc.P1pp:
-## 1. Drop cc.scm's auto-emitted exec tail —
-## `# entry stub: forwards argc=a0, argv=a1 to main`
-## `%fn(p1_main, 16, { %call(&main) })`. Library TUs must not
-## define :p1_main; the executable TU is the only owner.
-## 2. Drop the trailing `:ELF_end`. ELF.hex2 sizes p_filesz from
-## the first :ELF_end it sees; only the executable TU may emit it.
-## 3. Rename internal-linkage `cc__str_N` (anonymous string literals)
-## to `libc__cc__str_N`. cc.scm restarts that counter at 0 per TU,
-## so libc and the client otherwise duplicate :cc__str_0..N. hex2
-## resolves duplicates first-def-wins and silently mis-binds refs.
-## 4. Pad each `'<hex>'` literal up to an 8-byte boundary. cc.scm
-## emits strings at their natural length; if a string's byte
-## count is not a multiple of 4, every label that follows lands
-## at a non-4-aligned address and aarch64 BLR SIGBUSes.
-##
-## Then we append a fixed :p1_main wrapper that calls __libc_init
-## (lispcc-syscall.c — populates `environ` from argv's NULL terminator)
-## before forwarding to the client's :main.
-##
-## Same `# entry stub` strip is applied to the client TU so its own
-## auto-:p1_main doesn't collide with our wrapper.
-##
-## Usage: boot-libc-prepend.sh <libc.P1pp> <client.P1pp> <out.P1pp>
-
-set -eu
-[ "$#" -eq 3 ] || {
- echo "usage: $0 <libc.P1pp> <client.P1pp> <out.P1pp>" >&2; exit 2
-}
-
-LIBC=$1
-CLIENT=$2
-OUT=$3
-
-[ -r "$LIBC" ] || { echo "missing $LIBC" >&2; exit 1; }
-[ -r "$CLIENT" ] || { echo "missing $CLIENT" >&2; exit 1; }
-
-WORK=$(dirname "$OUT")
-mkdir -p "$WORK"
-
-LIBC_LIB=$WORK/libc.lib.P1pp
-CLIENT_NOM=$WORK/client.no-p1main.P1pp
-
-awk '
- /^# entry stub: forwards argc=a0, argv=a1 to main$/ { skip=1; next }
- skip && /^%fn\(p1_main,/ { next }
- skip && /^%call\(&main\)/ { next }
- skip && /^}\)$/ { skip=0; next }
- /^:ELF_end[ \t]*$/ { next }
- { gsub(/cc__str_/, "libc__cc__str_") }
- /^'\''[0-9A-Fa-f]+'\''$/ {
- print
- s = $0; gsub(/'\''/, "", s)
- bytes = length(s) / 2
- pad = (8 - bytes % 8) % 8
- if (pad > 0) {
- pstr = ""
- for (i = 0; i < pad; i++) pstr = pstr "00"
- printf "'\''%s'\''\n", pstr
- }
- next
- }
- { print }
-' "$LIBC" > "$LIBC_LIB"
-
-printf '%s\n' \
- '%fn(p1_main, 16, {' \
- ' %call(&__libc_init)' \
- ' %call(&main)' \
- '})' >> "$LIBC_LIB"
-
-awk '
- /^# entry stub: forwards argc=a0, argv=a1 to main$/ { skip=1; next }
- skip && /^%fn\(p1_main,/ { next }
- skip && /^%call\(&main\)/ { next }
- skip && /^}\)$/ { skip=0; next }
- { print }
-' "$CLIENT" > "$CLIENT_NOM"
-
-cat "$LIBC_LIB" "$CLIENT_NOM" > "$OUT"
diff --git a/scripts/boot-run-tests.sh b/scripts/boot-run-tests.sh
@@ -144,7 +144,7 @@ run_p1_suite() {
bin=build/$ARCH/p1-tests/$name
log=$bin.build.log
mkdir -p "$(dirname "$bin")"
- if ! sh scripts/boot-build-p1pp.sh "$fixture" "$bin" \
+ if ! sh scripts/boot-build-p1pp.sh "$bin" "$fixture" \
>"$log" 2>&1; then
fail "$label" "" "$log"
continue
@@ -363,7 +363,7 @@ _cc_runtime_suite() {
fi
p1pp_log=$outdir/p1pp.log
- if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" \
+ if ! sh scripts/boot-build-p1pp.sh "$elf" "$p1pp" \
>"$p1pp_log" 2>&1; then
fail "$label" "P1pp assemble failed:" "$p1pp_log"
continue
@@ -414,7 +414,7 @@ run_cc_suite() {
fi
p1pp_log=$outdir/p1pp.log
- if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" \
+ if ! sh scripts/boot-build-p1pp.sh "$elf" "$p1pp" \
>"$p1pp_log" 2>&1; then
fail "$label" "P1pp assemble failed:" "$p1pp_log"
continue
@@ -454,28 +454,29 @@ run_cc_libc_suite() {
fi
outdir=build/$ARCH/cc-libc/$name
client_p1pp=$outdir/$name.client.P1pp
- merged_p1pp=$outdir/$name.P1pp
elf=$outdir/$name
label="[$ARCH] cc-libc/$name"
mkdir -p "$outdir"
+ # Compile the client TU in lib mode so it doesn't emit its
+ # own :p1_main / :ELF_end and namespaces its anonymous string
+ # labels under app__cc__str_N — distinct from libc.P1pp's
+ # libc__cc__str_N.
cc_log=$outdir/cc.log
- if ! build/$ARCH/scheme1 build/$ARCH/cc/cc.scm "$src" "$client_p1pp" \
+ if ! build/$ARCH/scheme1 build/$ARCH/cc/cc.scm \
+ --lib=app__ "$src" "$client_p1pp" \
>"$cc_log" 2>&1; then
fail "$label" "cc compile failed:" "$cc_log"
continue
fi
- prepend_log=$outdir/prepend.log
- if ! sh scripts/boot-libc-prepend.sh \
- build/$ARCH/libc.P1pp "$client_p1pp" "$merged_p1pp" \
- >"$prepend_log" 2>&1; then
- fail "$label" "libc-prepend failed:" "$prepend_log"
- continue
- fi
-
+ # catm chain: entry-libc supplies :p1_main (calls __libc_init
+ # then main), libc.P1pp supplies the libc routines, the client
+ # supplies :main, elf-end supplies the :ELF_end terminator.
p1pp_log=$outdir/p1pp.log
- if ! sh scripts/boot-build-p1pp.sh "$merged_p1pp" "$elf" \
+ if ! sh scripts/boot-build-p1pp.sh "$elf" \
+ P1/entry-libc.P1pp build/$ARCH/libc.P1pp \
+ "$client_p1pp" P1/elf-end.P1pp \
>"$p1pp_log" 2>&1; then
fail "$label" "P1pp assemble failed:" "$p1pp_log"
continue
diff --git a/tests/cc-libc/00-exit.c b/tests/cc-libc/00-exit.c
@@ -1,5 +1,5 @@
/* Sanity: bare main returning a value. Doesn't touch libc — confirms
- * the cc-libc harness (cc.scm + boot-libc-prepend + boot-build-p1pp)
+ * the cc-libc harness (cc.scm --lib=app__ → catm chain → boot-build-p1pp)
* produces a runnable ELF when nothing in libc is exercised. */
int
main (void)