commit 54367ac4c8f926a97caf8a56ea6fef6381975169
parent a703b305fce34cd774525725b4803cf187147954
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 17 Jul 2026 11:05:21 -0700
cc: fix nested char[] string-literal init; collapse tcc fixed point to 2 bounces
cc.scm mis-compiled a char[N] array that is a struct field or array
element initialized by a string literal: it emitted a pointer to the
interned string instead of the string's bytes. The STR->char[] case
lived only in top-level parse-init-global, not in the nested
%global-init-elem / %local-init-elem walkers, so the string decayed to a
pointer via %const-init-piece.
tcc's ArHdr archive-header template (static struct of char[] fields) is
exactly this shape, so tcc0 -ar wrote raw pointers into .a headers and
produced corrupt archives. boot4 worked around this by linking tcc1 from
bare .o files (libc-first) — an asymmetric link line vs. the archive-
based later stages — which forced a third self-host bounce (the chain
converged at tcc2 == tcc3 instead of tcc1 == tcc2).
Fix %global-init-elem and %local-init-elem to encode a string-literal
initializer for a char[] field/element as bytes. With tcc0 -ar working,
boot4 stage C now uses the same archive-based link line as stage D, so
the chain is tcc0 -> tcc1 -> tcc2 and the fixed point is tcc1 == tcc2.
Drop tcc3 / stage E everywhere; rewire boot5/boot6/calibrate to tcc2.
Verified end-to-end on aarch64 (rebuilt boot3 + boot4): tcc1 == tcc2, and
the canonical tcc2 plus crt1.o / libc.a / libtcc1.a / hello are byte-
identical to the prior tcc3 build, so downstream stages are unaffected.
184 cc tests pass; new fixture 340-struct-char-array-string-init fails on
the old compiler and passes on the fixed one.
riscv64 has a separate, unrelated cc.scm bug (immediate-fold miscompile
in riscv64-gen.c) that independently breaks tcc1 == tcc2; it needs
TCC_BOOTSTRAP_RELAX_FIXEDPOINT=1 until fixed (see docs/TCC.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dTQvpe9XBV8fNdqkTGW81
Diffstat:
22 files changed, 229 insertions(+), 127 deletions(-)
diff --git a/Makefile b/Makefile
@@ -222,7 +222,7 @@ build/$1/$2/boot3/.stamp: \
build/$1/$2/boot3/tcc0 build/$1/$2/boot3/libc.P1pp \
build/$1/$2/boot3/tcc.flat.P1pp: build/$1/$2/boot3/.stamp ;
-# boot4: tcc0 -> tcc1 -> tcc2 -> tcc3 self-host chain (+ libc.a, libtcc1.a, hello)
+# boot4: tcc0 -> tcc1 -> tcc2 self-host chain (+ libc.a, libtcc1.a, hello)
build/$1/$2/boot4/.stamp: \
build/$1/$2/boot3/.stamp build/$1/$2/boot2/.stamp \
boot/boot4.sh boot/lib-arch.sh boot/lib-runscm.sh \
@@ -231,7 +231,7 @@ build/$1/$2/boot4/.stamp: \
@touch $$@
build/$1/$2/boot4/tcc1 build/$1/$2/boot4/tcc2 \
-build/$1/$2/boot4/tcc3 build/$1/$2/boot4/hello \
+build/$1/$2/boot4/hello \
build/$1/$2/boot4/crt1.o build/$1/$2/boot4/libc.a \
build/$1/$2/boot4/libtcc1.a: build/$1/$2/boot4/.stamp ;
@@ -248,7 +248,7 @@ build/$1/$2/boot5/libc.a build/$1/$2/boot5/crt1.o \
build/$1/$2/boot5/crti.o build/$1/$2/boot5/crtn.o \
build/$1/$2/boot5/hello: build/$1/$2/boot5/.stamp ;
-# boot6: seed-kernel ELF/Image, built with boot4's tcc3
+# boot6: seed-kernel ELF/Image, built with boot4's tcc2
build/$1/$2/boot6/.stamp: \
build/$1/$2/boot4/.stamp build/$1/$2/boot2/.stamp \
boot/boot6.sh boot/lib-arch.sh boot/lib-runscm.sh \
diff --git a/boot/boot3.sh b/boot/boot3.sh
@@ -1,15 +1,13 @@
#!/bin/sh
## boot3.sh — bootstrap tcc0 from cc.scm.
##
-## Stage A of the four-stage tcc chain: cc.scm compiles tcc.flat.c into
+## Stage A of the tcc self-host chain: cc.scm compiles tcc.flat.c into
## tcc0. boot4 picks up tcc0 and self-hosts the rest of the chain
-## (tcc0 → tcc1 → tcc2 → tcc3, with tcc2 == tcc3 as the fixed-point
-## check).
+## (tcc0 → tcc1 → tcc2, with tcc1 == tcc2 as the fixed-point check).
##
## tcc0 = tcc-source compiled by cc.scm ← produced here
## tcc1 = tcc-source compiled by tcc0 ← boot4
## tcc2 = tcc-source compiled by tcc1 ← boot4
-## tcc3 = tcc-source compiled by tcc2 ← boot4
##
## ─── Inputs (sources, from canonical tree) ───────────────────────────
## build/$ARCH/src/src/scheme1/prelude.scm scheme bundle
diff --git a/boot/boot4.sh b/boot/boot4.sh
@@ -2,18 +2,17 @@
## boot4.sh — self-host tcc rebuild stages on top of boot3's tcc0.
##
## boot3 produced tcc0 (cc.scm-built bootstrap). boot4 runs the rest of
-## the four-stage chain: tcc0 → tcc1 → tcc2 → tcc3. The bootstrap
-## fixed-point check is `tcc2 == tcc3`: once tcc is compiling itself
-## with no help from cc.scm, the chain reaches a byte-identical fixed
-## point. cc.scm and tcc emit different — but both correct — code from
-## the same source, so tcc0 ≠ tcc1. tcc1 and tcc2 are both built by tcc,
-## but tcc1 is itself a cc.scm-shaped binary, so its codegen choices in
-## tcc2 need one more bounce to reach the tcc-shaped fixed point.
+## the three-binary chain: tcc0 → tcc1 → tcc2. The bootstrap fixed-point
+## check is `tcc1 == tcc2`: tcc0 ≠ tcc1 because cc.scm and tcc emit
+## different — but both correct — code from the same source, but once tcc
+## is compiling itself the output is byte-identical. Stages C and D use
+## the same archive-based link line, so tcc0(tcc.c) and tcc1(tcc.c)
+## differ only where tcc0's runtime behavior differs from tcc1's — so the
+## check doubles as a proof that cc.scm compiled tcc.c faithfully.
##
## tcc0 = tcc-source compiled by cc.scm ← boot3
## tcc1 = tcc-source compiled by tcc0 ← produced here
## tcc2 = tcc-source compiled by tcc1 ← produced here
-## tcc3 = tcc-source compiled by tcc2 ← produced here
##
## ─── Inputs (sources, from canonical tree) ───────────────────────────
## build/$ARCH/src/src/tcc/libc/$ARCH/{start.S, sys_stubs.S}
@@ -37,25 +36,27 @@
## bootprep/stage1-flatten.sh, around the arm64-asm patch block).
##
## ─── Outputs ──────────────────────────────────────────────────────────
-## build/$ARCH/$DRIVER/boot4/{tcc1, tcc2, tcc3}
-## tcc2 and tcc3 are byte-identical (asserted
+## build/$ARCH/$DRIVER/boot4/{tcc1, tcc2}
+## tcc1 and tcc2 are byte-identical (asserted
## below) — that equality is the fixed-point.
## build/$ARCH/$DRIVER/boot4/crt1.o
-## tcc2-built startup object, kept outside
+## tcc1-built startup object, kept outside
## libc.a because it must lead link lines.
## build/$ARCH/$DRIVER/boot4/libc.a
-## tcc2-built archive of sys_stubs.o + mem.o
+## tcc1-built archive of sys_stubs.o + mem.o
## + libc.o
## build/$ARCH/$DRIVER/boot4/libtcc1.a
-## tcc2-built tcc compiler helper archive
+## tcc1-built tcc compiler helper archive
## build/$ARCH/$DRIVER/boot4/hello — mes-libc-linked smoke binary
##
## ─── Env knobs ────────────────────────────────────────────────────────
## TCC_BOOTSTRAP_RELAX_FIXEDPOINT=1
-## After a codegen-altering tcc patch, the two-stage rule needs a
-## third bounce to converge. Set this to accept tcc3 even when
-## tcc2 != tcc3; the next boot4 run, started from this run's
-## tcc3, will reach tcc2 == tcc3 with no extra knob.
+## After a codegen-altering tcc patch, tcc0 (still built by the old
+## cc.scm) may lag tcc1 by a bounce. Set this to accept tcc2 even
+## when tcc1 != tcc2; the next boot4 run, started from this run's
+## tcc2, will reach tcc1 == tcc2 with no extra knob. riscv64 also
+## needs this today: cc.scm miscompiles an immediate-fold predicate
+## in riscv64-gen.c, so tcc0 ≠ tcc1 there (see docs/TCC.md).
##
## Usage: boot/boot4.sh <arch>
## <arch> ∈ {aarch64, amd64, riscv64} for either DRIVER (default podman).
@@ -109,28 +110,28 @@ runscm_input_from_src tcc/tcc.flat.c
runscm_input_from_src libc/libc.flat.c
runscm_input_from_src test-fixtures/boot-hello.c hello.c
-runscm_export tcc1 tcc2 tcc3 s3-crt1.o s3-libc.a s3-libtcc1.a hello
+runscm_export tcc1 tcc2 s2-crt1.o s2-libc.a s2-libtcc1.a hello
runscm_run "${BOOT4_TIMEOUT:-5400}"
# ── fixed-point check (host-side) ─────────────────────────────────────
-if ! cmp -s "$OUT/tcc2" "$OUT/tcc3"; then
+if ! cmp -s "$OUT/tcc1" "$OUT/tcc2"; then
+ s1=$(wc -c <"$OUT/tcc1")
s2=$(wc -c <"$OUT/tcc2")
- s3=$(wc -c <"$OUT/tcc3")
if [ "${TCC_BOOTSTRAP_RELAX_FIXEDPOINT:-0}" = 1 ]; then
- echo "[$BOOT_TAG] WARN: tcc2 ($s2) != tcc3 ($s3); TCC_BOOTSTRAP_RELAX_FIXEDPOINT=1, accepting tcc3" >&2
+ echo "[$BOOT_TAG] WARN: tcc1 ($s1) != tcc2 ($s2); TCC_BOOTSTRAP_RELAX_FIXEDPOINT=1, accepting tcc2" >&2
else
- echo "[$BOOT_TAG] FIXED-POINT FAIL: tcc2 ($s2) != tcc3 ($s3)" >&2
+ echo "[$BOOT_TAG] FIXED-POINT FAIL: tcc1 ($s1) != tcc2 ($s2)" >&2
exit 1
fi
fi
-# ── normalize output names (drop s3- prefix) ──────────────────────────
+# ── normalize output names (drop s2- prefix) ──────────────────────────
# tcc1 / tcc2 are kept on disk: the test path (tcc-cc / tcc-libc suites)
# uses them as stage-2 / stage-3 self-built tcc binaries.
-mv "$OUT/s3-crt1.o" "$OUT/crt1.o"
-mv "$OUT/s3-libc.a" "$OUT/libc.a"
-mv "$OUT/s3-libtcc1.a" "$OUT/libtcc1.a"
-chmod 0700 "$OUT/tcc1" "$OUT/tcc2" "$OUT/tcc3" "$OUT/hello"
+mv "$OUT/s2-crt1.o" "$OUT/crt1.o"
+mv "$OUT/s2-libc.a" "$OUT/libc.a"
+mv "$OUT/s2-libtcc1.a" "$OUT/libtcc1.a"
+chmod 0700 "$OUT/tcc1" "$OUT/tcc2" "$OUT/hello"
echo "[$BOOT_TAG] sizes: libtcc1.a=$(wc -c <"$OUT/libtcc1.a") libc.a=$(wc -c <"$OUT/libc.a") hello=$(wc -c <"$OUT/hello")"
-echo "[$BOOT_TAG] OK -> $OUT/{tcc3, crt1.o, libc.a, libtcc1.a, hello} (fixed point: tcc2 == tcc3)"
+echo "[$BOOT_TAG] OK -> $OUT/{tcc2, crt1.o, libc.a, libtcc1.a, hello} (fixed point: tcc1 == tcc2)"
diff --git a/boot/boot5.sh b/boot/boot5.sh
@@ -1,14 +1,14 @@
#!/bin/sh
## boot5.sh — build musl-1.2.5 with boot4 artifacts and link hello.
##
-## Builds on top of boot4's verified-fixed-point tcc (tcc2 == tcc3) and
+## Builds on top of boot4's verified-fixed-point tcc (tcc1 == tcc2) and
## demonstrates that the same compiler can produce a working static libc
## from upstream musl source — patched only as far as needed to work
## around tcc's missing GCC extensions (register-asm-variable syscalls,
## attribute(alias) weak refs, _Complex, x86_64 SSE/x87 inline asm).
##
## ─── Inputs ──────────────────────────────────────────────────────────
-## build/$ARCH/$DRIVER/boot4/tcc3 — boot4's verified self-host tcc
+## build/$ARCH/$DRIVER/boot4/tcc2 — boot4's verified self-host tcc
## build/$ARCH/$DRIVER/boot4/libtcc1.a — boot4's tcc runtime archive
## build/$ARCH/$DRIVER/boot2/{catm, scheme1}
## build/$ARCH/src/src/musl/ — canonical musl tree (overrides
@@ -45,7 +45,7 @@ SRC=build/$ARCH/src
MUSL_DIR=$SRC/src/musl
# ── prerequisites ─────────────────────────────────────────────────────
-require_prev "$BOOT4" tcc3
+require_prev "$BOOT4" tcc2
require_prev "$BOOT2" catm scheme1
require_file "$BOOT4/libtcc1.a" "run boot/boot4.sh $ARCH"
require_file "$MUSL_DIR" "run bootprep/prep-src.sh $ARCH"
@@ -79,7 +79,7 @@ runscm_scheme1 "$BOOT2/scheme1"
runscm_prelude "$SRC/src/scheme1/prelude.scm"
# Chain binaries staged at flat in/ root (cwd-relative names in run.scm).
-runscm_input tcc "$BOOT4/tcc3"
+runscm_input tcc "$BOOT4/tcc2"
runscm_input libtcc1.a "$BOOT4/libtcc1.a"
runscm_input catm "$BOOT2/catm"
runscm_input_from_src tcc/stdarg-bridge.h tcc-stdarg-bridge.h
diff --git a/boot/boot6.sh b/boot/boot6.sh
@@ -1,7 +1,7 @@
#!/bin/sh
-## boot6.sh — build the seed-kernel ELF/Image with boot4's tcc3.
+## boot6.sh — build the seed-kernel ELF/Image with boot4's tcc2.
##
-## Drives tcc3 to compile + link the seed kernel directly: no `ld -T
+## Drives tcc2 to compile + link the seed kernel directly: no `ld -T
## kernel.lds`, no objcopy. aarch64 emits the flat Image QEMU expects;
## amd64/riscv64 emit the ELF consumed by QEMU's -kernel path.
##
@@ -11,7 +11,7 @@
## build/$ARCH/src/src/tcc/cc/mem.c
##
## ─── Inputs (binaries from prior stages) ──────────────────────────────
-## build/$ARCH/$DRIVER/boot4/tcc3
+## build/$ARCH/$DRIVER/boot4/tcc2
## build/$ARCH/$DRIVER/boot2/scheme1
##
## ─── Tools ────────────────────────────────────────────────────────────
@@ -43,7 +43,7 @@ BOOT4=build/$ARCH/$DRIVER/boot4
SRC=build/$ARCH/src
# ── prerequisites ─────────────────────────────────────────────────────
-require_prev "$BOOT4" tcc3
+require_prev "$BOOT4" tcc2
require_prev "$BOOT2" scheme1
for f in kernel/arch/$ARCH/kernel.S kernel/arch/$ARCH/mmu.c kernel/arch/$ARCH/arch.h kernel/kernel.c tcc/cc/mem.c; do
require_file "$SRC/src/$f"
@@ -57,7 +57,7 @@ runscm_runscm "$SRC/run/boot6.scm"
runscm_scheme1 "$BOOT2/scheme1"
runscm_prelude "$SRC/src/scheme1/prelude.scm"
-runscm_input tcc3 "$BOOT4/tcc3"
+runscm_input tcc "$BOOT4/tcc2"
runscm_input_from_src "kernel/arch/$ARCH/kernel.S"
runscm_input_from_src kernel/kernel.c
runscm_input_from_src "kernel/arch/$ARCH/arch.h"
diff --git a/boot/lib-arch.sh b/boot/lib-arch.sh
@@ -124,7 +124,7 @@ driver_init() {
;;
seed)
# DRIVER=seed always consumes the podman-built boot6 kernel —
- # tcc3 is platform-agnostic but we settled on a single canonical
+ # tcc2 is platform-agnostic but we settled on a single canonical
# build location to reduce surface area.
KERNEL_IMAGE=$ROOT/build/$ARCH/podman/boot6/$KERNEL_NAME
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
diff --git a/bootprep/boot4-gen-runscm.sh b/bootprep/boot4-gen-runscm.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-## boot4-gen-runscm.sh — emit run.scm driving boot4's tcc0→tcc1→tcc2→tcc3
+## boot4-gen-runscm.sh — emit run.scm driving boot4's tcc0→tcc1→tcc2
## chain inside the seed kernel. Mirrors boot/boot4.sh's per-stage shell
## emission; per-arch values resolved on the host so the .scm body is
## straight-line (run …) calls.
@@ -61,7 +61,7 @@ EOF
}
# emit_archive — uses prefix to namespace output object names per stage.
-# pfx="s2-"/"s3-" for stage2/3. The .o objects archived into libtcc1.a
+# pfx="s1-"/"s2-" for stage C/D. The .o objects archived into libtcc1.a
# keep their bare basenames (lib-arm64.o, …) — tcc -ar stores basenames
# only, so this matches podman's archive members exactly. They overwrite
# the stage's helper-named .o files; nothing post-archive in the same
@@ -91,11 +91,15 @@ emit_link_tcc() {
{
cat <<'PROLOGUE'
-;; boot4 run.scm — drive tcc0 -> tcc1 -> tcc2 -> tcc3 inside seed kernel.
+;; boot4 run.scm — drive tcc0 -> tcc1 -> tcc2 inside seed kernel.
;; Generated by bootprep/boot4-gen-runscm.sh; mirrors boot/boot4.sh's
;; podman path stage-for-stage. Reads use in/; writes (intermediates and
-;; exports) use out/. tcc0 is staged as in/tcc0; tcc1/tcc2/tcc3 are
-;; produced and exported under out/.
+;; exports) use out/. tcc0 is staged as in/tcc0; tcc1/tcc2 are produced
+;; and exported under out/.
+;;
+;; Stages C and D use the SAME archive-based link line, so tcc0's output
+;; (tcc1) is directly comparable to tcc1's output (tcc2): once tcc0 is a
+;; faithful compilation of tcc.c, tcc1 == tcc2 is the fixed point.
(define (must r tag)
(if (and (car r) (= 0 (cdr r)))
@@ -106,39 +110,31 @@ cat <<'PROLOGUE'
(write-string stderr "\n")
(exit 1))))
-(write-string stdout "boot4: stage B (tcc0 helpers)\n")
+(write-string stdout "boot4: stage C (tcc0 -> tcc1)\n")
PROLOGUE
-# Stage B: tcc0 builds helper objects (no archive).
+# Stage C: tcc0 builds helpers + archive, links tcc1 — same link line as
+# stage D so tcc1 is directly comparable to tcc2.
emit_helpers in/tcc0 tcc0
+emit_archive in/tcc0 tcc0 "s1-"
+emit_link_tcc in/tcc0 tcc0 "s1-" tcc1
cat <<EOF
-(write-string stdout "boot4: stage C (tcc0 -> tcc1)\n")
-(must (run "in/tcc0" "-nostdlib" $LINK_TTEXT "out/start.o" "out/sys_stubs.o" "out/mem.o" "out/libc.o" "out/$LIB_HELPER_OBJ" "in/tcc.flat.c" "-o" "out/tcc1") "tcc0 -> tcc1")
-
(write-string stdout "boot4: stage D (tcc1 -> tcc2)\n")
EOF
-# Stage D: tcc1 rebuilds helpers + archive, links tcc2.
+# Stage D: tcc1 rebuilds helpers + archive, links tcc2. tcc1 == tcc2 is
+# the bootstrap fixed point (asserted host-side in boot4.sh). The s2-
+# archives + crt1.o built here are boot4's exported deliverables.
emit_helpers out/tcc1 tcc1
emit_archive out/tcc1 tcc1 "s2-"
emit_link_tcc out/tcc1 tcc1 "s2-" tcc2
cat <<EOF
-(write-string stdout "boot4: stage E (tcc2 -> tcc3)\n")
-EOF
-
-# Stage E: tcc2 rebuilds helpers + archive, links tcc3.
-emit_helpers out/tcc2 tcc2
-emit_archive out/tcc2 tcc2 "s3-"
-emit_link_tcc out/tcc2 tcc2 "s3-" tcc3
-
-cat <<EOF
-
(write-string stdout "boot4: linking hello\n")
-(must (run "out/tcc2" "-nostdlib" $LINK_TTEXT "out/s3-crt1.o" "in/hello.c" "out/s3-libc.a" "out/s3-libtcc1.a" "out/s3-libc.a" "-o" "out/hello") "tcc2 -> hello")
+(must (run "out/tcc1" "-nostdlib" $LINK_TTEXT "out/s2-crt1.o" "in/hello.c" "out/s2-libc.a" "out/s2-libtcc1.a" "out/s2-libc.a" "-o" "out/hello") "tcc1 -> hello")
(write-string stdout "boot4: ALL-OK\n")
(exit 0)
EOF
diff --git a/bootprep/boot5-calibrate.sh b/bootprep/boot5-calibrate.sh
@@ -8,7 +8,7 @@
## the vendored musl artifacts.
##
## What it does:
-## 1. Stage the same prerequisites boot5.sh stages (boot4/tcc3,
+## 1. Stage the same prerequisites boot5.sh stages (boot4/tcc2,
## libtcc1.a, vendored overrides + deletes, pre-generated headers,
## stdarg bridge).
## 2. Run a skip-on-fail compile loop in the container over every
@@ -48,7 +48,7 @@ MUSL_GENERATED=vendor/musl/generated/$MUSL_ARCH
BRIDGE_FILE=build/$ARCH/vendor/tcc/stdarg-bridge.h
SKIP_OUT=vendor/musl/skip-$ARCH.txt
-[ -x "$BOOT4/tcc3" ] || { echo "missing $BOOT4/tcc3 (run boot/boot4.sh $ARCH)" >&2; exit 1; }
+[ -x "$BOOT4/tcc2" ] || { echo "missing $BOOT4/tcc2 (run boot/boot4.sh $ARCH)" >&2; exit 1; }
[ -e "$BOOT4/libtcc1.a" ] || { echo "missing $BOOT4/libtcc1.a" >&2; exit 1; }
[ -e "$MUSL_TARBALL" ] || { echo "missing $MUSL_TARBALL" >&2; exit 1; }
[ -d "$MUSL_OVERRIDES" ] || { echo "missing $MUSL_OVERRIDES" >&2; exit 1; }
@@ -64,7 +64,7 @@ fi
rm -rf "$STAGE"
mkdir -p "$STAGE/in" "$STAGE/out"
-cp "$BOOT4/tcc3" "$STAGE/in/tcc"
+cp "$BOOT4/tcc2" "$STAGE/in/tcc"
cp "$BOOT4/libtcc1.a" "$STAGE/in/libtcc1.a"
tar xzf "$MUSL_TARBALL" -C "$STAGE/in/"
MUSL_DIR=$STAGE/in/musl-1.2.5
diff --git a/bootprep/boot6-gen-runscm.sh b/bootprep/boot6-gen-runscm.sh
@@ -1,8 +1,8 @@
#!/bin/sh
-## boot6-gen-runscm.sh — emit run.scm driving tcc3 to build seed-kernel.
+## boot6-gen-runscm.sh — emit run.scm driving tcc2 to build seed-kernel.
##
-## tcc3 -c each translation unit (kernel.S, kernel.c, mem.c) → .o, then
-## tcc3 links + emits a flat arm64 boot Image directly (no `ld -T
+## tcc2 -c each translation unit (kernel.S, kernel.c, mem.c) → .o, then
+## tcc2 links + emits a flat arm64 boot Image directly (no `ld -T
## kernel.lds`, no objcopy). The link line is three tcc flags:
##
## -nostdlib -static freestanding link, no startfiles
@@ -20,7 +20,7 @@
## `.text`.
##
## Everything else the kernel needs from the linker is supplied by
-## conventions tcc3 already honors:
+## conventions tcc2 already honors:
##
## _start hard-coded entry symbol; kernel.S's arm64 Image
## header is named `_start` (not `_head`) so this works
@@ -62,7 +62,7 @@ esac
KCFLAGS='"-nostdlib" "-ffreestanding" "-static"'
cat > "$OUT" <<EOF
-;; boot6 run.scm — build seed-kernel ELF with tcc3.
+;; boot6 run.scm — build seed-kernel ELF with tcc2.
;; Generated by bootprep/boot6-gen-runscm.sh; reads use in/, writes out/.
(define (must r tag)
@@ -74,24 +74,24 @@ cat > "$OUT" <<EOF
(write-string stderr "\n")
(exit 1))))
-(write-string stdout "boot6: tcc3 -c kernel.S\n")
-(must (run "in/tcc3" $KCFLAGS "-c" "-o" "out/kernel-asm.o" "in/kernel.S")
+(write-string stdout "boot6: tcc2 -c kernel.S\n")
+(must (run "in/tcc" $KCFLAGS "-c" "-o" "out/kernel-asm.o" "in/kernel.S")
"kernel.S -> kernel-asm.o")
-(write-string stdout "boot6: tcc3 -c kernel.c\n")
-(must (run "in/tcc3" $KCFLAGS "-Iin" "-c" "-o" "out/kernel.o" "in/kernel.c")
+(write-string stdout "boot6: tcc2 -c kernel.c\n")
+(must (run "in/tcc" $KCFLAGS "-Iin" "-c" "-o" "out/kernel.o" "in/kernel.c")
"kernel.c -> kernel.o")
-(write-string stdout "boot6: tcc3 -c mmu.c\n")
-(must (run "in/tcc3" $KCFLAGS "-Iin" "-c" "-o" "out/mmu.o" "in/mmu.c")
+(write-string stdout "boot6: tcc2 -c mmu.c\n")
+(must (run "in/tcc" $KCFLAGS "-Iin" "-c" "-o" "out/mmu.o" "in/mmu.c")
"mmu.c -> mmu.o")
-(write-string stdout "boot6: tcc3 -c mem.c\n")
-(must (run "in/tcc3" $KCFLAGS "-c" "-o" "out/mem.o" "in/mem.c")
+(write-string stdout "boot6: tcc2 -c mem.c\n")
+(must (run "in/tcc" $KCFLAGS "-c" "-o" "out/mem.o" "in/mem.c")
"mem.c -> mem.o")
-(write-string stdout "boot6: tcc3 link $OUT_FILE\n")
-(must (run "in/tcc3" "-nostdlib" "-static"
+(write-string stdout "boot6: tcc2 link $OUT_FILE\n")
+(must (run "in/tcc" "-nostdlib" "-static"
"-Wl,-Ttext=$TTEXT"
$LINK_OFORMAT
"-o" "out/$OUT_FILE"
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -5672,6 +5672,32 @@
((equal? (car (car fields)) fname) (cdr fields))
(else (%init-drop-thru-field (cdr fields) fname))))
+;; #t when TY is an array of i8/u8 — a char[] a string literal may
+;; initialize directly (C11 §6.7.9 ¶14).
+(define (%char-arr-type? t)
+ (and (eq? (ctype-kind t) 'arr)
+ (let ((et (car (ctype-ext t))))
+ (or (eq? et %t-i8) (eq? et %t-u8)))))
+
+;; A char[] field/element written as `= "..."`: consume the STR token
+;; and return a byte-vector piece of the array's declared size, copying
+;; the string bytes (truncating an over-long string, zero-padding a
+;; short one). The nested-aggregate analogue of parse-init-global's
+;; top-level STR arm; `t` always has a declared length here (only a
+;; top-level array may infer its length from the string).
+(define (%str-arr-piece ps t)
+ (let* ((s (tok-value (peek ps)))
+ (slen (bytevector-length s))
+ (final (ctype-size t))
+ (bv (make-bytevector final 0)))
+ (advance ps)
+ (let loop ((i 0))
+ (cond
+ ((or (= i slen) (>= i final)) bv)
+ (else
+ (bytevector-u8-set! bv i (bytevector-u8-ref s i))
+ (loop (+ i 1)))))))
+
;; Element/field dispatch for global aggregate initializers. ELIDE? = #f
;; means caller has just consumed `{` for this element and we own the
;; matching `}`; ELIDE? = #t is C99 §6.7.8 ¶22 brace elision (the
@@ -5680,6 +5706,20 @@
(define (%global-init-elem ps t elide?)
(let ((k (ctype-kind t)))
(cond
+ ;; char[] initialized by a string literal (`{"..."}` or, under
+ ;; brace elision, a bare `"..."`). Without this, the `arr` arm
+ ;; below would treat the string as an element-list and encode it
+ ;; as a pointer to the string pool instead of the bytes.
+ ((and (eq? k 'arr)
+ (eq? (tok-kind (peek ps)) 'STR)
+ (%char-arr-type? t))
+ (let ((p (%str-arr-piece ps t)))
+ (cond
+ (elide? (list p))
+ (else
+ (cond ((at-punct? ps 'comma) (advance ps)))
+ (expect-punct ps 'rbrace)
+ (list p)))))
((eq? k 'arr)
(let-values (((p _c) (cond
(elide? (%parse-init-array-list/mode ps t #f))
@@ -5705,6 +5745,31 @@
(define (%local-init-elem ps sm eoff t elide?)
(let ((k (ctype-kind t)))
(cond
+ ;; char[] field/element initialized by a string literal — emit the
+ ;; bytes into the frame slot. Mirrors %global-init-elem's STR arm;
+ ;; without it the `arr` arm would parse the string as a scalar
+ ;; expression and store one byte of its decayed pointer.
+ ((and (eq? k 'arr)
+ (eq? (tok-kind (peek ps)) 'STR)
+ (%char-arr-type? t))
+ (let* ((s (tok-value (peek ps)))
+ (slen (bytevector-length s))
+ (final (ctype-size t)))
+ (advance ps)
+ (let loop ((i 0))
+ (cond
+ ((>= i final) 0)
+ (else
+ (let ((b (cond ((< i slen) (bytevector-u8-ref s i)) (else 0))))
+ (%push-frame-elem-lval ps (+ eoff i) %t-u8)
+ (cg-push-imm (ps-cg ps) %t-u8 b)
+ (cg-assign (ps-cg ps)) (cg-pop (ps-cg ps))
+ (loop (+ i 1))))))
+ (cond
+ (elide? 0)
+ (else
+ (cond ((at-punct? ps 'comma) (advance ps)))
+ (expect-punct ps 'rbrace)))))
((eq? k 'arr)
(cond
(elide? (%parse-init-local-array-list/mode ps sm eoff t #f))
diff --git a/docs/MUSL.md b/docs/MUSL.md
@@ -29,7 +29,7 @@ boot/boot5.sh <amd64|aarch64|riscv64>
| Path | Purpose |
|------|---------|
-| `build/$ARCH/boot4/tcc3` | fixed-point self-host tcc from boot4 |
+| `build/$ARCH/boot4/tcc2` | fixed-point self-host tcc from boot4 |
| `build/$ARCH/boot4/libtcc1.a` | tcc runtime archive produced by boot4 |
| `vendor/musl/1.2.5.tar.gz` | pristine upstream musl source |
| `vendor/musl/overrides/` | post-patch files vendored as a tree (replaces the old patch + `patch` binary) |
@@ -69,7 +69,7 @@ The entire `.boot5-stage` tree is disposable; every `boot5.sh` run rebuilds it.
## Pipeline
-1. **Stage inputs (host)**. Copy boot4 `tcc3` and `libtcc1.a` to `in/`.
+1. **Stage inputs (host)**. Copy boot4 `tcc2` and `libtcc1.a` to `in/`.
Extract the musl tarball into `in/musl-1.2.5/`. Overlay the vendored
`musl-1.2.5-overrides/` tree on top of it. Remove every path listed
in `musl-1.2.5-deletes.txt`. The result is the post-patch tree that
diff --git a/docs/TCC.md b/docs/TCC.md
@@ -204,22 +204,23 @@ For now: known limitation, document, move on. The scalar codegen
elsewhere on riscv64 is fine — only u32 narrowing of a wider source
trips it.
-### riscv64: tcc0 → tcc1 is not a fixed point (cc.scm behavioral bug)
+### riscv64: tcc1 ≠ tcc2 (cc.scm behavioral bug)
-`boot3.sh` + `boot4.sh` produce four staged compilers:
+`boot3.sh` + `boot4.sh` produce three staged compilers:
- `tcc0` = tcc-source compiled by cc.scm (boot3 output)
- `tcc1` = tcc-source compiled by tcc0 (boot4)
- `tcc2` = tcc-source compiled by tcc1 (boot4)
-- `tcc3` = tcc-source compiled by tcc2 (boot4)
-The fixed-point check is **`tcc2 == tcc3`** (asserted at the end of
-`boot4.sh`, verified on aarch64, amd64, riscv64). On riscv64 the
-weaker `tcc1 == tcc2` does *not* hold: `tcc0(tcc.flat.c)` produces
-a 616100-byte `.o` while `tcc1(tcc.flat.c)` and `tcc2(tcc.flat.c)`
-produce a byte-identical 615892-byte `.o` — 208 bytes larger from
-tcc0 (200 in `.text` + 8 ripple in symtab/reloc offsets). amd64 and
-aarch64 satisfy `tcc1 == tcc2`; only riscv64 diverges.
+The fixed-point check is **`tcc1 == tcc2`** (asserted at the end of
+`boot4.sh`). It holds on aarch64 and amd64: `tcc0(tcc.flat.c)` and
+`tcc1(tcc.flat.c)` are byte-identical, so the two stages' identical
+archive-based link lines yield identical binaries. On riscv64 it does
+*not* hold: `tcc0(tcc.flat.c)` produces a 616100-byte `.o` while
+`tcc1(tcc.flat.c)` and `tcc2(tcc.flat.c)` produce a byte-identical
+615892-byte `.o` — 208 bytes larger from tcc0 (200 in `.text` + 8
+ripple in symtab/reloc offsets). Until this is fixed, riscv64 boot4
+needs `TCC_BOOTSTRAP_RELAX_FIXEDPOINT=1`.
This is a **bug to investigate**, not just a "fatter code"
observation. cc.scm should be a *faithful* (semantics-preserving)
@@ -315,4 +316,4 @@ patches in `tccelf.c`:
After AT.2 the post-link `elf-pvh-note.c` tool, the amd64-only
branch in `boot6.sh`, and the amd64 fixup block in
`boot6-gen-runscm.sh` are all gone. The amd64 kernel is emitted
-ready-to-boot by tcc3.
+ready-to-boot by tcc2.
diff --git a/docs/TOUR.md b/docs/TOUR.md
@@ -21,7 +21,7 @@ prior stages produced, plus source from the canonical
| 1 | [boot1.sh](../boot/boot1.sh) | `M1pp`, `hex2pp` | first programs in the portable P1 pseudo-ISA |
| 2 | [boot2.sh](../boot/boot2.sh) | `catm` (rebuilt), `scheme1` | seed `catm` retired; Scheme interpreter arrives |
| 3 | [boot3.sh](../boot/boot3.sh) | `tcc0` | C arrives — `cc.scm` (in scheme1) compiles upstream tcc |
-| 4 | [boot4.sh](../boot/boot4.sh) | `tcc1`, `tcc2`, `tcc3`, `libc.a`, `libtcc1.a` | tcc self-host, byte-identical fixed point `tcc2 == tcc3`, minimal libc |
+| 4 | [boot4.sh](../boot/boot4.sh) | `tcc1`, `tcc2`, `libc.a`, `libtcc1.a` | tcc self-host, byte-identical fixed point `tcc1 == tcc2`, minimal libc |
| 5 | [boot5.sh](../boot/boot5.sh) | `libc.a`, `crt{1,i,n}.o` | musl-1.2.5 built by the self-hosted tcc |
| 6 | [boot6.sh](../boot/boot6.sh) | `Image` (aarch64) / `kernel.elf` | a minimal kernel that can host the chain (`DRIVER=seed`) |
@@ -194,28 +194,27 @@ smallest end-to-end exercise of the whole pipeline.
**You arrive with**: `tcc0` from boot3.
-**boot4 builds**: `tcc1`, `tcc2`, `tcc3`, plus `crt1.o`,
+**boot4 builds**: `tcc1`, `tcc2`, plus `crt1.o`,
`libc.a` (mes-libc), and `libtcc1.a`.
-**How**: tcc compiles tcc, three more times.
+**How**: tcc compiles tcc, twice more.
```
tcc0 = tcc-source compiled by cc.scm ← boot3
tcc1 = tcc-source compiled by tcc0 ← here
tcc2 = tcc-source compiled by tcc1 ← here
-tcc3 = tcc-source compiled by tcc2 ← here
```
-After the third bounce, the script asserts `tcc2 == tcc3` byte for
+After the second bounce, the script asserts `tcc1 == tcc2` byte for
byte. That's the fixed point: tcc compiling itself with no help from
cc.scm reaches a stable image, and any future build that walks through
-this stage will produce the same `tcc3` from the same sources.
+this stage will produce the same `tcc2` from the same sources.
-Why four stages, not two? `cc.scm` and tcc's own codegen produce
+Why three stages, not two? `cc.scm` and tcc's own codegen produce
different — but both correct — code for the same source, so `tcc0`
-and `tcc1` differ. `tcc1` and `tcc2` are both built by tcc, but `tcc1`
-itself was a cc.scm-shaped binary, so its codegen choices in `tcc2`
-need one more bounce to reach the tcc-shaped fixed point.
+and `tcc1` differ. But both stages compile+link tcc the same way, so
+`tcc1 == tcc2` holds as soon as `tcc0` is a faithful compilation of
+tcc.c — the check thus also proves cc.scm compiled tcc.c correctly.
**Trust extension**: a self-built tcc that demonstrates determinism
under self-application. Plus mes-libc, a small libc that's good
@@ -223,12 +222,12 @@ enough for tcc's own runtime, and `libtcc1.a` — tcc's helper archive
(division/intrinsics that tcc emits calls to).
**Worth reading**: the fixed-point check in
-[boot4.sh:114–123](../boot/boot4.sh) is the audit point of the entire
+[boot4.sh:115–124](../boot/boot4.sh) is the audit point of the entire
chain.
## §6. boot5 — a real libc
-**You arrive with**: `tcc3` and `libtcc1.a` from boot4; `catm` and
+**You arrive with**: `tcc2` and `libtcc1.a` from boot4; `catm` and
`scheme1` from boot2.
**boot5 builds**: `libc.a` and `crt{1,i,n}.o` from upstream
@@ -245,7 +244,7 @@ static archive.
**Trust extension**: a real libc. The chain no longer depends on
mes-libc for anything but the seed-kernel's `hello` link path; every
-later binary you build with this `tcc3` can use musl.
+later binary you build with this `tcc2` can use musl.
**Worth reading**: the musl skip / patch lists under
[vendor/musl/](../vendor/musl/) and the calibration script
@@ -254,12 +253,12 @@ Background: [docs/MUSL.md](MUSL.md), [docs/LIBC.md](LIBC.md).
## §7. boot6 — a kernel that runs the chain
-**You arrive with**: `tcc3` from boot4 and `scheme1` from boot2.
+**You arrive with**: `tcc2` from boot4 and `scheme1` from boot2.
**boot6 builds**: a minimal kernel image — `Image` on aarch64, `kernel.elf`
on amd64 / riscv64.
-**How**: tcc3 compiles `seed-kernel/kernel.c` plus the per-arch entry
+**How**: tcc2 compiles `seed-kernel/kernel.c` plus the per-arch entry
in `seed-kernel/arch/<arch>/{kernel.S, mmu.c, arch.h}` and `tcc/cc/mem.c`,
linking through tcc directly — no separate linker, no objcopy. On
aarch64, tcc's flat-binary mode produces an arm64 `Image`.
diff --git a/seed-kernel/arch/aarch64/kernel.S b/seed-kernel/arch/aarch64/kernel.S
@@ -16,8 +16,8 @@ _start:
* section ordering required.
*
* Entry symbol is `_start` (not `_head`) so a script-less linker
- * — tcc3 in particular — picks us up via its hard-coded default.
- * Image-size end marker is `_end`, which both gnu ld and tcc3
+ * — tcc2 in particular — picks us up via its hard-coded default.
+ * Image-size end marker is `_end`, which both gnu ld and tcc2
* auto-define at the end of `.bss`. */
b stext
.long 0
@@ -44,7 +44,7 @@ stext:
* SPSR=EL1h with DAIF masked, ELR=in_el1, eret.
*
* Address loads use `ldr Xn, =sym` rather than `adrp/add :lo12:` so this
- * file assembles under tcc3 (the MOVW_UABS_G{0..3} reloc chain that
+ * file assembles under tcc2 (the MOVW_UABS_G{0..3} reloc chain that
* `ldr =sym` lowers to is the same chain `arm64-gen.c` uses for every
* compiler-emitted symbol load). */
mov x9, #(1 << 31)
@@ -80,7 +80,7 @@ in_el1:
isb
/* Zero BSS. `__bss_start` and `_end` are auto-defined by both
- * gnu ld (via the kernel.lds bracket) and tcc3 (via the
+ * gnu ld (via the kernel.lds bracket) and tcc2 (via the
* `bss-start-symbol` simple-patch + tcc's existing `_end` def). */
ldr x1, =__bss_start
ldr x2, =_end
diff --git a/seed-kernel/arch/aarch64/kernel.lds b/seed-kernel/arch/aarch64/kernel.lds
@@ -15,7 +15,7 @@ SECTIONS {
* at its top), and it is linked first, so its contributions land at
* file offset 0 — exactly where QEMU's arm64 boot protocol expects
* the `ARM\x64` magic at byte 0x38. No `.head.text` indirection
- * needed; a script-less linker (tcc3) reaches the same layout via
+ * needed; a script-less linker (tcc2) reaches the same layout via
* its default input-order section concatenation. */
.text : ALIGN(8) {
*(.text .text.*)
@@ -33,7 +33,7 @@ SECTIONS {
* plain `.bss` content ending at `kstack_top`. kernel.S references
* `__bss_start` and `_end` to bracket the bss-zero loop; both names
* are de-facto linker conventions (gnu ld defines them via this
- * script, and tcc3 synthesizes them automatically via the
+ * script, and tcc2 synthesizes them automatically via the
* bss-start-symbol simple-patch + its built-in `_end` def). */
.bss : ALIGN(16) {
__bss_start = .;
diff --git a/seed-kernel/arch/amd64/kernel.S b/seed-kernel/arch/amd64/kernel.S
@@ -72,7 +72,7 @@ long_mode:
movw %ax, %ss
movq $kstack_top, %rsp
- /* Enable SSE/SSE2 for user mode. tcc-emitted user binaries (tcc3,
+ /* Enable SSE/SSE2 for user mode. tcc-emitted user binaries (tcc2,
* scheme1) save callee-saved xmm regs in every function prologue,
* so the first user `movq %xmm7, ...` would otherwise trap with
* #UD or #NM. CR0.MP=1, EM=0; CR4.OSFXSR=1, OSXMMEXCPT=1. The
diff --git a/tests/cc/340-struct-char-array-string-init.c b/tests/cc/340-struct-char-array-string-init.c
@@ -0,0 +1,42 @@
+/* tests/cc/340-struct-char-array-string-init.c
+ *
+ * Regression: a char[N] array that is a *struct field* or *array
+ * element* initialized by a string literal must be encoded as the
+ * string's BYTES, not a pointer to the string pool. This is the shape
+ * of tcc's ArHdr archive-header template (static struct of char[]
+ * fields); mis-encoding it as pointers produced corrupt `.a` files and
+ * cost the tcc self-host an extra fixed-point bounce.
+ */
+
+typedef struct { char a[16]; char b[6]; char c[6]; char e[2]; } Hdr;
+
+/* static (global) struct — bare and braced string field forms. */
+static Hdr gbare = { "/x", "0", "0", "`\n" };
+static Hdr gbrace = { {"/x"}, {"0"}, "0", "`\n" };
+
+/* array of char[] strings, each element a string literal. */
+static char rows[3][4] = { "ab", "cd", "ef" };
+
+static int check(const Hdr *h) {
+ if (h->a[0] != '/' || h->a[1] != 'x' || h->a[2] != 0) return 1;
+ if (h->b[0] != '0' || h->b[1] != 0) return 2;
+ if (h->c[0] != '0' || h->c[1] != 0) return 3;
+ if (h->e[0] != '`' || h->e[1] != '\n') return 4;
+ return 0;
+}
+
+int main(void) {
+ /* local struct with char[] string fields */
+ struct { char a[8]; char b[4]; } loc = { "hello", "wx" };
+
+ if (check(&gbare)) return 10;
+ if (check(&gbrace)) return 20;
+
+ if (rows[0][0] != 'a' || rows[1][0] != 'c' || rows[2][1] != 'f') return 30;
+ if (rows[0][2] != 0) return 31; /* zero-padded tail */
+
+ if (loc.a[0] != 'h' || loc.a[4] != 'o' || loc.a[5] != 0) return 40;
+ if (loc.b[0] != 'w' || loc.b[1] != 'x' || loc.b[2] != 0) return 41;
+
+ return 0;
+}
diff --git a/tests/cc/340-struct-char-array-string-init.expected-exit b/tests/cc/340-struct-char-array-string-init.expected-exit
@@ -0,0 +1 @@
+0
diff --git a/tests/seed-accept.sh b/tests/seed-accept.sh
@@ -72,8 +72,8 @@ if [ "$MODE" = "boot34" ]; then
exit 0
fi
- [ -x $PODMAN/boot4/tcc3 ] || {
- echo "$PODMAN/boot4/tcc3 missing — run boot/boot4.sh aarch64 under podman first" >&2
+ [ -x $PODMAN/boot4/tcc2 ] || {
+ echo "$PODMAN/boot4/tcc2 missing — run boot/boot4.sh aarch64 under podman first" >&2
exit 1
}
@@ -86,7 +86,7 @@ if [ "$MODE" = "boot34" ]; then
# tcc patch (simple-patches/tcc-0.9.26/) drops the /work/in/[tcc-lib/]
# mount prefix from STT_FILE entries, so seed's flat-basename staging
# and podman's /work/in/ mounts produce identical .o relocations.
- for f in tcc3 hello crt1.o libc.a libtcc1.a; do
+ for f in tcc2 hello crt1.o libc.a libtcc1.a; do
if ! cmp -s $SEED/boot4/$f $PODMAN/boot4/$f; then
s_seed=$(wc -c < $SEED/boot4/$f)
s_ref=$(wc -c < $PODMAN/boot4/$f)
@@ -95,7 +95,7 @@ if [ "$MODE" = "boot34" ]; then
fi
done
[ $fail -eq 0 ] || exit 4
- echo "[seed-accept boot34] boot4 PASS — tcc3/hello/crt1.o/libc.a/libtcc1.a byte-identical vs podman"
+ echo "[seed-accept boot34] boot4 PASS — tcc2/hello/crt1.o/libc.a/libtcc1.a byte-identical vs podman"
exit 0
fi
diff --git a/tools/mkrelease.sh b/tools/mkrelease.sh
@@ -129,7 +129,6 @@ boot3/libc.P1pp
boot3/tcc.flat.P1pp
boot4/tcc1
boot4/tcc2
-boot4/tcc3
boot4/hello
boot4/crt1.o
boot4/libc.a
diff --git a/tools/release/README.md b/tools/release/README.md
@@ -103,7 +103,7 @@ If you've already built and just want to re-check the manifest:
| var | default | meaning |
| ---------------- | ------- | ---------------------------------- |
| `BOOT3_TIMEOUT` | 1800 | scheme1-driven boot3, seconds |
-| `BOOT4_TIMEOUT` | 5400 | tcc1/tcc2/tcc3 self-host chain |
+| `BOOT4_TIMEOUT` | 5400 | tcc1/tcc2 self-host chain |
| `BOOT5_TIMEOUT` | 7200 | musl build |
| `BOOT6_TIMEOUT` | 1200 | seed-kernel link |
| `QEMU_MEM` | 3072M | guest RAM for `DRIVER=seed` |
diff --git a/vendor/mes-libc/libc.c b/vendor/mes-libc/libc.c
@@ -412,7 +412,7 @@ realloc (void *p, size_t size)
/* Internal parser shared by atoi / strtol / strtoul / strtoull and the
width/precision parser inside vformat_. `long long` accumulator so
values that don't fit in 32-bit signed don't sign-extend through the
- parse — tcc3 needs this for `-Wl,-Ttext=0x80200000` and the like. */
+ parse — tcc2 needs this for `-Wl,-Ttext=0x80200000` and the like. */
static long long
parse_int_ (char const **p, int base)
{