commit 10b8a9dad2ce5c7c85c994cd0c4f4935332e9c6e
parent ca23ef4abbc29a06c4b0da8fda4fa1d37d4acbe8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 1 Jun 2026 14:57:54 -0700
test: standardize all harnesses onto canonical types (U/C/K/D)
Replace per-harness ad-hoc test scaffolding with shared libraries so every
test conforms to one of four canonical types:
U unit-C test/lib/cfree_unit.h + unit.mk (2-regime build manifest:
public archive vs internal objects)
C corpus test/lib/cf_corpus.sh — file-corpus lane-matrix engine;
oracle is lane-local (exit/golden/byte/structural/negative/
cross-exec are all just lanes); serial|parallel via event
replay (CF_PARALLELIZABLE flag flip); deferred cross-arch
exec via cf_queue_e + exec_target flush
K scripted test/lib/cfree_sh_kit.sh (report + assert + scenario);
mode G golden transcript diff, mode P procedural asserts
D differential test/lib/cf_differential.sh — baseline-snapshot or
reference-tool agreement oracle
Shared report layer test/lib/cfree_sh_report.sh (cf_pass/fail/skip/skip_na/
xfail/xpass/time + unified summary/exit, TTY-gated color, CFREE_TEST_ALLOW_SKIP
gating) underpins all shell types; test/lib/cf_skip.sh unifies sidecar /
diagnostic-regex / tuple-applicability skips. xpass is always fatal, xfail is
fatal only under CF_STRICT_XFAIL. Engine proven by cf_corpus_selftest.sh
(serial==parallel determinism + the parallel-safety invariant: exec queued on
the parent, never in a worker), wired as `make test-cf-corpus-selftest`.
Migrations:
U: 20 unit tests onto cfree_unit.h, built via the unit.mk manifest.
C: parse, toy, link, elf, wasm, asm, asm round-trip(+toy), host-as(+cross),
pp(+errors), parse-errors, rt, smoke, bounce, libc(musl/glibc),
wasm-target (the 4 check_*.sh folded into one corpus runner).
K: ar/strip/objcopy/strings/objdump (mode G), cas/pkg/driver/coff-smokes
(mode P), dbg (mode G + xfail/xpass).
D: asm symmetry (baseline), diff-llvm (reference vs llvm-mc).
Also relocate the stray top-level test files: test/ar_test.c -> test/ar/,
test/smoke.c -> test/rt/, test/lib_deps.allowlist -> scripts/.
Full-suite run verification is in progress; individual failures are addressed
in follow-up commits.
Diffstat:
76 files changed, 6510 insertions(+), 7392 deletions(-)
diff --git a/doc/TESTING.md b/doc/TESTING.md
@@ -11,12 +11,65 @@ exercise see [ASM.md](ASM.md) (assembler/disassembler/`cc -S`),
[ARCH.md](ARCH.md) (per-arch ISA tables), and [FRONTENDS.md](FRONTENDS.md)
(the Toy and C frontends).
-The test tree lives under `test/`, with per-area subdirectories
-(`test/asm/`, `test/toy/`, `test/smoke/`, `test/libc/`, plus unit-test areas
-like `test/arch/`, `test/elf/`, `test/opt/`). Build/run wiring lives in
-`test/test.mk`; shared helpers (the C unit-test harness `test/lib/cfree_unit.h`
-and its build rules `test/lib/unit.mk`; the cross-exec helper
-`test/lib/exec_target.sh`) are factored out for reuse.
+The test tree lives under `test/`, with per-area subdirectories (`test/asm/`,
+`test/toy/`, `test/smoke/`, `test/libc/`, plus unit-test areas like
+`test/arch/`, `test/elf/`, `test/opt/`). Build/run wiring lives in
+`test/test.mk`. Every harness conforms to **one of four canonical test types**,
+each backed by a shared library under `test/lib/`, so test infrastructure is
+written once and reused rather than re-invented per area.
+
+## Canonical test types
+
+| Type | Library | What it is |
+|------|---------|------------|
+| **U** unit | `cfree_unit.h` (+ `unit.mk` build manifest) | a C translation unit linked against libcfree, self-checking in-process |
+| **C** corpus | `cf_corpus.sh` | a directory of case files run through one or more *lanes*, each with its own oracle |
+| **K** scripted | `cfree_sh_kit.sh` | a hand-written shell test driving the `cfree` binary, judged by golden transcript (mode G) or procedural asserts (mode P) |
+| **D** differential | `cf_differential.sh` | correctness defined as *agreement* — vs a checked-in baseline, or vs a reference tool |
+
+All four record results through one shared report layer
+`test/lib/cfree_sh_report.sh` (`cf_pass`/`cf_fail`/`cf_skip`/`cf_skip_na`/
+`cf_xfail`/`cf_xpass`/`cf_time` + a unified `cf_summary`/`cf_exit`);
+`cf_skip.sh` unifies the skip sources (sidecar files, phased-rollout diagnostic
+regexes, target-tuple applicability); `exec_target.sh` and `exec_kernel.sh` run
+guest binaries for cross-arch lanes. The sections below describe what each
+harness *tests*; this is how they are *structured*.
+
+**Type C — the corpus engine.** `cf_corpus.sh` owns the whole pipeline:
+discover a glob of cases, expand the {case × opt-level × target-tuple} matrix,
+run each enabled lane's recipe, apply that lane's oracle, report, and flush any
+deferred cross-arch exec. A runner supplies only its corpus glob, its lane set,
+and a `cf_lane_<ID>()` hook per lane — which writes only under a per-case
+`$CF_WORK` dir and records via exactly one `cf_*` verb. Because the oracle is
+**lane-local**, exit-code, golden-diff, byte-compare, structural-grep, negative
+(expect-failure) and cross-arch-exec are all just lane bodies, not separate
+harness types; a suite with disjoint sub-corpora (asm's encode/decode/listing,
+elf's layers) makes one `cf_corpus_run` call per sub-corpus. Execution is serial
+or parallel through the **same** code path: in parallel mode each case runs as a
+background worker that appends its results to an event file, and the parent
+replays those events in index order — so counts, failing-name order, and the
+cross-arch-exec queue are deterministic regardless of worker completion order,
+and a runner goes parallel by flipping `CF_PARALLELIZABLE` with no other change.
+The engine is proven by `test/lib/cf_corpus_selftest.sh`
+(`make test-cf-corpus-selftest`): it asserts serial≡parallel determinism and the
+load-bearing invariant that cross-arch exec is queued on the parent, never
+inside a worker.
+
+**Type K — scripted shell.** `cfree_sh_kit.sh` is the single source point for a
+hand-written tool/driver test: it pulls in the report layer plus the procedural
+assert verbs (`ok`/`run_ok`/`run_fail`/`contains`/`same_file`/`is_executable`/
+`check_mode`) and the golden-transcript runner `cf_scenario_case`. A suite picks
+a mode — **G** diffs a `cases/<name>.sh` transcript against `<name>.expected`
+(ar/strip/objcopy/strings/objdump, dbg); **P** runs procedural asserts over a
+`$work` sandbox (cas/pkg, the driver CLI suite, the COFF Windows smokes). Mode-P
+suites stay serial (they share one `$work` and mutate fixtures).
+
+**Type D — differential.** `cf_differential.sh` is for tests whose oracle is
+*agreement* rather than a fixed per-case answer: `cf_diff_baseline` regenerates a
+normalized report and gates on a zero delta vs a checked-in snapshot
+(`CF_DIFF_UPDATE=1` refreshes it), and `cf_diff_agree` requires two independent
+producers to match byte-for-byte (with explicit equivalence-skips). Used by the
+asm self-symmetry sweep and the cfree-vs-llvm-mc cross-check.
## Why codegen-driven round-trip testing
@@ -224,9 +277,10 @@ link/smoke/libc harnesses, so cross-exec policy lives in exactly one file.
The Toy frontend (`lang/toy/`, see [FRONTENDS.md](FRONTENDS.md)) is a small
language that exists to exercise the full CG API op set, and every case carries
-an exit-code oracle (`test/toy/cases/<name>.expected`). `test/toy/run.sh` runs
-each case through several **paths**, each a distinct backend/seam, all judged
-against the same oracle:
+an exit-code oracle (`test/toy/cases/<name>.expected`). `test/toy/run.sh` is a
+Type C corpus harness that runs each case through several **paths** — its
+`cf_corpus.sh` lanes, each a distinct backend/seam — all judged against the same
+oracle:
```
R cfree run in-process JIT, native
@@ -251,7 +305,26 @@ and both host-assembler lanes use it. This reuse found a real miscompile (a
multiply-high the disassembler couldn't decode, silently dropped by `as` until
the `.inst`-emits-the-word fix) that the hand corpus never reached.
-## Unit tests
+## Driver and tool-level tests (Type K)
+
+The `cfree` multitool's command-line behavior is covered by **Type K** scripted
+harnesses on `cfree_sh_kit.sh`, in two oracle modes:
+
+- **mode G (golden transcript):** `test/{ar,strip,objcopy,strings,objdump}/run.sh`
+ and `test/dbg/run.sh` each run a directory of `cases/<name>.sh` scripts in a
+ sandbox and diff combined stdout+stderr against a checked-in `<name>.expected`.
+ dbg additionally uses xfail/xpass: an xfail case that fails is expected
+ (`cf_xfail`); one that *unexpectedly passes* (`cf_xpass`) is always a failure —
+ the stale marker should be removed — and `DBG_STRICT_XFAIL` promotes even an
+ expected failure to a hard error.
+- **mode P (procedural asserts):** `test/driver/run.sh` (the `cc`/`ld`/`ar`/`nm`/
+ `size`/`addr2line`/… behavior suite), `test/cas/run.sh`, `test/pkg/run.sh`, and
+ the COFF Windows smokes (`test/coff/windows-*-smoke.sh`) drive a sequence of
+ `cfree` invocations over a `$work` sandbox and assert outcomes with
+ `run_ok`/`run_fail`/`contains`/`same_file`/`check_mode`. These stay serial
+ (they share `$work` and mutate fixtures).
+
+## Unit tests (Type U)
Lower-level invariants are covered by C unit-test binaries built from
`test/lib/unit.mk` and linking `test/lib/cfree_unit.h`. There are two link
@@ -273,7 +346,7 @@ push it through `exec_target_run` and `exec_target_queue`+`flush`, and assert th
expected exit code on both paths. The point is to validate the harness plumbing
(cross-compile → podman/qemu → recorded rc) before relying on it for the heavier
lanes, and to give a clear per-tool ok/MISSING diagnosis when a host lacks a
-runner. There is also a header smoke test (`test/smoke.c`): every freestanding
+runner. There is also a header smoke test (`test/rt/smoke.c`): every freestanding
header must parse and expose its required macros/typedefs under a strict
freestanding compile.
@@ -296,7 +369,7 @@ see [LINK.md](LINK.md). Arch selection is `CFREE_LIBC_ARCHES` (aa64/x64/rv64);
a missing sysroot or runtime for an enabled arch is SKIP, not failure.
A related guard, `test-lib-deps`, asserts `libcfree.a`'s set of external
-(undefined) symbols matches a checked-in allowlist (`test/lib_deps.allowlist`)
+(undefined) symbols matches a checked-in allowlist (`scripts/lib_deps.allowlist`)
and that a relocatable link of the library exposes no non-public symbols. This
keeps the freestanding library's dependency surface from drifting silently.
@@ -326,18 +399,29 @@ and to triage O1 codegen without `-g`, which perturbs object layout.
## Aggregation and conventions
`test/test.mk` defines the targets. A default `test` aggregate runs the
-host-independent lanes (frontend corpora, unit tests, L0/L1 round-trip, libc-dep
-guard); the exec-dependent and second-oracle lanes (L2 exec, symmetry, diff-llvm,
-hostas-toy/cross, smoke, libc conformance) are opt-in so the default run stays
-host-independent and fast. Bootstrap is *not* part of this test system: it is a
-separate top-level target (`make bootstrap`, `make bootstrap-debug/release`)
-driven from the top-level `Makefile`, not from `test/test.mk` — see the
-bootstrap section above. Conventions shared across harnesses:
-
+host-independent lanes (frontend corpora, unit tests, L0/L1 round-trip, the
+`cf_corpus` engine selftest, the libc-dep guard); the exec-dependent and
+second-oracle lanes (L2 exec, symmetry, diff-llvm, hostas-toy/cross, smoke, libc
+conformance) are opt-in so the default run stays host-independent and fast.
+Bootstrap is *not* part of this test system: it is a separate top-level target
+(`make bootstrap`, `make bootstrap-debug/release`) driven from the top-level
+`Makefile`, not from `test/test.mk` — see the bootstrap section above.
+Conventions shared across all four test types:
+
+- **One report layer.** Every shell harness records through `cf_pass`/`cf_fail`/
+ `cf_skip`/`cf_skip_na`/`cf_xfail`/`cf_xpass` and ends with `cf_summary` +
+ `cf_exit` — no harness rolls its own counters or summary. XPASS always gates
+ the exit; SKIP gates only when the suite opts in (`CF_SKIP_IS_FAILURE=1`, the
+ corpus default), overridable per run with `CFREE_TEST_ALLOW_SKIP=1`.
- **SKIP, never silently pass.** A lane that can't run (no runner, no cross
- toolchain, an unimplemented backend path) reports SKIP with a reason rather
- than vanishing. Several harnesses honor `CFREE_TEST_ALLOW_SKIP` to decide
- whether a SKIP is a soft pass or a hard fail in a given context.
+ toolchain, an unimplemented backend path) reports SKIP with a reason via the
+ shared `cf_skip.sh` (sidecar / phased-rollout diagnostic regex / target-tuple
+ applicability) rather than vanishing; a structurally-inapplicable case is an
+ uncounted SKIP-NA.
+- **Parallel by a flag flip.** A Type C corpus runs serial or parallel through
+ the same `cf_corpus.sh` event-replay path; hooks that write only under
+ `$CF_WORK` and record only via `cf_*` are parallel-safe by construction, so
+ `CF_PARALLELIZABLE` toggles dispatch with no other change.
- **Checked-in baselines turn incompleteness into a regression diff.** The
symmetry baseline and the per-case `.skip` sidecars document what is known-
incomplete; the gate fires only on a *change* to that set.
diff --git a/test/lib_deps.allowlist b/scripts/lib_deps.allowlist
diff --git a/test/api/abi_classify_test.c b/test/api/abi_classify_test.c
@@ -13,69 +13,24 @@
#include <stdlib.h>
#include <string.h>
+#include "lib/cfree_unit.h"
+
#include "abi/abi.h"
#include "core/core.h"
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-static int g_fail;
-
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
-
-/* Storage outlives every Compiler; cfree_compiler_new just stores `ctx`. */
-static CfreeContext g_ctx;
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static void expect_direct_1x_int(const char* tag, const ABIArgInfo* ai,
u32 want_size);
static CfreeCompiler* new_compiler(CfreeArchKind arch, CfreeOSKind os,
CfreeObjFmt obj) {
- CfreeTarget t;
+ CfreeTarget t = cfree_unit_target(arch, os, obj);
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof t);
- t.arch = arch;
- t.os = os;
- t.obj = obj;
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof g_ctx);
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "compiler_new failed for arch=%d os=%d\n", (int)arch,
(int)os);
exit(2);
@@ -576,6 +531,7 @@ static void test_aarch64_windows_variadic(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
check_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
check_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
check_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO);
@@ -584,10 +540,6 @@ int main(void) {
check_target(CFREE_ARCH_ARM_64, CFREE_OS_WINDOWS, CFREE_OBJ_COFF);
test_win64_specifics();
test_aarch64_windows_variadic();
- if (g_fail) {
- fprintf(stderr, "%d failures\n", g_fail);
- return 1;
- }
- fprintf(stderr, "abi_classify_test: OK\n");
- return 0;
+ cfree_unit_summary(&g_u, "abi_classify_test");
+ return cfree_unit_status(&g_u);
}
diff --git a/test/api/cg_switch_test.c b/test/api/cg_switch_test.c
@@ -23,46 +23,12 @@
#include <stdlib.h>
#include <string.h>
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-static int g_fail;
+#include "lib/cfree_unit.h"
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static CfreeObjBuilder* new_obj(CfreeCompiler* c) {
CfreeObjBuilder* ob = NULL;
@@ -202,7 +168,7 @@ static void build_switch_fn(CfreeCompiler* c, CfreeCgTypeId i32_ty,
cfree_cg_ret(cg);
cfree_cg_func_end(cg);
- EXPECT(g_fail == 0 || g_fail > 0, "shape-build sentinel"); /* no-op */
+ EXPECT(g_u.fails == 0 || g_u.fails > 0, "shape-build sentinel"); /* no-op */
free(case_lbls);
free(cases);
@@ -284,26 +250,15 @@ static void run_all_shapes(CfreeCompiler* c, CfreeCgTypeId i32_ty,
int main(void) {
CfreeTarget target;
- CfreeContext ctx;
CfreeCompiler* c = NULL;
CfreeCgBuiltinTypes bi;
CfreeCgTypeId i32_ty;
CfreeCgTypeId i64_ty;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
-
- memset(&ctx, 0, sizeof ctx);
- ctx.heap = &g_heap;
- ctx.file_io = NULL;
- ctx.diag = &g_diag;
- ctx.now = 0;
+ cfree_unit_init(&g_u);
+ target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
- if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) {
fprintf(stderr, "compiler_new failed\n");
return 2;
}
@@ -318,5 +273,6 @@ int main(void) {
run_all_shapes(c, i32_ty, i64_ty, /*opt_level=*/1);
cfree_compiler_free(c);
- return g_fail ? 1 : 0;
+ cfree_unit_summary(&g_u, "cg_switch_test");
+ return cfree_unit_status(&g_u);
}
diff --git a/test/api/cg_type_test.c b/test/api/cg_type_test.c
@@ -8,56 +8,13 @@
#include <stdlib.h>
#include <string.h>
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
+#include "lib/cfree_unit.h"
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static char g_last_diag[256];
-static int g_suppress_expected_panic_diag;
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- va_list copy;
- (void)s;
- (void)loc;
- va_copy(copy, ap);
- vsnprintf(g_last_diag, sizeof g_last_diag, fmt, copy);
- va_end(copy);
- if (g_suppress_expected_panic_diag && k == CFREE_DIAG_FATAL) return;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-static int g_fail;
-
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* One shared test context replaces the per-file heap/diag/counter globals.
+ * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged; the
+ * expected-panic helper uses g_u.suppress_fatal + g_u.last_diag. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static CfreeObjBuilder* new_obj(CfreeCompiler* c) {
CfreeObjBuilder* ob = NULL;
@@ -76,7 +33,7 @@ static int open_emitted_obj(CfreeCompiler* c, CfreeObjBuilder* ob,
if (!writer_out || !file_out) return 0;
*writer_out = NULL;
*file_out = NULL;
- if (cfree_writer_mem(&g_heap, &w) != CFREE_OK || !w) return 0;
+ if (cfree_writer_mem(&g_u.heap, &w) != CFREE_OK || !w) return 0;
if (cfree_obj_builder_emit(ob, w) != CFREE_OK) {
cfree_writer_close(w);
return 0;
@@ -112,11 +69,11 @@ static int expect_panic_contains(CfreeCompiler* c, void (*fn)(void*), void* arg,
CfreeStatus st;
ctx.fn = fn;
ctx.arg = arg;
- g_last_diag[0] = '\0';
- g_suppress_expected_panic_diag++;
+ g_u.last_diag[0] = '\0';
+ g_u.suppress_fatal++;
st = cfree_frontend_run(c, run_expected_panic, &ctx);
- g_suppress_expected_panic_diag--;
- return st == CFREE_ERR && strstr(g_last_diag, expected) != NULL;
+ g_u.suppress_fatal--;
+ return st == CFREE_ERR && strstr(g_u.last_diag, expected) != NULL;
}
static void exercise_cg_handles(CfreeCompiler* c, CfreeCgTypeId i32_ty,
@@ -1290,7 +1247,8 @@ static void exercise_cg_memory_mismatch_diags(CfreeCompiler* c,
(void)run_bad_scalar_access_to_aggregate;
EXPECT(expect_panic_contains(c, run_bad_store_value_size, &ctx,
"store value type/size mismatch"),
- "store size mismatch should diagnose clearly, got '%s'", g_last_diag);
+ "store size mismatch should diagnose clearly, got '%s'",
+ g_u.last_diag);
}
static void exercise_compile_session_two_deltas(CfreeCompiler* c) {
@@ -1352,7 +1310,6 @@ static void exercise_cg_begin_end_two_objects(CfreeCompiler* c) {
int main(void) {
CfreeTarget target;
- CfreeContext ctx;
CfreeCompiler* c;
CfreeCgBuiltinTypes bi;
CfreeCgTypeId void_ty;
@@ -1376,21 +1333,11 @@ int main(void) {
CfreeCgEnumValue vals[2];
uint64_t field_off;
- memset(&target, 0, sizeof(target));
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
-
- memset(&ctx, 0, sizeof(ctx));
- ctx.heap = &g_heap;
- ctx.file_io = NULL;
- ctx.diag = &g_diag;
- ctx.now = 0;
+ cfree_unit_init(&g_u);
+ target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
c = NULL;
- if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) {
fprintf(stderr, "compiler_new failed\n");
return 2;
}
@@ -1542,5 +1489,6 @@ int main(void) {
exercise_cg_begin_end_two_objects(c);
cfree_compiler_free(c);
- return g_fail ? 1 : 0;
+ cfree_unit_summary(&g_u, "cg_api_test");
+ return cfree_unit_status(&g_u);
}
diff --git a/test/ar/ar_test.c b/test/ar/ar_test.c
@@ -0,0 +1,973 @@
+/* Round-trip tests for the cfree ar reader/writer.
+ *
+ * Builds against just include/cfree.h + libcfree.a and a few libc calls
+ * (malloc/realloc/free, printf for diagnostics). cfree_ar_write itself
+ * makes no heap allocations, so the test does not need a CfreeHeap.
+ *
+ * Set CFREE_AR_TEST_HOST=1 to also dump the produced symbol-index
+ * archive to /tmp/cfree_ar_test.a and run the host's `ar t` and
+ * `nm --print-armap` on it as a cross-check. */
+#include <cfree/archive.h>
+#include <cfree/core.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lib/cfree_unit.h"
+
+/* ===== minimal CfreeWriter over a growing buffer ===== */
+
+typedef struct BufW {
+ CfreeWriter base;
+ uint8_t* data;
+ size_t len;
+ size_t cap;
+ CfreeStatus st;
+} BufW;
+
+static CfreeStatus bufw_write(CfreeWriter* w, const void* data, size_t n) {
+ BufW* b = (BufW*)w;
+ if (b->st != CFREE_OK) return b->st;
+ if (b->len + n > b->cap) {
+ size_t nc = b->cap ? b->cap * 2 : 256;
+ while (nc < b->len + n) nc *= 2;
+ uint8_t* p = (uint8_t*)realloc(b->data, nc);
+ if (!p) {
+ b->st = CFREE_NOMEM;
+ return CFREE_NOMEM;
+ }
+ b->data = p;
+ b->cap = nc;
+ }
+ memcpy(b->data + b->len, data, n);
+ b->len += n;
+ return CFREE_OK;
+}
+
+static CfreeStatus bufw_seek(CfreeWriter* w, uint64_t off) {
+ (void)w;
+ (void)off;
+ return CFREE_OK;
+}
+static uint64_t bufw_tell(CfreeWriter* w) { return ((BufW*)w)->len; }
+static CfreeStatus bufw_status(CfreeWriter* w) { return ((BufW*)w)->st; }
+static void bufw_close(CfreeWriter* w) { (void)w; }
+
+static void bufw_init(BufW* b) {
+ b->base.write = bufw_write;
+ b->base.seek = bufw_seek;
+ b->base.tell = bufw_tell;
+ b->base.status = bufw_status;
+ b->base.close = bufw_close;
+ b->data = NULL;
+ b->len = 0;
+ b->cap = 0;
+ b->st = CFREE_OK;
+}
+
+static void bufw_fini(BufW* b) { free(b->data); }
+
+/* ===== libc heap + context (needed by cfree_ar_iter_new) ===== */
+
+static CfreeUnit g_u;
+
+/* ar_test keeps its own context: now=-1 and no diag sink, since its archive
+ * assertions depend on exact header bytes (the clock) and never expect a
+ * diagnostic. The heap comes from g_u, filled by cfree_unit_init in main. */
+static CfreeContext g_ctx = {.heap = &g_u.heap, .now = -1};
+
+/* ===== assertion helpers ===== */
+
+/* EXPECT keeps its per-test early-return-on-fail shape via CU_CHECK_RET. */
+#define EXPECT(cond, ...) CU_CHECK_RET(&g_u, cond, __VA_ARGS__)
+
+static uint32_t be32(const uint8_t* p) {
+ return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
+ ((uint32_t)p[2] << 8) | (uint32_t)p[3];
+}
+
+/* Decode the ar_size field (10-byte ASCII decimal, space-padded). */
+static uint64_t ar_size_field(const uint8_t* hdr) {
+ uint64_t v = 0;
+ int j;
+ for (j = 48; j < 58; ++j) {
+ char c = (char)hdr[j];
+ if (c < '0' || c > '9') break;
+ v = v * 10 + (uint64_t)(c - '0');
+ }
+ return v;
+}
+
+/* Decode the ar_date field (12-byte ASCII decimal, space-padded). */
+static uint64_t ar_date_field(const uint8_t* hdr) {
+ uint64_t v = 0;
+ int j;
+ for (j = 16; j < 28; ++j) {
+ char c = (char)hdr[j];
+ if (c < '0' || c > '9') break;
+ v = v * 10 + (uint64_t)(c - '0');
+ }
+ return v;
+}
+
+/* ===== tests ===== */
+
+static int test_basic_roundtrip(void) {
+ CfreeArInput ms[2];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ int rc;
+
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"AAAA";
+ ms[0].bytes.len = 4;
+ ms[1].name = CFREE_SLICE_LIT("b.o");
+ ms[1].bytes.data = (const uint8_t*)"BBBBB";
+ ms[1].bytes.len = 5;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 2, NULL);
+ EXPECT(rc == 0, "cfree_ar_write returned %d", rc);
+ EXPECT(bw.st == CFREE_OK, "writer error");
+ EXPECT(bw.len >= 8, "archive too short");
+ EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+
+ EXPECT(cfree_ar_iter_next(it, &m), "first member");
+ EXPECT(cfree_slice_eq_cstr(m.name, "a.o"), "name 0 = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ EXPECT(m.size == 4 && memcmp(m.data, "AAAA", 4) == 0, "data 0");
+
+ EXPECT(cfree_ar_iter_next(it, &m), "second member");
+ EXPECT(cfree_slice_eq_cstr(m.name, "b.o"), "name 1 = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ EXPECT(m.size == 5 && memcmp(m.data, "BBBBB", 5) == 0, "data 1");
+
+ EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_long_name_table(void) {
+ /* >15 chars triggers the // long-name table. */
+ CfreeArInput ms[2];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ CfreeArWriteOptions opts = {0};
+ int rc;
+
+ opts.long_names = 1;
+ ms[0].name = CFREE_SLICE_LIT("short.o");
+ ms[0].bytes.data = (const uint8_t*)"x";
+ ms[0].bytes.len = 1;
+ ms[1].name = CFREE_SLICE_LIT("this_name_is_long_enough.o");
+ ms[1].bytes.data = (const uint8_t*)"yy";
+ ms[1].bytes.len = 2;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 2, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first member");
+ EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "name 0 = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ EXPECT(cfree_ar_iter_next(it, &m), "second member");
+ EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"),
+ "long name = %.*s", CFREE_SLICE_ARG(m.name));
+ EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_symbol_index_empty(void) {
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ CfreeArWriteOptions opts = {0};
+ int rc;
+ uint32_t nsyms;
+
+ opts.symbol_index = 1;
+ ms[0].name = CFREE_SLICE_LIT("lonely.o");
+ ms[0].bytes.data = (const uint8_t*)"P";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 1, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+
+ /* First member after magic must be `/` index with count=0. */
+ EXPECT(bw.len >= 8 + 60 + 4, "archive too short");
+ EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index name field");
+ EXPECT(ar_size_field(bw.data + 8) == 4, "index payload size = 4");
+ nsyms = be32(bw.data + 8 + 60);
+ EXPECT(nsyms == 0, "nsyms = %u", nsyms);
+
+ /* Iterator should skip the `/` and yield only the user member. */
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first user member");
+ EXPECT(cfree_slice_eq_cstr(m.name, "lonely.o"), "name = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_symbol_index_basic(void) {
+ /* 2 members with symbol lists; verify count, offsets, and names. */
+ CfreeSlice a_syms[] = {CFREE_SLICE_LIT("foo"), CFREE_SLICE_LIT("bar")};
+ CfreeSlice b_syms[] = {CFREE_SLICE_LIT("baz")};
+ CfreeArMemberSymbols msyms[2];
+ CfreeArInput ms[2];
+ BufW bw;
+ CfreeArWriteOptions opts = {0};
+ int rc;
+ uint32_t nsyms;
+ const uint8_t* p;
+ uint64_t index_payload;
+ uint32_t off0, off1, off2;
+ uint64_t a_hdr_off, b_hdr_off;
+ const char* name;
+
+ opts.symbol_index = 1;
+ opts.long_names = 1;
+
+ msyms[0].names = a_syms;
+ msyms[0].count = 2;
+ msyms[1].names = b_syms;
+ msyms[1].count = 1;
+ opts.member_symbols = msyms;
+
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"AAAA";
+ ms[0].bytes.len = 4;
+ ms[1].name = CFREE_SLICE_LIT("b.o");
+ ms[1].bytes.data = (const uint8_t*)"BBBBB";
+ ms[1].bytes.len = 5;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 2, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+
+ EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic");
+ EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index ar_name field");
+
+ index_payload = ar_size_field(bw.data + 8);
+ /* 4 (count) + 4*3 (offsets) + 3+1 + 3+1 + 3+1 (names "foo\0bar\0baz\0") = 28
+ */
+ EXPECT(index_payload == 28, "index payload = %llu",
+ (unsigned long long)index_payload);
+
+ p = bw.data + 8 + 60;
+ nsyms = be32(p);
+ p += 4;
+ EXPECT(nsyms == 3, "nsyms = %u", nsyms);
+
+ off0 = be32(p); /* foo → a.o */
+ off1 = be32(p + 4); /* bar → a.o */
+ off2 = be32(p + 8); /* baz → b.o */
+ p += 12;
+
+ /* Compute expected header offsets: index_total = 60+28 (no pad, even).
+ * No long-name table is emitted (basenames ≤ 15 chars). So:
+ * a.o header at offset 8 + 88 = 96
+ * b.o header at offset 96 + 60 + 4 (+0 pad) = 160 */
+ a_hdr_off = 8 + 60 + 28;
+ b_hdr_off = a_hdr_off + 60 + 4;
+ EXPECT(off0 == a_hdr_off, "off0 = %u, expected %llu", off0,
+ (unsigned long long)a_hdr_off);
+ EXPECT(off1 == a_hdr_off, "off1 = %u, expected %llu", off1,
+ (unsigned long long)a_hdr_off);
+ EXPECT(off2 == b_hdr_off, "off2 = %u, expected %llu", off2,
+ (unsigned long long)b_hdr_off);
+
+ /* Sanity: the member headers must actually live at those offsets. */
+ EXPECT(memcmp(bw.data + a_hdr_off, "a.o/", 4) == 0, "a.o at offset");
+ EXPECT(memcmp(bw.data + b_hdr_off, "b.o/", 4) == 0, "b.o at offset");
+
+ /* Names: "foo\0bar\0baz\0" */
+ name = (const char*)p;
+ EXPECT(strcmp(name, "foo") == 0, "name 0 = %s", name);
+ name += strlen(name) + 1;
+ EXPECT(strcmp(name, "bar") == 0, "name 1 = %s", name);
+ name += strlen(name) + 1;
+ EXPECT(strcmp(name, "baz") == 0, "name 2 = %s", name);
+
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_symbol_index_with_long_names(void) {
+ /* `/` member must come BEFORE `//` long-name table, and offsets must
+ * still point at correct member-header positions. */
+ CfreeSlice syms0[] = {CFREE_SLICE_LIT("alpha")};
+ CfreeSlice syms1[] = {CFREE_SLICE_LIT("beta")};
+ CfreeArMemberSymbols msyms[2];
+ CfreeArInput ms[2];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ CfreeArWriteOptions opts = {0};
+ int rc;
+ uint64_t index_payload, longtab_payload;
+ uint64_t index_total, longtab_total;
+ uint64_t m0_hdr, m1_hdr;
+ uint32_t off0, off1;
+ const uint8_t* p;
+
+ opts.symbol_index = 1;
+ opts.long_names = 1;
+ msyms[0].names = syms0;
+ msyms[0].count = 1;
+ msyms[1].names = syms1;
+ msyms[1].count = 1;
+ opts.member_symbols = msyms;
+
+ ms[0].name =
+ CFREE_SLICE_LIT("this_name_is_long_enough.o"); /* 26 chars → // */
+ ms[0].bytes.data = (const uint8_t*)"X";
+ ms[0].bytes.len = 1;
+ ms[1].name = CFREE_SLICE_LIT("short.o");
+ ms[1].bytes.data = (const uint8_t*)"YY";
+ ms[1].bytes.len = 2;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 2, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+
+ /* Layout: magic(8) | / index member | // long-name member | members. */
+ EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "/ first");
+ index_payload = ar_size_field(bw.data + 8);
+ /* 4 (count) + 4*2 (offsets) + 6 ("alpha\0") + 5 ("beta\0") = 23 → odd → pad 1
+ */
+ EXPECT(index_payload == 23, "index_payload = %llu",
+ (unsigned long long)index_payload);
+ index_total = 60 + index_payload + (index_payload & 1);
+
+ /* Verify // header sits at 8 + index_total. */
+ EXPECT(bw.data[8 + index_total] == '/', "// pos byte 0");
+ EXPECT(bw.data[8 + index_total + 1] == '/', "// pos byte 1");
+ longtab_payload = ar_size_field(bw.data + 8 + index_total);
+ /* "this_name_is_long_enough.o/\n" = 28 bytes → even → no pad */
+ EXPECT(longtab_payload == 28, "longtab payload = %llu",
+ (unsigned long long)longtab_payload);
+ longtab_total = 60 + longtab_payload + (longtab_payload & 1);
+
+ m0_hdr = 8 + index_total + longtab_total;
+ m1_hdr = m0_hdr + 60 + 1 + 1 /* parity pad for odd len 1 */;
+
+ p = bw.data + 8 + 60;
+ EXPECT(be32(p) == 2, "nsyms = %u", be32(p));
+ p += 4;
+ off0 = be32(p);
+ off1 = be32(p + 4);
+ EXPECT(off0 == m0_hdr, "off0 = %u, expected %llu", off0,
+ (unsigned long long)m0_hdr);
+ EXPECT(off1 == m1_hdr, "off1 = %u, expected %llu", off1,
+ (unsigned long long)m1_hdr);
+ /* Spot-check m1 starts with "short.o/" for sanity. */
+ EXPECT(memcmp(bw.data + m1_hdr, "short.o/", 8) == 0, "m1 hdr");
+
+ /* Iterator should walk past both /, // and yield 2 members. */
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "m0");
+ EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"),
+ "m0 name = %.*s", CFREE_SLICE_ARG(m.name));
+ EXPECT(cfree_ar_iter_next(it, &m), "m1");
+ EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "m1 name = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
+
+ /* Optional host cross-check. */
+ if (getenv("CFREE_AR_TEST_HOST")) {
+ FILE* f = fopen("/tmp/cfree_ar_test.a", "wb");
+ if (f) {
+ fwrite(bw.data, 1, bw.len, f);
+ fclose(f);
+ fprintf(stderr, "host cross-check: ar t /tmp/cfree_ar_test.a\n");
+ (void)!system("ar t /tmp/cfree_ar_test.a");
+ fprintf(stderr,
+ "host cross-check: nm --print-armap /tmp/cfree_ar_test.a\n");
+ (void)!system("nm --print-armap /tmp/cfree_ar_test.a 2>&1 || true");
+ }
+ }
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_iter_skips_index(void) {
+ /* Make sure the iterator never surfaces the `/` member as a user member. */
+ CfreeSlice s[] = {CFREE_SLICE_LIT("only_sym")};
+ CfreeArMemberSymbols msyms[1];
+ CfreeArInput ms[1];
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ BufW bw;
+ CfreeArWriteOptions opts = {0};
+ int seen = 0;
+
+ opts.symbol_index = 1;
+ msyms[0].names = s;
+ msyms[0].count = 1;
+ opts.member_symbols = msyms;
+ ms[0].name = CFREE_SLICE_LIT("only.o");
+ ms[0].bytes.data = (const uint8_t*)"Z";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ while (cfree_ar_iter_next(it, &m)) {
+ EXPECT(!cfree_slice_eq_cstr(m.name, "/"), "iter surfaced raw `/` member");
+ seen++;
+ }
+ EXPECT(seen == 1, "saw %d members", seen);
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_empty_archive(void) {
+ /* nmembers == 0 with NULL members should produce a magic-only archive. */
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ int rc;
+
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, NULL, 0, NULL);
+ EXPECT(rc == 0, "write rc=%d", rc);
+ EXPECT(bw.len == 8, "size = %zu", bw.len);
+ EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic only");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(!cfree_ar_iter_next(it, &m), "no members");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_epoch_field(void) {
+ /* opts.epoch is written into ar_date for every member. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeArWriteOptions opts = {0};
+ int rc;
+
+ ms[0].name = CFREE_SLICE_LIT("x.o");
+ ms[0].bytes.data = (const uint8_t*)"q";
+ ms[0].bytes.len = 1;
+
+ opts.epoch = 1234567890u;
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 1, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+ EXPECT(ar_date_field(bw.data + 8) == 1234567890u, "ar_date = %llu",
+ (unsigned long long)ar_date_field(bw.data + 8));
+ bufw_fini(&bw);
+
+ /* Default (epoch=0): single '0' followed by spaces. */
+ opts.epoch = 0;
+ bufw_init(&bw);
+ rc = cfree_ar_write(&bw.base, ms, 1, &opts);
+ EXPECT(rc == 0, "write rc=%d", rc);
+ EXPECT(bw.data[8 + 16] == '0', "epoch default first byte");
+ EXPECT(bw.data[8 + 17] == ' ', "epoch default second byte = 0x%02x",
+ bw.data[8 + 17]);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_path_basename(void) {
+ /* Member name with path components is stored as basename only. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+
+ ms[0].name = CFREE_SLICE_LIT("src/sub/foo.o");
+ ms[0].bytes.data = (const uint8_t*)"D";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(cfree_slice_eq_cstr(m.name, "foo.o"), "basename = %.*s",
+ CFREE_SLICE_ARG(m.name));
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_truncate_when_long_names_off(void) {
+ /* >15 chars without long_names: name is truncated to 15. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+
+ ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnopqrst.o"); /* 22 chars */
+ ms[0].bytes.data = (const uint8_t*)"D";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "truncated = %.*s",
+ CFREE_SLICE_ARG(m.name));
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_name_15_char_boundary(void) {
+ /* Exactly 15 chars: fits in-header even with long_names enabled. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ CfreeArWriteOptions opts = {0};
+
+ opts.long_names = 1;
+ ms[0].name = CFREE_SLICE_LIT("abcdefghijklmno"); /* 15 chars */
+ ms[0].bytes.data = (const uint8_t*)"X";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
+
+ /* No `//` long-name table: first member sits right after magic. */
+ EXPECT(memcmp(bw.data + 8, "abcdefghijklmno/", 16) == 0, "name field = %.16s",
+ (const char*)(bw.data + 8));
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "name = %.*s",
+ CFREE_SLICE_ARG(m.name));
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_name_16_char_boundary(void) {
+ /* Exactly 16 chars: triggers // long-name table. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ CfreeArWriteOptions opts = {0};
+
+ opts.long_names = 1;
+ ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnop"); /* 16 chars */
+ ms[0].bytes.data = (const uint8_t*)"Y";
+ ms[0].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
+ EXPECT(bw.data[8] == '/' && bw.data[9] == '/', "// header");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmnop"), "name = %.*s",
+ CFREE_SLICE_ARG(m.name));
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_empty_member_payload(void) {
+ /* len=0 (data=NULL): header only, no pad; followed by the next member. */
+ CfreeArInput ms[2];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+
+ ms[0].name = CFREE_SLICE_LIT("empty.o");
+ ms[0].bytes.data = NULL;
+ ms[0].bytes.len = 0;
+ ms[1].name = CFREE_SLICE_LIT("next.o");
+ ms[1].bytes.data = (const uint8_t*)"N";
+ ms[1].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 2, NULL) == 0, "write");
+ /* magic(8) + hdr(60) + 0 + hdr(60) + 1 + pad(1) = 130 */
+ EXPECT(bw.len == 130, "size = %zu", bw.len);
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(cfree_slice_eq_cstr(m.name, "empty.o") && m.size == 0,
+ "empty.o size=%zu", m.size);
+ EXPECT(cfree_ar_iter_next(it, &m), "second");
+ EXPECT(
+ cfree_slice_eq_cstr(m.name, "next.o") && m.size == 1 && m.data[0] == 'N',
+ "next.o");
+ EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_odd_payload_pad(void) {
+ /* Odd-length payloads add a '\n' parity pad; even lengths do not. */
+ CfreeArInput ms[3];
+ BufW bw;
+
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"x";
+ ms[0].bytes.len = 1;
+ ms[1].name = CFREE_SLICE_LIT("b.o");
+ ms[1].bytes.data = (const uint8_t*)"yy";
+ ms[1].bytes.len = 2;
+ ms[2].name = CFREE_SLICE_LIT("c.o");
+ ms[2].bytes.data = (const uint8_t*)"z";
+ ms[2].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 3, NULL) == 0, "write");
+ /* 8 + (60+1+1) + (60+2) + (60+1+1) = 194 */
+ EXPECT(bw.len == 194, "size = %zu", bw.len);
+ EXPECT(bw.data[8 + 60 + 1] == '\n', "pad after a.o = 0x%02x",
+ bw.data[8 + 60 + 1]);
+ /* No pad after b.o (even): next header begins immediately. */
+ EXPECT(bw.data[8 + 62 + 60 + 2] == 'c', "c.o name follows b.o without pad");
+
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_ar_list_output(void) {
+ /* cfree_ar_list emits one user member per line, skipping / and //. */
+ CfreeArInput ms[3];
+ BufW bw, lw;
+ CfreeSlice in;
+ CfreeArWriteOptions opts = {0};
+ const char* expected = "a.o\nlong_name_member.o\nb.o\n";
+
+ opts.symbol_index = 1;
+ opts.long_names = 1;
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"A";
+ ms[0].bytes.len = 1;
+ ms[1].name = CFREE_SLICE_LIT("long_name_member.o");
+ ms[1].bytes.data = (const uint8_t*)"B";
+ ms[1].bytes.len = 1;
+ ms[2].name = CFREE_SLICE_LIT("b.o");
+ ms[2].bytes.data = (const uint8_t*)"C";
+ ms[2].bytes.len = 1;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ bufw_init(&lw);
+ EXPECT(cfree_ar_list(&in, &lw.base) == 0, "list");
+ EXPECT(lw.len == strlen(expected), "list len = %zu, want %zu", lw.len,
+ strlen(expected));
+ EXPECT(memcmp(lw.data, expected, lw.len) == 0, "list = %.*s", (int)lw.len,
+ (const char*)lw.data);
+
+ bufw_fini(&lw);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_iter_bad_magic(void) {
+ /* iter_init must reject non-ar inputs. */
+ static const uint8_t bad[] = "NOT-AN-AR";
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+
+ in.data = bad;
+ in.len = sizeof(bad) - 1;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects bad magic");
+
+ in.data = NULL;
+ in.len = 0;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects empty");
+
+ in.data = bad;
+ in.len = 4; /* too short */
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects short");
+
+ return 1;
+}
+
+static int test_write_invalid_args(void) {
+ /* Bad argument combinations must return 1 from cfree_ar_write. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeArWriteOptions opts = {0};
+ CfreeSlice bad_syms[1];
+ CfreeArMemberSymbols msyms[1];
+
+ bufw_init(&bw);
+
+ EXPECT(cfree_ar_write(NULL, NULL, 0, NULL) != CFREE_OK,
+ "NULL writer rejected");
+ EXPECT(cfree_ar_write(&bw.base, NULL, 1, NULL) != CFREE_OK,
+ "NULL members with nmembers>0 rejected");
+
+ ms[0].name = CFREE_SLICE_NULL;
+ ms[0].bytes.data = (const uint8_t*)"X";
+ ms[0].bytes.len = 1;
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) != CFREE_OK,
+ "NULL name rejected");
+
+ /* NULL symbol-name with nonzero count. */
+ bad_syms[0] = CFREE_SLICE_NULL;
+ msyms[0].names = bad_syms;
+ msyms[0].count = 1;
+ opts.symbol_index = 1;
+ opts.member_symbols = msyms;
+ ms[0].name = CFREE_SLICE_LIT("ok.o");
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) != CFREE_OK,
+ "NULL symbol name rejected");
+
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_iter_skips_bsd_symdef(void) {
+ /* Hand-craft an archive containing a BSD __.SYMDEF SORTED member; the
+ * iterator must not surface it. (cfree_ar_write never emits BSD indexes,
+ * but the iterator is documented to handle them on read.) */
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+ char hdr[60];
+ size_t j;
+ int seen = 0;
+
+ bufw_init(&bw);
+ bw.base.write(&bw.base, "!<arch>\n", 8);
+
+ /* __.SYMDEF SORTED with a 4-byte payload. */
+ for (j = 0; j < 60; ++j) hdr[j] = ' ';
+ {
+ const char* nm = "__.SYMDEF SORTED";
+ for (j = 0; j < 16 && nm[j]; ++j) hdr[j] = nm[j];
+ }
+ hdr[16] = '0';
+ hdr[28] = '0';
+ hdr[34] = '0';
+ hdr[40] = '6';
+ hdr[41] = '4';
+ hdr[42] = '4';
+ hdr[48] = '4'; /* size = 4 */
+ hdr[58] = '`';
+ hdr[59] = '\n';
+ bw.base.write(&bw.base, hdr, 60);
+ bw.base.write(&bw.base, "ZZZZ", 4);
+
+ /* User member u.o size=1 + parity pad. */
+ for (j = 0; j < 60; ++j) hdr[j] = ' ';
+ hdr[0] = 'u';
+ hdr[1] = '.';
+ hdr[2] = 'o';
+ hdr[3] = '/';
+ hdr[16] = '0';
+ hdr[28] = '0';
+ hdr[34] = '0';
+ hdr[40] = '6';
+ hdr[41] = '4';
+ hdr[42] = '4';
+ hdr[48] = '1';
+ hdr[58] = '`';
+ hdr[59] = '\n';
+ bw.base.write(&bw.base, hdr, 60);
+ bw.base.write(&bw.base, "U\n", 2);
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ while (cfree_ar_iter_next(it, &m)) {
+ EXPECT(cfree_slice_eq_cstr(m.name, "u.o"), "unexpected name = %.*s",
+ CFREE_SLICE_ARG(m.name));
+ seen++;
+ }
+ EXPECT(seen == 1, "saw %d members", seen);
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_iter_data_aliases_archive(void) {
+ /* CfreeArMember.data must point into the archive's own bytes. */
+ CfreeArInput ms[1];
+ BufW bw;
+ CfreeSlice in;
+ CfreeArIter* it = NULL;
+ CfreeArMember m;
+
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"PAYLOAD";
+ ms[0].bytes.len = 7;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
+
+ in.data = bw.data;
+ in.len = bw.len;
+ EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
+ EXPECT(cfree_ar_iter_next(it, &m), "first");
+ EXPECT(m.data >= bw.data && m.data + m.size <= bw.data + bw.len,
+ "data aliases archive bytes");
+ EXPECT(memcmp(m.data, "PAYLOAD", 7) == 0, "payload");
+
+ cfree_ar_iter_free(it);
+ bufw_fini(&bw);
+ return 1;
+}
+
+static int test_symbol_index_partial_members(void) {
+ /* Members with 0 symbols mid-list: cur_offset must still advance for
+ * every member so later offsets land on the right header. */
+ CfreeSlice mid_syms[] = {CFREE_SLICE_LIT("midsym")};
+ CfreeArMemberSymbols msyms[3];
+ CfreeArInput ms[3];
+ BufW bw;
+ CfreeArWriteOptions opts = {0};
+ uint32_t nsyms, off;
+ uint64_t expected_b_hdr;
+
+ opts.symbol_index = 1;
+ msyms[0].names = NULL;
+ msyms[0].count = 0;
+ msyms[1].names = mid_syms;
+ msyms[1].count = 1;
+ msyms[2].names = NULL;
+ msyms[2].count = 0;
+ opts.member_symbols = msyms;
+
+ ms[0].name = CFREE_SLICE_LIT("a.o");
+ ms[0].bytes.data = (const uint8_t*)"AA";
+ ms[0].bytes.len = 2;
+ ms[1].name = CFREE_SLICE_LIT("b.o");
+ ms[1].bytes.data = (const uint8_t*)"BB";
+ ms[1].bytes.len = 2;
+ ms[2].name = CFREE_SLICE_LIT("c.o");
+ ms[2].bytes.data = (const uint8_t*)"CC";
+ ms[2].bytes.len = 2;
+
+ bufw_init(&bw);
+ EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write");
+
+ /* Index payload: 4(count) + 4(offset) + 7("midsym\0") = 15 → odd → +1 pad. */
+ EXPECT(ar_size_field(bw.data + 8) == 15, "index payload = %llu",
+ (unsigned long long)ar_size_field(bw.data + 8));
+
+ nsyms = be32(bw.data + 8 + 60);
+ EXPECT(nsyms == 1, "nsyms = %u", nsyms);
+
+ off = be32(bw.data + 8 + 60 + 4);
+ /* magic(8) + index(60+15+1) + a.o(60+2) = 146 */
+ expected_b_hdr = 8 + 60 + 15 + 1 + 60 + 2;
+ EXPECT(off == expected_b_hdr, "off = %u, expected %llu", off,
+ (unsigned long long)expected_b_hdr);
+ EXPECT(memcmp(bw.data + off, "b.o/", 4) == 0, "b.o at offset");
+
+ bufw_fini(&bw);
+ return 1;
+}
+
+int main(void) {
+ int passes = 0;
+ int total = 0;
+#define RUN(t) \
+ do { \
+ total++; \
+ passes += (t)(); \
+ } while (0)
+
+ cfree_unit_init(&g_u);
+ RUN(test_basic_roundtrip);
+ RUN(test_long_name_table);
+ RUN(test_symbol_index_empty);
+ RUN(test_symbol_index_basic);
+ RUN(test_symbol_index_with_long_names);
+ RUN(test_iter_skips_index);
+ RUN(test_empty_archive);
+ RUN(test_epoch_field);
+ RUN(test_path_basename);
+ RUN(test_truncate_when_long_names_off);
+ RUN(test_name_15_char_boundary);
+ RUN(test_name_16_char_boundary);
+ RUN(test_empty_member_payload);
+ RUN(test_odd_payload_pad);
+ RUN(test_ar_list_output);
+ RUN(test_iter_bad_magic);
+ RUN(test_write_invalid_args);
+ RUN(test_iter_skips_bsd_symdef);
+ RUN(test_iter_data_aliases_archive);
+ RUN(test_symbol_index_partial_members);
+
+ if (g_u.fails) {
+ fprintf(stderr, "ar_test: %d failure(s) (%d/%d passed)\n", g_u.fails, passes,
+ total);
+ return 1;
+ }
+ printf("ar_test: OK (%d/%d)\n", passes, total);
+ return 0;
+}
diff --git a/test/ar/run.sh b/test/ar/run.sh
@@ -1,80 +1,33 @@
#!/bin/sh
# Driver-level `cfree ar` test harness.
#
-# Each test/ar/cases/*.sh is a scenario script. The harness allocates a
-# fresh sandbox dir per case, cd's in, sources the script with $CFREE
-# exported, captures stdout, and diffs against the matching .expected.
-# Stderr is forwarded straight through so write/read failures still
-# surface.
+# Each test/ar/cases/*.sh is a scenario script. The harness allocates a fresh
+# sandbox dir per case, cd's in, runs the script with $CFREE exported, captures
+# stdout+stderr, and diffs against the matching .expected. The shared loop,
+# reporting, and sandbox/cleanup live in test/lib/cfree_sh_report.sh.
#
-# Honors $CFREE for the binary path; defaults to build/cfree relative
-# to the repo root.
+# Honors $CFREE for the binary path; defaults to build/cfree.
set -u
script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree ar-driver
-if [ ! -x "$CFREE" ]; then
- echo "ar-driver: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-ar-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
-
-pass=0
-fail=0
-failures=
-
+CF_WORK=$(cf_workdir ar)
+cf_report_init
for sh in "$cases_dir"/*.sh; do
[ -e "$sh" ] || continue
name=$(basename "${sh%.sh}")
- expected="${sh%.sh}.expected"
- actual="$work_root/$name.actual"
-
- if [ ! -e "$expected" ]; then
- printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- sandbox="$work_root/$name"
- mkdir -p "$sandbox"
- ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1
- case_rc=$?
-
- if [ "$case_rc" -ne 0 ]; then
- printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- if diff -u "$expected" "$actual" >/dev/null 2>&1; then
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s\n' "$name"
- diff -u "$expected" "$actual" || true
- # Keep .actual on failure for debugging by copying out of the
- # sandbox before it's cleaned up.
- cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true
- fail=$((fail + 1))
- failures="$failures $name"
- fi
+ # Pass cases_dir so a failing case's .actual is kept there for debugging
+ # (the sandbox under CF_WORK is removed on exit).
+ cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir"
done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nar-driver: failures:%s\n' "$failures"
- printf 'ar-driver: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\nar-driver: %d/%d passed\n' "$pass" "$total"
+cf_summary ar-driver
+cf_exit
diff --git a/test/ar_test.c b/test/ar_test.c
@@ -1,991 +0,0 @@
-/* Round-trip tests for the cfree ar reader/writer.
- *
- * Builds against just include/cfree.h + libcfree.a and a few libc calls
- * (malloc/realloc/free, printf for diagnostics). cfree_ar_write itself
- * makes no heap allocations, so the test does not need a CfreeHeap.
- *
- * Set CFREE_AR_TEST_HOST=1 to also dump the produced symbol-index
- * archive to /tmp/cfree_ar_test.a and run the host's `ar t` and
- * `nm --print-armap` on it as a cross-check. */
-#include <cfree/archive.h>
-#include <cfree/core.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-/* ===== minimal CfreeWriter over a growing buffer ===== */
-
-typedef struct BufW {
- CfreeWriter base;
- uint8_t* data;
- size_t len;
- size_t cap;
- CfreeStatus st;
-} BufW;
-
-static CfreeStatus bufw_write(CfreeWriter* w, const void* data, size_t n) {
- BufW* b = (BufW*)w;
- if (b->st != CFREE_OK) return b->st;
- if (b->len + n > b->cap) {
- size_t nc = b->cap ? b->cap * 2 : 256;
- while (nc < b->len + n) nc *= 2;
- uint8_t* p = (uint8_t*)realloc(b->data, nc);
- if (!p) {
- b->st = CFREE_NOMEM;
- return CFREE_NOMEM;
- }
- b->data = p;
- b->cap = nc;
- }
- memcpy(b->data + b->len, data, n);
- b->len += n;
- return CFREE_OK;
-}
-
-static CfreeStatus bufw_seek(CfreeWriter* w, uint64_t off) {
- (void)w;
- (void)off;
- return CFREE_OK;
-}
-static uint64_t bufw_tell(CfreeWriter* w) { return ((BufW*)w)->len; }
-static CfreeStatus bufw_status(CfreeWriter* w) { return ((BufW*)w)->st; }
-static void bufw_close(CfreeWriter* w) { (void)w; }
-
-static void bufw_init(BufW* b) {
- b->base.write = bufw_write;
- b->base.seek = bufw_seek;
- b->base.tell = bufw_tell;
- b->base.status = bufw_status;
- b->base.close = bufw_close;
- b->data = NULL;
- b->len = 0;
- b->cap = 0;
- b->st = CFREE_OK;
-}
-
-static void bufw_fini(BufW* b) { free(b->data); }
-
-/* ===== libc heap + context (needed by cfree_ar_iter_new) ===== */
-
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static CfreeContext g_ctx = {.heap = &g_heap, .now = -1};
-
-/* ===== assertion helpers ===== */
-
-static int g_fail;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- g_fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fprintf(stderr, "\n"); \
- return 0; \
- } \
- } while (0)
-
-static uint32_t be32(const uint8_t* p) {
- return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
- ((uint32_t)p[2] << 8) | (uint32_t)p[3];
-}
-
-/* Decode the ar_size field (10-byte ASCII decimal, space-padded). */
-static uint64_t ar_size_field(const uint8_t* hdr) {
- uint64_t v = 0;
- int j;
- for (j = 48; j < 58; ++j) {
- char c = (char)hdr[j];
- if (c < '0' || c > '9') break;
- v = v * 10 + (uint64_t)(c - '0');
- }
- return v;
-}
-
-/* Decode the ar_date field (12-byte ASCII decimal, space-padded). */
-static uint64_t ar_date_field(const uint8_t* hdr) {
- uint64_t v = 0;
- int j;
- for (j = 16; j < 28; ++j) {
- char c = (char)hdr[j];
- if (c < '0' || c > '9') break;
- v = v * 10 + (uint64_t)(c - '0');
- }
- return v;
-}
-
-/* ===== tests ===== */
-
-static int test_basic_roundtrip(void) {
- CfreeArInput ms[2];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- int rc;
-
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"AAAA";
- ms[0].bytes.len = 4;
- ms[1].name = CFREE_SLICE_LIT("b.o");
- ms[1].bytes.data = (const uint8_t*)"BBBBB";
- ms[1].bytes.len = 5;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 2, NULL);
- EXPECT(rc == 0, "cfree_ar_write returned %d", rc);
- EXPECT(bw.st == CFREE_OK, "writer error");
- EXPECT(bw.len >= 8, "archive too short");
- EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
-
- EXPECT(cfree_ar_iter_next(it, &m), "first member");
- EXPECT(cfree_slice_eq_cstr(m.name, "a.o"), "name 0 = %.*s",
- CFREE_SLICE_ARG(m.name));
- EXPECT(m.size == 4 && memcmp(m.data, "AAAA", 4) == 0, "data 0");
-
- EXPECT(cfree_ar_iter_next(it, &m), "second member");
- EXPECT(cfree_slice_eq_cstr(m.name, "b.o"), "name 1 = %.*s",
- CFREE_SLICE_ARG(m.name));
- EXPECT(m.size == 5 && memcmp(m.data, "BBBBB", 5) == 0, "data 1");
-
- EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_long_name_table(void) {
- /* >15 chars triggers the // long-name table. */
- CfreeArInput ms[2];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- CfreeArWriteOptions opts = {0};
- int rc;
-
- opts.long_names = 1;
- ms[0].name = CFREE_SLICE_LIT("short.o");
- ms[0].bytes.data = (const uint8_t*)"x";
- ms[0].bytes.len = 1;
- ms[1].name = CFREE_SLICE_LIT("this_name_is_long_enough.o");
- ms[1].bytes.data = (const uint8_t*)"yy";
- ms[1].bytes.len = 2;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 2, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first member");
- EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "name 0 = %.*s",
- CFREE_SLICE_ARG(m.name));
- EXPECT(cfree_ar_iter_next(it, &m), "second member");
- EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"),
- "long name = %.*s", CFREE_SLICE_ARG(m.name));
- EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_symbol_index_empty(void) {
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- CfreeArWriteOptions opts = {0};
- int rc;
- uint32_t nsyms;
-
- opts.symbol_index = 1;
- ms[0].name = CFREE_SLICE_LIT("lonely.o");
- ms[0].bytes.data = (const uint8_t*)"P";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 1, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
-
- /* First member after magic must be `/` index with count=0. */
- EXPECT(bw.len >= 8 + 60 + 4, "archive too short");
- EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index name field");
- EXPECT(ar_size_field(bw.data + 8) == 4, "index payload size = 4");
- nsyms = be32(bw.data + 8 + 60);
- EXPECT(nsyms == 0, "nsyms = %u", nsyms);
-
- /* Iterator should skip the `/` and yield only the user member. */
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first user member");
- EXPECT(cfree_slice_eq_cstr(m.name, "lonely.o"), "name = %.*s",
- CFREE_SLICE_ARG(m.name));
- EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_symbol_index_basic(void) {
- /* 2 members with symbol lists; verify count, offsets, and names. */
- CfreeSlice a_syms[] = {CFREE_SLICE_LIT("foo"), CFREE_SLICE_LIT("bar")};
- CfreeSlice b_syms[] = {CFREE_SLICE_LIT("baz")};
- CfreeArMemberSymbols msyms[2];
- CfreeArInput ms[2];
- BufW bw;
- CfreeArWriteOptions opts = {0};
- int rc;
- uint32_t nsyms;
- const uint8_t* p;
- uint64_t index_payload;
- uint32_t off0, off1, off2;
- uint64_t a_hdr_off, b_hdr_off;
- const char* name;
-
- opts.symbol_index = 1;
- opts.long_names = 1;
-
- msyms[0].names = a_syms;
- msyms[0].count = 2;
- msyms[1].names = b_syms;
- msyms[1].count = 1;
- opts.member_symbols = msyms;
-
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"AAAA";
- ms[0].bytes.len = 4;
- ms[1].name = CFREE_SLICE_LIT("b.o");
- ms[1].bytes.data = (const uint8_t*)"BBBBB";
- ms[1].bytes.len = 5;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 2, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
-
- EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic");
- EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index ar_name field");
-
- index_payload = ar_size_field(bw.data + 8);
- /* 4 (count) + 4*3 (offsets) + 3+1 + 3+1 + 3+1 (names "foo\0bar\0baz\0") = 28
- */
- EXPECT(index_payload == 28, "index payload = %llu",
- (unsigned long long)index_payload);
-
- p = bw.data + 8 + 60;
- nsyms = be32(p);
- p += 4;
- EXPECT(nsyms == 3, "nsyms = %u", nsyms);
-
- off0 = be32(p); /* foo → a.o */
- off1 = be32(p + 4); /* bar → a.o */
- off2 = be32(p + 8); /* baz → b.o */
- p += 12;
-
- /* Compute expected header offsets: index_total = 60+28 (no pad, even).
- * No long-name table is emitted (basenames ≤ 15 chars). So:
- * a.o header at offset 8 + 88 = 96
- * b.o header at offset 96 + 60 + 4 (+0 pad) = 160 */
- a_hdr_off = 8 + 60 + 28;
- b_hdr_off = a_hdr_off + 60 + 4;
- EXPECT(off0 == a_hdr_off, "off0 = %u, expected %llu", off0,
- (unsigned long long)a_hdr_off);
- EXPECT(off1 == a_hdr_off, "off1 = %u, expected %llu", off1,
- (unsigned long long)a_hdr_off);
- EXPECT(off2 == b_hdr_off, "off2 = %u, expected %llu", off2,
- (unsigned long long)b_hdr_off);
-
- /* Sanity: the member headers must actually live at those offsets. */
- EXPECT(memcmp(bw.data + a_hdr_off, "a.o/", 4) == 0, "a.o at offset");
- EXPECT(memcmp(bw.data + b_hdr_off, "b.o/", 4) == 0, "b.o at offset");
-
- /* Names: "foo\0bar\0baz\0" */
- name = (const char*)p;
- EXPECT(strcmp(name, "foo") == 0, "name 0 = %s", name);
- name += strlen(name) + 1;
- EXPECT(strcmp(name, "bar") == 0, "name 1 = %s", name);
- name += strlen(name) + 1;
- EXPECT(strcmp(name, "baz") == 0, "name 2 = %s", name);
-
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_symbol_index_with_long_names(void) {
- /* `/` member must come BEFORE `//` long-name table, and offsets must
- * still point at correct member-header positions. */
- CfreeSlice syms0[] = {CFREE_SLICE_LIT("alpha")};
- CfreeSlice syms1[] = {CFREE_SLICE_LIT("beta")};
- CfreeArMemberSymbols msyms[2];
- CfreeArInput ms[2];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- CfreeArWriteOptions opts = {0};
- int rc;
- uint64_t index_payload, longtab_payload;
- uint64_t index_total, longtab_total;
- uint64_t m0_hdr, m1_hdr;
- uint32_t off0, off1;
- const uint8_t* p;
-
- opts.symbol_index = 1;
- opts.long_names = 1;
- msyms[0].names = syms0;
- msyms[0].count = 1;
- msyms[1].names = syms1;
- msyms[1].count = 1;
- opts.member_symbols = msyms;
-
- ms[0].name =
- CFREE_SLICE_LIT("this_name_is_long_enough.o"); /* 26 chars → // */
- ms[0].bytes.data = (const uint8_t*)"X";
- ms[0].bytes.len = 1;
- ms[1].name = CFREE_SLICE_LIT("short.o");
- ms[1].bytes.data = (const uint8_t*)"YY";
- ms[1].bytes.len = 2;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 2, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
-
- /* Layout: magic(8) | / index member | // long-name member | members. */
- EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "/ first");
- index_payload = ar_size_field(bw.data + 8);
- /* 4 (count) + 4*2 (offsets) + 6 ("alpha\0") + 5 ("beta\0") = 23 → odd → pad 1
- */
- EXPECT(index_payload == 23, "index_payload = %llu",
- (unsigned long long)index_payload);
- index_total = 60 + index_payload + (index_payload & 1);
-
- /* Verify // header sits at 8 + index_total. */
- EXPECT(bw.data[8 + index_total] == '/', "// pos byte 0");
- EXPECT(bw.data[8 + index_total + 1] == '/', "// pos byte 1");
- longtab_payload = ar_size_field(bw.data + 8 + index_total);
- /* "this_name_is_long_enough.o/\n" = 28 bytes → even → no pad */
- EXPECT(longtab_payload == 28, "longtab payload = %llu",
- (unsigned long long)longtab_payload);
- longtab_total = 60 + longtab_payload + (longtab_payload & 1);
-
- m0_hdr = 8 + index_total + longtab_total;
- m1_hdr = m0_hdr + 60 + 1 + 1 /* parity pad for odd len 1 */;
-
- p = bw.data + 8 + 60;
- EXPECT(be32(p) == 2, "nsyms = %u", be32(p));
- p += 4;
- off0 = be32(p);
- off1 = be32(p + 4);
- EXPECT(off0 == m0_hdr, "off0 = %u, expected %llu", off0,
- (unsigned long long)m0_hdr);
- EXPECT(off1 == m1_hdr, "off1 = %u, expected %llu", off1,
- (unsigned long long)m1_hdr);
- /* Spot-check m1 starts with "short.o/" for sanity. */
- EXPECT(memcmp(bw.data + m1_hdr, "short.o/", 8) == 0, "m1 hdr");
-
- /* Iterator should walk past both /, // and yield 2 members. */
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "m0");
- EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"),
- "m0 name = %.*s", CFREE_SLICE_ARG(m.name));
- EXPECT(cfree_ar_iter_next(it, &m), "m1");
- EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "m1 name = %.*s",
- CFREE_SLICE_ARG(m.name));
- EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
-
- /* Optional host cross-check. */
- if (getenv("CFREE_AR_TEST_HOST")) {
- FILE* f = fopen("/tmp/cfree_ar_test.a", "wb");
- if (f) {
- fwrite(bw.data, 1, bw.len, f);
- fclose(f);
- fprintf(stderr, "host cross-check: ar t /tmp/cfree_ar_test.a\n");
- (void)!system("ar t /tmp/cfree_ar_test.a");
- fprintf(stderr,
- "host cross-check: nm --print-armap /tmp/cfree_ar_test.a\n");
- (void)!system("nm --print-armap /tmp/cfree_ar_test.a 2>&1 || true");
- }
- }
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_iter_skips_index(void) {
- /* Make sure the iterator never surfaces the `/` member as a user member. */
- CfreeSlice s[] = {CFREE_SLICE_LIT("only_sym")};
- CfreeArMemberSymbols msyms[1];
- CfreeArInput ms[1];
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- BufW bw;
- CfreeArWriteOptions opts = {0};
- int seen = 0;
-
- opts.symbol_index = 1;
- msyms[0].names = s;
- msyms[0].count = 1;
- opts.member_symbols = msyms;
- ms[0].name = CFREE_SLICE_LIT("only.o");
- ms[0].bytes.data = (const uint8_t*)"Z";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- while (cfree_ar_iter_next(it, &m)) {
- EXPECT(!cfree_slice_eq_cstr(m.name, "/"), "iter surfaced raw `/` member");
- seen++;
- }
- EXPECT(seen == 1, "saw %d members", seen);
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_empty_archive(void) {
- /* nmembers == 0 with NULL members should produce a magic-only archive. */
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- int rc;
-
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, NULL, 0, NULL);
- EXPECT(rc == 0, "write rc=%d", rc);
- EXPECT(bw.len == 8, "size = %zu", bw.len);
- EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic only");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(!cfree_ar_iter_next(it, &m), "no members");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_epoch_field(void) {
- /* opts.epoch is written into ar_date for every member. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeArWriteOptions opts = {0};
- int rc;
-
- ms[0].name = CFREE_SLICE_LIT("x.o");
- ms[0].bytes.data = (const uint8_t*)"q";
- ms[0].bytes.len = 1;
-
- opts.epoch = 1234567890u;
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 1, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
- EXPECT(ar_date_field(bw.data + 8) == 1234567890u, "ar_date = %llu",
- (unsigned long long)ar_date_field(bw.data + 8));
- bufw_fini(&bw);
-
- /* Default (epoch=0): single '0' followed by spaces. */
- opts.epoch = 0;
- bufw_init(&bw);
- rc = cfree_ar_write(&bw.base, ms, 1, &opts);
- EXPECT(rc == 0, "write rc=%d", rc);
- EXPECT(bw.data[8 + 16] == '0', "epoch default first byte");
- EXPECT(bw.data[8 + 17] == ' ', "epoch default second byte = 0x%02x",
- bw.data[8 + 17]);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_path_basename(void) {
- /* Member name with path components is stored as basename only. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
-
- ms[0].name = CFREE_SLICE_LIT("src/sub/foo.o");
- ms[0].bytes.data = (const uint8_t*)"D";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(cfree_slice_eq_cstr(m.name, "foo.o"), "basename = %.*s",
- CFREE_SLICE_ARG(m.name));
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_truncate_when_long_names_off(void) {
- /* >15 chars without long_names: name is truncated to 15. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
-
- ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnopqrst.o"); /* 22 chars */
- ms[0].bytes.data = (const uint8_t*)"D";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "truncated = %.*s",
- CFREE_SLICE_ARG(m.name));
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_name_15_char_boundary(void) {
- /* Exactly 15 chars: fits in-header even with long_names enabled. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- CfreeArWriteOptions opts = {0};
-
- opts.long_names = 1;
- ms[0].name = CFREE_SLICE_LIT("abcdefghijklmno"); /* 15 chars */
- ms[0].bytes.data = (const uint8_t*)"X";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
-
- /* No `//` long-name table: first member sits right after magic. */
- EXPECT(memcmp(bw.data + 8, "abcdefghijklmno/", 16) == 0, "name field = %.16s",
- (const char*)(bw.data + 8));
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "name = %.*s",
- CFREE_SLICE_ARG(m.name));
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_name_16_char_boundary(void) {
- /* Exactly 16 chars: triggers // long-name table. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- CfreeArWriteOptions opts = {0};
-
- opts.long_names = 1;
- ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnop"); /* 16 chars */
- ms[0].bytes.data = (const uint8_t*)"Y";
- ms[0].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write");
- EXPECT(bw.data[8] == '/' && bw.data[9] == '/', "// header");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmnop"), "name = %.*s",
- CFREE_SLICE_ARG(m.name));
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_empty_member_payload(void) {
- /* len=0 (data=NULL): header only, no pad; followed by the next member. */
- CfreeArInput ms[2];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
-
- ms[0].name = CFREE_SLICE_LIT("empty.o");
- ms[0].bytes.data = NULL;
- ms[0].bytes.len = 0;
- ms[1].name = CFREE_SLICE_LIT("next.o");
- ms[1].bytes.data = (const uint8_t*)"N";
- ms[1].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 2, NULL) == 0, "write");
- /* magic(8) + hdr(60) + 0 + hdr(60) + 1 + pad(1) = 130 */
- EXPECT(bw.len == 130, "size = %zu", bw.len);
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(cfree_slice_eq_cstr(m.name, "empty.o") && m.size == 0,
- "empty.o size=%zu", m.size);
- EXPECT(cfree_ar_iter_next(it, &m), "second");
- EXPECT(
- cfree_slice_eq_cstr(m.name, "next.o") && m.size == 1 && m.data[0] == 'N',
- "next.o");
- EXPECT(!cfree_ar_iter_next(it, &m), "iter end");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_odd_payload_pad(void) {
- /* Odd-length payloads add a '\n' parity pad; even lengths do not. */
- CfreeArInput ms[3];
- BufW bw;
-
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"x";
- ms[0].bytes.len = 1;
- ms[1].name = CFREE_SLICE_LIT("b.o");
- ms[1].bytes.data = (const uint8_t*)"yy";
- ms[1].bytes.len = 2;
- ms[2].name = CFREE_SLICE_LIT("c.o");
- ms[2].bytes.data = (const uint8_t*)"z";
- ms[2].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 3, NULL) == 0, "write");
- /* 8 + (60+1+1) + (60+2) + (60+1+1) = 194 */
- EXPECT(bw.len == 194, "size = %zu", bw.len);
- EXPECT(bw.data[8 + 60 + 1] == '\n', "pad after a.o = 0x%02x",
- bw.data[8 + 60 + 1]);
- /* No pad after b.o (even): next header begins immediately. */
- EXPECT(bw.data[8 + 62 + 60 + 2] == 'c', "c.o name follows b.o without pad");
-
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_ar_list_output(void) {
- /* cfree_ar_list emits one user member per line, skipping / and //. */
- CfreeArInput ms[3];
- BufW bw, lw;
- CfreeSlice in;
- CfreeArWriteOptions opts = {0};
- const char* expected = "a.o\nlong_name_member.o\nb.o\n";
-
- opts.symbol_index = 1;
- opts.long_names = 1;
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"A";
- ms[0].bytes.len = 1;
- ms[1].name = CFREE_SLICE_LIT("long_name_member.o");
- ms[1].bytes.data = (const uint8_t*)"B";
- ms[1].bytes.len = 1;
- ms[2].name = CFREE_SLICE_LIT("b.o");
- ms[2].bytes.data = (const uint8_t*)"C";
- ms[2].bytes.len = 1;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write");
-
- in.data = bw.data;
- in.len = bw.len;
- bufw_init(&lw);
- EXPECT(cfree_ar_list(&in, &lw.base) == 0, "list");
- EXPECT(lw.len == strlen(expected), "list len = %zu, want %zu", lw.len,
- strlen(expected));
- EXPECT(memcmp(lw.data, expected, lw.len) == 0, "list = %.*s", (int)lw.len,
- (const char*)lw.data);
-
- bufw_fini(&lw);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_iter_bad_magic(void) {
- /* iter_init must reject non-ar inputs. */
- static const uint8_t bad[] = "NOT-AN-AR";
- CfreeSlice in;
- CfreeArIter* it = NULL;
-
- in.data = bad;
- in.len = sizeof(bad) - 1;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects bad magic");
-
- in.data = NULL;
- in.len = 0;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects empty");
-
- in.data = bad;
- in.len = 4; /* too short */
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects short");
-
- return 1;
-}
-
-static int test_write_invalid_args(void) {
- /* Bad argument combinations must return 1 from cfree_ar_write. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeArWriteOptions opts = {0};
- CfreeSlice bad_syms[1];
- CfreeArMemberSymbols msyms[1];
-
- bufw_init(&bw);
-
- EXPECT(cfree_ar_write(NULL, NULL, 0, NULL) != CFREE_OK,
- "NULL writer rejected");
- EXPECT(cfree_ar_write(&bw.base, NULL, 1, NULL) != CFREE_OK,
- "NULL members with nmembers>0 rejected");
-
- ms[0].name = CFREE_SLICE_NULL;
- ms[0].bytes.data = (const uint8_t*)"X";
- ms[0].bytes.len = 1;
- EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) != CFREE_OK,
- "NULL name rejected");
-
- /* NULL symbol-name with nonzero count. */
- bad_syms[0] = CFREE_SLICE_NULL;
- msyms[0].names = bad_syms;
- msyms[0].count = 1;
- opts.symbol_index = 1;
- opts.member_symbols = msyms;
- ms[0].name = CFREE_SLICE_LIT("ok.o");
- EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) != CFREE_OK,
- "NULL symbol name rejected");
-
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_iter_skips_bsd_symdef(void) {
- /* Hand-craft an archive containing a BSD __.SYMDEF SORTED member; the
- * iterator must not surface it. (cfree_ar_write never emits BSD indexes,
- * but the iterator is documented to handle them on read.) */
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
- char hdr[60];
- size_t j;
- int seen = 0;
-
- bufw_init(&bw);
- bw.base.write(&bw.base, "!<arch>\n", 8);
-
- /* __.SYMDEF SORTED with a 4-byte payload. */
- for (j = 0; j < 60; ++j) hdr[j] = ' ';
- {
- const char* nm = "__.SYMDEF SORTED";
- for (j = 0; j < 16 && nm[j]; ++j) hdr[j] = nm[j];
- }
- hdr[16] = '0';
- hdr[28] = '0';
- hdr[34] = '0';
- hdr[40] = '6';
- hdr[41] = '4';
- hdr[42] = '4';
- hdr[48] = '4'; /* size = 4 */
- hdr[58] = '`';
- hdr[59] = '\n';
- bw.base.write(&bw.base, hdr, 60);
- bw.base.write(&bw.base, "ZZZZ", 4);
-
- /* User member u.o size=1 + parity pad. */
- for (j = 0; j < 60; ++j) hdr[j] = ' ';
- hdr[0] = 'u';
- hdr[1] = '.';
- hdr[2] = 'o';
- hdr[3] = '/';
- hdr[16] = '0';
- hdr[28] = '0';
- hdr[34] = '0';
- hdr[40] = '6';
- hdr[41] = '4';
- hdr[42] = '4';
- hdr[48] = '1';
- hdr[58] = '`';
- hdr[59] = '\n';
- bw.base.write(&bw.base, hdr, 60);
- bw.base.write(&bw.base, "U\n", 2);
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- while (cfree_ar_iter_next(it, &m)) {
- EXPECT(cfree_slice_eq_cstr(m.name, "u.o"), "unexpected name = %.*s",
- CFREE_SLICE_ARG(m.name));
- seen++;
- }
- EXPECT(seen == 1, "saw %d members", seen);
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_iter_data_aliases_archive(void) {
- /* CfreeArMember.data must point into the archive's own bytes. */
- CfreeArInput ms[1];
- BufW bw;
- CfreeSlice in;
- CfreeArIter* it = NULL;
- CfreeArMember m;
-
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"PAYLOAD";
- ms[0].bytes.len = 7;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write");
-
- in.data = bw.data;
- in.len = bw.len;
- EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init");
- EXPECT(cfree_ar_iter_next(it, &m), "first");
- EXPECT(m.data >= bw.data && m.data + m.size <= bw.data + bw.len,
- "data aliases archive bytes");
- EXPECT(memcmp(m.data, "PAYLOAD", 7) == 0, "payload");
-
- cfree_ar_iter_free(it);
- bufw_fini(&bw);
- return 1;
-}
-
-static int test_symbol_index_partial_members(void) {
- /* Members with 0 symbols mid-list: cur_offset must still advance for
- * every member so later offsets land on the right header. */
- CfreeSlice mid_syms[] = {CFREE_SLICE_LIT("midsym")};
- CfreeArMemberSymbols msyms[3];
- CfreeArInput ms[3];
- BufW bw;
- CfreeArWriteOptions opts = {0};
- uint32_t nsyms, off;
- uint64_t expected_b_hdr;
-
- opts.symbol_index = 1;
- msyms[0].names = NULL;
- msyms[0].count = 0;
- msyms[1].names = mid_syms;
- msyms[1].count = 1;
- msyms[2].names = NULL;
- msyms[2].count = 0;
- opts.member_symbols = msyms;
-
- ms[0].name = CFREE_SLICE_LIT("a.o");
- ms[0].bytes.data = (const uint8_t*)"AA";
- ms[0].bytes.len = 2;
- ms[1].name = CFREE_SLICE_LIT("b.o");
- ms[1].bytes.data = (const uint8_t*)"BB";
- ms[1].bytes.len = 2;
- ms[2].name = CFREE_SLICE_LIT("c.o");
- ms[2].bytes.data = (const uint8_t*)"CC";
- ms[2].bytes.len = 2;
-
- bufw_init(&bw);
- EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write");
-
- /* Index payload: 4(count) + 4(offset) + 7("midsym\0") = 15 → odd → +1 pad. */
- EXPECT(ar_size_field(bw.data + 8) == 15, "index payload = %llu",
- (unsigned long long)ar_size_field(bw.data + 8));
-
- nsyms = be32(bw.data + 8 + 60);
- EXPECT(nsyms == 1, "nsyms = %u", nsyms);
-
- off = be32(bw.data + 8 + 60 + 4);
- /* magic(8) + index(60+15+1) + a.o(60+2) = 146 */
- expected_b_hdr = 8 + 60 + 15 + 1 + 60 + 2;
- EXPECT(off == expected_b_hdr, "off = %u, expected %llu", off,
- (unsigned long long)expected_b_hdr);
- EXPECT(memcmp(bw.data + off, "b.o/", 4) == 0, "b.o at offset");
-
- bufw_fini(&bw);
- return 1;
-}
-
-int main(void) {
- int passes = 0;
- int total = 0;
-#define RUN(t) \
- do { \
- total++; \
- passes += (t)(); \
- } while (0)
-
- RUN(test_basic_roundtrip);
- RUN(test_long_name_table);
- RUN(test_symbol_index_empty);
- RUN(test_symbol_index_basic);
- RUN(test_symbol_index_with_long_names);
- RUN(test_iter_skips_index);
- RUN(test_empty_archive);
- RUN(test_epoch_field);
- RUN(test_path_basename);
- RUN(test_truncate_when_long_names_off);
- RUN(test_name_15_char_boundary);
- RUN(test_name_16_char_boundary);
- RUN(test_empty_member_payload);
- RUN(test_odd_payload_pad);
- RUN(test_ar_list_output);
- RUN(test_iter_bad_magic);
- RUN(test_write_invalid_args);
- RUN(test_iter_skips_bsd_symdef);
- RUN(test_iter_data_aliases_archive);
- RUN(test_symbol_index_partial_members);
-
- if (g_fail) {
- fprintf(stderr, "ar_test: %d failure(s) (%d/%d passed)\n", g_fail, passes,
- total);
- return 1;
- }
- printf("ar_test: OK (%d/%d)\n", passes, total);
- return 0;
-}
diff --git a/test/arch/aa64_inline_test.c b/test/arch/aa64_inline_test.c
@@ -76,8 +76,8 @@ int main(void) {
aa64_bad_ldr_zr, "zero register"),
"expected ldr w0, [xzr] to panic");
- if (env.fail) {
- fprintf(stderr, "%d failure(s)\n", env.fail);
+ if (env.fails) {
+ fprintf(stderr, "%d failure(s)\n", env.fails);
return 1;
}
printf("aa64_inline_test: ok\n");
diff --git a/test/arch/aa64_isa_test.c b/test/arch/aa64_isa_test.c
@@ -16,23 +16,27 @@
#include "arch/aa64/isa.h"
#include "core/strbuf.h"
+#include "lib/cfree_unit.h"
-static int fails = 0;
-static int cases = 0;
+/* Shared test context replaces the per-file fails/cases counter globals; each
+ * check() invocation records exactly one case on g_u (one ++g_u.checks, plus
+ * ++g_u.fails on a miss) so the "N cases ok" tally and exit status are
+ * unchanged. */
+static CfreeUnit g_u;
static void check(u32 word, const char* want_mnem,
const char* want_ops_substr) {
- ++cases;
+ ++g_u.checks;
const AA64InsnDesc* d = aa64_disasm_find(word);
if (!d) {
fprintf(stderr, "FAIL 0x%08x: aa64_disasm_find returned NULL\n", word);
- ++fails;
+ ++g_u.fails;
return;
}
if (!slice_eq_cstr(d->mnemonic, want_mnem)) {
fprintf(stderr, "FAIL 0x%08x: mnemonic = %.*s, want %s\n", word,
SLICE_ARG(d->mnemonic), want_mnem);
- ++fails;
+ ++g_u.fails;
return;
}
char buf[128];
@@ -42,18 +46,20 @@ static void check(u32 word, const char* want_mnem,
if (sb.truncated) {
fprintf(stderr, "FAIL 0x%08x %s: operand print truncated\n", word,
want_mnem);
- ++fails;
+ ++g_u.fails;
return;
}
if (want_ops_substr && !strstr(buf, want_ops_substr)) {
fprintf(stderr, "FAIL 0x%08x %s: operands=%s\n expected substring %s\n",
word, want_mnem, buf, want_ops_substr);
- ++fails;
+ ++g_u.fails;
return;
}
}
int main(void) {
+ cfree_unit_init(&g_u);
+
/* ---- per-format coverage ---- */
/* MOVEWIDE: movz x0, #0x1234 → sf=1, opc=10, hw=0, imm16=0x1234, Rd=0 */
@@ -177,12 +183,12 @@ int main(void) {
stderr,
"FAIL: alias precedence — ORR-as-MOV resolved to %.*s (want mov)\n",
SLICE_ARG(d ? d->mnemonic : SLICE_LIT("(null)")));
- ++fails;
+ ++g_u.fails;
} else if (!(d->flags & AA64_ASMFL_ALIAS)) {
fprintf(stderr, "FAIL: alias precedence — resolved row missing ALIAS\n");
- ++fails;
+ ++g_u.fails;
}
- ++cases;
+ ++g_u.checks;
}
/* ORR X1, X3, X2 (Rn != ZR) must not resolve to "mov". */
@@ -192,32 +198,32 @@ int main(void) {
if (d == NULL || !slice_eq_cstr(d->mnemonic, "orr")) {
fprintf(stderr, "FAIL: non-alias ORR resolved to %.*s (want orr)\n",
SLICE_ARG(d ? d->mnemonic : SLICE_LIT("(null)")));
- ++fails;
+ ++g_u.fails;
}
- ++cases;
+ ++g_u.checks;
}
/* Logical immediates: 0xff00 must not be encoded as the rotated
* 0xff000000 mask. */
{
u32 N = 0, immr = 0, imms = 0;
- ++cases;
+ ++g_u.checks;
if (!aa64_logimm_encode(0xff00u, 0, &N, &immr, &imms)) {
fprintf(stderr, "FAIL: aa64_logimm_encode rejected 0xff00\n");
- ++fails;
+ ++g_u.fails;
} else {
u32 w = aa64_and_imm(0, 19, 19, N, immr, imms);
if (w != 0x12181e73u) {
fprintf(stderr, "FAIL: and w19,w19,#0xff00 encoded 0x%08x\n", w);
- ++fails;
+ ++g_u.fails;
}
}
}
- if (fails) {
- fprintf(stderr, "%d / %d failed\n", fails, cases);
+ if (g_u.fails) {
+ fprintf(stderr, "%d / %d failed\n", g_u.fails, g_u.checks);
return 1;
}
- printf("aa64_isa_test: %d cases ok\n", cases);
+ printf("aa64_isa_test: %d cases ok\n", g_u.checks);
return 0;
}
diff --git a/test/arch/inline_public_test.h b/test/arch/inline_public_test.h
@@ -4,19 +4,24 @@
#include <cfree/cg.h>
#include <cfree/frontend.h>
#include <cfree/object.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-typedef struct InlineTestEnv {
- CfreeHeap heap;
- CfreeDiagSink diag;
- CfreeContext ctx;
- char last_diag[256];
- int suppress_diag;
- int fail;
-} InlineTestEnv;
+#include "lib/cfree_unit.h"
+
+/* Inline-asm backend tests build on the shared unit scaffolding: the test env
+ * is a CfreeUnit (host heap + diag sink that captures last_diag + ctx +
+ * fail/check counters). Only the inline-asm-specific emit/panic/operand
+ * helpers live here; the heap/diag/check/target boilerplate comes from
+ * cfree_unit.h. */
+typedef CfreeUnit InlineTestEnv;
+
+/* Like cfree_unit_init but with now=-1, the value this harness has always
+ * used (inert for these byte-level assertions). */
+#define it_env_init(env) (cfree_unit_init(env), (void)((env)->ctx.now = -1))
+
+/* IT_EXPECT / it_contains map onto the shared check + byte-search helpers so
+ * existing call sites read unchanged. */
+#define IT_EXPECT(env, cond, ...) CU_EXPECT(env, cond, __VA_ARGS__)
+#define it_contains cfree_unit_contains
typedef struct InlineText {
CfreeWriter* writer;
@@ -34,85 +39,11 @@ typedef struct InlineEmit {
const char* name;
} InlineEmit;
-static void* it_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* it_realloc(CfreeHeap* h, void* p, size_t old_n, size_t n,
- size_t a) {
- (void)h;
- (void)old_n;
- (void)a;
- return realloc(p, n);
-}
-
-static void it_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-
-static void it_diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- InlineTestEnv* env = (InlineTestEnv*)s->user;
- va_list copy;
- (void)loc;
- va_copy(copy, ap);
- vsnprintf(env->last_diag, sizeof env->last_diag, fmt, copy);
- va_end(copy);
- if (env->suppress_diag && k == CFREE_DIAG_FATAL) return;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static void it_env_init(InlineTestEnv* env) {
- memset(env, 0, sizeof *env);
- env->heap.alloc = it_alloc;
- env->heap.realloc = it_realloc;
- env->heap.free = it_free;
- env->diag.emit = it_diag_emit;
- env->diag.user = env;
- env->ctx.heap = &env->heap;
- env->ctx.diag = &env->diag;
- env->ctx.now = -1;
-}
-
-#define IT_EXPECT(env, cond, ...) \
- do { \
- if (!(cond)) { \
- (env)->fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
-
-static CfreeTarget it_target(CfreeArchKind arch) {
- CfreeTarget t;
- memset(&t, 0, sizeof t);
- t.arch = arch;
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
- t.ptr_size = 8;
- t.ptr_align = 8;
- return t;
-}
-
-static int it_contains(const uint8_t* data, size_t len, const uint8_t* pat,
- size_t pat_len) {
- size_t i;
- if (pat_len == 0) return 1;
- if (len < pat_len) return 0;
- for (i = 0; i <= len - pat_len; ++i) {
- if (memcmp(data + i, pat, pat_len) == 0) return 1;
- }
- return 0;
+static inline CfreeTarget it_target(CfreeArchKind arch) {
+ return cfree_unit_target(arch, CFREE_OS_LINUX, CFREE_OBJ_ELF);
}
-static CfreeStatus it_emit_func(CfreeCompiler* c, void* user) {
+static inline CfreeStatus it_emit_func(CfreeCompiler* c, void* user) {
InlineEmit* emit = (InlineEmit*)user;
CfreeCg* cg = NULL;
CfreeCgBuiltinTypes bi;
@@ -150,8 +81,9 @@ static CfreeStatus it_emit_func(CfreeCompiler* c, void* user) {
return CFREE_OK;
}
-static int it_emit_text(InlineTestEnv* env, CfreeArchKind arch,
- const char* name, InlineBodyFn body, InlineText* text) {
+static inline int it_emit_text(InlineTestEnv* env, CfreeArchKind arch,
+ const char* name, InlineBodyFn body,
+ InlineText* text) {
CfreeCompiler* c = NULL;
InlineEmit emit;
CfreeSlice bytes;
@@ -195,7 +127,7 @@ done:
return ok;
}
-static void it_text_close(InlineText* text) {
+static inline void it_text_close(InlineText* text) {
if (text->file) cfree_obj_free(text->file);
if (text->writer) cfree_writer_close(text->writer);
memset(text, 0, sizeof *text);
@@ -206,7 +138,7 @@ typedef struct InlinePanic {
const char* name;
} InlinePanic;
-static CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) {
+static inline CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) {
InlinePanic* panic = (InlinePanic*)user;
InlineEmit emit;
CfreeStatus st;
@@ -218,9 +150,9 @@ static CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) {
return st;
}
-static int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch,
- const char* name, InlineBodyFn body,
- const char* expected) {
+static inline int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch,
+ const char* name, InlineBodyFn body,
+ const char* expected) {
CfreeCompiler* c = NULL;
InlinePanic panic;
CfreeStatus st;
@@ -230,18 +162,19 @@ static int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch,
panic.body = body;
panic.name = name;
env->last_diag[0] = '\0';
- env->suppress_diag++;
+ env->suppress_fatal++;
st = cfree_frontend_run(c, it_run_panic_body, &panic);
- env->suppress_diag--;
+ env->suppress_fatal--;
ok = st == CFREE_ERR &&
(!expected || strstr(env->last_diag, expected) != NULL);
cfree_compiler_free(c);
return ok;
}
-static CfreeCgAsmOperand it_asm_op(CfreeCompiler* c, const char* constraint,
- const char* name, CfreeCgTypeId type,
- CfreeCgAsmDir dir) {
+static inline CfreeCgAsmOperand it_asm_op(CfreeCompiler* c,
+ const char* constraint,
+ const char* name, CfreeCgTypeId type,
+ CfreeCgAsmDir dir) {
CfreeCgAsmOperand op;
memset(&op, 0, sizeof op);
op.constraint = cfree_sym_intern(c, cfree_slice_cstr(constraint));
@@ -251,11 +184,12 @@ static CfreeCgAsmOperand it_asm_op(CfreeCompiler* c, const char* constraint,
return op;
}
-static void it_inline_asm(CfreeCompiler* c, CfreeCg* cg, const char* tmpl,
- const CfreeCgAsmOperand* outs, uint32_t nouts,
- const CfreeCgAsmOperand* ins, uint32_t nins,
- const char* const* clobber_names,
- uint32_t nclobbers) {
+static inline void it_inline_asm(CfreeCompiler* c, CfreeCg* cg,
+ const char* tmpl,
+ const CfreeCgAsmOperand* outs, uint32_t nouts,
+ const CfreeCgAsmOperand* ins, uint32_t nins,
+ const char* const* clobber_names,
+ uint32_t nclobbers) {
CfreeCgInlineAsm a;
CfreeSym clobbers[8];
uint32_t i;
diff --git a/test/arch/rv64_decode_test.c b/test/arch/rv64_decode_test.c
@@ -13,61 +13,18 @@
#include "arch/arch.h"
#include "arch/rv64/isa.h"
+#include "lib/cfree_unit.h"
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-static CfreeContext g_ctx;
-
-static int fails;
-
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static CfreeCompiler* new_compiler(void) {
- CfreeTarget t;
+ CfreeTarget t = cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX,
+ CFREE_OBJ_ELF);
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof(t));
- t.arch = CFREE_ARCH_RV64;
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof(g_ctx));
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "compiler_new failed\n");
exit(2);
}
@@ -161,14 +118,13 @@ static void format_decoded_record(CfreeCompiler* pub) {
}
int main(void) {
- CfreeCompiler* c = new_compiler();
+ CfreeCompiler* c;
+ cfree_unit_init(&g_u);
+ c = new_compiler();
decode_addi(c);
decode_block_stops_at_ecall(c);
format_decoded_record(c);
cfree_compiler_free(c);
- if (fails) {
- fprintf(stderr, "rv64_decode_test: %d failure(s)\n", fails);
- return 1;
- }
- return 0;
+ cfree_unit_summary(&g_u, "rv64_decode_test");
+ return cfree_unit_status(&g_u);
}
diff --git a/test/arch/rv64_inline_test.c b/test/arch/rv64_inline_test.c
@@ -74,8 +74,8 @@ int main(void) {
rv64_bad_operand, "operand index"),
"expected out-of-range rv64 asm operand to panic");
- if (env.fail) {
- fprintf(stderr, "%d failure(s)\n", env.fail);
+ if (env.fails) {
+ fprintf(stderr, "%d failure(s)\n", env.fails);
return 1;
}
printf("rv64_inline_test: ok\n");
diff --git a/test/arch/x64_dbg_test.c b/test/arch/x64_dbg_test.c
@@ -10,18 +10,13 @@
#include "arch/arch.h"
#include "core/bytes.h"
+#include "lib/cfree_unit.h"
-static int g_fail = 0;
-
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- g_fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fprintf(stderr, "\n"); \
- } \
- } while (0)
+/* Shared test context replaces the per-file counter global; EXPECT aliases
+ * CU_EXPECT so the call sites are unchanged. This test exercises only the
+ * arch lookup, so it uses none of the heap/diag/compiler wiring. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static const ArchDbgOps* x64_dbg(void) {
const ArchImpl* a = arch_lookup(CFREE_ARCH_X86_64);
@@ -133,15 +128,17 @@ static void check_branches_and_calls(const ArchDbgOps* dbg) {
}
int main(void) {
- const ArchDbgOps* dbg = x64_dbg();
+ const ArchDbgOps* dbg;
+ cfree_unit_init(&g_u);
+ dbg = x64_dbg();
EXPECT(dbg != NULL, "x64 dbg ops missing");
if (!dbg) return 1;
check_breakpoint(dbg);
check_rip_relative(dbg);
check_fs_tls_copy(dbg);
check_branches_and_calls(dbg);
- if (g_fail) {
- fprintf(stderr, "%d FAILED\n", g_fail);
+ if (g_u.fails) {
+ fprintf(stderr, "%d FAILED\n", g_u.fails);
return 1;
}
printf("x64 dbg test: OK\n");
diff --git a/test/arch/x64_inline_test.c b/test/arch/x64_inline_test.c
@@ -52,8 +52,8 @@ int main(void) {
x64_bad_operand, "operand index"),
"expected out-of-range x64 asm operand to panic");
- if (env.fail) {
- fprintf(stderr, "%d failure(s)\n", env.fail);
+ if (env.fails) {
+ fprintf(stderr, "%d failure(s)\n", env.fails);
return 1;
}
printf("x64_inline_test: ok\n");
diff --git a/test/asm/diff_llvm.sh b/test/asm/diff_llvm.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
-# test/asm/diff_llvm.sh — differential cross-check of cfree against llvm (aa64).
+# test/asm/diff_llvm.sh — Type D (differential, reference mode):
+# cross-check cfree against llvm (aa64) as a second, independent oracle.
#
# Two byte-level lanes (robust: no disassembly-text normalization, which would
# founder on alias/format differences like movz-vs-mov or #16-vs-#0x10):
@@ -16,12 +17,25 @@
# decode (one a self-round-trip can't, since cfree's own re-encode would
# repeat the mistake).
#
+# Type D oracle: AGREEMENT between two independent producers (cfree vs llvm-mc),
+# recorded per case via cf_diff_agree. Two recognized equivalence-skips precede
+# the comparison and are cf_skip'd, never flagged:
+# - llvm-mc REJECTS an input cfree accepts (forms outside llvm's dialect).
+# - the benign same-section-call reloc-vs-resolve difference: cfree codegen
+# keeps a CALL26/JUMP26/CONDBR reloc for a same-section call/branch to a
+# defined local symbol, where llvm-mc (like GNU as) resolves it in place and
+# drops the reloc. Both link to the same bytes; only the relocatable form
+# differs.
+#
# Opt-in; requires llvm-mc (+ a cfree-readable object). Skips cleanly if the
# tools are absent. See doc/ASM_ROUNDTRIP_TESTING.md.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+. "$ROOT/test/lib/cfree_sh_report.sh"
+. "$ROOT/test/lib/cf_differential.sh"
+
CFREE="$ROOT/build/cfree"
ENCODE_DIR="$ROOT/test/asm/encode"
RT_DIR="$ROOT/test/asm/roundtrip"
@@ -32,17 +46,17 @@ OPTS="${CFREE_TEST_OPTS:-O1}"
LLVM_MC="${LLVM_MC:-$(command -v llvm-mc || echo /opt/homebrew/bin/llvm-mc)}"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
+cf_report_init
if [ ! -x "$LLVM_MC" ]; then
- printf 'diff-llvm: %s llvm-mc not found (set $LLVM_MC); skipping\n' \
- "$(color_yel SKIP)"
- exit 0
+ # No second oracle available: skip cleanly. cf_skip (not a hard exit) so the
+ # summary/exit path is uniform with the rest of the harness.
+ cf_skip diff-llvm "llvm-mc not found (set \$LLVM_MC)"
+ cf_summary diff-llvm
+ cf_exit
fi
if [ ! -x "$CFREE" ]; then
- printf 'diff-llvm: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2
+ echo "diff-llvm: cfree missing — run \"make bin\"" >&2
exit 2
fi
mkdir -p "$WORK"
@@ -62,9 +76,7 @@ text_relocs() { "$CFREE" objdump -r "$1" 2>/dev/null | awk '
f && /^[0-9a-f]/{print $2, $3}'; }
mc() { "$LLVM_MC" -triple="$TRIPLE" -mattr="$MATTR" -filetype=obj "$1" -o "$2" 2>"$WORK/mc.err"; }
-agree=0; differ=0; reject=0; reloc_skip=0
-fails=()
-
+# --- encode lane: cfree as vs llvm-mc over the encode corpus ----------------
printf 'diff-llvm: encode lane (cfree as vs llvm-mc over the encode corpus)\n'
shopt -s nullglob
for s in "$ENCODE_DIR"/aa64_*.s; do
@@ -73,19 +85,17 @@ for s in "$ENCODE_DIR"/aa64_*.s; do
[ -f "$tg" ] && ! grep -qE 'aa64|aarch64|arm64' "$tg" && continue
"$CFREE" as -target "$TRIPLE" "$s" -o "$WORK/c.o" 2>/dev/null || continue
if ! mc "$s" "$WORK/l.o"; then
- reject=$((reject+1))
- printf ' %s %s: llvm-mc rejected (%s)\n' "$(color_yel SKIP)" "$name" \
- "$(head -1 "$WORK/mc.err" | sed 's|.*error: *||')"
+ # llvm-mc rejected a form cfree accepts: recognized dialect divergence.
+ cf_skip "encode:$name" "llvm-mc rejected ($(head -1 "$WORK/mc.err" | sed 's|.*error: *||'))"
continue
fi
- if [ "$(raw "$WORK/c.o")" = "$(raw "$WORK/l.o")" ]; then
- agree=$((agree+1))
- else
- differ=$((differ+1)); fails+=("encode:$name")
- printf ' %s %s: .text bytes differ\n' "$(color_red DIFF)" "$name"
- fi
+ # Compare the normalized .text byte extraction (same shape both sides).
+ raw "$WORK/c.o" > "$WORK/c.bytes"
+ raw "$WORK/l.o" > "$WORK/l.bytes"
+ cf_diff_agree "encode:$name" "$WORK/c.bytes" "$WORK/l.bytes"
done
+# --- disasm lane: cc -c bytes vs llvm-mc of cc -S ---------------------------
printf 'diff-llvm: disasm lane (cc -c bytes vs llvm-mc of cc -S, opts="%s")\n' "$OPTS"
for src in "$RT_DIR"/*.c; do
name="$(basename "$src" .c)"
@@ -94,32 +104,24 @@ for src in "$RT_DIR"/*.c; do
"$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$WORK/cc.o" 2>/dev/null || continue
"$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$WORK/s.s" 2>/dev/null || continue
if ! mc "$WORK/s.s" "$WORK/l.o"; then
- reject=$((reject+1))
- printf ' %s %s[-%s]: llvm-mc rejected cc -S (%s)\n' "$(color_yel SKIP)" \
- "$name" "$opt" "$(head -1 "$WORK/mc.err" | sed 's|.*error: *||')"
+ # llvm-mc rejected cfree's cc -S text: recognized dialect divergence.
+ cf_skip "disasm:$name[-$opt]" "llvm-mc rejected cc -S ($(head -1 "$WORK/mc.err" | sed 's|.*error: *||'))"
continue
fi
- if [ "$(raw "$WORK/cc.o")" = "$(raw "$WORK/l.o")" ]; then
- agree=$((agree+1))
- elif [ "$(text_relocs "$WORK/cc.o")" != "$(text_relocs "$WORK/l.o")" ]; then
- # Reloc tables differ: cfree kept a same-section call/branch reloc
- # that llvm-mc resolved in place. Link-equivalent — not a decode bug.
- reloc_skip=$((reloc_skip+1))
- else
- differ=$((differ+1)); fails+=("disasm:$name[-$opt]")
- printf ' %s %s[-%s]: cc -c vs llvm-mc(cc -S) bytes differ (relocs match)\n' \
- "$(color_red DIFF)" "$name" "$opt"
+ raw "$WORK/cc.o" > "$WORK/cc.bytes"
+ raw "$WORK/l.o" > "$WORK/l.bytes"
+ if [ "$(text_relocs "$WORK/cc.o")" != "$(text_relocs "$WORK/l.o")" ] &&
+ ! cmp -s "$WORK/cc.bytes" "$WORK/l.bytes"; then
+ # Reloc tables differ AND bytes differ: cfree kept a same-section
+ # call/branch reloc that llvm-mc resolved in place. Link-equivalent
+ # — recognized benign divergence, skip rather than flag.
+ cf_skip "disasm:$name[-$opt]" "same-section-call reloc-vs-resolve (link-equivalent)"
+ continue
fi
+ cf_diff_agree "disasm:$name[-$opt]" "$WORK/cc.bytes" "$WORK/l.bytes"
done
done
shopt -u nullglob
-printf '\n'
-if [ "${#fails[@]}" -gt 0 ]; then
- printf 'diff-llvm: %s %d agree, %d differ, %d reloc-equiv, %d llvm-skip\n' \
- "$(color_red 'cfree disagrees with llvm')" "$agree" "$differ" "$reloc_skip" "$reject"
- exit 1
-fi
-printf 'diff-llvm: %s %d agree, %d reloc-equiv, %d llvm-skip\n' \
- "$(color_grn 'cfree agrees with llvm')" "$agree" "$reloc_skip" "$reject"
-exit 0
+cf_summary diff-llvm
+cf_exit
diff --git a/test/asm/hostas_cross.sh b/test/asm/hostas_cross.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# test/asm/hostas_cross.sh — cross-compile + cross-exec extension of the
# host-assembler lane (test/asm/hostas_toy.sh) to ELF Linux targets.
+# (Type C — shared corpus harness; one cf_corpus_run per ELF target.)
#
# Where hostas_toy.sh proves `cc -S` on the *native* target, this proves it
# CROSS: for each ELF target (aarch64/x86_64/riscv64-linux) it emits ONE
@@ -19,9 +20,10 @@
# freestanding crt test/link/harness/start.c compiled `-Dtest_main=main`, so
# `_start` runs ctors then calls the toy's `main` and exits with its return
# (the oracle) via a raw syscall — no libc/loader needed, so any same-arch
-# Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper
-# (one batched `podman run` per target) — the same path test/{link,smoke}
-# already use.
+# Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper.
+# Lane A (and, under ENFORCE_CLANG=1, lane B) defers exec to one batched
+# `podman run` per target via the corpus engine's cf_queue_e; lane B under
+# ENFORCE_CLANG=0 runs inline (bounded) so its exec verdict can be XFAIL/XPASS.
#
# Self-probing: each target is SKIPPED (not failed) unless the host has (1) a
# clang cross-compiler for it, (2) a runner (podman/qemu) per exec_target, (3) a
@@ -44,18 +46,31 @@
#
# Override the matrix with CFREE_HOSTAS_CROSS_TARGETS="tag:triple ..." and the
# clang-as gate with CFREE_HOSTAS_ENFORCE_CLANG=0 (demote lane B to XFAIL).
+# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner
+# is parallel-safe; CFREE_HOSTAS_PARALLEL flips dispatch.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CFREE="$ROOT/build/cfree"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
+CFREE="${CFREE:-$ROOT/build/cfree}"
CASES="$ROOT/test/toy/cases"
-WORK="$ROOT/build/test/asm/hostas_cross"
+BUILD_DIR="$ROOT/build/test/asm/hostas_cross"
START_SRC="$ROOT/test/link/harness/start.c"
-OPTS="${CFREE_TEST_OPTS:-O0 O1}"
-FILTER="${1:-}"
ENFORCE_CLANG="${CFREE_HOSTAS_ENFORCE_CLANG:-1}"
EXEC_SMOKE_TIMEOUT="${CFREE_HOSTAS_EXEC_TIMEOUT:-45}"
+PAR="${CFREE_HOSTAS_PARALLEL:-1}"
+
+# Opt axis (see hostas_toy.sh): map a CFREE_TEST_OPTS override of O-spellings
+# onto the engine's bare opt levels; default 0 1.
+OPT_LEVELS="0 1"
+if [ -n "${CFREE_TEST_OPTS:-}" ]; then
+ OPT_LEVELS=""
+ for _o in $CFREE_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
+fi
# "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. All three ELF
# targets are in the gating default (each SKIPs cleanly if its clang cross
@@ -63,8 +78,13 @@ EXEC_SMOKE_TIMEOUT="${CFREE_HOSTAS_EXEC_TIMEOUT:-45}"
# CFREE_HOSTAS_CROSS_TARGETS, e.g. CFREE_HOSTAS_CROSS_TARGETS="x64-linux:x86_64-linux-gnu".
TARGETS="${CFREE_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}"
-# Same TLS-symbolization skip as the sibling lanes.
-SKIP="141_threadlocal_mutate"
+# Filter ($1) preserved -> the engine filters discovery by CFREE_TEST_FILTER.
+FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
+
+# The original exits on a_fail (+ b_efail when enforcing) only — skips (the known
+# 141 case) never gated. Keep that: SKIP must not fail the run.
+CF_SKIP_IS_FAILURE=0
CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}"
@@ -80,7 +100,7 @@ if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then
printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)"
exit 0
fi
-mkdir -p "$WORK"
+mkdir -p "$BUILD_DIR"
# ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------
have_qemu=0
@@ -102,11 +122,20 @@ export have_qemu QEMU_BIN QEMU_RV64_BIN have_podman is_aarch64
: "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}"
: "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}"
export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE
-EXEC_TARGET_MOUNT_ROOT="$WORK"
+EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+. "$ROOT/test/lib/exec_target.sh"
-is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; }
+# Same TLS-symbolization skip as the sibling lanes — driven by the shared
+# sidecar 141_threadlocal_mutate.link.skip via CF_READ_CASE (skips the WHOLE
+# case here too, matching hostas_toy.sh).
+hostas_read_case() {
+ local reason
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "link"); then
+ CF_SKIP_CASE="$reason (cc -S symbolizer gap)"
+ fi
+}
oracle() {
local name="$1" exp=0
@@ -155,143 +184,147 @@ cfree_ld_static() {
return 0
}
-printf 'hostas-cross: cfree=%s\n' "$CFREE"
-printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \
- "$CLANG" "$OPTS" "$ENFORCE_CLANG" "$have_podman"
-printf 'hostas-cross: targets="%s"\n' "$TARGETS"
+# ---- shared `cc -S -target` build for both lanes ---------------------------
+# One cross .s per item, cached in $CF_WORK so lanes A and B reuse it. A cc -S
+# failure is a lane-A failure only (lane B records nothing for the item).
+# Reads the per-target $TGT_TRIPLE (set before each target's cf_corpus_run).
+_ccs_build() {
+ CCS_S="$CF_WORK/s.s"
+ [ -f "$CF_WORK/.ccs.done" ] && { CCS_RC=$(cat "$CF_WORK/.ccs.rc"); return "$CCS_RC"; }
+ if "$CFREE" cc -S "-O$CF_OPT" -target "$TGT_TRIPLE" "$CF_SRC" -o "$CCS_S" 2>"$CF_WORK/ccs.err"; then
+ CCS_RC=0
+ else
+ CCS_RC=1
+ fi
+ echo "$CCS_RC" >"$CF_WORK/.ccs.rc"; : >"$CF_WORK/.ccs.done"
+ return "$CCS_RC"
+}
-a_pass=0; a_fail=0
-b_pass=0; b_xfail=0; b_xpass=0; b_efail=0
-skip_cases=0
-tgt_run=0; tgt_skip=0
-a_failnames=()
+# ---- lane A: cfree-as -> cfree ld -static -> run (deferred batched exec) ----
+# Reads per-target $TGT_TRIPLE, $TGT_TAG, $TGT_CRT (set before cf_corpus_run).
+cf_lane_A() {
+ if ! _ccs_build; then
+ cf_fail "$TGT_TAG/$CF_NAME/cfree-as" "cc -S: $(err_reason "$CF_WORK/ccs.err")"
+ return
+ fi
+ if ! "$CFREE" as -target "$TGT_TRIPLE" "$CCS_S" -o "$CF_WORK/a.o" 2>"$CF_WORK/a.as.err"; then
+ cf_fail "$TGT_TAG/$CF_NAME/cfree-as" "$(err_reason "$CF_WORK/a.as.err")"; return
+ fi
+ if ! cfree_ld_static "$CF_WORK/a.o" "$CF_WORK/a.out" "$CF_WORK/a.ld.err" "$TGT_CRT"; then
+ cf_fail "$TGT_TAG/$CF_NAME/cfree-as ld" "$(err_reason "$CF_WORK/a.ld.err")"; return
+ fi
+ cf_queue_e "$TGT_TAG/$CF_NAME/cfree-as" "$CF_WORK/a.out" \
+ "$CF_WORK/a.run.out" "$CF_WORK/a.run.err" "$CF_WORK/a.rc" \
+ "$CF_EXPECTED" "$TGT_TAG"
+}
-shopt -s nullglob
-for entry in $TARGETS; do
- tag="${entry%%:*}"; triple="${entry##*:}"
- tdir="$WORK/$tag"; rm -rf "$tdir"; mkdir -p "$tdir"
+# ---- lane B: clang-as -> cfree ld -static -> run ---------------------------
+# Gated by ENFORCE_CLANG. Build-stage failures are recorded inline (FAIL when
+# enforcing, XFAIL otherwise). The exec stage: when enforcing, defer to the
+# batched flush via cf_queue_e (pass/fail). When NOT enforcing, run inline
+# (bounded) so the exec verdict can be XPASS (pass) / XFAIL (fail).
+cf_lane_B() {
+ if ! _ccs_build; then return; fi
+ local name="$TGT_TAG/$CF_NAME/clang-as"
+ if ! "$CLANG" --target="$TGT_TRIPLE" -c "$CCS_S" -o "$CF_WORK/b.o" 2>"$CF_WORK/b.as.err"; then
+ if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$name" "$(err_reason "$CF_WORK/b.as.err")"
+ else cf_xfail "$name" "$(err_reason "$CF_WORK/b.as.err")"; fi
+ return
+ fi
+ if ! cfree_ld_static "$CF_WORK/b.o" "$CF_WORK/b.out" "$CF_WORK/b.ld.err" "$TGT_CRT"; then
+ if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$name" "ld: $(err_reason "$CF_WORK/b.ld.err")"
+ else cf_xfail "$name"; fi
+ return
+ fi
+ if [ "$ENFORCE_CLANG" = "1" ]; then
+ cf_queue_e "$name" "$CF_WORK/b.out" \
+ "$CF_WORK/b.run.out" "$CF_WORK/b.run.err" "$CF_WORK/b.rc" \
+ "$CF_EXPECTED" "$TGT_TAG"
+ return
+ fi
+ # ENFORCE_CLANG=0: inline bounded exec -> XPASS/XFAIL.
+ bounded_exec "${EXEC_CASE_TIMEOUT:-20}" "$TGT_TAG" "$CF_WORK/b.out" \
+ "$CF_WORK/b.run.out" "$CF_WORK/b.run.err"
+ if [ "$SMOKE_RC" = "$((CF_EXPECTED & 255))" ]; then cf_xpass "$name"
+ else cf_xfail "$name"; fi
+}
- # --- per-target capability gates (SKIP, never FAIL) ---
+# ---- per-target capability gates (SKIP-TGT, never FAIL) --------------------
+# Returns 0 if the target is runnable (and sets TGT_CRT); prints a single
+# SKIP-TGT line and returns 1 otherwise. Mirrors the original gate chain.
+target_gate() {
+ local tag="$1" triple="$2" tdir="$3"
if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; continue
+ printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
fi
if ! exec_target_supported "$tag"; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; continue
+ printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; return 1
fi
- crt="$tdir/start.o"
+ TGT_CRT="$tdir/start.o"
if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \
- -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$crt" 2>"$tdir/crt.err"; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; continue
+ -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$TGT_CRT" 2>"$tdir/crt.err"; then
+ printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; return 1
fi
- # Pick a representative non-skip case for the smokes.
- smoke=""; for s in "$CASES"/*.toy; do n="$(basename "$s" .toy)"; is_skip "$n" && continue; smoke="$n"; break; done
- sd="$tdir/_smoke"; mkdir -p "$sd"
+ # Pick a representative non-skip case for the smokes (first case without a
+ # 141-style .link.skip sidecar).
+ local smoke="" s n
+ for s in "$CASES"/*.toy; do
+ n="$(basename "$s" .toy)"
+ cf_skip_sidecar "$CASES" "$n" "" "link" >/dev/null && continue
+ smoke="$n"; break
+ done
+ local sd="$tdir/_smoke"; mkdir -p "$sd"
if ! "$CFREE" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; continue
+ printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; return 1
fi
if ! "$CFREE" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; continue
+ printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; return 1
fi
- if ! cfree_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$crt"; then
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — cfree ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; continue
+ if ! cfree_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$TGT_CRT"; then
+ printf ' %s %s — cfree ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; return 1
fi
bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err"
- sexp=$(oracle "$smoke")
+ local sexp; sexp=$(oracle "$smoke")
if [ "$SMOKE_RC" != "$sexp" ]; then
- reason="exit $SMOKE_RC != $sexp"; [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)"
- tgt_skip=$((tgt_skip+1)); printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; continue
+ local reason="exit $SMOKE_RC != $sexp"
+ [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)"
+ printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; return 1
fi
+ return 0
+}
- # --- full corpus for this target: build everything, then one batched run ---
- tgt_run=$((tgt_run+1))
- printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$tag" "$triple"
- EXEC_TARGET_TAGS=(); EXEC_TARGET_NAMES=(); EXEC_TARGET_EXES=()
- EXEC_TARGET_OUTS=(); EXEC_TARGET_ERRS=(); EXEC_TARGET_RCS=()
- # exec_target_flush() clears its arrays on return, so keep our own
- # reconciliation bookkeeping (lane / case / oracle / rc-file path).
- Q_LANE=(); Q_NAME=(); Q_EXP=(); Q_RCF=()
+# ---- drive the corpus, one target at a time --------------------------------
+printf 'hostas-cross: cfree=%s\n' "$CFREE"
+printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \
+ "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" "$have_podman"
+printf 'hostas-cross: targets="%s"\n' "$TARGETS"
- for src in "$CASES"/*.toy; do
- name="$(basename "$src" .toy)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if is_skip "$name"; then skip_cases=$((skip_cases+1)); continue; fi
- exp=$(oracle "$name")
- for opt in $OPTS; do
- w="$tdir/$name/$opt"; rm -rf "$w"; mkdir -p "$w"
- if ! "$CFREE" cc -S "-$opt" -target "$triple" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then
- a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt] cc-S: $(err_reason "$w/ccs.err")")
- printf ' %s %s/%s[-%s] cc -S: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/ccs.err")"
- continue
- fi
- # Lane A: cfree as -> cfree ld -static.
- if ! "$CFREE" as -target "$triple" "$w/s.s" -o "$w/a.o" 2>"$w/a.as.err"; then
- a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as: $(err_reason "$w/a.as.err")")
- printf ' %s %s/%s[-%s]/cfree-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.as.err")"
- elif ! cfree_ld_static "$w/a.o" "$w/a.out" "$w/a.ld.err" "$crt"; then
- a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as ld: $(err_reason "$w/a.ld.err")")
- printf ' %s %s/%s[-%s]/cfree-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.ld.err")"
- else
- exec_target_queue "$tag" "A:$name[-$opt]" "$w/a.out" "$w/a.run.out" "$w/a.run.err" "$w/a.rc"
- Q_LANE+=("A"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/a.rc")
- fi
- # Lane B: clang -c (third-party assembler) -> cfree ld -static.
- if ! "$CLANG" --target="$triple" -c "$w/s.s" -o "$w/b.o" 2>"$w/b.as.err"; then
- if [ "$ENFORCE_CLANG" = "1" ]; then
- b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")"
- else
- b_xfail=$((b_xfail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_yel XFAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")"
- fi
- elif ! cfree_ld_static "$w/b.o" "$w/b.out" "$w/b.ld.err" "$crt"; then
- if [ "$ENFORCE_CLANG" = "1" ]; then
- b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.ld.err")"
- else
- b_xfail=$((b_xfail+1))
- fi
- else
- exec_target_queue "$tag" "B:$name[-$opt]" "$w/b.out" "$w/b.run.out" "$w/b.run.err" "$w/b.rc"
- Q_LANE+=("B"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/b.rc")
- fi
- done
- done
+tgt_run=0
+shopt -s nullglob
+for entry in $TARGETS; do
+ TGT_TAG="${entry%%:*}"; TGT_TRIPLE="${entry##*:}"
+ tdir="$BUILD_DIR/$TGT_TAG"; rm -rf "$tdir"; mkdir -p "$tdir"
+ if ! target_gate "$TGT_TAG" "$TGT_TRIPLE" "$tdir"; then continue; fi
- # Drain this target's queue in one batched container run, then reconcile.
- exec_target_flush
- qn="${#Q_LANE[@]}"; qi=0
- while [ "$qi" -lt "$qn" ]; do
- lane="${Q_LANE[$qi]}"; nm="${Q_NAME[$qi]}"; exp="${Q_EXP[$qi]}"
- rcf="${Q_RCF[$qi]}"
- rc=127; [ -f "$rcf" ] && rc="$(cat "$rcf")"
- if [ "$lane" = "A" ]; then
- if [ "$rc" = "$exp" ]; then a_pass=$((a_pass+1)); else
- a_fail=$((a_fail+1)); a_failnames+=("$tag/$nm/cfree-as exit $rc != $exp")
- printf ' %s %s/%s/cfree-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp"
- fi
- else
- if [ "$rc" = "$exp" ]; then
- if [ "$ENFORCE_CLANG" = "1" ]; then b_pass=$((b_pass+1)); else b_xpass=$((b_xpass+1)); fi
- else
- if [ "$ENFORCE_CLANG" = "1" ]; then
- b_efail=$((b_efail+1)); printf ' %s %s/%s/clang-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp"
- else
- b_xfail=$((b_xfail+1))
- fi
- fi
- fi
- qi=$((qi+1))
- done
+ tgt_run=$((tgt_run + 1))
+ printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$TGT_TAG" "$TGT_TRIPLE"
+ # TGT_TAG / TGT_TRIPLE / TGT_CRT are set; lane hooks (incl. parallel
+ # workers, forked after this point) read them. One cf_corpus_run per target,
+ # like elf's A/B/C layers — counters accumulate into the shared summary.
+ export TGT_TAG TGT_TRIPLE TGT_CRT
+ CF_LABEL=test-hostas-cross CF_BUILD_DIR="$tdir" \
+ CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \
+ CF_LANES="A B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TGT_TAG" \
+ CF_TARGETS_EXT="" CF_READ_CASE=hostas_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
done
shopt -u nullglob
printf '\n'
-[ "${#a_failnames[@]}" -gt 0 ] && { printf 'cfree-as failures:\n'; for f in "${a_failnames[@]}"; do printf ' %s\n' "$f"; done; }
-printf 'hostas-cross: targets %d run, %d skip | cfree-as %d pass, %d fail | clang-as %d pass, %d xfail, %d xpass, %d efail | %d case-skip\n' \
- "$tgt_run" "$tgt_skip" "$a_pass" "$a_fail" "$b_pass" "$b_xfail" "$b_xpass" "$b_efail" "$skip_cases"
if [ "$tgt_run" -eq 0 ]; then
printf 'hostas-cross: no target ran (all SKIP-TGT) — needs a clang cross target + podman/qemu + a working cc -S|as for some ELF arch.\n'
fi
-rc=0
-[ "$a_fail" -gt 0 ] && rc=1
-[ "$ENFORCE_CLANG" = "1" ] && [ "$b_efail" -gt 0 ] && rc=1
-exit $rc
+cf_summary test-hostas-cross
+cf_exit
diff --git a/test/asm/hostas_toy.sh b/test/asm/hostas_toy.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# test/asm/hostas_toy.sh — prove cfree's `cc -S` is STANDARD assembly by feeding
# the SAME `cc -S` output to two assemblers, linking + running both, and asserting
-# the toy exit-code oracle for each.
+# the toy exit-code oracle for each. (Type C — shared corpus harness.)
#
# Per toy case (native target; both O0 and O1):
# cfree cc -S -> s.s (one native .s, shared by both)
@@ -22,29 +22,50 @@
# target it emits clean Mach-O assembly clang/llvm-mc accept — no ELF `.type`/
# `.size`, `.section __SEG,__SECT`, `.p2align`, and `sym@PAGE`/`@PAGEOFF`
# relocation operands. cfree's own `as` parses the same Mach-O dialect (it
-# dispatches on its target format), so BOTH lanes pass. The clang-as lane gates
-# by default; set CFREE_HOSTAS_ENFORCE_CLANG=0 to demote it to XFAIL (e.g. while
-# bringing up a new arch/format whose printer side isn't done yet).
+# dispatches on its target format), so BOTH lanes pass. The clang-as lane (B)
+# gates by default; set CFREE_HOSTAS_ENFORCE_CLANG=0 to demote it to XFAIL (e.g.
+# while bringing up a new arch/format whose printer side isn't done yet).
+#
+# Lanes (no CFREE_TEST_PATHS knob — both lanes always run; lane B's verdict is
+# gated by CFREE_HOSTAS_ENFORCE_CLANG instead):
+# A cfree-as -> cfree ld -> run exit == oracle (the baseline)
+# B clang-as -> cfree ld -> run exit == oracle (the real test)
+# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner
+# is parallel-safe; CFREE_HOSTAS_PARALLEL flips dispatch.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CFREE="$ROOT/build/cfree"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
+CFREE="${CFREE:-$ROOT/build/cfree}"
CASES="$ROOT/test/toy/cases"
-WORK="$ROOT/build/test/asm/hostas_toy"
-OPTS="${CFREE_TEST_OPTS:-O0 O1}"
-FILTER="${1:-}"
+BUILD_DIR="$ROOT/build/test/asm/hostas_toy"
ENFORCE_CLANG="${CFREE_HOSTAS_ENFORCE_CLANG:-1}"
+PAR="${CFREE_HOSTAS_PARALLEL:-1}"
+
+# The original harness exits on a_fail (+ b_efail when enforcing) only — skips
+# (just the known 141 case) never gated. Keep that: SKIP must not fail the run.
+CF_SKIP_IS_FAILURE=0
+
+# Opt axis: the original took O0/O1 spellings via CFREE_TEST_OPTS; the corpus
+# engine expands bare levels (CF_OPT_LEVELS="0 1"). Map a CFREE_TEST_OPTS
+# override (e.g. "O0 O1" or "O0") onto bare levels to preserve the env knob.
+OPT_LEVELS="0 1"
+if [ -n "${CFREE_TEST_OPTS:-}" ]; then
+ OPT_LEVELS=""
+ for _o in $CFREE_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
+fi
-# Cases blocked on a separate, known cc -S symbolizer gap (the round-trip lane
-# quarantines the same set): 141 emits an unsymbolized `adrp x,0x0` for a
-# thread-local access (TLS symbolization, tracked separately).
-SKIP="141_threadlocal_mutate"
+# Filter ($1) preserved -> the engine filters discovery by CFREE_TEST_FILTER.
+FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}"
color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
color_yel() { printf '\033[33m%s\033[0m' "$1"; }
if [ ! -x "$CFREE" ]; then
@@ -55,9 +76,17 @@ if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then
printf 'hostas-toy: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)"
exit 0
fi
-mkdir -p "$WORK"
-
-is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; }
+mkdir -p "$BUILD_DIR"
+
+# Native target tuple/triple for the corpus engine + `cc -S`/`as`.
+TEST_ARCH="${CFREE_TEST_ARCH:-aa64}"
+TEST_OBJ="${CFREE_TEST_OBJ:-macho}"
+case "$TEST_ARCH" in
+ aa64|aarch64|arm64) TEST_ARCH=aa64 ;;
+ x64|x86_64|amd64) TEST_ARCH=x64 ;;
+ rv64|riscv64) TEST_ARCH=rv64 ;;
+esac
+CUR_TUPLE="$TEST_ARCH-$TEST_OBJ"
# First meaningful diagnostic from an assembler's stderr. clang prints a harmless
# `-Wmissing-sysroot` warning first (we never link with clang, so the SDK is
@@ -69,98 +98,102 @@ err_reason() {
[ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||')
printf '%s' "$line"
}
+# Terse first-line reason (cfree's own diagnostics: "<tool>: <msg>").
+first_reason() { head -1 "$1" 2>/dev/null | sed 's|.*: ||'; }
-a_pass=0; a_fail=0
-b_pass=0; b_xfail=0; b_xpass=0; b_efail=0
-skip=0
-a_failnames=()
+# Cases blocked on a separate, known `cc -S` symbolizer gap (the round-trip lane
+# quarantines the same set): 141 emits an unsymbolized `adrp x,0x0` for a
+# thread-local access (TLS symbolization, tracked separately). Sidecar-driven
+# via test/toy/cases/141_threadlocal_mutate.link.skip, read by CF_READ_CASE
+# under the synthetic "link" lane name so the same .link.skip the toy/roundtrip
+# harnesses honor skips the WHOLE case here too.
+hostas_read_case() {
+ local reason
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "link"); then
+ CF_SKIP_CASE="$reason (cc -S symbolizer gap)"
+ fi
+}
-printf 'hostas-toy: cfree=%s\n' "$CFREE"
-printf 'hostas-toy: clang=%s opts="%s" enforce_clang=%s\n' "$CLANG" "$OPTS" "$ENFORCE_CLANG"
-
-shopt -s nullglob
-for src in "$CASES"/*.toy; do
- name="$(basename "$src" .toy)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if is_skip "$name"; then
- skip=$((skip+1)); printf ' %s %s — known cc -S symbolizer gap\n' "$(color_yel SKIP)" "$name"; continue
+# Shared `cc -S` build for both lanes (one native .s per item). Cached per item
+# in $CF_WORK so lanes A and B reuse it. Sets CCS_S (path) + CCS_RC (0 ok).
+# A cc -S failure is a lane-A failure only (lane B records nothing for the item,
+# matching the original `continue`).
+_ccs_build() {
+ CCS_S="$CF_WORK/s.s"
+ [ -f "$CF_WORK/.ccs.done" ] && { CCS_RC=$(cat "$CF_WORK/.ccs.rc"); return "$CCS_RC"; }
+ if "$CFREE" cc -S "-O$CF_OPT" "$CF_SRC" -o "$CCS_S" 2>"$CF_WORK/ccs.err"; then
+ CCS_RC=0
+ else
+ CCS_RC=1
fi
- exp=0; [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected")
- exp=$((exp & 255))
- for opt in $OPTS; do
- w="$WORK/$name/$opt"; rm -rf "$w"; mkdir -p "$w"
-
- # One native cc -S, shared by both lanes.
- if ! "$CFREE" cc -S "-$opt" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then
- a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt] cc-S: $(head -1 "$w/ccs.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s] cc -S failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ccs.err"|sed 's|.*: ||')"
- continue
- fi
-
- # Lane A: cfree as -> cfree ld -> run.
- if ! "$CFREE" as "$w/s.s" -o "$w/a.o" 2>"$w/a.as.err"; then
- a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as: $(head -1 "$w/a.as.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s]/cfree-as: as: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.as.err"|sed 's|.*: ||')"
- elif ! "$CFREE" ld "$w/a.o" -o "$w/a.out" 2>"$w/a.ld.err" || [ -s "$w/a.ld.err" ]; then
- a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as ld: $(head -1 "$w/a.ld.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s]/cfree-as: ld: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.ld.err"|sed 's|.*: ||')"
- else
- chmod +x "$w/a.out" 2>/dev/null || true
- "$w/a.out" >"$w/a.out.txt" 2>"$w/a.run.err"; arc=$?
- if [ -s "$w/a.run.err" ]; then
- a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as run stderr")
- printf ' %s %s[-%s]/cfree-as: run stderr: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.run.err")"
- elif [ "$arc" -eq "$exp" ]; then
- a_pass=$((a_pass+1))
- else
- a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as exit $arc != $exp")
- printf ' %s %s[-%s]/cfree-as: exit %d != %d\n' "$(color_red FAIL)" "$name" "$opt" "$arc" "$exp"
- fi
- fi
-
- # Lane B: clang -c (third-party assembler) -> cfree ld -> run.
- # cfree ld on both lanes isolates the assembler as the only variable.
- b_ok=0; b_reason=""
- if ! "$CLANG" -c "$w/s.s" -o "$w/b.o" 2>"$w/b.as.err"; then
- b_reason="clang as: $(err_reason "$w/b.as.err")"
- elif ! "$CFREE" ld "$w/b.o" -o "$w/b.out" 2>"$w/b.ld.err" || [ -s "$w/b.ld.err" ]; then
- b_reason="cfree ld: $(head -1 "$w/b.ld.err"|sed 's|.*: ||')"
- else
- chmod +x "$w/b.out" 2>/dev/null || true
- "$w/b.out" >"$w/b.out.txt" 2>"$w/b.run.err"; brc=$?
- if [ -s "$w/b.run.err" ]; then b_reason="run stderr: $(head -1 "$w/b.run.err")"
- elif [ "$brc" -eq "$exp" ]; then b_ok=1
- else b_reason="exit $brc != $exp"; fi
- fi
- if [ "$b_ok" -eq 1 ]; then
- if [ "$ENFORCE_CLANG" = "1" ]; then
- b_pass=$((b_pass+1)); printf ' %s %s[-%s]/clang-as\n' "$(color_grn PASS)" "$name" "$opt"
- else
- b_xpass=$((b_xpass+1)); printf ' %s %s[-%s]/clang-as — now passes; set CFREE_HOSTAS_ENFORCE_CLANG=1\n' "$(color_grn XPASS)" "$name" "$opt"
- fi
- else
- if [ "$ENFORCE_CLANG" = "1" ]; then
- b_efail=$((b_efail+1)); printf ' %s %s[-%s]/clang-as: %s\n' "$(color_red FAIL)" "$name" "$opt" "$b_reason"
- else
- b_xfail=$((b_xfail+1)); printf ' %s %s[-%s]/clang-as: %s\n' "$(color_yel XFAIL)" "$name" "$opt" "$b_reason"
- fi
- fi
- done
-done
-shopt -u nullglob
-
-printf '\n'
-[ "${#a_failnames[@]}" -gt 0 ] && { printf 'cfree-as failures:\n'; for f in "${a_failnames[@]}"; do printf ' %s\n' "$f"; done; }
-printf 'hostas-toy: cfree-as %d pass, %d fail | clang-as %d pass, %d xfail, %d xpass, %d efail | %d skip\n' \
- "$a_pass" "$a_fail" "$b_pass" "$b_xfail" "$b_xpass" "$b_efail" "$skip"
-if [ "$ENFORCE_CLANG" != "1" ] && [ "$b_xfail" -gt 0 ]; then
- printf 'hostas-toy: clang-as demoted to XFAIL (CFREE_HOSTAS_ENFORCE_CLANG=0): %d not accepted by clang. The clang-as lane gates by default; unset the env var to enforce.\n' "$b_xfail"
-fi
-if [ "$ENFORCE_CLANG" != "1" ] && [ "$b_xpass" -gt 0 ]; then
- printf 'hostas-toy: clang-as had %d XPASS under opt-out — re-enable gating (default) to enforce.\n' "$b_xpass"
-fi
+ echo "$CCS_RC" >"$CF_WORK/.ccs.rc"; : >"$CF_WORK/.ccs.done"
+ return "$CCS_RC"
+}
-rc=0
-[ "$a_fail" -gt 0 ] && rc=1
-[ "$ENFORCE_CLANG" = "1" ] && [ "$b_efail" -gt 0 ] && rc=1
-exit $rc
+# ---- lane A: cfree-as -> cfree ld -> run (the baseline) ---------------------
+cf_lane_A() {
+ if ! _ccs_build; then
+ cf_fail "$CF_NAME/cfree-as" "cc -S: $(first_reason "$CF_WORK/ccs.err")"
+ return
+ fi
+ if ! "$CFREE" as "$CCS_S" -o "$CF_WORK/a.o" 2>"$CF_WORK/a.as.err"; then
+ cf_fail "$CF_NAME/cfree-as" "as: $(first_reason "$CF_WORK/a.as.err")"; return
+ fi
+ if ! "$CFREE" ld "$CF_WORK/a.o" -o "$CF_WORK/a.out" 2>"$CF_WORK/a.ld.err" || [ -s "$CF_WORK/a.ld.err" ]; then
+ cf_fail "$CF_NAME/cfree-as" "ld: $(first_reason "$CF_WORK/a.ld.err")"; return
+ fi
+ chmod +x "$CF_WORK/a.out" 2>/dev/null || true
+ "$CF_WORK/a.out" >"$CF_WORK/a.out.txt" 2>"$CF_WORK/a.run.err"; local arc=$?
+ local exp=$((CF_EXPECTED & 255)) # exit codes wrap at 256 (oracle 312 -> 56)
+ if [ -s "$CF_WORK/a.run.err" ]; then
+ cf_fail "$CF_NAME/cfree-as" "run stderr: $(head -1 "$CF_WORK/a.run.err")"
+ elif [ "$arc" -eq "$exp" ]; then
+ cf_pass "$CF_NAME/cfree-as"
+ else
+ cf_fail "$CF_NAME/cfree-as" "exit $arc != $exp"
+ fi
+}
+
+# ---- lane B: clang-as -> cfree ld -> run (the real test) -------------------
+# Gated by ENFORCE_CLANG: pass+enforce -> PASS, pass+!enforce -> XPASS;
+# fail+enforce -> FAIL, fail+!enforce -> XFAIL. cfree ld on both lanes isolates
+# the assembler as the only variable. Records NOTHING on a shared cc -S failure
+# (matching the original `continue`).
+cf_lane_B() {
+ if ! _ccs_build; then return; fi
+ local ok=0 reason="" exp=$((CF_EXPECTED & 255))
+ if ! "$CLANG" -c "$CCS_S" -o "$CF_WORK/b.o" 2>"$CF_WORK/b.as.err"; then
+ reason="clang as: $(err_reason "$CF_WORK/b.as.err")"
+ elif ! "$CFREE" ld "$CF_WORK/b.o" -o "$CF_WORK/b.out" 2>"$CF_WORK/b.ld.err" || [ -s "$CF_WORK/b.ld.err" ]; then
+ reason="cfree ld: $(first_reason "$CF_WORK/b.ld.err")"
+ else
+ chmod +x "$CF_WORK/b.out" 2>/dev/null || true
+ "$CF_WORK/b.out" >"$CF_WORK/b.out.txt" 2>"$CF_WORK/b.run.err"; local brc=$?
+ if [ -s "$CF_WORK/b.run.err" ]; then reason="run stderr: $(head -1 "$CF_WORK/b.run.err")"
+ elif [ "$brc" -eq "$exp" ]; then ok=1
+ else reason="exit $brc != $exp"; fi
+ fi
+ if [ "$ok" -eq 1 ]; then
+ if [ "$ENFORCE_CLANG" = "1" ]; then cf_pass "$CF_NAME/clang-as"
+ else cf_xpass "$CF_NAME/clang-as"; fi
+ else
+ if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$CF_NAME/clang-as" "$reason"
+ else cf_xfail "$CF_NAME/clang-as" "$reason"; fi
+ fi
+}
+
+# ---- drive the corpus ------------------------------------------------------
+printf 'hostas-toy: cfree=%s\n' "$CFREE"
+printf 'hostas-toy: clang=%s opts="%s" enforce_clang=%s\n' "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG"
+
+# Lane B's XFAIL/XPASS already carry the enforce semantics; gate xpass under
+# CFREE_HOSTAS_ENFORCE_CLANG=1 is N/A here (enforce=1 means lane B counts as
+# PASS/FAIL, not XPASS/XFAIL), so leave CF_STRICT_XFAIL at its default.
+CF_LABEL=test-hostas-toy CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \
+ CF_LANES="A B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_READ_CASE=hostas_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+cf_summary test-hostas-toy
+cf_exit
diff --git a/test/asm/roundtrip.sh b/test/asm/roundtrip.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
-# test/asm/roundtrip.sh — codegen round-trip completeness harness.
+# test/asm/roundtrip.sh — codegen round-trip completeness harness, on the
+# shared corpus harness (test/lib/cf_corpus.sh).
#
# Measures completeness of the per-arch assembler, disassembler, and the link
# relocation path by round-tripping the *compiler's own output* rather than a
@@ -8,7 +9,7 @@
# Corpus: test/asm/roundtrip/*.c — each defines `int test_main(...)` so the L2
# lane can execute it through the shared exec harness (jit-runner).
#
-# Three lanes (default "012"):
+# Three lanes (CFREE_TEST_PATHS, default "012"; digits 0/1/2 -> lanes L0/L1/L2):
#
# 0 L0 decode-complete — `cfree cc -S` and assert no in-function decode
# failure marker (aarch64 `.inst`) inside .text. Catches an instruction
@@ -17,7 +18,7 @@
# 1 L1 byte round-trip — `cfree cc -c` (direct.o) vs `cfree cc -S | cfree as`
# (rt.o); diff the .text bytes AND the .text relocation table. Catches
# assembler/disassembler disagreements (round-trip violations). Exact,
-# host-independent. Gated on L0 passing first.
+# host-independent. Gated on L0's `cc -S` step succeeding first.
# 2 L2 exec equivalence — run direct.o and rt.o and compare exit codes (and,
# when present, against <name>.expected). Tolerant of benign encoding
# differences; the end-to-end "it runs the same" signal. Executes via
@@ -25,21 +26,31 @@
#
# Opt levels: CFREE_TEST_OPTS (default "O1"). Each case is run at every level.
#
+# Per-case applicability: <name>.targets (tuple list) -> SKIP-NA; <name>.skip
+# quarantines the whole case; <name>.expected is the L2 exit-code oracle.
+#
# Filtering:
# ./roundtrip.sh [name_filter] [lanes]
# name_filter substring match against case basename
# lanes subset of "012" (default "012")
# Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS, CFREE_TEST_OPTS.
-
+#
+# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner
+# is parallel-safe by construction (CFREE_ASM_PARALLEL flips dispatch). The
+# L1-depends-on-L0 ordering (L1 reuses the listing L0 produced) is preserved
+# inside the L1 lane body: each lane recompiles its own `cc -S` listing under
+# $CF_WORK rather than sharing artifacts across lanes/workers.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
TEST_DIR="$ROOT/test/asm"
CORPUS_DIR="$TEST_DIR/roundtrip"
-BUILD_DIR="$ROOT/build/test"
-WORK_ROOT="$BUILD_DIR/asm/roundtrip"
-CFREE="$ROOT/build/cfree"
-JIT_RUNNER="$BUILD_DIR/jit-runner"
+BUILD_DIR="$ROOT/build/test/asm/roundtrip"
+CFREE_BIN="${CFREE:-$ROOT/build/cfree}"
+JIT_RUNNER="$ROOT/build/test/jit-runner"
# CFREE_TEST_ARCH selects the cross-target. Mirrors test/asm/run.sh.
CFREE_TEST_ARCH="${CFREE_TEST_ARCH:-aa64}"
@@ -49,27 +60,25 @@ case "$CFREE_TEST_ARCH" in
rv64|riscv64) TEST_ARCH=rv64; TRIPLE=riscv64-linux-gnu ;;
*) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;;
esac
-export CFREE_TEST_ARCH
+export CFREE_TEST_ARCH="$TEST_ARCH"
+# Opt axis. CFREE_TEST_OPTS carries the "O"-prefixed levels (e.g. "O0 O1"); the
+# engine matrix wants the bare digits, and the lanes build "-O<level>".
OPTS="${CFREE_TEST_OPTS:-O1}"
+OPT_LEVELS=""
+for _o in $OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
FILTER="${1:-${CFREE_TEST_FILTER:-}}"
PATHS="${2:-${CFREE_TEST_PATHS:-012}}"
-case "$PATHS" in *0*) RUN_L0=1;; *) RUN_L0=0;; esac
-case "$PATHS" in *1*) RUN_L1=1;; *) RUN_L1=0;; esac
-case "$PATHS" in *2*) RUN_L2=1;; *) RUN_L2=0;; esac
-
-PASS=0; FAIL=0; SKIP=0
-FAIL_NAMES=()
+export CFREE_TEST_FILTER="$FILTER"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
+# Map the PATHS digit alphabet (0/1/2) onto the engine lane ids L0/L1/L2.
+LANES=""
+case "$PATHS" in *0*) LANES="$LANES L0";; esac
+case "$PATHS" in *1*) LANES="$LANES L1";; esac
+case "$PATHS" in *2*) LANES="$LANES L2";; esac
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-note_na() { printf ' %s %s — not applicable to %s\n' "$(color_yel SKIP-NA)" "$1" "$TEST_ARCH"; }
+PAR="${CFREE_ASM_PARALLEL:-1}"
# is_native_target=1 when the cross-target arch matches the host arch (needed
# for in-process JIT exec in the L2 lane).
@@ -85,25 +94,28 @@ have_jit_runner=0
[ -x "$JIT_RUNNER" ] && have_jit_runner=1
# ---- per-case applicability (mirrors test/asm/run.sh) ----------------------
-
-case_applies() {
- local name="$1" targets tuple
- targets="$CORPUS_DIR/$name.targets"
+# A <name>.targets sidecar lists the arches a case applies to, using arch-only
+# tokens with synonyms (aa64/aarch64/arm64, x64/x86_64/amd64, rv64/riscv64) —
+# NOT the engine's "<arch>-<obj>" tuple form. Reimplemented as a CF_READ_CASE
+# hook (rather than the engine's CF_TARGETS_EXT tuple matcher) so the exact
+# original token grammar and SKIP-NA verdict are preserved.
+rt_read_case() {
+ local targets="$CF_SIDECAR_DIR/$CF_BASE.targets" tuple
[ -f "$targets" ] || return 0 # no .targets => applies to all arches
for tuple in $(cat "$targets"); do
case "$tuple:$TEST_ARCH" in
- aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;;
- x64:x64|x86_64:x64|amd64:x64) return 0 ;;
- rv64:rv64|riscv64:rv64) return 0 ;;
+ aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;;
+ x64:x64|x86_64:x64|amd64:x64) return 0 ;;
+ rv64:rv64|riscv64:rv64) return 0 ;;
esac
done
- return 1
+ CF_SKIP_NA_CASE=1
}
-# ---- extraction helpers ----------------------------------------------------
+# ---- extraction helpers (lane-local) ---------------------------------------
# .text bytes as objdump hex-dump lines (filename header stripped).
-text_bytes() { "$CFREE" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f]+ /'; }
+text_bytes() { "$CFREE_BIN" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f]+ /'; }
# Relocation records (kind/offset/target) for the sections `cc -S` reproduces
# — .text, .rodata, .data — so the comparison covers code relocs AND data
@@ -112,7 +124,7 @@ text_bytes() { "$CFREE" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f]
# is not flagged. The section header is printed so a reloc at the same offset
# in two sections stays distinct.
reproduced_relocs() {
- "$CFREE" objdump -r "$1" 2>/dev/null | awk '
+ "$CFREE_BIN" objdump -r "$1" 2>/dev/null | awk '
/^RELOCATION RECORDS FOR \[\.(text|rodata|data)\]/ { f=1; print; next }
/^RELOCATION RECORDS FOR/ { f=0; next }
f && /^[0-9a-f]/ { print }'
@@ -131,120 +143,142 @@ decode_failures() {
' "$1"
}
-# ---- run -------------------------------------------------------------------
-
-printf 'roundtrip: arch=%s triple=%s opts="%s" lanes=%s native=%d\n' \
- "$TEST_ARCH" "$TRIPLE" "$OPTS" "$PATHS" "$is_native_target"
+# Build the shared `cc -S` assembly listing for this case/opt under $CF_WORK.
+# Echoes the listing path on success; nonzero (with the log left in place) on
+# failure. Both L0 and L1 funnel through this so the L1-after-L0 ordering holds
+# regardless of dispatch order: each lane materializes the listing it needs.
+rt_compile_asm() {
+ local asm="$CF_WORK/out.s"
+ if [ -f "$asm" ]; then printf '%s' "$asm"; return 0; fi
+ if "$CFREE_BIN" cc -S "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$asm" \
+ >"$CF_WORK/cc_s.log" 2>&1; then
+ printf '%s' "$asm"; return 0
+ fi
+ return 1
+}
-if [ ! -x "$CFREE" ]; then
- printf ' %s cfree binary missing — run "make bin"\n' "$(color_red FATAL)" >&2
- exit 1
-fi
-if [ $RUN_L2 -eq 1 ] && [ $is_native_target -eq 1 ] && [ $have_jit_runner -eq 0 ]; then
- printf ' %s jit-runner missing; L2 lane will skip\n' "$(color_yel warn)"
-fi
+# ---- lanes -----------------------------------------------------------------
-mkdir -p "$WORK_ROOT"
+# L0: decode completeness. cc -S then assert no in-function `.inst` markers.
+cf_lane_L0() {
+ local asm
+ if ! asm=$(rt_compile_asm); then
+ cf_fail "$CF_NAME/L0" "cc -S failed; see $CF_WORK/cc_s.log"; return
+ fi
+ if decode_failures "$asm" >"$CF_WORK/decode_fail"; then
+ cf_fail "$CF_NAME/L0" "undecoded insn in .text; see $CF_WORK/decode_fail"
+ else
+ cf_pass "$CF_NAME/L0"
+ fi
+}
-shopt -s nullglob
-for src in "$CORPUS_DIR"/*.c; do
- name="$(basename "$src" .c)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if ! case_applies "$name"; then
- note_na "$name"
- continue
+# L1: byte + reloc round-trip. cc -c (direct.o) vs cc -S | as (rt.o); diff the
+# .text bytes AND the reproduced reloc tables. Gated on the cc -S listing (L0's
+# producer) succeeding first.
+#
+# Intra-function branches now round-trip: `as` relaxes same-section local-label
+# branches at finalize (matching codegen), so the .text reloc tables agree. A
+# case that still can't round-trip is gated with a per-case `<name>.skip` file
+# rather than auto-skipped here.
+cf_lane_L1() {
+ local asm direct="$CF_WORK/direct.o" rt="$CF_WORK/rt.o"
+ if ! asm=$(rt_compile_asm); then
+ cf_fail "$CF_NAME/L1" "cc -S failed; see $CF_WORK/cc_s.log"; return
+ fi
+ if ! "$CFREE_BIN" cc -c "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$direct" \
+ >"$CF_WORK/cc_c.log" 2>&1; then
+ cf_fail "$CF_NAME/L1" "cc -c failed; see $CF_WORK/cc_c.log"; return
+ fi
+ if ! "$CFREE_BIN" as -target "$TRIPLE" "$asm" -o "$rt" \
+ >"$CF_WORK/as.log" 2>&1; then
+ cf_fail "$CF_NAME/L1" "as failed; see $CF_WORK/as.log"; return
fi
- if [ -e "$CORPUS_DIR/$name.skip" ]; then
- note_skip "$name" "$(head -n1 "$CORPUS_DIR/$name.skip")"
- continue
+ text_bytes "$direct" >"$CF_WORK/direct.text"
+ text_bytes "$rt" >"$CF_WORK/rt.text"
+ reproduced_relocs "$direct" >"$CF_WORK/direct.rel"
+ reproduced_relocs "$rt" >"$CF_WORK/rt.rel"
+ if ! diff -u "$CF_WORK/direct.text" "$CF_WORK/rt.text" >"$CF_WORK/text.diff"; then
+ cf_fail "$CF_NAME/L1" ".text bytes differ; see $CF_WORK/text.diff"
+ elif ! diff -u "$CF_WORK/direct.rel" "$CF_WORK/rt.rel" >"$CF_WORK/rel.diff"; then
+ cf_fail "$CF_NAME/L1" "relocs differ (.text/.rodata/.data); see $CF_WORK/rel.diff"
+ else
+ cf_pass "$CF_NAME/L1"
fi
+}
- for opt in $OPTS; do
- tag="$name[-$opt]"
- work="$WORK_ROOT/$name/$opt"
- mkdir -p "$work"
- asm="$work/out.s"
- direct="$work/direct.o"
- rt="$work/rt.o"
-
- # Shared compile: assembly listing (L0/L1) + direct object (L1/L2).
- if ! "$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$asm" \
- >"$work/cc_s.log" 2>&1; then
- note_fail "$tag/L0 (cc -S failed; see $work/cc_s.log)"
- continue
+# L2: exec equivalence. Build direct.o and rt.o, run both through jit-runner,
+# require equal exit codes + identical stdout, and (when a <name>.expected
+# oracle exists) require the exit code to match it. Native target only.
+cf_lane_L2() {
+ if [ "$is_native_target" -eq 0 ]; then
+ cf_skip "$CF_NAME/L2" "non-native target ($arch_raw); cross-exec lane TODO"; return
+ fi
+ if [ "$have_jit_runner" -eq 0 ]; then
+ cf_skip "$CF_NAME/L2" "jit-runner unavailable"; return
+ fi
+ local asm direct="$CF_WORK/direct.o" rt="$CF_WORK/rt.o"
+ # Reuse direct.o/rt.o from L1 if present; otherwise build them here.
+ if [ ! -f "$direct" ]; then
+ if ! "$CFREE_BIN" cc -c "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$direct" \
+ >"$CF_WORK/cc_c.log" 2>&1; then
+ cf_fail "$CF_NAME/L2" "cc -c failed; see $CF_WORK/cc_c.log"; return
fi
-
- # ---- L0: decode completeness -------------------------------------
- if [ $RUN_L0 -eq 1 ]; then
- if decode_failures "$asm" >"$work/decode_fail"; then
- note_fail "$tag/L0 (undecoded insn in .text; see $work/decode_fail)"
- else
- note_pass "$tag/L0"
- fi
+ fi
+ if [ ! -f "$rt" ]; then
+ if ! asm=$(rt_compile_asm); then
+ cf_fail "$CF_NAME/L2" "cc -S failed; see $CF_WORK/cc_s.log"; return
fi
-
- # ---- L1: byte + reloc round-trip ---------------------------------
- # Intra-function branches now round-trip: `as` relaxes same-section
- # local-label branches at finalize (matching codegen), so the .text
- # reloc tables agree. A case that still can't round-trip is gated with
- # a per-case `<name>.skip` file rather than auto-skipped here.
- if [ $RUN_L1 -eq 1 ]; then
- l1_ok=1; l1_why=""
- if ! "$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$direct" \
- >"$work/cc_c.log" 2>&1; then
- l1_ok=0; l1_why="cc -c failed; see $work/cc_c.log"
- elif ! "$CFREE" as -target "$TRIPLE" "$asm" -o "$rt" \
- >"$work/as.log" 2>&1; then
- l1_ok=0; l1_why="as failed; see $work/as.log"
- else
- text_bytes "$direct" >"$work/direct.text"
- text_bytes "$rt" >"$work/rt.text"
- reproduced_relocs "$direct" >"$work/direct.rel"
- reproduced_relocs "$rt" >"$work/rt.rel"
- if ! diff -u "$work/direct.text" "$work/rt.text" >"$work/text.diff"; then
- l1_ok=0; l1_why=".text bytes differ; see $work/text.diff"
- elif ! diff -u "$work/direct.rel" "$work/rt.rel" >"$work/rel.diff"; then
- l1_ok=0; l1_why="relocs differ (.text/.rodata/.data); see $work/rel.diff"
- fi
- fi
- if [ $l1_ok -eq 1 ]; then note_pass "$tag/L1"; else note_fail "$tag/L1 ($l1_why)"; fi
+ if ! "$CFREE_BIN" as -target "$TRIPLE" "$asm" -o "$rt" \
+ >"$CF_WORK/as.log" 2>&1; then
+ cf_fail "$CF_NAME/L2" "as failed; see $CF_WORK/as.log"; return
fi
+ fi
+ "$JIT_RUNNER" "$direct" >"$CF_WORK/direct.out" 2>"$CF_WORK/direct.err"; local rc_direct=$?
+ "$JIT_RUNNER" "$rt" >"$CF_WORK/rt.out" 2>"$CF_WORK/rt.err"; local rc_rt=$?
+ if [ "$rc_direct" != "$rc_rt" ]; then
+ cf_fail "$CF_NAME/L2" "exit codes differ: direct=$rc_direct rt=$rc_rt"; return
+ fi
+ if ! diff -q "$CF_WORK/direct.out" "$CF_WORK/rt.out" >/dev/null; then
+ cf_fail "$CF_NAME/L2" "stdout differs"; return
+ fi
+ # CF_EXPECTED is the <name>.expected oracle (engine default 0 when absent).
+ # The original lane only enforced the oracle when the sidecar was present;
+ # every roundtrip case carries one, so honoring CF_EXPECTED unconditionally
+ # is the same verdict.
+ if [ "$rc_direct" != "$CF_EXPECTED" ]; then
+ cf_fail "$CF_NAME/L2" "exit $rc_direct != expected $CF_EXPECTED"; return
+ fi
+ cf_pass "$CF_NAME/L2"
+}
- # ---- L2: exec equivalence ----------------------------------------
- if [ $RUN_L2 -eq 1 ]; then
- if [ $is_native_target -eq 0 ]; then
- note_skip "$tag/L2" "non-native target ($arch_raw); cross-exec lane TODO"
- elif [ $have_jit_runner -eq 0 ]; then
- note_skip "$tag/L2" "jit-runner unavailable"
- else
- # Reuse direct.o/rt.o from L1 if present; otherwise build them.
- [ -f "$direct" ] || "$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$direct" >"$work/cc_c.log" 2>&1
- [ -f "$rt" ] || "$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$asm" >"$work/cc_s.log" 2>&1 && \
- "$CFREE" as -target "$TRIPLE" "$asm" -o "$rt" >"$work/as.log" 2>&1
- "$JIT_RUNNER" "$direct" >"$work/direct.out" 2>"$work/direct.err"; rc_direct=$?
- "$JIT_RUNNER" "$rt" >"$work/rt.out" 2>"$work/rt.err"; rc_rt=$?
- l2_ok=1; l2_why=""
- if [ "$rc_direct" != "$rc_rt" ]; then
- l2_ok=0; l2_why="exit codes differ: direct=$rc_direct rt=$rc_rt"
- elif ! diff -q "$work/direct.out" "$work/rt.out" >/dev/null; then
- l2_ok=0; l2_why="stdout differs"
- elif [ -f "$CORPUS_DIR/$name.expected" ]; then
- exp="$(head -n1 "$CORPUS_DIR/$name.expected")"
- if [ "$rc_direct" != "$exp" ]; then
- l2_ok=0; l2_why="exit $rc_direct != expected $exp"
- fi
- fi
- if [ $l2_ok -eq 1 ]; then note_pass "$tag/L2"; else note_fail "$tag/L2 ($l2_why)"; fi
- fi
- fi
- done
-done
-shopt -u nullglob
+# ---- drive the corpus ------------------------------------------------------
+
+printf 'roundtrip: arch=%s triple=%s opts="%s" lanes=%s native=%d\n' \
+ "$TEST_ARCH" "$TRIPLE" "$OPTS" "$PATHS" "$is_native_target"
-printf '\n'
-if [ "${#FAIL_NAMES[@]}" -gt 0 ]; then
- printf 'Failed:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done
+if [ ! -x "$CFREE_BIN" ]; then
+ printf ' FATAL cfree binary missing — run "make bin"\n' >&2
+ exit 1
fi
-printf 'Results: %d pass, %d fail, %d skip\n' "$PASS" "$FAIL" "$SKIP"
-[ "$FAIL" -eq 0 ]
+case " $LANES " in
+ *" L2 "*)
+ if [ "$is_native_target" -eq 1 ] && [ "$have_jit_runner" -eq 0 ]; then
+ printf ' warn jit-runner missing; L2 lane will skip\n'
+ fi
+ ;;
+esac
+
+mkdir -p "$BUILD_DIR"
+
+# Skips here are informational (L2 skips on a non-native host / absent
+# jit-runner); the original exited 0 with skips + no fails, so do not gate the
+# exit on skips.
+CF_SKIP_IS_FAILURE=0
+CF_LABEL=test-asm-roundtrip CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$CORPUS_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CORPUS_DIR" \
+ CF_LANES="$LANES" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TEST_ARCH-elf" \
+ CF_TARGETS_EXT="" CF_READ_CASE=rt_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+cf_summary test-asm-roundtrip
+cf_exit
diff --git a/test/asm/roundtrip_toy.sh b/test/asm/roundtrip_toy.sh
@@ -1,14 +1,16 @@
#!/usr/bin/env bash
-# test/asm/roundtrip_toy.sh — L2 exec round-trip over the Toy corpus (native).
+# test/asm/roundtrip_toy.sh — L2 exec round-trip over the Toy corpus (native),
+# on the shared corpus harness (test/lib/cf_corpus.sh).
#
# The Toy frontend exercises the full CG op set, and each case carries an
# exit-code oracle (test/toy/cases/<name>.expected). This lane reuses that
# corpus for free round-trip coverage far beyond the hand-written
# test/asm/roundtrip/ set: for every case, compare the DIRECT compile to the
-# round-tripped one and require the same exit code —
+# round-tripped one and require the same exit code.
#
-# Two exec lanes over the re-assembled object, both compared to the case's
-# exit-code oracle (test/toy/cases/<name>.expected, default 0):
+# One lane (L2) emitting two exec sub-results over the re-assembled object,
+# both compared to the case's exit-code oracle (test/toy/cases/<name>.expected,
+# default 0):
# /run: cfree cc -S | cfree as | cfree run <obj> (in-process JIT)
# /ld: cfree cc -S | cfree as | cfree ld | ./a.out (native link + exec)
#
@@ -17,95 +19,123 @@
# (a multiply-high the disassembler couldn't decode, dropped by `as` until the
# `.inst` fix) that the hand corpus never reached.
#
-# Any case that hits a `cc -S` symbolizer gap can be quarantined in SKIP below
-# so the lane stays green and gates regressions; SKIP is currently empty. Opt-in.
-
+# Opt levels: CFREE_TEST_OPTS (default "O0 O1"). Each case runs at every level.
+#
+# Any case that hits a `cc -S` symbolizer gap is quarantined via a whole-case
+# <name>.skip-style entry in SKIP below so the lane stays green and gates
+# regressions. Opt-in.
+#
+# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner
+# is parallel-safe (CFREE_ASM_PARALLEL flips dispatch); the /run-then-/ld
+# ordering is preserved inside the lane body.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-CFREE="$ROOT/build/cfree"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
+CFREE_BIN="${CFREE:-$ROOT/build/cfree}"
CASES="$ROOT/test/toy/cases"
-WORK="$ROOT/build/test/asm/roundtrip_toy"
+BUILD_DIR="$ROOT/build/test/asm/roundtrip_toy"
+
+# Opt axis. CFREE_TEST_OPTS carries the "O"-prefixed levels (e.g. "O0 O1"); the
+# engine matrix wants the bare digits, and the lane builds "-O<level>".
OPTS="${CFREE_TEST_OPTS:-O0 O1}"
-FILTER="${1:-}"
+OPT_LEVELS=""
+for _o in $OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done
-# Named sections now round-trip (closed 118_decl_extra_attrs): cc -S emits the
-# section with its "flags"/@type/entsize in GNU-as syntax and `as` reconstructs
-# it. SKIP quarantines cases blocked on a *separate*, known cc -S symbolizer gap:
+FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
+
+PAR="${CFREE_ASM_PARALLEL:-1}"
+
+# Whole-case SKIP set (known cc -S symbolizer gaps). Quarantines a case on a
+# *separate*, known gap so the lane stays green and gates regressions:
# - 141_threadlocal_mutate: a thread-local access emits an unsymbolized
# `adrp x, 0x0` (the TLS GOT/descriptor reloc is not yet symbolized in cc -S),
# which `as` rejects ("adr/adrp: symbol required"). This is TLS symbolization,
# tracked separately from named-section round-trip. See
# doc/ASM_ROUNDTRIP_TESTING.md.
+# Named sections now round-trip (closed 118_decl_extra_attrs): cc -S emits the
+# section with its "flags"/@type/entsize in GNU-as syntax and `as` reconstructs
+# it.
SKIP="141_threadlocal_mutate"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
+# strip the leading "<path>: " prefix off a diagnostic's first line.
+rt_diag() { head -n1 "$1" 2>/dev/null | sed 's|.*: ||'; }
+
+# CF_READ_CASE hook: apply the whole-case SKIP quarantine. The engine already
+# reads <name>.expected into CF_EXPECTED (default 0); we just mask it to a byte
+# to match the original `exp=$((exp & 255))`.
+rt_read_case() {
+ case " $SKIP " in
+ *" $CF_BASE "*) CF_SKIP_CASE="known cc -S symbolizer gap"; return ;;
+ esac
+ CF_EXPECTED=$(( CF_EXPECTED & 255 ))
+}
-if [ ! -x "$CFREE" ]; then
- printf 'roundtrip-toy: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2
+# ---- the single L2 lane ----------------------------------------------------
+# cc -S | as, then run the re-assembled object two ways, each compared to the
+# exit-code oracle. /run uses the in-process JIT; /ld links a real native
+# executable and runs it through the OS loader. Native host only (no -target).
+cf_lane_L2() {
+ local s="$CF_WORK/s.s" rt="$CF_WORK/rt.o"
+ # Shared cc -S | as steps. A failure here is a SINGLE result for the case
+ # (matching the original's one-fail-then-skip-rest behavior), not one per
+ # sub-result — the /run and /ld sub-results only exist once an object does.
+ if ! "$CFREE_BIN" cc -S "-O$CF_OPT" "$CF_SRC" -o "$s" 2>"$CF_WORK/ccs.err"; then
+ cf_fail "$CF_NAME cc-S" "cc -S failed"; return
+ fi
+ if ! "$CFREE_BIN" as "$s" -o "$rt" 2>"$CF_WORK/as.err"; then
+ cf_fail "$CF_NAME as" "as: $(rt_diag "$CF_WORK/as.err")"; return
+ fi
+
+ # Sub-result /run: cfree run JIT-links and executes the object in-process.
+ "$CFREE_BIN" run "$rt" >"$CF_WORK/out" 2>"$CF_WORK/run.err"; local rc=$?
+ if [ -s "$CF_WORK/run.err" ]; then
+ cf_fail "$CF_NAME/run" "$(rt_diag "$CF_WORK/run.err")"
+ elif [ "$rc" -eq "$CF_EXPECTED" ]; then
+ cf_pass "$CF_NAME/run"
+ else
+ cf_fail "$CF_NAME/run" "exit $rc != $CF_EXPECTED"
+ fi
+
+ # Sub-result /ld: cfree ld links a real native executable; run it via the OS
+ # loader and compare exit. Exercises cfree ld (layout + relocation + image
+ # emit) and a real process exit — none of which the JIT 'run' path covers.
+ if ! "$CFREE_BIN" ld "$rt" -o "$CF_WORK/a.out" 2>"$CF_WORK/ld.err" || [ -s "$CF_WORK/ld.err" ]; then
+ cf_fail "$CF_NAME/ld" "$(rt_diag "$CF_WORK/ld.err")"; return
+ fi
+ chmod +x "$CF_WORK/a.out" 2>/dev/null || true
+ "$CF_WORK/a.out" >"$CF_WORK/ldout" 2>"$CF_WORK/ldrun.err"; local ldrc=$?
+ if [ -s "$CF_WORK/ldrun.err" ]; then
+ cf_fail "$CF_NAME/ld" "$(rt_diag "$CF_WORK/ldrun.err")"
+ elif [ "$ldrc" -eq "$CF_EXPECTED" ]; then
+ cf_pass "$CF_NAME/ld"
+ else
+ cf_fail "$CF_NAME/ld" "exit $ldrc != $CF_EXPECTED"
+ fi
+}
+
+# ---- drive the corpus ------------------------------------------------------
+
+if [ ! -x "$CFREE_BIN" ]; then
+ printf 'roundtrip-toy: FATAL cfree missing — run "make bin"\n' >&2
exit 1
fi
-mkdir -p "$WORK"
+mkdir -p "$BUILD_DIR"
-pass=0; fail=0; skip=0; failnames=()
-is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; }
+# Skips are informational (e.g. the 141_threadlocal_mutate quarantine), as in
+# the original which exited 0 regardless of skips — do not gate on skips.
+CF_SKIP_IS_FAILURE=0
-shopt -s nullglob
-for src in "$CASES"/*.toy; do
- name="$(basename "$src" .toy)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if is_skip "$name"; then
- skip=$((skip+1)); printf ' %s %s — known cc -S symbolizer gap\n' "$(color_yel SKIP)" "$name"; continue
- fi
- exp=0; [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected")
- exp=$((exp & 255))
- for opt in $OPTS; do
- w="$WORK/$name/$opt"; mkdir -p "$w"
- if ! "$CFREE" cc -S "-$opt" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then
- fail=$((fail+1)); failnames+=("$name[-$opt] cc-S"); printf ' %s %s[-%s] cc -S failed\n' "$(color_red FAIL)" "$name" "$opt"; continue
- fi
- if ! "$CFREE" as "$w/s.s" -o "$w/rt.o" 2>"$w/as.err"; then
- fail=$((fail+1)); failnames+=("$name[-$opt] as: $(head -1 "$w/as.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s] as failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/as.err"|sed 's|.*: ||')"; continue
- fi
- # Lane /run: cfree run JIT-links and executes the object in-process.
- "$CFREE" run "$w/rt.o" >"$w/out" 2>"$w/run.err"; rc=$?
- if [ -s "$w/run.err" ]; then
- fail=$((fail+1)); failnames+=("$name[-$opt]/run: $(head -1 "$w/run.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s]/run failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/run.err"|sed 's|.*: ||')"
- elif [ "$rc" -eq "$exp" ]; then
- pass=$((pass+1))
- else
- fail=$((fail+1)); failnames+=("$name[-$opt]/run exit $rc != $exp")
- printf ' %s %s[-%s]/run exit %d != expected %d\n' "$(color_red FAIL)" "$name" "$opt" "$rc" "$exp"
- fi
-
- # Lane /ld: cfree ld links a real native executable; run it via the OS
- # loader and compare exit. Exercises cfree ld (layout + relocation +
- # image emit) and a real process exit — none of which the JIT 'run'
- # path covers. Native host only (no -target is passed).
- if ! "$CFREE" ld "$w/rt.o" -o "$w/a.out" 2>"$w/ld.err" || [ -s "$w/ld.err" ]; then
- fail=$((fail+1)); failnames+=("$name[-$opt]/ld: $(head -1 "$w/ld.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s]/ld failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ld.err"|sed 's|.*: ||')"; continue
- fi
- chmod +x "$w/a.out" 2>/dev/null || true
- "$w/a.out" >"$w/ldout" 2>"$w/ldrun.err"; ldrc=$?
- if [ -s "$w/ldrun.err" ]; then
- fail=$((fail+1)); failnames+=("$name[-$opt]/ld-run: $(head -1 "$w/ldrun.err"|sed 's|.*: ||')")
- printf ' %s %s[-%s]/ld-run stderr: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ldrun.err"|sed 's|.*: ||')"
- elif [ "$ldrc" -eq "$exp" ]; then
- pass=$((pass+1))
- else
- fail=$((fail+1)); failnames+=("$name[-$opt]/ld exit $ldrc != $exp")
- printf ' %s %s[-%s]/ld exit %d != expected %d\n' "$(color_red FAIL)" "$name" "$opt" "$ldrc" "$exp"
- fi
- done
-done
-shopt -u nullglob
-
-printf '\n'
-[ "${#failnames[@]}" -gt 0 ] && { printf 'Failed:\n'; for f in "${failnames[@]}"; do printf ' %s\n' "$f"; done; }
-printf 'roundtrip-toy: %d pass, %d fail, %d skip\n' "$pass" "$fail" "$skip"
-[ "$fail" -eq 0 ]
+# Native host, no -target. The L2 lane uses no tuple-specific behavior; pin a
+# single nominal tuple so the engine runs each case once per opt level.
+CF_LABEL=test-asm-roundtrip-toy CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \
+ CF_LANES="L2" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="native-native" \
+ CF_TARGETS_EXT="" CF_READ_CASE=rt_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+cf_summary test-asm-roundtrip-toy
+cf_exit
diff --git a/test/asm/run.sh b/test/asm/run.sh
@@ -1,37 +1,45 @@
#!/usr/bin/env bash
-# test/asm/run.sh — file-driven assembler / disassembler test harness.
+# test/asm/run.sh — file-driven assembler / disassembler test harness, on the
+# shared corpus harness (test/lib/cf_corpus.sh).
#
-# Three sub-corpora under test/asm/, one per asm-runner static-output mode:
+# Three sub-corpora under test/asm/, one per asm-runner static-output mode.
+# Each is a SEQUENTIAL corpus with a disjoint case set (its own cf_corpus_run,
+# no opt axis, single arch), NOT a parallel lane within a case:
#
-# encode/ <name>.s + <name>.expected.hex golden hex bytes
-# decode/ <name>.hex + <name>.expected.txt golden decoded text
-# listing/ <name>.in.bin + <name>.expected.lst golden objdump-style
+# encode/ <name>.s + <name>.expected.hex golden hex bytes
+# (+ optional <name>.expected integer exit code)
+# decode/ <name>.hex + <name>.expected.txt golden decoded text
+# listing/ <name>.in.bin + <name>.expected.lst golden objdump-style
#
# Path matrix (6 letters, default HTLDJE):
#
# H Hex encode — encode/ only. asm-runner --encode IN.s → hex; diff
-# vs <name>.expected.hex.
+# vs <name>.expected.hex. Missing golden => SKIP.
# T Text decode — decode/ only. asm-runner --decode IN.hex → text; diff
-# vs <name>.expected.txt.
+# vs <name>.expected.txt. Missing golden => FAIL.
# L Listing — listing/ only. asm-runner --listing IN.in.bin → text;
-# diff vs <name>.expected.lst.
+# diff vs <name>.expected.lst. Missing golden => FAIL.
# D Direct JIT — encode/ only, when <name>.expected (integer exit) is
# present. asm-runner --jit IN.s → exit code matches.
-# Host arch must match the cross-target.
+# Host arch must match the cross-target. Missing
+# .expected exit => SKIP.
# J JIT via file — encode/ only, when <name>.expected is present.
# asm-runner --emit + jit-runner. Host arch must match.
# E ELF exec — encode/ only, when <name>.expected is present.
# asm-runner --emit + start.o → link-exe-runner →
-# qemu/podman → exit code. Cross-host friendly.
+# qemu/podman → exit code. Cross-host friendly (deferred
+# batched exec via cf_queue_e).
#
# Reuses the test/link harness binaries (link-exe-runner, jit-runner) plus
# test/link/harness/start.c verbatim — same convention as test/parse/run.sh.
#
-# Phase 1 (doc/ASM.md §5): asm_parse and the disasm iterator are still
-# stubs in src/api/stubs.c. Every smoke case carries a .skip sidecar so
-# the harness reports SKIP cleanly; the wiring still runs on every CI
-# pass. CFREE_TEST_ALLOW_SKIP defaults to 1 here for the duration of
-# phase 1 — flip to 0 (matching the rest of the suite) once the
+# Whole-case <name>.targets carries bare arch tokens (aa64/x64/rv64 + synonyms);
+# a case applies only when one of them matches CFREE_TEST_ARCH (else SKIP-NA).
+# Per-case <name>.skip (+ .<arch>.skip / .<lane>.skip) sidecars skip cleanly.
+#
+# Phase 1 (doc/ASM.md §5): asm_parse and the disasm iterator are still stubs
+# in src/api/stubs.c. CFREE_TEST_ALLOW_SKIP defaults to 1 here for the
+# duration of phase 1 — flip to 0 (matching the rest of the suite) once the
# assembler / disasm iterator are real.
#
# Filtering:
@@ -39,6 +47,10 @@
# name_filter substring match against case basename
# paths subset of "HTLDJE" (default "HTLDJE")
# Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS.
+#
+# Parallelism: every lane hook writes only under $CF_WORK and records only via
+# cf_*, so the console summary is identical serial or parallel. CFREE_ASM_PARALLEL
+# flips dispatch (default on).
set -u
@@ -48,10 +60,20 @@ LINK_TEST_DIR="$ROOT/test/link"
BUILD_DIR="$ROOT/build/test"
LIB_AR="$ROOT/build/libcfree.a"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
ASM_RUNNER="$BUILD_DIR/asm-runner"
LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner"
JIT_RUNNER="$BUILD_DIR/jit-runner"
+# Phase 1: ALLOW_SKIP defaults to 1 (smoke cases skip cleanly because
+# asm_parse / cfree_disasm_iter_* are still stubs). The engine's cf_exit reads
+# CFREE_TEST_ALLOW_SKIP (default 0), so export the phase-1 default of 1 unless
+# the caller overrides it.
+export CFREE_TEST_ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-1}"
+
# CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the
# pre-multiarch behavior. The asm-runner reads the same env via
# test/lib/cfree_test_target.h.
@@ -63,14 +85,17 @@ case "$CFREE_TEST_ARCH" in
*) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;;
esac
export CFREE_TEST_ARCH
-
CLANG_TARGET="--target=$CLANG_TRIPLE"
-# Phase 1: ALLOW_SKIP defaults to 1 (smoke cases skip cleanly because
-# asm_parse / cfree_disasm_iter_* are still stubs). Flip to 0 once the
-# assembler / disassembler land.
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-1}"
+# Synthetic single tuple — there is no opt axis and this harness is single-arch.
+# Whole-case .targets applicability is computed by cf_read_case (bare arch
+# tokens), so the engine's tuple matcher is disabled (CF_TARGETS_EXT="").
+CUR_TUPLE="$TEST_ARCH-asm"
+
+# Filtering: positional [name_filter] [paths] mirror CFREE_TEST_FILTER / PATHS.
+# The engine's discovery honors CFREE_TEST_FILTER, so export the positional one.
FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+[ -n "$FILTER" ] && export CFREE_TEST_FILTER="$FILTER"
PATHS="${2:-${CFREE_TEST_PATHS:-HTLDJE}}"
case "$PATHS" in *H*) RUN_H=1;; *) RUN_H=0;; esac
case "$PATHS" in *T*) RUN_T=1;; *) RUN_T=0;; esac
@@ -78,22 +103,8 @@ case "$PATHS" in *L*) RUN_L=1;; *) RUN_L=0;; esac
case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac
case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
-T_H=0; T_T=0; T_L=0; T_D=0; T_J=0; T_E=0
-now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; }
-
-mkdir -p "$BUILD_DIR" "$BUILD_DIR/asm"
-PASS=0; FAIL=0; SKIP=0
-FAIL_NAMES=(); SKIP_NAMES=()
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-note_na() { printf ' %s %s — not applicable to %s\n' "$(color_yel SKIP-NA)" "$1" "$TEST_ARCH"; }
+PAR="${CFREE_ASM_PARALLEL:-1}"
# ---- tool detection (mirrors test/parse/run.sh) ----------------------------
@@ -124,37 +135,39 @@ case "$TEST_ARCH" in
rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;;
esac
-# Shared per-arch exec helper — see test/lib/exec_target.sh.
+# Shared per-arch exec helper — see test/lib/exec_target.sh. Path E queues bare
+# EXEC_ARCH tags (== <arch>-linux), so set the caller-contract knobs it reads.
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+. "$ROOT/test/lib/exec_target.sh"
# ---- harness binaries ------------------------------------------------------
printf 'Checking harness...\n'
if [ ! -x "$ASM_RUNNER" ]; then
- printf ' %s asm-runner missing — run "make test-asm"\n' \
- "$(color_red FATAL)" >&2
+ printf ' %sFATAL%s asm-runner missing — run "make test-asm"\n' \
+ "$_CF_RED" "$_CF_RST" >&2
exit 1
fi
-printf ' %s asm-runner\n' "$(color_grn found)"
+printf ' %sfound%s asm-runner\n' "$_CF_GRN" "$_CF_RST"
# link-exe-runner — for path E.
if [ -x "$LINK_EXE_RUNNER" ]; then
have_exe_runner=1
- printf ' %s link-exe-runner\n' "$(color_grn found)"
+ printf ' %sfound%s link-exe-runner\n' "$_CF_GRN" "$_CF_RST"
else
- printf ' %s link-exe-runner missing; E path will skip\n' "$(color_yel warn)"
+ printf ' %swarn%s link-exe-runner missing; E path will skip\n' "$_CF_YEL" "$_CF_RST"
fi
# jit-runner — for path J. Only meaningful when host arch matches the cross-target.
if [ $is_native_target -eq 1 ]; then
if [ -x "$JIT_RUNNER" ]; then
have_jit_runner=1
- printf ' %s jit-runner\n' "$(color_grn found)"
+ printf ' %sfound%s jit-runner\n' "$_CF_GRN" "$_CF_RST"
else
- printf ' %s jit-runner missing; J path will skip\n' "$(color_yel warn)"
+ printf ' %swarn%s jit-runner missing; J path will skip\n' "$_CF_YEL" "$_CF_RST"
fi
fi
@@ -172,23 +185,17 @@ fi
printf 'Running cases...\n'
-# ---- helpers --------------------------------------------------------------
-
-# diff_case <name>/<P> <work> <expected> <actual> <link_dt_or_0>
-# Emits PASS or FAIL based on byte-exact match.
-diff_case() {
- local label="$1" work="$2" expected="$3" actual="$4"
- if diff -u "$expected" "$actual" >"$work/diff" 2>&1; then
- note_pass "$label"
- else
- note_fail "$label (golden mismatch; see $work/diff)"
- fi
-}
-
-case_applies() {
- local dir="$1" name="$2" targets tuple
- targets="$dir/$name.targets"
+# ---- whole-case .targets applicability (bare arch tokens) ------------------
+# The original .targets sidecars list bare arch names (aa64/x64/rv64 and
+# synonyms), NOT <arch>-<obj> tuples, so the engine's cf_tuple_applicable does
+# not apply. Read them here and emit SKIP-NA via CF_SKIP_NA_CASE. The engine's
+# CF_TARGETS_EXT matcher stays disabled (CF_TARGETS_EXT="").
+cf_read_case() {
+ # Reset per-case emit state (serial mode reuses this shell across cases).
+ _asm_emit_done=0
+ local targets="$CF_SIDECAR_DIR/$CF_BASE.targets"
[ -f "$targets" ] || return 0
+ local tuple
for tuple in $(cat "$targets"); do
case "$tuple:$TEST_ARCH" in
aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;;
@@ -196,272 +203,184 @@ case_applies() {
rv64:rv64|riscv64:rv64) return 0 ;;
esac
done
- return 1
+ CF_SKIP_NA_CASE=1
+}
+CF_READ_CASE=cf_read_case
+
+# ---- golden-diff helper (CF_WORK-confined) ---------------------------------
+# cf_golden_diff LABEL EXPECTED ACTUAL : PASS on byte-exact match, else FAIL.
+cf_golden_diff() {
+ local label="$1" expected="$2" actual="$3"
+ if diff -u "$expected" "$actual" >"$CF_WORK/diff" 2>&1; then
+ cf_pass "$label"
+ else
+ cf_fail "$label" "golden mismatch; see $CF_WORK/diff"
+ fi
}
-# ---- decode and listing loops — single-path, golden-driven only -----------
-
-if [ $RUN_T -eq 1 ] && [ -d "$TEST_DIR/decode" ]; then
- for in_path in "$TEST_DIR"/decode/*.hex; do
- [ -e "$in_path" ] || continue
- name="$(basename "$in_path" .hex)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if ! case_applies "$TEST_DIR/decode" "$name"; then
- note_na "$name/T"
- continue
- fi
- work="$BUILD_DIR/asm/decode/$name"
- mkdir -p "$work"
- if [ -e "$TEST_DIR/decode/$name.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/decode/$name.skip")
- note_skip "$name/T" "$reason"
- continue
- fi
- expected="$TEST_DIR/decode/$name.expected.txt"
- if [ ! -e "$expected" ]; then
- note_fail "$name/T (missing golden $(basename "$expected"))"
- continue
- fi
- t0=$(now_ms)
- out="$work/out.txt"
- if ! "$ASM_RUNNER" --decode "$in_path" "$out" >"$work/stdout" 2>"$work/stderr"; then
- dt=$(( $(now_ms) - t0 )); T_T=$(( T_T + dt ))
- note_fail "$name/T (asm-runner --decode failed; see $work/stderr, ${dt}ms)"
- continue
- fi
- dt=$(( $(now_ms) - t0 )); T_T=$(( T_T + dt ))
- diff_case "$name/T (${dt}ms)" "$work" "$expected" "$out"
- done
-fi
-
-if [ $RUN_L -eq 1 ] && [ -d "$TEST_DIR/listing" ]; then
- for in_path in "$TEST_DIR"/listing/*.in.bin; do
- [ -e "$in_path" ] || continue
- name="$(basename "$in_path" .in.bin)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if ! case_applies "$TEST_DIR/listing" "$name"; then
- note_na "$name/L"
- continue
- fi
- work="$BUILD_DIR/asm/listing/$name"
- mkdir -p "$work"
- if [ -e "$TEST_DIR/listing/$name.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/listing/$name.skip")
- note_skip "$name/L" "$reason"
- continue
- fi
- expected="$TEST_DIR/listing/$name.expected.lst"
- if [ ! -e "$expected" ]; then
- note_fail "$name/L (missing golden $(basename "$expected"))"
- continue
- fi
- t0=$(now_ms)
- out="$work/out.lst"
- if ! "$ASM_RUNNER" --listing "$in_path" "$out" >"$work/stdout" 2>"$work/stderr"; then
- dt=$(( $(now_ms) - t0 )); T_L=$(( T_L + dt ))
- note_fail "$name/L (asm-runner --listing failed; see $work/stderr, ${dt}ms)"
- continue
- fi
- dt=$(( $(now_ms) - t0 )); T_L=$(( T_L + dt ))
- diff_case "$name/L (${dt}ms)" "$work" "$expected" "$out"
- done
-fi
+# ---- decode corpus: lane T (text golden-diff) ------------------------------
+cf_lane_T() {
+ local expected="$CF_SIDECAR_DIR/$CF_BASE.expected.txt"
+ if [ ! -e "$expected" ]; then
+ cf_fail "$CF_NAME/T" "missing golden $(basename "$expected")"; return
+ fi
+ local out="$CF_WORK/out.txt"
+ if ! "$ASM_RUNNER" --decode "$CF_SRC" "$out" >"$CF_WORK/stdout" 2>"$CF_WORK/stderr"; then
+ cf_fail "$CF_NAME/T" "asm-runner --decode failed; see $CF_WORK/stderr"; return
+ fi
+ cf_golden_diff "$CF_NAME/T" "$expected" "$out"
+}
-# ---- encode loop — drives H + D + J + E per .s case ----------------------
-
-# Path E result bookkeeping — same shape as test/parse.
-E_NAMES=()
-E_WORK=()
-E_LINK_MS=()
-E_EXPECTED=()
-
-if [ -d "$TEST_DIR/encode" ]; then
- for src in "$TEST_DIR"/encode/*.s; do
- [ -e "$src" ] || continue
- name="$(basename "$src" .s)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- if ! case_applies "$TEST_DIR/encode" "$name"; then
- [ $RUN_H -eq 1 ] && note_na "$name/H"
- [ $RUN_D -eq 1 ] && note_na "$name/D"
- [ $RUN_J -eq 1 ] && note_na "$name/J"
- [ $RUN_E -eq 1 ] && note_na "$name/E"
- continue
- fi
- work="$BUILD_DIR/asm/encode/$name"
- mkdir -p "$work"
-
- # Per-case skip sidecar — applies to every path for this case.
- if [ -e "$TEST_DIR/encode/$name.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/encode/$name.skip")
- [ $RUN_H -eq 1 ] && note_skip "$name/H" "$reason"
- [ $RUN_D -eq 1 ] && note_skip "$name/D" "$reason"
- [ $RUN_J -eq 1 ] && note_skip "$name/J" "$reason"
- [ $RUN_E -eq 1 ] && note_skip "$name/E" "$reason"
- continue
- fi
+# ---- listing corpus: lane L (listing golden-diff) --------------------------
+cf_lane_L() {
+ local expected="$CF_SIDECAR_DIR/$CF_BASE.expected.lst"
+ if [ ! -e "$expected" ]; then
+ cf_fail "$CF_NAME/L" "missing golden $(basename "$expected")"; return
+ fi
+ local out="$CF_WORK/out.lst"
+ if ! "$ASM_RUNNER" --listing "$CF_SRC" "$out" >"$CF_WORK/stdout" 2>"$CF_WORK/stderr"; then
+ cf_fail "$CF_NAME/L" "asm-runner --listing failed; see $CF_WORK/stderr"; return
+ fi
+ cf_golden_diff "$CF_NAME/L" "$expected" "$out"
+}
- # Expected exit code (for D/J/E). Absent → those paths skip.
- expected_exit_file="$TEST_DIR/encode/$name.expected"
- has_exit=0
- if [ -e "$expected_exit_file" ]; then
- expected=$(head -n1 "$expected_exit_file")
- expected_byte=$(( expected & 0xff ))
- has_exit=1
- fi
+# ---- encode corpus lanes: H / D / J / E ------------------------------------
+# CF_EXPECTED carries the .expected integer exit code (0 when absent); a missing
+# .expected file means the exec lanes (D/J/E) SKIP. Detect "has exit" via the
+# sidecar's presence directly so a legitimate expected exit of 0 still runs.
+_asm_has_exit() { [ -f "$CF_SIDECAR_DIR/$CF_BASE.expected" ]; }
+_asm_exit_byte() { echo $(( CF_EXPECTED & 0xff )); }
+
+# Path H: hex encode roundtrip. Missing .expected.hex golden => SKIP.
+cf_lane_H() {
+ local expected_hex="$CF_SIDECAR_DIR/$CF_BASE.expected.hex"
+ if [ ! -e "$expected_hex" ]; then
+ cf_skip "$CF_NAME/H" "no .expected.hex golden"; return
+ fi
+ local out="$CF_WORK/out.hex"
+ if ! "$ASM_RUNNER" --encode "$CF_SRC" "$out" >"$CF_WORK/h.out" 2>"$CF_WORK/h.err"; then
+ cf_fail "$CF_NAME/H" "asm-runner --encode failed; see $CF_WORK/h.err"; return
+ fi
+ cf_golden_diff "$CF_NAME/H" "$expected_hex" "$out"
+}
- # ---- Path H: hex encode roundtrip ----------------------------------
- if [ $RUN_H -eq 1 ]; then
- expected_hex="$TEST_DIR/encode/$name.expected.hex"
- if [ ! -e "$expected_hex" ]; then
- note_skip "$name/H" "no .expected.hex golden"
- else
- t0=$(now_ms)
- out="$work/out.hex"
- if ! "$ASM_RUNNER" --encode "$src" "$out" >"$work/h.out" 2>"$work/h.err"; then
- dt=$(( $(now_ms) - t0 )); T_H=$(( T_H + dt ))
- note_fail "$name/H (asm-runner --encode failed; see $work/h.err, ${dt}ms)"
- else
- dt=$(( $(now_ms) - t0 )); T_H=$(( T_H + dt ))
- diff_case "$name/H (${dt}ms)" "$work" "$expected_hex" "$out"
- fi
- fi
- fi
+# Ensure the .o needed by J/E exists. Sets ASM_OBJ to its path and returns 0 on
+# success; on the first failure emits FAIL "$CF_NAME/emit" (matching the
+# original standalone emit failure record) and returns 1. Must NOT be called in
+# a $(...) subshell — cf_fail mutates counters that a subshell would discard.
+# D does not need a .o — asm-runner --jit does the full parse+jit in process.
+_asm_emit_done=0
+_asm_emit() {
+ ASM_OBJ="$CF_WORK/$CF_BASE.o"
+ [ -f "$ASM_OBJ" ] && return 0
+ if [ "$_asm_emit_done" -eq 1 ]; then return 1; fi
+ _asm_emit_done=1
+ if "$ASM_RUNNER" --emit "$CF_SRC" "$ASM_OBJ" 2>"$CF_WORK/emit.err"; then
+ return 0
+ fi
+ cf_fail "$CF_NAME/emit" "asm-runner --emit failed; see $CF_WORK/emit.err"
+ return 1
+}
- # ---- emit (needed by D/J/E exec paths) -----------------------------
- # D doesn't strictly need a .o on disk — asm-runner --jit does the
- # full parse+jit in process. But J and E need the file emit.
- obj="$work/$name.o"
- need_emit=0
- if [ $has_exit -eq 1 ]; then
- if [ $RUN_J -eq 1 ] || [ $RUN_E -eq 1 ]; then need_emit=1; fi
- fi
- if [ $need_emit -eq 1 ]; then
- if ! "$ASM_RUNNER" --emit "$src" "$obj" 2>"$work/emit.err"; then
- # D may still run independently; the J/E paths below detect
- # the missing .o and skip themselves.
- printf ' %s %s/emit (asm-runner --emit failed; see %s)\n' \
- "$(color_red FAIL)" "$name" "$work/emit.err"
- FAIL=$((FAIL+1)); FAIL_NAMES+=("$name/emit")
- fi
- fi
+# Path D: in-process JIT. Missing .expected exit => SKIP.
+cf_lane_D() {
+ if ! _asm_has_exit; then cf_skip "$CF_NAME/D" "no .expected exit code"; return; fi
+ if [ $is_native_target -eq 0 ]; then
+ cf_skip "$CF_NAME/D" "host arch != $TEST_ARCH (no native JIT)"; return
+ fi
+ local want; want=$(_asm_exit_byte)
+ "$ASM_RUNNER" --jit "$CF_SRC" >"$CF_WORK/d.out" 2>"$CF_WORK/d.err"
+ local rc=$?
+ if [ "$rc" -eq "$want" ]; then cf_pass "$CF_NAME/D"
+ else cf_fail "$CF_NAME/D" "expected $want got $rc"; fi
+}
- # ---- Path D: in-process JIT ----------------------------------------
- if [ $RUN_D -eq 1 ]; then
- if [ $has_exit -eq 0 ]; then
- note_skip "$name/D" "no .expected exit code"
- elif [ $is_native_target -eq 0 ]; then
- note_skip "$name/D" "host arch != $TEST_ARCH (no native JIT)"
- else
- t0=$(now_ms)
- "$ASM_RUNNER" --jit "$src" >"$work/d.out" 2>"$work/d.err"
- d_rc=$?
- dt=$(( $(now_ms) - t0 )); T_D=$(( T_D + dt ))
- if [ "$d_rc" -eq "$expected_byte" ]; then
- note_pass "$name/D (${dt}ms)"
- else
- note_fail "$name/D (expected $expected_byte got $d_rc, ${dt}ms)"
- fi
- fi
- fi
+# Path J: jit-via-file. Missing .expected exit => SKIP.
+cf_lane_J() {
+ if ! _asm_has_exit; then cf_skip "$CF_NAME/J" "no .expected exit code"; return; fi
+ if [ $have_jit_runner -eq 0 ]; then
+ cf_skip "$CF_NAME/J" "no jit-runner (host arch != $TEST_ARCH)"; return
+ fi
+ _asm_emit || { cf_skip "$CF_NAME/J" "no .o (--emit failed)"; return; }
+ local want; want=$(_asm_exit_byte)
+ "$JIT_RUNNER" "$ASM_OBJ" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err"
+ local rc=$?
+ if [ "$rc" -eq "$want" ]; then cf_pass "$CF_NAME/J"
+ else cf_fail "$CF_NAME/J" "expected $want got $rc"; fi
+}
- # ---- Path J: jit-via-file ------------------------------------------
- if [ $RUN_J -eq 1 ]; then
- if [ $has_exit -eq 0 ]; then
- note_skip "$name/J" "no .expected exit code"
- elif [ $have_jit_runner -eq 0 ]; then
- note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH)"
- elif [ ! -e "$obj" ]; then
- note_skip "$name/J" "no .o (--emit failed)"
- else
- t0=$(now_ms)
- "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err"
- j_rc=$?
- dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt ))
- if [ "$j_rc" -eq "$expected_byte" ]; then
- note_pass "$name/J (${dt}ms)"
- else
- note_fail "$name/J (expected $expected_byte got $j_rc, ${dt}ms)"
- fi
- fi
- fi
+# Path E: link + deferred (batched) qemu/podman exec. Missing .expected => SKIP.
+cf_lane_E() {
+ if ! _asm_has_exit; then cf_skip "$CF_NAME/E" "no .expected exit code"; return; fi
+ if [ $have_exe_runner -eq 0 ] || [ $have_clang_cross -eq 0 ] || [ $have_start_obj -eq 0 ]; then
+ cf_skip "$CF_NAME/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"; return
+ fi
+ _asm_emit || { cf_skip "$CF_NAME/E" "no .o (--emit failed)"; return; }
+ local exe="$CF_WORK/linked.exe"
+ if ! "$LINK_EXE_RUNNER" -o "$exe" "$ASM_OBJ" "$START_OBJ" \
+ >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then
+ cf_fail "$CF_NAME/E" "link failed"; return
+ fi
+ if exec_target_supported "$EXEC_ARCH"; then
+ cf_queue_e "$CF_NAME/E" "$exe" \
+ "$CF_WORK/exec.out" "$CF_WORK/exec.err" "$CF_WORK/exec.rc" \
+ "$(_asm_exit_byte)" "$EXEC_ARCH"
+ else
+ cf_skip "$CF_NAME/E" "no runner for $EXEC_ARCH"
+ fi
+}
- # ---- Path E: link + (batched) qemu/podman --------------------------
- if [ $RUN_E -eq 1 ]; then
- if [ $has_exit -eq 0 ]; then
- note_skip "$name/E" "no .expected exit code"
- elif [ $have_exe_runner -eq 0 ] || [ $have_clang_cross -eq 0 ] \
- || [ $have_start_obj -eq 0 ]; then
- note_skip "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"
- elif [ ! -e "$obj" ]; then
- note_skip "$name/E" "no .o (--emit failed)"
- else
- t0=$(now_ms)
- exe="$work/linked.exe"
- if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \
- >"$work/exec_link.out" 2>"$work/exec_link.err"; then
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- note_fail "$name/E (link failed, ${dt}ms)"
- elif exec_target_supported "$EXEC_ARCH"; then
- link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt ))
- E_NAMES+=("$name")
- E_WORK+=("$work")
- E_LINK_MS+=("$link_dt")
- E_EXPECTED+=("$expected_byte")
- exec_target_queue "$EXEC_ARCH" "$name" "$exe" \
- "$work/exec.out" "$work/exec.err" "$work/exec.rc"
- else
- note_skip "$name/E" "no runner for $EXEC_ARCH"
- fi
- fi
+# ---- has-cases guard -------------------------------------------------------
+# cf_corpus_run exits 2 if a sub-corpus has zero matching cases (after the
+# CFREE_TEST_FILTER), so only invoke it when at least one case is present.
+# _asm_have_cases GLOB EXT : mirrors the engine's discovery (basename minus
+# .EXT, CFREE_TEST_FILTER substring) and returns 0 iff a case survives.
+_asm_have_cases() {
+ local glob="$1" ext="$2" f base
+ shopt -s nullglob
+ for f in $glob; do
+ base="$(basename "$f")"; base="${base%.$ext}"
+ if [ -n "${CFREE_TEST_FILTER:-}" ]; then
+ case "$base" in *"$CFREE_TEST_FILTER"*) ;; *) continue ;; esac
fi
+ return 0
done
-fi
+ return 1
+}
-# ---- batched path-E flush + verification -----------------------------------
-
-T_E_BATCH=0
-if [ "$(exec_target_queue_size)" -gt 0 ]; then
- printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)"
- t0=$(now_ms)
- exec_target_flush
- T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
-
- i=0
- while [ $i -lt ${#E_NAMES[@]} ]; do
- name="${E_NAMES[$i]}"
- work="${E_WORK[$i]}"
- link_dt="${E_LINK_MS[$i]}"
- expected_byte="${E_EXPECTED[$i]}"
- if [ ! -f "$work/exec.rc" ]; then
- note_fail "$name/E (no rc; podman batch did not produce results)"
- else
- RUN_RC="$(cat "$work/exec.rc")"
- if [ "$RUN_RC" -eq "$expected_byte" ]; then
- note_pass "$name/E (link ${link_dt}ms)"
- else
- note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)"
- fi
- fi
- i=$((i+1))
- done
+# ---- drive the three sub-corpora -------------------------------------------
+
+# (1) encode/*.s — lanes H (golden hex), D (in-proc JIT), J (jit-runner),
+# E (link + deferred exec). Active lanes in PATHS order.
+ENCODE_LANES=
+[ "$RUN_H" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES H"
+[ "$RUN_D" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES D"
+[ "$RUN_J" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES J"
+[ "$RUN_E" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES E"
+if [ -n "$ENCODE_LANES" ] && _asm_have_cases "$TEST_DIR/encode/*.s" s; then
+ CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/encode" \
+ CF_CORPUS_GLOBS="$TEST_DIR/encode/*.s" CF_CORPUS_EXT=s CF_SIDECAR_DIR="$TEST_DIR/encode" \
+ CF_LANES="$ENCODE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run
fi
-# ---- summary ---------------------------------------------------------------
-
-if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
- printf '\nFailed:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done
+# (2) decode/*.hex — lane T (golden decoded text).
+if [ "$RUN_T" -eq 1 ] && _asm_have_cases "$TEST_DIR/decode/*.hex" hex; then
+ CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/decode" \
+ CF_CORPUS_GLOBS="$TEST_DIR/decode/*.hex" CF_CORPUS_EXT=hex CF_SIDECAR_DIR="$TEST_DIR/decode" \
+ CF_LANES="T" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run
fi
-if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then
- printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n'
- for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done
+# (3) listing/*.in.bin — lane L (golden listing).
+if [ "$RUN_L" -eq 1 ] && _asm_have_cases "$TEST_DIR/listing/*.in.bin" in.bin; then
+ CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/listing" \
+ CF_CORPUS_GLOBS="$TEST_DIR/listing/*.in.bin" CF_CORPUS_EXT=in.bin CF_SIDECAR_DIR="$TEST_DIR/listing" \
+ CF_LANES="L" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run
fi
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: H=%dms T=%dms L=%dms D=%dms J=%dms E=%dms (batch %dms)\n' \
- "$T_H" "$T_T" "$T_L" "$T_D" "$T_J" "$T_E" "$T_E_BATCH"
+# ---- summary ---------------------------------------------------------------
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-asm
+cf_exit
diff --git a/test/asm/symmetry.sh b/test/asm/symmetry.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
-# test/asm/symmetry.sh — asm<->disasm self-symmetry sweep (aa64).
+# test/asm/symmetry.sh — Type D (differential, baseline mode):
+# asm<->disasm self-symmetry sweep (aa64).
#
# Systematically checks that the assembler and disassembler agree on the
# instruction set, in both directions:
@@ -22,26 +23,35 @@
# the baseline documents the disasm-completeness backlog. Closing a gap shrinks
# the baseline (regenerate with --update). See doc/ASM_ROUNDTRIP_TESTING.md.
#
+# Type D oracle: AGREEMENT against a checked-in baseline. The producer here is
+# the freshly-generated, sorted asymmetry report; cf_diff_baseline compares it
+# to test/asm/symmetry.baseline (or regenerates it under --update /
+# CF_DIFF_UPDATE=1).
+#
# Opt-in; host-independent (no execution). Decode-side assembles line-by-line,
# so it is a few seconds.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+. "$ROOT/test/lib/cfree_sh_report.sh"
+. "$ROOT/test/lib/cf_differential.sh"
+
AR="$ROOT/build/test/asm-runner"
GEN="$ROOT/build/test/aa64_sweep_gen"
ENCODE_DIR="$ROOT/test/asm/encode"
WORK="$ROOT/build/test/asm/symmetry"
BASELINE="$ROOT/test/asm/symmetry.baseline"
-UPDATE=0
-[ "${1:-}" = "--update" ] && UPDATE=1
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
+# --update is the long-standing regen flag; CF_DIFF_UPDATE=1 is the canonical
+# Type D knob. Either one regenerates the baseline (cf_diff_baseline honors
+# CF_DIFF_UPDATE).
+[ "${1:-}" = "--update" ] && CF_DIFF_UPDATE=1
+: "${CF_DIFF_UPDATE:=0}"
+export CF_DIFF_UPDATE
if [ ! -x "$AR" ] || [ ! -x "$GEN" ]; then
- printf '%s asm-runner / aa64_sweep_gen missing — run the test target\n' \
- "$(color_red FATAL)" >&2
+ echo "symmetry: asm-runner / aa64_sweep_gen missing — run the test target" >&2
exit 2
fi
@@ -50,6 +60,8 @@ mkdir -p "$WORK"
report="$WORK/report"
: > "$report"
+cf_report_init
+
strip_off() { sed -E 's/^[0-9a-f]+:\t//'; }
# ---- decode-side: table sweep ---------------------------------------------
@@ -102,19 +114,10 @@ shopt -u nullglob
sort -u "$report" -o "$report"
# ---- compare against the baseline snapshot --------------------------------
-if [ $UPDATE -eq 1 ]; then
- cp "$report" "$BASELINE"
- printf 'symmetry: baseline updated (%d known asymmetries)\n' "$(wc -l < "$report" | tr -d ' ')"
- exit 0
-fi
-[ -f "$BASELINE" ] || : > "$BASELINE"
-if diff -u "$BASELINE" "$report" > "$WORK/delta.diff" 2>&1; then
- printf 'symmetry: %s (%d known asymmetries in baseline)\n' \
- "$(color_grn 'no new asm<->disasm asymmetry')" \
- "$(wc -l < "$BASELINE" | tr -d ' ')"
- exit 0
-fi
-printf 'symmetry: %s\n' "$(color_red 'asymmetry set changed vs baseline')"
-printf ' (+ = new asymmetry / regression, - = fixed; regenerate with --update)\n'
-grep -E '^[-+][^-+]' "$WORK/delta.diff" | head -30 | sed 's/^/ /'
-exit 1
+# Single Type D verdict: the freshly produced, normalized report must equal the
+# checked-in baseline (cf_diff_baseline regenerates it under CF_DIFF_UPDATE=1
+# and shows the unified delta on drift).
+cf_diff_baseline symmetry "$BASELINE" "$report"
+
+cf_summary symmetry
+cf_exit
diff --git a/test/bounce/bounce.sh b/test/bounce/bounce.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
-# test/bounce/bounce.sh — format-bounce stress test.
+# test/bounce/bounce.sh — format-bounce stress test, on the shared corpus
+# harness (test/lib/cf_corpus.sh).
#
# Goal: stress cfree's object read/write/reloc, archive, and partial-link
# paths by taking one compiled program and *bouncing* its object through
@@ -8,8 +9,11 @@
# cross-arch run coverage elsewhere; this test exists to exercise the
# format machinery, not the arches.
#
-# For each (arch, case, -O level) the program is compiled to a relocatable
-# object once, then each bounce chain rebuilds an executable from it:
+# Matrix = case x opt x tuple (one <arch>-linux tuple per requested arch).
+# For each (case, opt, arch) item the program is compiled to a relocatable
+# object once, then each applicable bounce CHAIN rebuilds an executable from
+# it (the chain axis is iterated inside the lane, like wasm's multi-result
+# lanes):
#
# direct baseline: link prog.o + crt.o straight through
# macho_rt ELF -> Mach-O -> ELF (objcopy) then link
@@ -19,13 +23,15 @@
# strip_dbg objcopy --strip-debug on prog.o, then link
# ar `ar rcs` prog.o into an archive, on-demand link with crt.o
#
-# Every chain's executable is run (batched through one podman exec) and its
-# exit code compared to a host-cc reference for that case. Skip-vs-fail
-# follows the smoke harness: a skip is a failure unless CFREE_TEST_ALLOW_SKIP=1.
+# Each chain's executable is queued through cf_queue_e and run in ONE batched
+# container exec at flush; its exit code is compared to a host-cc reference
+# for that case. The chain transforms + compute_ref host-reference stay
+# LANE-LOCAL. Every lane hook writes only under $CF_WORK and records via cf_*,
+# so the runner is parallel-safe; CFREE_BOUNCE_PARALLEL flips dispatch.
#
# Arch selection: defaults to the host-native Linux arch (fast, no
# emulation). Override with CFREE_BOUNCE_ARCHES="aarch64 x64 rv64" to sweep
-# others through podman/qemu.
+# others through podman/qemu. Skip is a failure unless CFREE_TEST_ALLOW_SKIP=1.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
@@ -33,21 +39,15 @@ BIN="${CFREE_BIN:-$ROOT/build/cfree}"
HOSTCC="${CC:-cc}"
BUILD="$ROOT/build/test/bounce"
CRT="$ROOT/test/bounce/crt/crt.c"
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
OPT_LEVELS="${CFREE_BOUNCE_OPTS:-0 1}"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
rm -rf "$BUILD"
mkdir -p "$BUILD"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-PASS=0; FAIL=0; SKIP=0
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-
# ---- arch selection --------------------------------------------------------
case "$(uname -m 2>/dev/null)" in
@@ -83,7 +83,7 @@ export have_podman have_qemu QEMU_BIN is_aarch64
EXEC_TARGET_MOUNT_ROOT="$BUILD"
export EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+. "$ROOT/test/lib/exec_target.sh"
# ---- prerequisites ---------------------------------------------------------
@@ -92,36 +92,35 @@ if [ ! -x "$BIN" ]; then
exit 2
fi
if [ -z "$ARCHES" ]; then
- note_skip "arch" "unsupported host arch $(uname -m) — set CFREE_BOUNCE_ARCHES"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1
+ cf_skip "arch" "unsupported host arch $(uname -m) — set CFREE_BOUNCE_ARCHES"
+ cf_summary test-bounce; cf_exit
fi
if ! "$HOSTCC" -x c -c /dev/null -o /dev/null 2>/dev/null; then
- note_skip "hostcc" "host '$HOSTCC' cannot compile — set CC"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1
+ cf_skip "hostcc" "host '$HOSTCC' cannot compile — set CC"
+ cf_summary test-bounce; cf_exit
fi
-CASES="$(ls "$ROOT"/test/bounce/cases/*.c 2>/dev/null | sort)"
-if [ -z "$CASES" ]; then
+CASES_GLOB="$ROOT/test/bounce/cases/*.c"
+# shellcheck disable=SC2086
+if ! ls $CASES_GLOB >/dev/null 2>&1; then
echo "no cases under test/bounce/cases" >&2
exit 2
fi
-# ---- per-case host reference ----------------------------------------------
+# ---- per-case host reference (LANE-LOCAL) ----------------------------------
# Reference exit code for a case = running it on the host. The cases are
# portable LP64 integer C, so the host result is the oracle every target
-# and every bounce chain must reproduce.
+# and every bounce chain must reproduce. Writes only under $CF_WORK.
compute_ref() {
- local case="$1" name="$2" ref_exe="$BUILD/$name.ref" drv="$BUILD/$name.refmain.c"
+ local case="$1" ref_exe="$CF_WORK/ref" drv="$CF_WORK/refmain.c"
printf 'int bounce_main(void);\nint main(void){return bounce_main();}\n' >"$drv"
- if ! "$HOSTCC" -O1 "$case" "$drv" -o "$ref_exe" 2>"$BUILD/$name.ref.err"; then
+ if ! "$HOSTCC" -O1 "$case" "$drv" -o "$ref_exe" 2>"$CF_WORK/ref.err"; then
return 1
fi
"$ref_exe"; echo $?
}
-# ---- bounce chains ---------------------------------------------------------
+# ---- bounce chains (LANE-LOCAL) --------------------------------------------
# Each chain takes prog.o + crt.o and produces an executable at $out, or
# returns nonzero (with a diagnostic on stdout) if any transform/link fails.
# $1=prog.o $2=crt.o $3=out $4=workdir
@@ -174,90 +173,72 @@ chain_applies() {
esac
}
-# ---- queue bookkeeping (parallel indexed arrays; bash 3.2 safe) -----------
-Q_LABEL=(); Q_RCFILE=(); Q_EXPECT=()
-
-# ---- main sweep ------------------------------------------------------------
-
-for arch in $ARCHES; do
+# ---- lane B: compile prog.o, then run every applicable chain ---------------
+# CF_ARCH is the corpus tuple's arch (aarch64/x64/rv64); CF_OBJ is "linux".
+# crt + host reference + prog.o are built here (CF_WORK-confined). Each chain's
+# executable is deferred via cf_queue_e and scored at flush (rc == host ref).
+cf_lane_B() {
+ local arch="$CF_ARCH" exec_tag="$CF_ARCH-linux"
+ # Disambiguate result names by arch+opt (the engine's CF_NAME does not carry
+ # the tuple), matching the original "<name>.<arch>.O<opt>.<chain>" labels.
+ local stem="$CF_BASE.$arch.O$CF_OPT"
+ local triple define
triple="$(arch_triple "$arch")"
define="$(arch_define "$arch")"
- if [ -z "$triple" ]; then
- note_skip "$arch" "unknown arch tag"
- continue
- fi
- if ! exec_target_supported "$arch"; then
- note_skip "$arch" "no runner (need podman or qemu for $arch-linux)"
- continue
+ if [ -z "$triple" ]; then cf_skip "$stem" "unknown arch tag"; return; fi
+ if ! exec_target_supported "$exec_tag"; then
+ cf_skip "$stem" "no runner (need podman or qemu for $arch-linux)"; return
fi
- for opt in $OPT_LEVELS; do
- # crt depends only on (arch, opt); build it once.
- crt_o="$BUILD/crt.$arch.O$opt.o"
- if ! "$BIN" cc -target "$triple" -D"$define" -O"$opt" -c "$CRT" \
- -o "$crt_o" 2>"$BUILD/crt.$arch.O$opt.err"; then
- note_fail "$arch/O$opt crt.c (see $BUILD/crt.$arch.O$opt.err)"
- continue
- fi
+ # crt depends only on (arch, opt); built per-item under $CF_WORK.
+ local crt_o="$CF_WORK/crt.o"
+ if ! "$BIN" cc -target "$triple" -D"$define" -O"$CF_OPT" -c "$CRT" \
+ -o "$crt_o" 2>"$CF_WORK/crt.err"; then
+ cf_fail "$stem" "crt.c (see $CF_WORK/crt.err)"; return
+ fi
- for case in $CASES; do
- name="$(basename "$case" .c)"
- ref="$(compute_ref "$case" "$name")"
- if [ -z "$ref" ]; then
- note_fail "$name reference build (host cc; see $BUILD/$name.ref.err)"
- continue
- fi
+ local ref
+ ref="$(compute_ref "$CF_SRC")"
+ if [ -z "$ref" ]; then
+ cf_fail "$stem" "reference build (host cc; see $CF_WORK/ref.err)"; return
+ fi
- prog_o="$BUILD/$name.$arch.O$opt.o"
- if ! "$BIN" cc -target "$triple" -O"$opt" -c "$case" -o "$prog_o" \
- 2>"$BUILD/$name.$arch.O$opt.err"; then
- note_fail "$name $arch/O$opt compile (see $BUILD/$name.$arch.O$opt.err)"
- continue
- fi
+ local prog_o="$CF_WORK/prog.o"
+ if ! "$BIN" cc -target "$triple" -O"$CF_OPT" -c "$CF_SRC" -o "$prog_o" \
+ 2>"$CF_WORK/prog.err"; then
+ cf_fail "$stem" "compile (see $CF_WORK/prog.err)"; return
+ fi
- for chain in $CHAINS; do
- chain_applies "$arch" "$chain" || continue
- tag="$name.$arch.O$opt.$chain"
- wd="$BUILD/$tag.d"; mkdir -p "$wd"
- exe="$BUILD/$tag.exe"
- if ! "chain_$chain" "$prog_o" "$crt_o" "$exe" "$wd" \
- >"$BUILD/$tag.link.log" 2>&1; then
- note_fail "$tag (transform/link; see $BUILD/$tag.link.log)"
- continue
- fi
- # Re-read the produced executable's headers to catch malformed
- # output the linker accepted but a reader would choke on.
- rcfile="$BUILD/$tag.rc"
- Q_LABEL+=("$tag"); Q_RCFILE+=("$rcfile"); Q_EXPECT+=("$ref")
- exec_target_queue "$arch" "$tag" "$exe" \
- "$BUILD/$tag.out" "$BUILD/$tag.err" "$rcfile"
- done
- done
+ local chain tag wd exe
+ for chain in $CHAINS; do
+ chain_applies "$arch" "$chain" || continue
+ tag="$stem.$chain"
+ wd="$CF_WORK/$chain.d"; mkdir -p "$wd"
+ exe="$CF_WORK/$chain.exe"
+ if ! "chain_$chain" "$prog_o" "$crt_o" "$exe" "$wd" \
+ >"$CF_WORK/$chain.link.log" 2>&1; then
+ cf_fail "$tag" "transform/link; see $CF_WORK/$chain.link.log"
+ continue
+ fi
+ # Defer exec: the engine batches all queued cases into one container run
+ # at flush, then scores rc against the host reference (EXPECTED).
+ cf_queue_e "$tag" "$exe" \
+ "$CF_WORK/$chain.out" "$CF_WORK/$chain.err" "$CF_WORK/$chain.rc" \
+ "$ref" "$exec_tag"
done
-done
-
-# ---- run everything in batched container execs, then score -----------------
-exec_target_flush
+}
-i=0; n="${#Q_LABEL[@]}"
-while [ "$i" -lt "$n" ]; do
- label="${Q_LABEL[$i]}"; rcfile="${Q_RCFILE[$i]}"; expect="${Q_EXPECT[$i]}"
- i=$((i+1))
- if [ ! -f "$rcfile" ]; then
- note_fail "$label (no rc recorded)"
- continue
- fi
- got="$(cat "$rcfile")"
- if [ "$got" = "$expect" ]; then
- note_pass "$label (rc=$got)"
- else
- note_fail "$label (expected $expect got $got)"
- fi
+# ---- drive the corpus ------------------------------------------------------
+# One tuple per requested arch; opt axis from CFREE_BOUNCE_OPTS; single lane B.
+TUPLES=
+for arch in $ARCHES; do
+ TUPLES="$TUPLES $arch-linux"
done
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-[ "$FAIL" -eq 0 ] || exit 1
-if [ "$PASS" -eq 0 ]; then
- [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1
-fi
-exit 0
+CF_LABEL=test-bounce CF_BUILD_DIR="$BUILD/work" \
+ CF_CORPUS_GLOBS="$CASES_GLOB" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$ROOT/test/bounce/cases" \
+ CF_LANES="B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TUPLES" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="${CFREE_BOUNCE_PARALLEL:-1}" cf_corpus_run
+
+cf_summary test-bounce
+cf_exit
diff --git a/test/cas/run.sh b/test/cas/run.sh
@@ -18,138 +18,12 @@ trap 'rm -rf "$work"' EXIT
mkdir -p "$work/src/bin" "$work/src/share" "$work/cas" "$work/out"
-pass=0
-fail=0
-skip=0
-failures=
-
-ok() {
- printf 'PASS %s\n' "$1"
- pass=$((pass + 1))
-}
-
-not_ok() {
- printf 'FAIL %s\n' "$1"
- if [ "$#" -gt 1 ] && [ -s "$2" ]; then
- sed 's/^/ | /' "$2"
- fi
- fail=$((fail + 1))
- failures="$failures $1"
-}
-
-skip_test() {
- printf 'SKIP %s\n' "$1"
- skip=$((skip + 1))
-}
-
-run_ok() {
- name=$1
- shift
- if "$@" > "$work/$name.out" 2> "$work/$name.err"; then
- ok "$name"
- else
- not_ok "$name" "$work/$name.err"
- fi
-}
-
-run_fail() {
- name=$1
- shift
- if "$@" > "$work/$name.out" 2> "$work/$name.err"; then
- {
- echo "command unexpectedly succeeded"
- sed 's/^/stdout: /' "$work/$name.out"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- else
- ok "$name"
- fi
-}
-
-contains() {
- name=$1
- file=$2
- needle=$3
- if grep -F "$needle" "$file" >/dev/null 2>&1; then
- ok "$name"
- else
- {
- printf 'missing text: %s\n' "$needle"
- sed 's/^/file: /' "$file"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
-same_file() {
- name=$1
- want=$2
- got=$3
- if cmp -s "$want" "$got"; then
- ok "$name"
- else
- {
- printf 'files differ:\n'
- printf ' want: %s\n' "$want"
- printf ' got: %s\n' "$got"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
-is_executable() {
- name=$1
- file=$2
- if [ -x "$file" ]; then
- ok "$name"
- else
- echo "not executable: $file" > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
-first_hex_id() {
- sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p'
-}
-
-id_prefix() {
- printf '%.2s' "$1"
-}
-
-cas_object_path() {
- root=$1
- kind=$2
- id=$3
- prefix=$(id_prefix "$id")
- printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id"
-}
-
-tree_blob_for_path() {
- tree_file=$1
- want=$2
- awk -v want="$want" '
- $0 == "[file]" { in_file = 1; path = ""; blob = ""; next }
- in_file && /^path = / { path = substr($0, 8); next }
- in_file && /^blob = / {
- blob = substr($0, 8);
- if (path == want) {
- print blob;
- exit;
- }
- }
- ' "$tree_file"
-}
-
-assert_file_exists() {
- name=$1
- file=$2
- if [ -f "$file" ]; then
- ok "$name"
- else
- echo "missing file: $file" > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
+# Type-K mode-P kit: ok/run_ok/run_fail/contains/same_file/... + the CAS
+# helpers (first_hex_id/cas_object_path/tree_blob_for_path), all recording
+# through the unified cf_* counters over $work.
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
+cf_report_init
make_fixtures() {
printf alpha > "$work/src/share/a.txt"
@@ -273,10 +147,5 @@ else
skip_test "cas-corruption-tests"
fi
-if [ "$fail" -ne 0 ]; then
- printf 'cas: failures:%s\n' "$failures"
- printf 'cas: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip"
- exit 1
-fi
-
-printf 'cas: %d passed, %d skipped\n' "$pass" "$skip"
+cf_summary cas
+cf_exit
diff --git a/test/cg/ir_recorder_test.c b/test/cg/ir_recorder_test.c
@@ -7,55 +7,16 @@
#include <string.h>
#include "core/pool.h"
+#include "lib/cfree_unit.h"
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static int g_fails;
-static int g_checks;
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-#define EXPECT(cond, ...) \
- do { \
- ++g_checks; \
- if (!(cond)) { \
- ++g_fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* One shared test context replaces the per-file heap/diag/counter globals.
+ * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged. The
+ * table-of-tests harness re-creates a compiler per test via tc_init, but the
+ * single g_u (with ctx.now == -1, set once in main) backs them all. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
typedef struct TestCtx {
- CfreeContext ctx;
Compiler* c;
CfreeCgTypeId i32;
CfreeCgTypeId ptr;
@@ -65,16 +26,8 @@ static void tc_init(TestCtx* tc) {
CfreeTarget target;
CfreeCgBuiltinTypes b;
memset(tc, 0, sizeof *tc);
- tc->ctx.heap = &g_heap;
- tc->ctx.diag = &g_diag;
- tc->ctx.now = -1;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_X86_64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
- if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) !=
+ target = cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
+ if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) !=
CFREE_OK ||
!tc->c) {
fprintf(stderr, "fatal: compiler allocation failed\n");
@@ -388,7 +341,7 @@ static void test_func_dump_renders_text(void) {
t->func_end(t);
f = cg_ir_recorder_module(t)->funcs[0];
- cfree_writer_mem(&g_heap, &w);
+ cfree_writer_mem(&g_u.heap, &w);
cg_ir_func_dump(f, w);
bytes = cfree_writer_mem_bytes(w, &len);
EXPECT(len > 0 && bytes && len < sizeof s, "dump should produce output");
@@ -408,11 +361,14 @@ static void test_func_dump_renders_text(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
test_records_basic_function_shape();
test_deep_copies_call_switch_and_const_payloads();
test_labels_scopes_and_address_taken_locals();
test_aliases_and_data_label_diagnostic_hook();
test_func_dump_renders_text();
- fprintf(stderr, "ir-recorder: %d checks, %d failures\n", g_checks, g_fails);
- return g_fails ? 1 : 0;
+ fprintf(stderr, "ir-recorder: %d checks, %d failures\n", g_u.checks,
+ g_u.fails);
+ return g_u.fails ? 1 : 0;
}
diff --git a/test/cg/native_direct_target_test.c b/test/cg/native_direct_target_test.c
@@ -7,55 +7,15 @@
#include <string.h>
#include "core/arena.h"
+#include "lib/cfree_unit.h"
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static int g_fails;
-static int g_checks;
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-#define EXPECT(cond, ...) \
- do { \
- ++g_checks; \
- if (!(cond)) { \
- ++g_fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original
+ * ctx.now of -1 is preserved (set once in main after cfree_unit_init). */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
typedef struct TestCtx {
- CfreeContext ctx;
Compiler* c;
CfreeCgTypeId i32;
CfreeCgTypeId ptr;
@@ -65,16 +25,9 @@ static void tc_init(TestCtx* tc) {
CfreeTarget target;
CfreeCgBuiltinTypes b;
memset(tc, 0, sizeof *tc);
- tc->ctx.heap = &g_heap;
- tc->ctx.diag = &g_diag;
- tc->ctx.now = -1;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_X86_64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
- if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) !=
+ target =
+ cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
+ if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) !=
CFREE_OK ||
!tc->c) {
fprintf(stderr, "fatal: compiler allocation failed\n");
@@ -653,16 +606,18 @@ static void test_b_call_still_flushes(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
test_frame_locals_scratch_storeback_and_branches();
test_call_barrier_storeback_and_max_outgoing();
test_b_cached_pointer_base_not_reloaded();
test_b_cache_survives_store();
test_b_volatile_load_flushes();
test_b_call_still_flushes();
- if (g_fails) {
- fprintf(stderr, "%d/%d checks failed\n", g_fails, g_checks);
+ if (g_u.fails) {
+ fprintf(stderr, "%d/%d checks failed\n", g_u.fails, g_u.checks);
return 1;
}
- printf("native_direct_target_test: %d checks passed\n", g_checks);
+ printf("native_direct_target_test: %d checks passed\n", g_u.checks);
return 0;
}
diff --git a/test/coff/windows-system-dlls-smoke.sh b/test/coff/windows-system-dlls-smoke.sh
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
-# Windows system-DLL coverage smoke.
+# test/coff/windows-system-dlls-smoke.sh — Type-K (mode P) Windows system-DLL
+# coverage smoke, on the shared scripted-shell kit (test/lib/cfree_sh_kit.sh).
#
-# Companion to windows-ucrt-hosted-smoke.sh: that script proves the UCRT
-# console + GUI link round-trip for one program per surface (Sleep,
-# runtime, stdio, TLS, GUI WinMain). This script broadens the surface
-# across the typical large system DLLs an application links against:
+# Companion to windows-ucrt-hosted-smoke.sh: that proves the UCRT console + GUI
+# round-trip for one program per surface; this broadens coverage across the
+# large system DLLs an application links against:
#
# user32 + gdi32 (GUI window + drawing)
# advapi32 (registry)
@@ -15,20 +15,26 @@
# libucrt.a (mixed short-import + long-form members)
#
# Each program is built with `cfree cc` for both x86_64-windows and
-# aarch64-windows; the link-level check inspects the PE import
-# directory via `cfree objdump -p`. The Wine runtime check is run
-# conditionally — same pattern as windows-ucrt-hosted-smoke.sh, and
-# silently skipped when the matching podman/Wine container is absent.
+# aarch64-windows; the link-level check inspects the PE import directory via
+# `cfree objdump -p` (run_ok + contains). The Wine runtime check is run
+# conditionally and self-skips when the matching podman/Wine container is
+# absent.
#
-# Skip semantics: prints `SKIP: ...` and exits 0 when the llvm-mingw
-# UCRT sysroot is not discoverable.
-
-set -euo pipefail
+# Serial by design (mode P): all programs share one $work sandbox.
+#
+# Self-skip: prints SKIP and exits 0 when no llvm-mingw UCRT sysroot is found.
+set -u
ROOT=${CFREE_TEST_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}
CFREE=${CFREE:-"$ROOT/build/cfree"}
SDK=${CFREE_MINGW_SYSROOT:-}
+CF_KIT_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cfree_sh_kit.sh"
+cf_report_init
+
+LABEL_SUITE=windows-system-dlls-smoke
+
find_sdk() {
local arch=$1
local d
@@ -63,24 +69,26 @@ sdk_for_arch() {
find_sdk "$arch"
}
+# cfree presence: was a hard FAIL+exit 1 in the original. Preserve that verdict.
if [ ! -x "$CFREE" ]; then
- echo "FAIL windows-system-dlls-smoke: cfree binary not found: $CFREE" >&2
- exit 1
+ cf_fail "$LABEL_SUITE/cfree-present" "cfree binary not found: $CFREE"
+ cf_summary "$LABEL_SUITE"
+ cf_exit
fi
TMP=${TMPDIR:-/tmp}
-WORK=$(mktemp -d "$TMP/cfree-windows-system-dlls-smoke.XXXXXX")
-WORK_REAL=$(cd "$WORK" && pwd -P)
-trap 'rm -rf "$WORK"' EXIT
-
-GUI_C=$WORK/gui_hello_window.c
-GDI_C=$WORK/gdi_drawing.c
-REG_C=$WORK/advapi32_registry.c
-WS_C=$WORK/ws2_32_socket.c
-OLE_C=$WORK/ole32_coinit.c
-SHELL_C=$WORK/shell32_argv.c
-COMCTL_C=$WORK/comctl32_init.c
-MIXED_C=$WORK/mixed_ucrt.c
+work=$(mktemp -d "$TMP/cfree-windows-system-dlls-smoke.XXXXXX")
+WORK_REAL=$(cd "$work" && pwd -P)
+trap 'rm -rf "$work"' EXIT
+
+GUI_C=$work/gui_hello_window.c
+GDI_C=$work/gdi_drawing.c
+REG_C=$work/advapi32_registry.c
+WS_C=$work/ws2_32_socket.c
+OLE_C=$work/ole32_coinit.c
+SHELL_C=$work/shell32_argv.c
+COMCTL_C=$work/comctl32_init.c
+MIXED_C=$work/mixed_ucrt.c
# A hidden window + minimal message-pump program. Wine in a headless
# container may legitimately refuse to create a real window; the
@@ -244,61 +252,55 @@ int main(void) {
}
SRC
-check_no_legacy_crt_imports() {
- local dump=$1
- local what=$2
+# ---- assert helpers (mode-P verbs, $work-confined) -------------------------
+
+no_legacy_crt_imports() {
+ local name=$1 dump=$2
if grep -Eiq 'DLL Name: (msvcrt|ucrt)\.dll' "$dump"; then
- echo "FAIL windows-system-dlls-smoke: $what imports legacy CRT DLL" >&2
- grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" >&2
- exit 1
+ grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ else
+ ok "$name"
fi
}
run_wine_if_available() {
- local label=$1
- local image=$2
- local pod_arch=$3
- local exe=$4
+ local label=$1 image=$2 pod_arch=$3 exe=$4
shift 4
-
if ! command -v podman >/dev/null 2>&1; then
- echo "SKIP windows-system-dlls-smoke: podman unavailable for $label Wine run"
+ skip_test "$label-wine" "podman unavailable"
return 0
fi
if ! podman image exists "$image" >/dev/null 2>&1; then
- echo "SKIP windows-system-dlls-smoke: $image unavailable for $label Wine run"
+ skip_test "$label-wine" "$image unavailable"
return 0
fi
-
- podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \
- bash -lc "
- export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix
- timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $*
- rc=\$?
- echo \"$label exit=\$rc\"
- test \"\$rc\" -eq 0
- "
+ if podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \
+ bash -lc "
+ export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix
+ timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $*
+ rc=\$?
+ echo \"$label exit=\$rc\"
+ test \"\$rc\" -eq 0
+ " > "$work/$label-wine.out" 2> "$work/$label-wine.err"; then
+ ok "$label-wine"
+ else
+ not_ok "$label-wine" "$work/$label-wine.err"
+ fi
}
# build_and_check <label> <c-source> <exe> <dump> <link-mode> <libs>
# <expected-dll-1> [<expected-dll-2> ...] -- [<expected-sym-1> ...]
#
-# link-mode is "console" or "windows" (drives -mconsole vs -mwindows).
-# libs is a space-separated list of `-l<name>` archives to add (e.g.
-# "gdi32 ws2_32") beyond the driver-auto-linked set
-# (kernel32/user32/advapi32/shell32/msvcrt/mingwex/mingw32/moldname).
+# link-mode is "console" or "windows" (drives -mconsole vs -mwindows). libs is
+# a space-separated list of `-l<name>` archives to add (e.g. "gdi32 ws2_32")
+# beyond the driver-auto-linked set (kernel32/user32/advapi32/shell32/msvcrt/
+# mingwex/mingw32/moldname). Each expected DLL/symbol is one mode-P assert.
build_and_check() {
- local label=$1
- local csrc=$2
- local exe=$3
- local dump=$4
- local mode=$5
- local libs=$6
+ local label=$1 csrc=$2 exe=$3 dump=$4 mode=$5 libs=$6
shift 6
- local dlls=()
- local syms=()
- local in_syms=0
+ local dlls=() syms=() in_syms=0
while [ $# -gt 0 ]; do
if [ "$1" = "--" ]; then in_syms=1; shift; continue; fi
if [ "$in_syms" -eq 0 ]; then dlls+=("$1"); else syms+=("$1"); fi
@@ -308,36 +310,32 @@ build_and_check() {
local mode_flag=-mconsole
if [ "$mode" = "windows" ]; then mode_flag=-mwindows; fi
- local extra_lflags=()
- local lib
+ local extra_lflags=() lib
for lib in $libs; do extra_lflags+=("-l$lib"); done
- "$CFREE" cc -target "$TARGET" --sysroot "$ARCH_SDK" "$mode_flag" \
- "$csrc" "${extra_lflags[@]}" -o "$exe"
- "$CFREE" objdump -p "$exe" >"$dump"
- check_no_legacy_crt_imports "$dump" "$label PE"
+ local bn; bn=$(printf '%s' "$label" | tr ' ' '-')
+
+ run_ok "$bn-build" "$CFREE" cc -target "$TARGET" --sysroot "$ARCH_SDK" \
+ "$mode_flag" "$csrc" "${extra_lflags[@]}" -o "$exe"
+ if [ ! -f "$exe" ]; then return 1; fi
+ run_ok "$bn-objdump" "$CFREE" objdump -p "$exe"
+ if [ ! -s "$work/$bn-objdump.out" ]; then return 1; fi
+ cp "$work/$bn-objdump.out" "$dump"
+
+ no_legacy_crt_imports "$bn-no-legacy-crt" "$dump"
local d
for d in "${dlls[@]}"; do
- if ! grep -Fq "DLL Name: $d" "$dump"; then
- echo "FAIL windows-system-dlls-smoke: $label: expected import of $d" >&2
- grep -F 'DLL Name:' "$dump" >&2 || true
- exit 1
- fi
+ contains "$bn-dll-$d" "$dump" "DLL Name: $d"
done
local s
for s in "${syms[@]}"; do
- if ! grep -Fq "Name: $s" "$dump"; then
- echo "FAIL windows-system-dlls-smoke: $label: expected import symbol $s" >&2
- exit 1
- fi
+ contains "$bn-sym-$s" "$dump" "Name: $s"
done
- if [ "$mode" = "windows" ] &&
- ! grep -Fq "Subsystem: 2 (WINDOWS_GUI)" "$dump"; then
- echo "FAIL windows-system-dlls-smoke: $label: subsystem != WINDOWS_GUI" >&2
- exit 1
+ if [ "$mode" = "windows" ]; then
+ contains "$bn-subsystem-gui" "$dump" "Subsystem: 2 (WINDOWS_GUI)"
fi
}
@@ -359,86 +357,83 @@ for arch in x86_64 aarch64; do
esac
if ! ARCH_SDK=$(sdk_for_arch "$arch"); then
- echo "SKIP windows-system-dlls-smoke: no $arch llvm-mingw UCRT sysroot"
+ skip_test "$LABEL_SUITE/$LABEL-sysroot" "no $arch llvm-mingw UCRT sysroot"
continue
fi
+ # Discovered-but-invalid sysroot was a SKIP (exit 0 via continue) originally.
if [ ! -r "$ARCH_SDK/include/windows.h" ] ||
[ ! -r "$ARCH_SDK/lib/libucrt.a" ]; then
- echo "SKIP windows-system-dlls-smoke: invalid UCRT llvm-mingw sysroot: $ARCH_SDK"
+ skip_test "$LABEL_SUITE/$LABEL-sysroot" "invalid UCRT llvm-mingw sysroot: $ARCH_SDK"
continue
fi
ran=1
# ---- GUI hello window (user32 + gdi32 + kernel32) ----
- GUI_EXE=$WORK/gui_hello_window-$LABEL.exe
- GUI_DUMP=$WORK/gui_hello_window-$LABEL.dump
+ GUI_EXE=$work/gui_hello_window-$LABEL.exe
+ GUI_DUMP=$work/gui_hello_window-$LABEL.dump
build_and_check "$LABEL gui_hello_window" "$GUI_C" "$GUI_EXE" "$GUI_DUMP" \
windows "" USER32.dll KERNEL32.dll -- RegisterClassExW CreateWindowExW \
DefWindowProcW PeekMessageW DispatchMessageW
- run_wine_if_available "$LABEL gui_hello_window" "$IMAGE" "$POD_ARCH" \
- "$GUI_EXE"
+ run_wine_if_available "$LABEL gui_hello_window" "$IMAGE" "$POD_ARCH" "$GUI_EXE"
# ---- gdi32 surface ----
- GDI_EXE=$WORK/gdi_drawing-$LABEL.exe
- GDI_DUMP=$WORK/gdi_drawing-$LABEL.dump
+ GDI_EXE=$work/gdi_drawing-$LABEL.exe
+ GDI_DUMP=$work/gdi_drawing-$LABEL.dump
build_and_check "$LABEL gdi_drawing" "$GDI_C" "$GDI_EXE" "$GDI_DUMP" \
console gdi32 GDI32.dll USER32.dll -- CreateCompatibleDC GetStockObject \
SelectObject DeleteDC
run_wine_if_available "$LABEL gdi_drawing" "$IMAGE" "$POD_ARCH" "$GDI_EXE"
# ---- advapi32 surface ----
- REG_EXE=$WORK/advapi32_registry-$LABEL.exe
- REG_DUMP=$WORK/advapi32_registry-$LABEL.dump
+ REG_EXE=$work/advapi32_registry-$LABEL.exe
+ REG_DUMP=$work/advapi32_registry-$LABEL.dump
build_and_check "$LABEL advapi32_registry" "$REG_C" "$REG_EXE" "$REG_DUMP" \
console "" ADVAPI32.dll KERNEL32.dll -- RegOpenKeyExW RegCloseKey \
RegQueryInfoKeyW
- run_wine_if_available "$LABEL advapi32_registry" "$IMAGE" "$POD_ARCH" \
- "$REG_EXE"
+ run_wine_if_available "$LABEL advapi32_registry" "$IMAGE" "$POD_ARCH" "$REG_EXE"
# ---- ws2_32 surface ----
- WS_EXE=$WORK/ws2_32_socket-$LABEL.exe
- WS_DUMP=$WORK/ws2_32_socket-$LABEL.dump
+ WS_EXE=$work/ws2_32_socket-$LABEL.exe
+ WS_DUMP=$work/ws2_32_socket-$LABEL.dump
build_and_check "$LABEL ws2_32_socket" "$WS_C" "$WS_EXE" "$WS_DUMP" \
console ws2_32 WS2_32.dll KERNEL32.dll -- WSAStartup WSACleanup socket \
closesocket
run_wine_if_available "$LABEL ws2_32_socket" "$IMAGE" "$POD_ARCH" "$WS_EXE"
# ---- ole32 surface ----
- OLE_EXE=$WORK/ole32_coinit-$LABEL.exe
- OLE_DUMP=$WORK/ole32_coinit-$LABEL.dump
+ OLE_EXE=$work/ole32_coinit-$LABEL.exe
+ OLE_DUMP=$work/ole32_coinit-$LABEL.dump
build_and_check "$LABEL ole32_coinit" "$OLE_C" "$OLE_EXE" "$OLE_DUMP" \
console ole32 ole32.dll KERNEL32.dll -- CoInitializeEx CoUninitialize
run_wine_if_available "$LABEL ole32_coinit" "$IMAGE" "$POD_ARCH" "$OLE_EXE"
# ---- shell32 surface ----
- SHELL_EXE=$WORK/shell32_argv-$LABEL.exe
- SHELL_DUMP=$WORK/shell32_argv-$LABEL.dump
+ SHELL_EXE=$work/shell32_argv-$LABEL.exe
+ SHELL_DUMP=$work/shell32_argv-$LABEL.dump
build_and_check "$LABEL shell32_argv" "$SHELL_C" "$SHELL_EXE" "$SHELL_DUMP" \
console "" SHELL32.dll KERNEL32.dll -- CommandLineToArgvW
- run_wine_if_available "$LABEL shell32_argv" "$IMAGE" "$POD_ARCH" \
- "$SHELL_EXE"
+ run_wine_if_available "$LABEL shell32_argv" "$IMAGE" "$POD_ARCH" "$SHELL_EXE"
# ---- comctl32 surface ----
- COMCTL_EXE=$WORK/comctl32_init-$LABEL.exe
- COMCTL_DUMP=$WORK/comctl32_init-$LABEL.dump
+ COMCTL_EXE=$work/comctl32_init-$LABEL.exe
+ COMCTL_DUMP=$work/comctl32_init-$LABEL.dump
build_and_check "$LABEL comctl32_init" "$COMCTL_C" "$COMCTL_EXE" \
"$COMCTL_DUMP" console comctl32 COMCTL32.dll KERNEL32.dll -- \
InitCommonControls InitCommonControlsEx
- run_wine_if_available "$LABEL comctl32_init" "$IMAGE" "$POD_ARCH" \
- "$COMCTL_EXE"
+ run_wine_if_available "$LABEL comctl32_init" "$IMAGE" "$POD_ARCH" "$COMCTL_EXE"
# ---- mixed libucrt.a (short-import + long-form helper) ----
- MIXED_EXE=$WORK/mixed_ucrt-$LABEL.exe
- MIXED_DUMP=$WORK/mixed_ucrt-$LABEL.dump
+ MIXED_EXE=$work/mixed_ucrt-$LABEL.exe
+ MIXED_DUMP=$work/mixed_ucrt-$LABEL.dump
build_and_check "$LABEL mixed_ucrt" "$MIXED_C" "$MIXED_EXE" "$MIXED_DUMP" \
console "" KERNEL32.dll api-ms-win-crt-stdio-l1-1-0.dll -- fflush
run_wine_if_available "$LABEL mixed_ucrt" "$IMAGE" "$POD_ARCH" "$MIXED_EXE"
done
if [ "$ran" -eq 0 ]; then
- echo "SKIP windows-system-dlls-smoke: set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*"
- exit 0
+ skip_test "$LABEL_SUITE" "set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*"
fi
-echo "PASS windows-system-dlls-smoke: user32/gdi32, advapi32, ws2_32, ole32, shell32, comctl32, mixed UCRT for x64/aarch64"
+cf_summary "$LABEL_SUITE"
+cf_exit
diff --git a/test/coff/windows-ucrt-hosted-smoke.sh b/test/coff/windows-ucrt-hosted-smoke.sh
@@ -1,10 +1,29 @@
#!/usr/bin/env bash
-set -euo pipefail
+# test/coff/windows-ucrt-hosted-smoke.sh — Type-K (mode P) hosted Windows UCRT
+# smoke, on the shared scripted-shell kit (test/lib/cfree_sh_kit.sh).
+#
+# One program per UCRT surface (Sleep, windows.h coverage, runtime, UCRT stdio,
+# imported-data, TLS, GUI WinMain) is built with `cfree cc` for both
+# x86_64-windows and aarch64-windows; the link-level checks inspect the PE
+# import directory via `cfree objdump -p` (run_ok + contains), and a negative
+# check asserts no legacy CRT DLL is imported directly. The Wine runtime check
+# is run conditionally and self-skips when podman/Wine are absent.
+#
+# Serial by design (mode P): all programs share one $work sandbox.
+#
+# Self-skip: prints SKIP and exits 0 when no llvm-mingw UCRT sysroot is found.
+set -u
ROOT=${CFREE_TEST_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}
CFREE=${CFREE:-"$ROOT/build/cfree"}
SDK=${CFREE_MINGW_SYSROOT:-}
+CF_KIT_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cfree_sh_kit.sh"
+cf_report_init
+
+LABEL_SUITE=windows-ucrt-hosted-smoke
+
find_sdk() {
local arch=$1
local d
@@ -39,23 +58,25 @@ sdk_for_arch() {
find_sdk "$arch"
}
+# cfree presence: was a hard FAIL+exit 1 in the original. Preserve that verdict.
if [ ! -x "$CFREE" ]; then
- echo "FAIL windows-ucrt-hosted-smoke: cfree binary not found: $CFREE" >&2
- exit 1
+ cf_fail "$LABEL_SUITE/cfree-present" "cfree binary not found: $CFREE"
+ cf_summary "$LABEL_SUITE"
+ cf_exit
fi
TMP=${TMPDIR:-/tmp}
-WORK=$(mktemp -d "$TMP/cfree-windows-ucrt-smoke.XXXXXX")
-WORK_REAL=$(cd "$WORK" && pwd -P)
-trap 'rm -rf "$WORK"' EXIT
-
-CONSOLE_C=$WORK/windows-h.c
-HEADER_C=$WORK/windows-h-coverage.c
-RUNTIME_C=$WORK/runtime.c
-STDIO_C=$WORK/stdio.c
-IMPORTDATA_C=$WORK/import-data.c
-GUI_C=$WORK/gui.c
-TLS_C=$WORK/tls.c
+work=$(mktemp -d "$TMP/cfree-windows-ucrt-smoke.XXXXXX")
+WORK_REAL=$(cd "$work" && pwd -P)
+trap 'rm -rf "$work"' EXIT
+
+CONSOLE_C=$work/windows-h.c
+HEADER_C=$work/windows-h-coverage.c
+RUNTIME_C=$work/runtime.c
+STDIO_C=$work/stdio.c
+IMPORTDATA_C=$work/import-data.c
+GUI_C=$work/gui.c
+TLS_C=$work/tls.c
cat >"$CONSOLE_C" <<'SRC'
#include <windows.h>
@@ -297,40 +318,63 @@ int main(void) {
}
SRC
-check_no_legacy_crt_imports() {
- local dump=$1
- local what=$2
+# ---- assert helpers (mode-P verbs, $work-confined) -------------------------
+
+# no_legacy_crt_imports NAME DUMP : pass iff DUMP imports no msvcrt/ucrt DLL.
+no_legacy_crt_imports() {
+ local name=$1 dump=$2
if grep -Eiq 'DLL Name: (msvcrt|ucrt)\.dll' "$dump"; then
- echo "FAIL windows-ucrt-hosted-smoke: $what imports legacy CRT DLL directly" >&2
- grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" >&2
- exit 1
+ grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ else
+ ok "$name"
fi
}
+# not_contains NAME FILE NEEDLE : pass iff NEEDLE is absent from FILE.
+not_contains() {
+ local name=$1 file=$2 needle=$3
+ if grep -Fq "$needle" "$file"; then
+ { printf 'unexpected text: %s\n' "$needle"; } > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ else
+ ok "$name"
+ fi
+}
+
+# matches NAME FILE EREGEX : pass iff FILE has a line matching EREGEX.
+matches() {
+ local name=$1 file=$2 re=$3
+ if grep -Eq "$re" "$file"; then ok "$name"
+ else { printf 'no line matched: %s\n' "$re"; } > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi
+}
+
+# run_wine_if_available LABEL IMAGE POD_ARCH EXE [ARGS...] : conditional Wine
+# run. Self-skips (cf_skip) when podman or the container image is absent;
+# otherwise pass iff the program exits 0 under Wine.
run_wine_if_available() {
- local label=$1
- local image=$2
- local pod_arch=$3
- local exe=$4
+ local label=$1 image=$2 pod_arch=$3 exe=$4
shift 4
-
if ! command -v podman >/dev/null 2>&1; then
- echo "SKIP windows-ucrt-hosted-smoke: podman unavailable for $label Wine run"
+ skip_test "$label-wine" "podman unavailable"
return 0
fi
if ! podman image exists "$image" >/dev/null 2>&1; then
- echo "SKIP windows-ucrt-hosted-smoke: $image unavailable for $label Wine run"
+ skip_test "$label-wine" "$image unavailable"
return 0
fi
-
- podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \
- bash -lc "
- export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix CFREE_WIN_PROBE=present
- timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $*
- rc=\$?
- echo \"$label exit=\$rc\"
- test \"\$rc\" -eq 0
- "
+ if podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \
+ bash -lc "
+ export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix CFREE_WIN_PROBE=present
+ timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $*
+ rc=\$?
+ echo \"$label exit=\$rc\"
+ test \"\$rc\" -eq 0
+ " > "$work/$label-wine.out" 2> "$work/$label-wine.err"; then
+ ok "$label-wine"
+ else
+ not_ok "$label-wine" "$work/$label-wine.err"
+ fi
}
ran=0
@@ -351,100 +395,111 @@ for arch in x86_64 aarch64; do
esac
if ! ARCH_SDK=$(sdk_for_arch "$arch"); then
- echo "SKIP windows-ucrt-hosted-smoke: no $arch llvm-mingw UCRT sysroot"
+ skip_test "$LABEL_SUITE/$label-sysroot" "no $arch llvm-mingw UCRT sysroot"
continue
fi
+ # Discovered-but-invalid sysroot was a hard FAIL+exit 1 originally.
if [ ! -r "$ARCH_SDK/include/windows.h" ] ||
[ ! -r "$ARCH_SDK/lib/libmsvcrt.a" ]; then
- echo "FAIL windows-ucrt-hosted-smoke: invalid UCRT llvm-mingw sysroot: $ARCH_SDK" >&2
- exit 1
+ echo "invalid UCRT llvm-mingw sysroot: $ARCH_SDK" > "$work/$label-sysroot.diag"
+ not_ok "$LABEL_SUITE/$label-sysroot" "$work/$label-sysroot.diag"
+ cf_summary "$LABEL_SUITE"
+ cf_exit
fi
ran=1
- CONSOLE_EXE=$WORK/windows-h-$arch.exe
- CONSOLE_DUMP=$WORK/windows-h-$arch.dump
- HEADER_EXE=$WORK/windows-h-coverage-$arch.exe
- HEADER_DUMP=$WORK/windows-h-coverage-$arch.dump
- RUNTIME_EXE=$WORK/runtime-$arch.exe
- RUNTIME_DUMP=$WORK/runtime-$arch.dump
- STDIO_EXE=$WORK/stdio-$arch.exe
- STDIO_DUMP=$WORK/stdio-$arch.dump
- IMPORTDATA_EXE=$WORK/import-data-$arch.exe
- IMPORTDATA_DUMP=$WORK/import-data-$arch.dump
- TLS_EXE=$WORK/tls-$arch.exe
- TLS_DUMP=$WORK/tls-$arch.dump
- GUI_EXE=$WORK/gui-$arch.exe
- GUI_DUMP=$WORK/gui-$arch.dump
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mconsole \
- "$CONSOLE_C" -o "$CONSOLE_EXE"
- "$CFREE" objdump -p "$CONSOLE_EXE" >"$CONSOLE_DUMP"
- check_no_legacy_crt_imports "$CONSOLE_DUMP" "$label console PE"
- if grep -Fq "Name: __set_app_type" "$CONSOLE_DUMP"; then
- echo "FAIL windows-ucrt-hosted-smoke: weak alias leaked as __set_app_type import" >&2
- exit 1
+ CONSOLE_EXE=$work/windows-h-$arch.exe
+ CONSOLE_DUMP=$work/windows-h-$arch.dump
+ HEADER_EXE=$work/windows-h-coverage-$arch.exe
+ HEADER_DUMP=$work/windows-h-coverage-$arch.dump
+ RUNTIME_EXE=$work/runtime-$arch.exe
+ RUNTIME_DUMP=$work/runtime-$arch.dump
+ STDIO_EXE=$work/stdio-$arch.exe
+ STDIO_DUMP=$work/stdio-$arch.dump
+ IMPORTDATA_EXE=$work/import-data-$arch.exe
+ IMPORTDATA_DUMP=$work/import-data-$arch.dump
+ TLS_EXE=$work/tls-$arch.exe
+ TLS_DUMP=$work/tls-$arch.dump
+ GUI_EXE=$work/gui-$arch.exe
+ GUI_DUMP=$work/gui-$arch.dump
+
+ # build_dump NAME EXE DUMP -- CC_ARGS... : build EXE, then `objdump -p` it
+ # into DUMP. Returns 0 only if both the EXE and the DUMP were produced, so
+ # the per-program import asserts below run iff the link round-tripped.
+ build_dump() {
+ local bn=$1 exe=$2 dump=$3; shift 3
+ [ "$1" = "--" ] && shift
+ run_ok "$bn-build" "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" "$@" -o "$exe"
+ [ -f "$exe" ] || return 1
+ run_ok "$bn-objdump" "$CFREE" objdump -p "$exe"
+ [ -s "$work/$bn-objdump.out" ] || return 1
+ cp "$work/$bn-objdump.out" "$dump"
+ }
+
+ # ---- console Sleep ----
+ if build_dump "$label-console" "$CONSOLE_EXE" "$CONSOLE_DUMP" -- -mconsole "$CONSOLE_C"; then
+ no_legacy_crt_imports "$label-console-no-legacy-crt" "$CONSOLE_DUMP"
+ not_contains "$label-console-no-weak-set-app-type" "$CONSOLE_DUMP" "Name: __set_app_type"
+ contains "$label-console-kernel32" "$CONSOLE_DUMP" "DLL Name: KERNEL32.dll"
+ contains "$label-console-sleep" "$CONSOLE_DUMP" "Name: Sleep"
+ contains "$label-console-crt-runtime-dll" "$CONSOLE_DUMP" "DLL Name: api-ms-win-crt-runtime-l1-1-0.dll"
+ contains "$label-console-set-app-type" "$CONSOLE_DUMP" "Name: _set_app_type"
+ run_wine_if_available "$label-console-Sleep" "$image" "$pod_arch" "$CONSOLE_EXE"
fi
- grep -Fq "DLL Name: KERNEL32.dll" "$CONSOLE_DUMP"
- grep -Fq "Name: Sleep" "$CONSOLE_DUMP"
- grep -Fq "DLL Name: api-ms-win-crt-runtime-l1-1-0.dll" "$CONSOLE_DUMP"
- grep -Fq "Name: _set_app_type" "$CONSOLE_DUMP"
- run_wine_if_available "$label Sleep" "$image" "$pod_arch" "$CONSOLE_EXE"
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mconsole \
- "$HEADER_C" -o "$HEADER_EXE"
- "$CFREE" objdump -p "$HEADER_EXE" >"$HEADER_DUMP"
- check_no_legacy_crt_imports "$HEADER_DUMP" "$label windows.h coverage PE"
- grep -Fq "Name: CreateFileW" "$HEADER_DUMP"
- grep -Fq "Name: CreateThread" "$HEADER_DUMP"
- grep -Fq "Name: WaitForSingleObject" "$HEADER_DUMP"
- grep -Fq "Name: MessageBoxW" "$HEADER_DUMP"
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \
- "$RUNTIME_C" -o "$RUNTIME_EXE"
- "$CFREE" objdump -p "$RUNTIME_EXE" >"$RUNTIME_DUMP"
- check_no_legacy_crt_imports "$RUNTIME_DUMP" "$label runtime PE"
- grep -Fq "Name: HeapAlloc" "$RUNTIME_DUMP"
- grep -Fq "Name: CreateFileA" "$RUNTIME_DUMP"
- grep -Fq "Name: qsort" "$RUNTIME_DUMP"
- run_wine_if_available "$label runtime" "$image" "$pod_arch" "$RUNTIME_EXE" \
- alpha beta
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \
- "$STDIO_C" -o "$STDIO_EXE"
- "$CFREE" objdump -p "$STDIO_EXE" >"$STDIO_DUMP"
- check_no_legacy_crt_imports "$STDIO_DUMP" "$label UCRT stdio PE"
- grep -Fq "DLL Name: api-ms-win-crt-stdio-l1-1-0.dll" "$STDIO_DUMP"
- grep -Fq "Name: fflush" "$STDIO_DUMP"
- run_wine_if_available "$label UCRT stdio" "$image" "$pod_arch" "$STDIO_EXE"
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \
- "$IMPORTDATA_C" -o "$IMPORTDATA_EXE"
- "$CFREE" objdump -p "$IMPORTDATA_EXE" >"$IMPORTDATA_DUMP"
- check_no_legacy_crt_imports "$IMPORTDATA_DUMP" "$label imported-data PE"
- grep -Fq "DLL Name: api-ms-win-crt-private-l1-1-0.dll" "$IMPORTDATA_DUMP"
- grep -Fq "Name: __dcrt_initial_narrow_environment" "$IMPORTDATA_DUMP"
- run_wine_if_available "$label imported-data" "$image" "$pod_arch" \
- "$IMPORTDATA_EXE"
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \
- "$TLS_C" -o "$TLS_EXE"
- "$CFREE" objdump -p "$TLS_EXE" >"$TLS_DUMP"
- check_no_legacy_crt_imports "$TLS_DUMP" "$label TLS PE"
- grep -Eq '^[[:space:]]*9[[:space:]]+TLS[[:space:]]+0x[0-9a-fA-F]+[[:space:]]+0x00000028' \
- "$TLS_DUMP"
-
- "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mwindows \
- "$GUI_C" -o "$GUI_EXE"
- "$CFREE" objdump -p "$GUI_EXE" >"$GUI_DUMP"
- grep -Fq "Subsystem: 2 (WINDOWS_GUI)" "$GUI_DUMP"
- check_no_legacy_crt_imports "$GUI_DUMP" "$label GUI PE"
-
- run_wine_if_available "$label TLS" "$image" "$pod_arch" "$TLS_EXE"
+
+ # ---- windows.h coverage ----
+ if build_dump "$label-header" "$HEADER_EXE" "$HEADER_DUMP" -- -mconsole "$HEADER_C"; then
+ no_legacy_crt_imports "$label-header-no-legacy-crt" "$HEADER_DUMP"
+ contains "$label-header-CreateFileW" "$HEADER_DUMP" "Name: CreateFileW"
+ contains "$label-header-CreateThread" "$HEADER_DUMP" "Name: CreateThread"
+ contains "$label-header-WaitForSingleObject" "$HEADER_DUMP" "Name: WaitForSingleObject"
+ contains "$label-header-MessageBoxW" "$HEADER_DUMP" "Name: MessageBoxW"
+ fi
+
+ # ---- runtime ----
+ if build_dump "$label-runtime" "$RUNTIME_EXE" "$RUNTIME_DUMP" -- "$RUNTIME_C"; then
+ no_legacy_crt_imports "$label-runtime-no-legacy-crt" "$RUNTIME_DUMP"
+ contains "$label-runtime-HeapAlloc" "$RUNTIME_DUMP" "Name: HeapAlloc"
+ contains "$label-runtime-CreateFileA" "$RUNTIME_DUMP" "Name: CreateFileA"
+ contains "$label-runtime-qsort" "$RUNTIME_DUMP" "Name: qsort"
+ run_wine_if_available "$label-runtime" "$image" "$pod_arch" "$RUNTIME_EXE" alpha beta
+ fi
+
+ # ---- UCRT stdio ----
+ if build_dump "$label-stdio" "$STDIO_EXE" "$STDIO_DUMP" -- "$STDIO_C"; then
+ no_legacy_crt_imports "$label-stdio-no-legacy-crt" "$STDIO_DUMP"
+ contains "$label-stdio-crt-stdio-dll" "$STDIO_DUMP" "DLL Name: api-ms-win-crt-stdio-l1-1-0.dll"
+ contains "$label-stdio-fflush" "$STDIO_DUMP" "Name: fflush"
+ run_wine_if_available "$label-UCRT-stdio" "$image" "$pod_arch" "$STDIO_EXE"
+ fi
+
+ # ---- imported-data ----
+ if build_dump "$label-importdata" "$IMPORTDATA_EXE" "$IMPORTDATA_DUMP" -- "$IMPORTDATA_C"; then
+ no_legacy_crt_imports "$label-importdata-no-legacy-crt" "$IMPORTDATA_DUMP"
+ contains "$label-importdata-crt-private-dll" "$IMPORTDATA_DUMP" "DLL Name: api-ms-win-crt-private-l1-1-0.dll"
+ contains "$label-importdata-dcrt-env" "$IMPORTDATA_DUMP" "Name: __dcrt_initial_narrow_environment"
+ run_wine_if_available "$label-imported-data" "$image" "$pod_arch" "$IMPORTDATA_EXE"
+ fi
+
+ # ---- TLS ----
+ if build_dump "$label-tls" "$TLS_EXE" "$TLS_DUMP" -- "$TLS_C"; then
+ no_legacy_crt_imports "$label-tls-no-legacy-crt" "$TLS_DUMP"
+ matches "$label-tls-directory" "$TLS_DUMP" \
+ '^[[:space:]]*9[[:space:]]+TLS[[:space:]]+0x[0-9a-fA-F]+[[:space:]]+0x00000028'
+ fi
+
+ # ---- GUI WinMain ----
+ if build_dump "$label-gui" "$GUI_EXE" "$GUI_DUMP" -- -mwindows "$GUI_C"; then
+ contains "$label-gui-subsystem" "$GUI_DUMP" "Subsystem: 2 (WINDOWS_GUI)"
+ no_legacy_crt_imports "$label-gui-no-legacy-crt" "$GUI_DUMP"
+ fi
+
+ run_wine_if_available "$label-TLS" "$image" "$pod_arch" "$TLS_EXE"
done
if [ "$ran" -eq 0 ]; then
- echo "SKIP windows-ucrt-hosted-smoke: set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*"
- exit 0
+ skip_test "$LABEL_SUITE" "set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*"
fi
-echo "PASS windows-ucrt-hosted-smoke: Sleep, windows.h, runtime, UCRT stdio/imports/imported-data, GUI, and TLS for x64/aarch64"
+cf_summary "$LABEL_SUITE"
+cf_exit
diff --git a/test/dbg/run.sh b/test/dbg/run.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# Scripted `cfree dbg` transcript tests.
+# Scripted `cfree dbg` transcript tests — Type K, mode G (golden transcript).
#
# Each case lives in test/dbg/cases/<name>/ and may contain:
# args one cfree-dbg argument per line; @CASE@ expands to the case dir
@@ -8,8 +8,15 @@
# stderr optional exact stderr golden; absent means stderr must be empty
# xfail optional reason; expected failure until the feature is implemented
#
+# dbg is mode G, but cf_scenario_case (the shared mode-G oracle) does not cover
+# dbg's needs: per-case args/stdin templating, an stdin script piped to the
+# REPL, a multi-stage stdout normalizer, separate stderr handling, and xfail/
+# xpass. So the per-case oracle (dbg_case) and the normalizer live lane-local
+# here, while results, summary, and exit go through the shared Type-K kit.
+#
# The stdout normalizer removes interactive prompts so goldens focus on
-# debugger-visible events. It intentionally leaves generation numbers and
+# debugger-visible events, masks run-to-run addresses, and re-tokenizes the
+# case dir back to @CASE@. It intentionally leaves generation numbers and
# command output intact.
set -u
@@ -18,14 +25,15 @@ script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
+
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree dbg
-if [ ! -x "$CFREE" ]; then
- echo "dbg: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
+# dbg drives a JIT debugger that single-steps native code; only hosts whose
+# native arch matches the JIT target can run it. (Preserved host gate.)
host_arch=$(uname -m 2>/dev/null || true)
host_os=$(uname -s 2>/dev/null || true)
case "$host_os:$host_arch" in
@@ -36,16 +44,18 @@ case "$host_os:$host_arch" in
;;
esac
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-dbg-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
+CF_WORK=$(cf_workdir dbg)
-pass=0
-fail=0
-xfail=0
-xpass=0
-skip=0
-failures=
+# DBG_STRICT_XFAIL drives the report layer's strict-xfail gate: an xfail case
+# that unexpectedly passes becomes a real failure (stale marker) under strict.
+CF_STRICT_XFAIL=${DBG_STRICT_XFAIL:-0}
+cf_report_init
+
+# normalize_stdout RAW CASE_DIR : multi-stage stdout normalizer (lane-local).
+# Strips the "(cfree) " prompt and continuation markers, re-tokenizes CASE_DIR
+# to @CASE@, masks run-to-run hex addresses to 0xADDR, trims trailing
+# whitespace, and elides blank lines. Idempotent on already-normalized goldens.
normalize_stdout() {
sed -e "s|$2|@CASE@|g" \
-e 's/(cfree) //g' -e 's/^ > //' -e 's/^expr > //' "$1" |
@@ -54,6 +64,8 @@ normalize_stdout() {
sed '/^$/d'
}
+# case_selected NAME : honor the DBG_CASE comma-separated glob filter. With no
+# filter, every case runs. (Preserved selection knob.)
case_selected() {
[ -n "${DBG_CASE:-}" ] || return 0
for pat in $(printf '%s' "$DBG_CASE" | tr ',' ' '); do
@@ -64,141 +76,138 @@ case_selected() {
return 1
}
-for case_dir in "$cases_dir"/*; do
- [ -d "$case_dir" ] || continue
- name=$(basename "$case_dir")
- if ! case_selected "$name"; then
- skip=$((skip + 1))
- continue
- fi
- stdin_file="$case_dir/stdin"
- expected="$case_dir/expected"
- expected_stderr="$case_dir/stderr"
- xfail_file="$case_dir/xfail"
- raw_stdout="$work_root/$name.stdout.raw"
- actual_stdout="$work_root/$name.stdout"
- raw_stderr="$work_root/$name.stderr"
- actual_stderr="$work_root/$name.stderr.actual"
- case_stdin="$work_root/$name.stdin"
- is_xfail=0
- reason=
- ok=1
- why=
-
- if [ -e "$xfail_file" ]; then
- is_xfail=1
- reason=$(sed -n '1p' "$xfail_file")
+# dbg_case NAME CASE_DIR : run one transcript case and record exactly one
+# verdict via the shared cf_* verbs (cf_pass/cf_fail/cf_xfail/cf_xpass).
+dbg_case() {
+ dc_name=$1
+ dc_dir=$2
+ dc_stdin_file="$dc_dir/stdin"
+ dc_expected="$dc_dir/expected"
+ dc_expected_stderr="$dc_dir/stderr"
+ dc_xfail_file="$dc_dir/xfail"
+ dc_raw_stdout="$CF_WORK/$dc_name.stdout.raw"
+ dc_actual_stdout="$CF_WORK/$dc_name.stdout"
+ dc_expected_norm="$CF_WORK/$dc_name.expected"
+ dc_raw_stderr="$CF_WORK/$dc_name.stderr"
+ dc_actual_stderr="$CF_WORK/$dc_name.stderr.actual"
+ dc_case_stdin="$CF_WORK/$dc_name.stdin"
+ dc_is_xfail=0
+ dc_reason=
+ dc_ok=1
+ dc_why=
+
+ if [ -e "$dc_xfail_file" ]; then
+ dc_is_xfail=1
+ dc_reason=$(sed -n '1p' "$dc_xfail_file")
fi
- if [ ! -e "$stdin_file" ]; then
- ok=0
- why="missing stdin"
- elif [ ! -e "$expected" ]; then
- ok=0
- why="missing expected"
+ # Missing required inputs: a setup failure, routed through xfail when the
+ # case is marked (and not strict), else a plain failure.
+ if [ ! -e "$dc_stdin_file" ]; then
+ dc_ok=0
+ dc_why="missing stdin"
+ elif [ ! -e "$dc_expected" ]; then
+ dc_ok=0
+ dc_why="missing expected"
fi
-
- if [ "$ok" -eq 0 ]; then
- if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then
- printf 'XFAIL %s (%s)\n' "$name" "$why"
- xfail=$((xfail + 1))
+ if [ "$dc_ok" -eq 0 ]; then
+ if [ "$dc_is_xfail" -eq 1 ]; then
+ cf_xfail "$dc_name" "$dc_why"
else
- printf 'FAIL %s (%s)\n' "$name" "$why"
- fail=$((fail + 1))
- failures="$failures $name"
+ cf_fail "$dc_name" "$dc_why"
fi
- continue
+ return
fi
+ # Build the dbg argv, templating @CASE@ -> case dir; skip blanks/#comments.
set --
- args_file="$case_dir/args"
- if [ -e "$args_file" ]; then
- while IFS= read -r arg || [ -n "$arg" ]; do
- [ -n "$arg" ] || continue
- case "$arg" in
+ dc_args_file="$dc_dir/args"
+ if [ -e "$dc_args_file" ]; then
+ while IFS= read -r dc_arg || [ -n "$dc_arg" ]; do
+ [ -n "$dc_arg" ] || continue
+ case "$dc_arg" in
\#*) continue ;;
esac
- arg=$(printf '%s' "$arg" | sed "s|@CASE@|$case_dir|g")
- set -- "$@" "$arg"
- done < "$args_file"
+ dc_arg=$(printf '%s' "$dc_arg" | sed "s|@CASE@|$dc_dir|g")
+ set -- "$@" "$dc_arg"
+ done < "$dc_args_file"
fi
- sed "s|@CASE@|$case_dir|g" "$stdin_file" > "$case_stdin"
- "$CFREE" dbg "$@" < "$case_stdin" > "$raw_stdout" 2> "$raw_stderr"
- rc=$?
- normalize_stdout "$raw_stdout" "$case_dir" > "$actual_stdout"
- if [ -e "$expected_stderr" ]; then
- cp "$raw_stderr" "$actual_stderr"
+ sed "s|@CASE@|$dc_dir|g" "$dc_stdin_file" > "$dc_case_stdin"
+ "$CFREE" dbg "$@" < "$dc_case_stdin" > "$dc_raw_stdout" 2> "$dc_raw_stderr"
+ dc_rc=$?
+
+ # Normalize BOTH actual and the (already-normalized, so idempotent) golden.
+ normalize_stdout "$dc_raw_stdout" "$dc_dir" > "$dc_actual_stdout"
+ normalize_stdout "$dc_expected" "$dc_dir" > "$dc_expected_norm"
+ if [ -e "$dc_expected_stderr" ]; then
+ cp "$dc_raw_stderr" "$dc_actual_stderr"
else
- : > "$actual_stderr"
+ : > "$dc_actual_stderr"
fi
- if [ "$rc" -ne 0 ]; then
- ok=0
- why="cfree dbg exit=$rc"
- elif ! diff -u "$expected" "$actual_stdout" >/dev/null 2>&1; then
- ok=0
- why="stdout"
- elif [ -e "$expected_stderr" ]; then
- if ! diff -u "$expected_stderr" "$actual_stderr" >/dev/null 2>&1; then
- ok=0
- why="stderr"
+ if [ "$dc_rc" -ne 0 ]; then
+ dc_ok=0
+ dc_why="cfree dbg exit=$dc_rc"
+ elif ! diff -u "$dc_expected_norm" "$dc_actual_stdout" >/dev/null 2>&1; then
+ dc_ok=0
+ dc_why="stdout"
+ elif [ -e "$dc_expected_stderr" ]; then
+ if ! diff -u "$dc_expected_stderr" "$dc_actual_stderr" >/dev/null 2>&1; then
+ dc_ok=0
+ dc_why="stderr"
fi
- elif [ -s "$raw_stderr" ]; then
- ok=0
- why="unexpected stderr"
+ elif [ -s "$dc_raw_stderr" ]; then
+ dc_ok=0
+ dc_why="unexpected stderr"
fi
- if [ "$ok" -eq 1 ]; then
- if [ "$is_xfail" -eq 1 ]; then
- printf 'XPASS %s' "$name"
- [ -n "$reason" ] && printf ' (%s)' "$reason"
- printf '\n'
- xpass=$((xpass + 1))
- fail=$((fail + 1))
- failures="$failures $name"
+ if [ "$dc_ok" -eq 1 ]; then
+ if [ "$dc_is_xfail" -eq 1 ]; then
+ # Marked xfail but passed (a stale xfail marker): cf_xpass is ALWAYS
+ # a failure in the report layer — matching the original's
+ # unconditional xpass-counts-as-fail behavior.
+ cf_xpass "$dc_name"
else
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
+ cf_pass "$dc_name"
fi
- continue
+ return
fi
- if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then
- printf 'XFAIL %s (%s' "$name" "$why"
- [ -n "$reason" ] && printf ': %s' "$reason"
- printf ')\n'
- xfail=$((xfail + 1))
- continue
+ if [ "$dc_is_xfail" -eq 1 ]; then
+ if [ -n "$dc_reason" ]; then
+ cf_xfail "$dc_name" "$dc_why: $dc_reason"
+ else
+ cf_xfail "$dc_name" "$dc_why"
+ fi
+ return
fi
- printf 'FAIL %s (%s)\n' "$name" "$why"
- case "$why" in
+ cf_fail "$dc_name" "$dc_why"
+ case "$dc_why" in
stdout)
- diff -u "$expected" "$actual_stdout" || true
- cp "$actual_stdout" "$case_dir/actual" 2>/dev/null || true
+ diff -u "$dc_expected_norm" "$dc_actual_stdout" || true
+ cp "$dc_actual_stdout" "$dc_dir/actual" 2>/dev/null || true
;;
stderr)
- diff -u "$expected_stderr" "$actual_stderr" || true
+ diff -u "$dc_expected_stderr" "$dc_actual_stderr" || true
;;
"unexpected stderr")
- sed 's/^/ | /' "$raw_stderr"
+ sed 's/^/ | /' "$dc_raw_stderr"
;;
cfree\ dbg\ exit=*)
- sed 's/^/ stdout| /' "$raw_stdout"
- sed 's/^/ stderr| /' "$raw_stderr"
+ sed 's/^/ stdout| /' "$dc_raw_stdout"
+ sed 's/^/ stderr| /' "$dc_raw_stderr"
;;
esac
- fail=$((fail + 1))
- failures="$failures $name"
+}
+
+for case_dir in "$cases_dir"/*; do
+ [ -d "$case_dir" ] || continue
+ name=$(basename "$case_dir")
+ case_selected "$name" || continue
+ dbg_case "$name" "$case_dir"
done
-total=$((pass + fail + xfail + xpass))
-if [ "$fail" -gt 0 ]; then
- printf '\ndbg: failures:%s\n' "$failures"
- printf 'dbg: %d/%d passed, %d xfailed, %d xpassed, %d skipped\n' \
- "$pass" "$total" "$xfail" "$xpass" "$skip"
- exit 1
-fi
-printf '\ndbg: %d/%d passed, %d xfailed, %d skipped\n' \
- "$pass" "$total" "$xfail" "$skip"
+cf_summary dbg
+cf_exit
diff --git a/test/debug/cfi_unit.c b/test/debug/cfi_unit.c
@@ -20,52 +20,14 @@
#include "core/core.h"
#include "core/pool.h"
#include "debug/dwarf_defs.h"
+#include "lib/cfree_unit.h"
#include "obj/obj.h"
-/* ---- env ---- */
-
-static void* heap_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* heap_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void heap_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "[%s] ",
- k == CFREE_DIAG_ERROR ? "error"
- : k == CFREE_DIAG_WARN ? "warning"
- : "note");
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0};
-static CfreeContext g_ctx = {.heap = &g_heap, .diag = &g_sink, .now = -1};
-
-static int g_fail = 0;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- g_fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fprintf(stderr, "\n"); \
- } \
- } while (0)
+/* One shared test context replaces the per-file heap/diag/counter globals.
+ * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged. The
+ * original ctx used now = -1, replicated in main() after cfree_unit_init. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static const Section* sec_by_name(const ObjBuilder* ob, Pool* pool,
const char* name) {
@@ -140,16 +102,11 @@ static void check_arch(const CfiExpect* ex) {
u32 size;
u32 off;
- memset(&t, 0, sizeof(t));
- t.arch = ex->arch;
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
- t.ptr_size = 8;
- t.ptr_align = 8;
+ t = cfree_unit_target(ex->arch, CFREE_OS_LINUX, CFREE_OBJ_ELF);
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "[%s] compiler_new failed\n", ex->tag);
- g_fail++;
+ g_u.fails++;
return;
}
ob = obj_new(c);
@@ -326,6 +283,9 @@ cleanup:
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1; /* preserve the original ctx.now */
+
/* aa64: RA=x30 (DWARF 30), code_align=4, data_align=-8, CFA init = sp. */
{
CfiExpect ex = {
@@ -377,8 +337,8 @@ int main(void) {
check_arch(&ex);
}
- if (g_fail) {
- fprintf(stderr, "%d FAILED\n", g_fail);
+ if (g_u.fails) {
+ fprintf(stderr, "%d FAILED\n", g_u.fails);
return 1;
}
printf("debug cfi_unit: OK\n");
diff --git a/test/debug/roundtrip_unit.c b/test/debug/roundtrip_unit.c
@@ -26,54 +26,14 @@
#include "core/core.h"
#include "core/pool.h"
#include "debug/debug.h"
+#include "lib/cfree_unit.h"
#include "obj/obj.h"
-/* ---- env ---- */
-
-static void* heap_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* heap_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void heap_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "[%s] ",
- k == CFREE_DIAG_ERROR ? "error"
- : k == CFREE_DIAG_WARN ? "warning"
- : "note");
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0};
-static CfreeContext g_ctx = {.heap = &g_heap, .diag = &g_sink, .now = -1};
-
-/* ---- fail counters ---- */
-
-static int g_fail = 0;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- g_fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fprintf(stderr, "\n"); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original
+ * context used now=-1, set in main after cfree_unit_init. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static const Section* sec_by_name(const ObjBuilder* ob, Pool* pool,
const char* name) {
@@ -126,14 +86,9 @@ static int run_one(CfreeArchKind arch, uint32_t nop_word, const char* tag) {
Pool* pool;
int local_fail = 0;
- memset(&t, 0, sizeof(t));
- t.arch = arch;
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
- t.ptr_size = 8;
- t.ptr_align = 8;
+ t = cfree_unit_target(arch, CFREE_OS_LINUX, CFREE_OBJ_ELF);
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "[%s] compiler_new failed\n", tag);
return 2;
}
@@ -356,14 +311,9 @@ static int run_x64_debug_line_check(void) {
ObjSymId xfsym;
Pool* xpool;
- memset(&xt, 0, sizeof(xt));
- xt.arch = CFREE_ARCH_X86_64;
- xt.os = CFREE_OS_LINUX;
- xt.obj = CFREE_OBJ_ELF;
- xt.ptr_size = 8;
- xt.ptr_align = 8;
+ xt = cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
- if (cfree_compiler_new(xt, &g_ctx, &xc) != CFREE_OK || !xc) {
+ if (cfree_unit_compiler_new(&g_u, xt, &xc) != CFREE_OK || !xc) {
fprintf(stderr, "x64 compiler_new failed\n");
return 2;
}
@@ -416,13 +366,17 @@ static int run_x64_debug_line_check(void) {
int main(void) {
int rc = 0;
+
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
+
rc |= run_one(CFREE_ARCH_ARM_64, ARCH_NOP_AA64, "aa64");
rc |= run_one(CFREE_ARCH_RV64, ARCH_NOP_RV64, "rv64");
rc |= run_x64_debug_line_check();
run_arch_register_checks();
- if (g_fail || rc) {
- fprintf(stderr, "%d FAILED\n", g_fail);
+ if (g_u.fails || rc) {
+ fprintf(stderr, "%d FAILED\n", g_u.fails);
return 1;
}
printf("debug roundtrip_unit: OK\n");
diff --git a/test/driver/run.sh b/test/driver/run.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# Driver-level permission checks for executable outputs.
+# Driver-level behavior checks for the cfree multitool CLI.
set -u
@@ -13,17 +13,19 @@ if [ ! -x "$CFREE" ]; then
exit 2
fi
-stat_mode() {
- if mode=$(stat -f '%Lp' "$1" 2>/dev/null); then
- printf '%s' "$mode"
- else
- stat -c '%a' "$1"
- fi
-}
-
work=$(mktemp -d "${TMPDIR:-/tmp}/cfree-driver-test.XXXXXX")
trap 'rm -rf "$work"' EXIT
+# Type-K mode-P kit: ok/run_ok/run_fail/contains/same_file/is_executable/
+# assert_file_exists/check_mode, all recording through the unified cf_* counters
+# over $work. check_mode replaces the inline stat -f/-c permission probe; the
+# driver-specific scenarios that need shell control flow (umask, cd, stdin
+# piping, runtime auto-build via nm) stay inline but route verdicts through
+# ok/not_ok. Mode-P suites are SERIAL — fixtures under $work are shared/mutated.
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
+cf_report_init
+
cat > "$work/main.c" <<'SRC'
int main(void) { return 0; }
int _start(void) { return 0; }
@@ -33,30 +35,12 @@ cat > "$work/other.c" <<'SRC'
int other(void) { return 0; }
SRC
-pass=0
-fail=0
-
-check_mode() {
- name=$1
- path=$2
- want=$3
- got=$(stat_mode "$path")
- if [ "$got" = "$want" ]; then
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s (mode %s, want %s)\n' "$name" "$got" "$want"
- fail=$((fail + 1))
- fi
-}
-
+# ---- executable permission bits (cc/ld) ----
if (umask 077; "$CFREE" cc "$work/main.c" -o "$work/cc-exe") \
> "$work/cc.out" 2> "$work/cc.err"; then
check_mode "cc-executable-mode" "$work/cc-exe" 700
else
- printf 'FAIL cc-executable-mode (cfree cc failed)\n'
- sed 's/^/ | /' "$work/cc.err"
- fail=$((fail + 1))
+ not_ok "cc-executable-mode" "$work/cc.err"
fi
rm -f "$work/a.out"
@@ -65,13 +49,11 @@ if (cd "$work" && umask 077 && "$CFREE" cc main.c) \
if [ -f "$work/a.out" ]; then
check_mode "cc-link-default-output" "$work/a.out" 700
else
- printf 'FAIL %s (a.out not created)\n' "cc-link-default-output"
- fail=$((fail + 1))
+ echo "a.out not created" > "$work/cc-link-default.diag"
+ not_ok "cc-link-default-output" "$work/cc-link-default.diag"
fi
else
- printf 'FAIL cc-link-default-output (cfree cc failed)\n'
- sed 's/^/ | /' "$work/cc-link-default.err"
- fail=$((fail + 1))
+ not_ok "cc-link-default-output" "$work/cc-link-default.err"
fi
if (umask 077; "$CFREE" cc -c "$work/main.c" -o "$work/main.o") \
@@ -82,46 +64,39 @@ if (umask 077; "$CFREE" cc -c "$work/main.c" -o "$work/main.o") \
> "$work/ld.out" 2> "$work/ld.err"; then
check_mode "ld-executable-mode" "$work/ld-exe" 700
else
- printf 'FAIL ld-executable-mode (cfree ld failed)\n'
- sed 's/^/ | /' "$work/ld.err"
- fail=$((fail + 1))
+ not_ok "ld-executable-mode" "$work/ld.err"
fi
else
- printf 'FAIL ld-executable-mode (cfree cc -c failed)\n'
- sed 's/^/ | /' "$work/cc-c.err"
- fail=$((fail + 1))
+ not_ok "ld-executable-mode" "$work/cc-c.err"
fi
+# ---- stdin piping: -x asm/s/asm-cpp/S → object ----
for xlang in asm s asm-cpp S; do
if printf '.globl asm_stdin\nasm_stdin:\n ret\n' |
"$CFREE" cc -target x86_64-linux -x "$xlang" -c - \
-o "$work/stdin-$xlang.o" \
> "$work/stdin-$xlang.out" 2> "$work/stdin-$xlang.err" &&
[ -f "$work/stdin-$xlang.o" ]; then
- printf 'PASS %s\n' "cc-stdin-x-$xlang"
- pass=$((pass + 1))
+ ok "cc-stdin-x-$xlang"
else
- printf 'FAIL %s\n' "cc-stdin-x-$xlang"
- sed 's/^/ | /' "$work/stdin-$xlang.err"
- fail=$((fail + 1))
+ not_ok "cc-stdin-x-$xlang" "$work/stdin-$xlang.err"
fi
done
+# ---- stdin piping: -x wasm/wat → emitted C ----
for xlang in wasm wat; do
if printf '(module (func (export "test_main") (result i32) i32.const 0))\n' |
"$CFREE" cc --emit=c -x "$xlang" - \
-o "$work/stdin-$xlang.c" \
> "$work/stdin-$xlang.out" 2> "$work/stdin-$xlang.err" &&
[ -s "$work/stdin-$xlang.c" ]; then
- printf 'PASS %s\n' "cc-stdin-x-$xlang"
- pass=$((pass + 1))
+ ok "cc-stdin-x-$xlang"
else
- printf 'FAIL %s\n' "cc-stdin-x-$xlang"
- sed 's/^/ | /' "$work/stdin-$xlang.err"
- fail=$((fail + 1))
+ not_ok "cc-stdin-x-$xlang" "$work/stdin-$xlang.err"
fi
done
+# ---- ld -r partial link: output mode + relinkability ----
cat > "$work/partial-main.c" <<'SRC'
int foo(void);
int _start(void) { return foo(); }
@@ -144,121 +119,100 @@ if "$CFREE" cc -target x86_64-linux -c "$work/partial-main.c" \
check_mode "ld-r-output-mode" "$work/partial.o" 644
if "$CFREE" ld "$work/partial.o" -o "$work/partial-exe" \
> "$work/ld-r-final.out" 2> "$work/ld-r-final.err"; then
- printf 'PASS %s\n' "ld-r-final-link"
- pass=$((pass + 1))
+ ok "ld-r-final-link"
else
- printf 'FAIL %s (final link of partial object failed)\n' \
- "ld-r-final-link"
- sed 's/^/ | /' "$work/ld-r-final.err"
- fail=$((fail + 1))
+ not_ok "ld-r-final-link" "$work/ld-r-final.err"
fi
else
- printf 'FAIL ld-r-output-mode (cfree ld -r failed)\n'
- sed 's/^/ | /' "$work/ld-r.err"
- fail=$((fail + 1))
+ not_ok "ld-r-output-mode" "$work/ld-r.err"
fi
else
- printf 'FAIL ld-r-output-mode (setup compile failed)\n'
- sed 's/^/ | /' "$work/partial-main.err" "$work/partial-foo.err"
- fail=$((fail + 1))
+ { sed 's/^/main: /' "$work/partial-main.err"
+ sed 's/^/foo: /' "$work/partial-foo.err"; } > "$work/ld-r-setup.diag"
+ not_ok "ld-r-output-mode" "$work/ld-r-setup.diag"
fi
+# ---- default output names (cc -c) ----
rm -f "$work/main.o"
if (cd "$work" && "$CFREE" cc -c main.c) \
> "$work/cc-c-default.out" 2> "$work/cc-c-default.err"; then
if [ -f "$work/main.o" ]; then
- printf 'PASS %s\n' "cc-c-default-output"
- pass=$((pass + 1))
+ ok "cc-c-default-output"
else
- printf 'FAIL %s (main.o not created)\n' "cc-c-default-output"
- fail=$((fail + 1))
+ echo "main.o not created" > "$work/cc-c-default.diag"
+ not_ok "cc-c-default-output" "$work/cc-c-default.diag"
fi
else
- printf 'FAIL cc-c-default-output (cfree cc -c failed)\n'
- sed 's/^/ | /' "$work/cc-c-default.err"
- fail=$((fail + 1))
+ not_ok "cc-c-default-output" "$work/cc-c-default.err"
fi
rm -f "$work/main.o" "$work/other.o"
if (cd "$work" && "$CFREE" cc -c main.c other.c) \
> "$work/cc-c-multi.out" 2> "$work/cc-c-multi.err"; then
if [ -f "$work/main.o" ] && [ -f "$work/other.o" ]; then
- printf 'PASS %s\n' "cc-c-multi-default-output"
- pass=$((pass + 1))
+ ok "cc-c-multi-default-output"
else
- printf 'FAIL %s (expected main.o and other.o)\n' "cc-c-multi-default-output"
- fail=$((fail + 1))
+ echo "expected main.o and other.o" > "$work/cc-c-multi.diag"
+ not_ok "cc-c-multi-default-output" "$work/cc-c-multi.diag"
fi
else
- printf 'FAIL cc-c-multi-default-output (cfree cc -c failed)\n'
- sed 's/^/ | /' "$work/cc-c-multi.err"
- fail=$((fail + 1))
+ not_ok "cc-c-multi-default-output" "$work/cc-c-multi.err"
fi
+# ---- cc -E → stdout ----
if "$CFREE" cc -E "$work/main.c" > "$work/cc-E-default.out" 2> "$work/cc-E-default.err"; then
if [ -s "$work/cc-E-default.out" ]; then
- printf 'PASS %s\n' "cc-E-default-stdout"
- pass=$((pass + 1))
+ ok "cc-E-default-stdout"
else
- printf 'FAIL %s (stdout was empty)\n' "cc-E-default-stdout"
- fail=$((fail + 1))
+ echo "stdout was empty" > "$work/cc-E-default.diag"
+ not_ok "cc-E-default-stdout" "$work/cc-E-default.diag"
fi
else
- printf 'FAIL cc-E-default-stdout (cfree cc -E failed)\n'
- sed 's/^/ | /' "$work/cc-E-default.err"
- fail=$((fail + 1))
+ not_ok "cc-E-default-stdout" "$work/cc-E-default.err"
fi
+# ---- cc -dumpmachine probe ----
if "$CFREE" cc -target riscv64-linux -dumpmachine \
> "$work/cc-dumpmachine.out" 2> "$work/cc-dumpmachine.err" &&
[ "$(cat "$work/cc-dumpmachine.out")" = "riscv64-linux" ]; then
- printf 'PASS %s\n' "cc-dumpmachine-probe"
- pass=$((pass + 1))
+ ok "cc-dumpmachine-probe"
else
- printf 'FAIL %s\n' "cc-dumpmachine-probe"
- sed 's/^/ | /' "$work/cc-dumpmachine.err"
- fail=$((fail + 1))
+ not_ok "cc-dumpmachine-probe" "$work/cc-dumpmachine.err"
fi
+# ---- cc -print-file-name probe ----
if "$CFREE" cc -print-file-name=crt1.o \
> "$work/cc-print-file-name.out" 2> "$work/cc-print-file-name.err" &&
[ "$(cat "$work/cc-print-file-name.out")" = "crt1.o" ]; then
- printf 'PASS %s\n' "cc-print-file-name-probe"
- pass=$((pass + 1))
+ ok "cc-print-file-name-probe"
else
- printf 'FAIL %s\n' "cc-print-file-name-probe"
- sed 's/^/ | /' "$work/cc-print-file-name.err"
- fail=$((fail + 1))
+ not_ok "cc-print-file-name-probe" "$work/cc-print-file-name.err"
fi
+# ---- accepted-but-ignored compatibility flags log to stderr ----
if "$CFREE" cc -Wall -Wextra -std=c11 -ffreestanding -c "$work/main.c" \
-o "$work/ignored.o" > "$work/cc-ignored.out" 2> "$work/cc-ignored.err"; then
if grep -q "ignoring accepted compatibility flag: -Wall" "$work/cc-ignored.err" &&
grep -q "ignoring accepted compatibility flag: -std=c11" "$work/cc-ignored.err"; then
- printf 'PASS %s\n' "cc-ignored-compat-flags"
- pass=$((pass + 1))
+ ok "cc-ignored-compat-flags"
else
- printf 'FAIL %s (missing ignore log)\n' "cc-ignored-compat-flags"
- sed 's/^/ | /' "$work/cc-ignored.err"
- fail=$((fail + 1))
+ cp "$work/cc-ignored.err" "$work/cc-ignored.diag"
+ not_ok "cc-ignored-compat-flags" "$work/cc-ignored.diag"
fi
else
- printf 'FAIL cc-ignored-compat-flags (cfree cc failed)\n'
- sed 's/^/ | /' "$work/cc-ignored.err"
- fail=$((fail + 1))
+ not_ok "cc-ignored-compat-flags" "$work/cc-ignored.err"
fi
+# ---- --output= long form (cc) ----
if "$CFREE" cc -c "$work/main.c" --output="$work/cc-long-output.o" \
> "$work/cc-long-output.out" 2> "$work/cc-long-output.err" &&
[ -f "$work/cc-long-output.o" ]; then
- printf 'PASS %s\n' "cc-long-output"
- pass=$((pass + 1))
+ ok "cc-long-output"
else
- printf 'FAIL %s\n' "cc-long-output"
- sed 's/^/ | /' "$work/cc-long-output.err"
- fail=$((fail + 1))
+ not_ok "cc-long-output" "$work/cc-long-output.err"
fi
+# ---- -iquote include path ----
mkdir -p "$work/quote-inc"
cat > "$work/quote-inc/q.h" <<'SRC'
#define Q_VALUE 7
@@ -267,33 +221,19 @@ cat > "$work/quote-include.c" <<'SRC'
#include "q.h"
int q(void) { return Q_VALUE; }
SRC
-if "$CFREE" cc -c "$work/quote-include.c" -iquote "$work/quote-inc" \
- -o "$work/quote-include.o" \
- > "$work/quote-include.out" 2> "$work/quote-include.err"; then
- printf 'PASS %s\n' "cc-iquote-include"
- pass=$((pass + 1))
-else
- printf 'FAIL %s\n' "cc-iquote-include"
- sed 's/^/ | /' "$work/quote-include.err"
- fail=$((fail + 1))
-fi
+run_ok "cc-iquote-include" "$CFREE" cc -c "$work/quote-include.c" \
+ -iquote "$work/quote-inc" -o "$work/quote-include.o"
+# ---- implicit freestanding headers ----
cat > "$work/implicit-header.c" <<'SRC'
#include <stddef.h>
#include <stdint.h>
int f(void) { return (int)sizeof(size_t) + (int)UINT8_MAX; }
SRC
-if "$CFREE" cc -target aarch64-linux -c "$work/implicit-header.c" \
- -o "$work/implicit-header.o" \
- > "$work/implicit-header.out" 2> "$work/implicit-header.err"; then
- printf 'PASS %s\n' "cc-implicit-freestanding-headers"
- pass=$((pass + 1))
-else
- printf 'FAIL %s (cfree cc failed)\n' "cc-implicit-freestanding-headers"
- sed 's/^/ | /' "$work/implicit-header.err"
- fail=$((fail + 1))
-fi
+run_ok "cc-implicit-freestanding-headers" "$CFREE" cc -target aarch64-linux \
+ -c "$work/implicit-header.c" -o "$work/implicit-header.o"
+# ---- runtime auto-build + link via nm (aarch64) ----
mkdir -p "$work/rt-support/rt"
cp -R "$repo_root/rt/include" "$work/rt-support/rt/include"
cp -R "$repo_root/rt/lib" "$work/rt-support/rt/lib"
@@ -316,12 +256,9 @@ if "$CFREE" cc --support-dir "$work/rt-support" -target aarch64-linux \
> "$work/rt-div.out" 2> "$work/rt-div.err" &&
"$CFREE" nm "$work/rt-div" 2> "$work/rt-div-nm.err" \
| grep -qE '[Tt] __udivti3'; then
- printf 'PASS %s\n' "cc-auto-builds-and-links-libcfree-rt"
- pass=$((pass + 1))
+ ok "cc-auto-builds-and-links-libcfree-rt"
else
- printf 'FAIL %s (cfree cc failed)\n' "cc-auto-builds-and-links-libcfree-rt"
- sed 's/^/ | /' "$work/rt-div.err"
- fail=$((fail + 1))
+ not_ok "cc-auto-builds-and-links-libcfree-rt" "$work/rt-div.err"
fi
cat > "$work/rt-x64-start.c" <<'SRC'
@@ -342,16 +279,14 @@ if "$CFREE" cc --support-dir "$work/rt-support" -target x86_64-linux \
-o "$work/rt-x64" > "$work/rt-x64.out" 2> "$work/rt-x64.err" &&
"$CFREE" nm "$work/rt-x64" 2> "$work/rt-x64-nm.err" \
| grep -qE '[Tt] vsnprintf'; then
- printf 'PASS %s\n' "cc-auto-builds-and-links-libcfree-rt-x64"
- pass=$((pass + 1))
+ ok "cc-auto-builds-and-links-libcfree-rt-x64"
else
- printf 'FAIL %s (cfree cc failed)\n' \
- "cc-auto-builds-and-links-libcfree-rt-x64"
- sed 's/^/ | /' "$work/rt-x64.err"
- sed 's/^/ | /' "$work/rt-x64-nm.err" 2>/dev/null || true
- fail=$((fail + 1))
+ { sed 's/^/cc: /' "$work/rt-x64.err"
+ sed 's/^/nm: /' "$work/rt-x64-nm.err" 2>/dev/null; } > "$work/rt-x64.diag"
+ not_ok "cc-auto-builds-and-links-libcfree-rt-x64" "$work/rt-x64.diag"
fi
+# ---- ld auto-builds + links the runtime (rt archive cached under support-dir) ----
mkdir -p "$work/ld-rt-support/rt"
cp -R "$repo_root/rt/include" "$work/ld-rt-support/rt/include"
cp -R "$repo_root/rt/lib" "$work/ld-rt-support/rt/lib"
@@ -362,27 +297,23 @@ if "$CFREE" cc --support-dir "$work/ld-rt-support" -target aarch64-linux \
-e _start "$work/ld-rt-div.o" -o "$work/ld-rt-div" \
> "$work/ld-rt-div.out" 2> "$work/ld-rt-div.err" &&
[ -f "$work/ld-rt-support/build/rt/aarch64-linux/libcfree_rt.a" ]; then
- printf 'PASS %s\n' "ld-auto-builds-and-links-libcfree-rt"
- pass=$((pass + 1))
+ ok "ld-auto-builds-and-links-libcfree-rt"
else
- printf 'FAIL %s (cfree ld failed)\n' \
- "ld-auto-builds-and-links-libcfree-rt"
- sed 's/^/ | /' "$work/ld-rt-div-cc.err"
- sed 's/^/ | /' "$work/ld-rt-div.err" 2>/dev/null || true
- fail=$((fail + 1))
+ { sed 's/^/cc: /' "$work/ld-rt-div-cc.err"
+ sed 's/^/ld: /' "$work/ld-rt-div.err" 2>/dev/null; } > "$work/ld-rt.diag"
+ not_ok "ld-auto-builds-and-links-libcfree-rt" "$work/ld-rt.diag"
fi
+# ---- --output= long form (ld) ----
if "$CFREE" ld "$work/main.o" --output="$work/ld-long-output" \
> "$work/ld-long-output.out" 2> "$work/ld-long-output.err" &&
[ -f "$work/ld-long-output" ]; then
- printf 'PASS %s\n' "ld-long-output"
- pass=$((pass + 1))
+ ok "ld-long-output"
else
- printf 'FAIL %s\n' "ld-long-output"
- sed 's/^/ | /' "$work/ld-long-output.err"
- fail=$((fail + 1))
+ not_ok "ld-long-output" "$work/ld-long-output.err"
fi
+# ---- ld --no-undefined rejects unresolved refs in a shared object ----
cat > "$work/shared-undef.c" <<'SRC'
int missing(void);
int f(void) { return missing(); }
@@ -390,33 +321,23 @@ SRC
if "$CFREE" cc -target x86_64-linux -fPIC -c "$work/shared-undef.c" \
-o "$work/shared-undef.o" > "$work/shared-undef-cc.out" \
2> "$work/shared-undef-cc.err"; then
- if ! "$CFREE" ld -shared --no-undefined "$work/shared-undef.o" \
- -o "$work/shared-undef.so" > "$work/shared-undef-ld.out" \
- 2> "$work/shared-undef-ld.err"; then
- printf 'PASS %s\n' "ld-no-undefined"
- pass=$((pass + 1))
- else
- printf 'FAIL %s (link unexpectedly succeeded)\n' "ld-no-undefined"
- fail=$((fail + 1))
- fi
+ run_fail "ld-no-undefined" "$CFREE" ld -shared --no-undefined \
+ "$work/shared-undef.o" -o "$work/shared-undef.so"
else
- printf 'FAIL %s (setup compile failed)\n' "ld-no-undefined"
- sed 's/^/ | /' "$work/shared-undef-cc.err"
- fail=$((fail + 1))
+ not_ok "ld-no-undefined" "$work/shared-undef-cc.err"
fi
+# ---- objdump -x aggregate (sections + symbol table) ----
if "$CFREE" objdump -x "$work/main.o" \
> "$work/objdump-x.out" 2> "$work/objdump-x.err" &&
grep -q "Sections:" "$work/objdump-x.out" &&
grep -q "SYMBOL TABLE:" "$work/objdump-x.out"; then
- printf 'PASS %s\n' "objdump-x-aggregate"
- pass=$((pass + 1))
+ ok "objdump-x-aggregate"
else
- printf 'FAIL %s\n' "objdump-x-aggregate"
- sed 's/^/ | /' "$work/objdump-x.err"
- fail=$((fail + 1))
+ not_ok "objdump-x-aggregate" "$work/objdump-x.err"
fi
+# ---- Mach-O -ffunction-sections atom coalescing ----
{
printf 'int live(void) { return 0; }\n'
i=0
@@ -441,21 +362,21 @@ if "$CFREE" cc -target arm64-apple-macos -O1 -ffunction-sections \
if [ "$macho_sec_count" -le 8 ] &&
! grep -q '\*UND\*' "$work/macho-many-dump.out" &&
[ "$macho_flags" = "00002000" ]; then
- printf 'PASS %s\n' "macho-function-sections-atoms"
- pass=$((pass + 1))
+ ok "macho-function-sections-atoms"
else
- printf 'FAIL %s (sections=%s flags=%s; unexpected UND/flags/fanout)\n' \
- "macho-function-sections-atoms" "$macho_sec_count" "$macho_flags"
- sed 's/^/ | /' "$work/macho-many-dump.out"
- fail=$((fail + 1))
+ { printf 'sections=%s flags=%s; unexpected UND/flags/fanout\n' \
+ "$macho_sec_count" "$macho_flags"
+ sed 's/^/ | /' "$work/macho-many-dump.out"; } \
+ > "$work/macho-many.diag"
+ not_ok "macho-function-sections-atoms" "$work/macho-many.diag"
fi
else
- printf 'FAIL %s (compile or objdump failed)\n' \
- "macho-function-sections-atoms"
- sed 's/^/ | /' "$work/macho-many-cc.err" "$work/macho-many-dump.err"
- fail=$((fail + 1))
+ { sed 's/^/cc: /' "$work/macho-many-cc.err"
+ sed 's/^/dump: /' "$work/macho-many-dump.err"; } > "$work/macho-many-setup.diag"
+ not_ok "macho-function-sections-atoms" "$work/macho-many-setup.diag"
fi
+# ---- run: JIT compile a source + archive on demand, exit status is the result ----
cat > "$work/run-main.c" <<'SRC'
int add42(int);
int main(void) { return add42(0); }
@@ -474,20 +395,19 @@ if "$CFREE" cc -I"$repo_root/rt/include" \
> "$work/run-archive.out" 2> "$work/run-archive.err"
run_status=$?
if [ "$run_status" -eq 42 ]; then
- printf 'PASS %s\n' "run-source-archive-demand"
- pass=$((pass + 1))
+ ok "run-source-archive-demand"
else
- printf 'FAIL %s (status %s, want 42)\n' \
- "run-source-archive-demand" "$run_status"
- sed 's/^/ | /' "$work/run-archive.err"
- fail=$((fail + 1))
+ { printf 'status %s, want 42\n' "$run_status"
+ sed 's/^/ | /' "$work/run-archive.err"; } > "$work/run-archive.diag"
+ not_ok "run-source-archive-demand" "$work/run-archive.diag"
fi
else
- printf 'FAIL run-source-archive-demand (setup failed)\n'
- sed 's/^/ | /' "$work/run-lib.err" "$work/run-ar.err"
- fail=$((fail + 1))
+ { sed 's/^/lib: /' "$work/run-lib.err"
+ sed 's/^/ar: /' "$work/run-ar.err"; } > "$work/run-setup.diag"
+ not_ok "run-source-archive-demand" "$work/run-setup.diag"
fi
+# ---- archive link order is enforced (def after ref vs ref after def) ----
cat > "$work/order-main.c" <<'SRC'
int foo(void);
int _start(void) { return foo(); }
@@ -506,18 +426,17 @@ if "$CFREE" cc -target x86_64-linux -c "$work/order-main.c" -o "$work/order-main
-o "$work/order-right" > "$work/order-right.out" 2> "$work/order-right.err" &&
! "$CFREE" cc -target x86_64-linux -L"$work" -lfoo "$work/order-main.o" \
-o "$work/order-wrong" > "$work/order-wrong.out" 2> "$work/order-wrong.err"; then
- printf 'PASS %s\n' "cc-link-archive-order"
- pass=$((pass + 1))
+ ok "cc-link-archive-order"
else
- printf 'FAIL %s (archive order was not enforced)\n' "cc-link-archive-order"
- sed 's/^/ right| /' "$work/order-right.err"
- sed 's/^/ wrong| /' "$work/order-wrong.err"
- fail=$((fail + 1))
+ { sed 's/^/right| /' "$work/order-right.err"
+ sed 's/^/wrong| /' "$work/order-wrong.err"; } > "$work/order.diag"
+ not_ok "cc-link-archive-order" "$work/order.diag"
fi
else
- printf 'FAIL cc-link-archive-order (setup failed)\n'
- sed 's/^/ | /' "$work/order-main.err" "$work/order-foo.err" "$work/order-ar.err"
- fail=$((fail + 1))
+ { sed 's/^/main: /' "$work/order-main.err"
+ sed 's/^/foo: /' "$work/order-foo.err"
+ sed 's/^/ar: /' "$work/order-ar.err"; } > "$work/order-setup.diag"
+ not_ok "cc-link-archive-order" "$work/order-setup.diag"
fi
# ---- rv64 cross-target end-to-end (as, cc, ld, objdump) ----
@@ -535,17 +454,13 @@ if "$CFREE" as -target riscv64-linux "$work/rv64-asm.S" -o "$work/rv64-asm.o" \
if "$CFREE" objdump -h "$work/rv64-asm.o" \
> "$work/rv64-as-h.out" 2> "$work/rv64-as-h.err" &&
grep -q "elf64-riscv64" "$work/rv64-as-h.out"; then
- printf 'PASS %s\n' "rv64-as-cc-objdump-elf"
- pass=$((pass + 1))
+ ok "rv64-as-cc-objdump-elf"
else
- printf 'FAIL %s (objdump did not report elf64-riscv64)\n' "rv64-as-cc-objdump-elf"
- sed 's/^/ | /' "$work/rv64-as-h.out"
- fail=$((fail + 1))
+ cp "$work/rv64-as-h.out" "$work/rv64-as-h.diag"
+ not_ok "rv64-as-cc-objdump-elf" "$work/rv64-as-h.diag"
fi
else
- printf 'FAIL %s (cfree as failed)\n' "rv64-as-cc-objdump-elf"
- sed 's/^/ | /' "$work/rv64-as.err"
- fail=$((fail + 1))
+ not_ok "rv64-as-cc-objdump-elf" "$work/rv64-as.err"
fi
cat > "$work/rv64-cc.c" <<'SRC'
@@ -556,17 +471,13 @@ if "$CFREE" cc -target riscv64-linux -c "$work/rv64-cc.c" -o "$work/rv64-cc.o" \
if "$CFREE" objdump -d "$work/rv64-cc.o" \
> "$work/rv64-cc-d.out" 2> "$work/rv64-cc-d.err" &&
grep -q "ret" "$work/rv64-cc-d.out"; then
- printf 'PASS %s\n' "rv64-cc-emits-ret"
- pass=$((pass + 1))
+ ok "rv64-cc-emits-ret"
else
- printf 'FAIL %s (objdump -d missing ret)\n' "rv64-cc-emits-ret"
- sed 's/^/ | /' "$work/rv64-cc-d.out"
- fail=$((fail + 1))
+ cp "$work/rv64-cc-d.out" "$work/rv64-cc-d.diag"
+ not_ok "rv64-cc-emits-ret" "$work/rv64-cc-d.diag"
fi
else
- printf 'FAIL %s (cfree cc failed)\n' "rv64-cc-emits-ret"
- sed 's/^/ | /' "$work/rv64-cc.err"
- fail=$((fail + 1))
+ not_ok "rv64-cc-emits-ret" "$work/rv64-cc.err"
fi
cat > "$work/rv64-ld-start.c" <<'SRC'
@@ -583,37 +494,30 @@ if "$CFREE" cc -target riscv64-linux -ffreestanding -fno-PIC \
# rv64 ELF executable without needing objdump to parse ET_EXEC.
em_byte=$(od -An -tx1 -j 18 -N 1 "$work/rv64-ld.exe" | tr -d ' \n')
if [ "$em_byte" = "f3" ]; then
- printf 'PASS %s\n' "rv64-ld-static-exe"
- pass=$((pass + 1))
+ ok "rv64-ld-static-exe"
else
- printf 'FAIL %s (e_machine byte=%s want=f3)\n' \
- "rv64-ld-static-exe" "$em_byte"
- fail=$((fail + 1))
+ printf 'e_machine byte=%s want=f3\n' "$em_byte" \
+ > "$work/rv64-ld.diag"
+ not_ok "rv64-ld-static-exe" "$work/rv64-ld.diag"
fi
else
- printf 'FAIL %s (cfree ld failed)\n' "rv64-ld-static-exe"
- sed 's/^/ | /' "$work/rv64-ld.err"
- fail=$((fail + 1))
+ not_ok "rv64-ld-static-exe" "$work/rv64-ld.err"
fi
else
- printf 'FAIL %s (cfree cc -c failed)\n' "rv64-ld-static-exe"
- sed 's/^/ | /' "$work/rv64-ld-cc.err"
- fail=$((fail + 1))
+ not_ok "rv64-ld-static-exe" "$work/rv64-ld-cc.err"
fi
+# ---- check: frontend-only, emits no output files ----
rm -f "$work/check.o" "$work/a.out"
if "$CFREE" check "$work/main.c" > "$work/check.out" 2> "$work/check.err"; then
if [ ! -e "$work/check.o" ] && [ ! -e "$work/a.out" ]; then
- printf 'PASS %s\n' "check-no-output"
- pass=$((pass + 1))
+ ok "check-no-output"
else
- printf 'FAIL %s (unexpected output file)\n' "check-no-output"
- fail=$((fail + 1))
+ echo "unexpected output file" > "$work/check.diag"
+ not_ok "check-no-output" "$work/check.diag"
fi
else
- printf 'FAIL %s (cfree check failed)\n' "check-no-output"
- sed 's/^/ | /' "$work/check.err"
- fail=$((fail + 1))
+ not_ok "check-no-output" "$work/check.err"
fi
cat > "$work/check-bad.c" <<'SRC'
@@ -621,16 +525,14 @@ int broken( { return 0; }
SRC
if "$CFREE" check "$work/check-bad.c" \
> "$work/check-bad.out" 2> "$work/check-bad.err"; then
- printf 'FAIL %s (bad source passed)\n' "check-reports-errors"
- fail=$((fail + 1))
+ echo "bad source passed" > "$work/check-bad.diag"
+ not_ok "check-reports-errors" "$work/check-bad.diag"
else
if grep -q "fatal:" "$work/check-bad.err"; then
- printf 'PASS %s\n' "check-reports-errors"
- pass=$((pass + 1))
+ ok "check-reports-errors"
else
- printf 'FAIL %s (missing diagnostic)\n' "check-reports-errors"
- sed 's/^/ | /' "$work/check-bad.err"
- fail=$((fail + 1))
+ cp "$work/check-bad.err" "$work/check-bad-missing.diag"
+ not_ok "check-reports-errors" "$work/check-bad-missing.diag"
fi
fi
@@ -642,9 +544,7 @@ rm -f "$work/nm-main.o"
nm_obj_ok=1
if ! "$CFREE" cc -target aarch64-linux -c "$work/main.c" -o "$work/nm-main.o" \
> "$work/nm-cc.out" 2> "$work/nm-cc.err"; then
- printf 'FAIL nm-setup (cfree cc -c failed)\n'
- sed 's/^/ | /' "$work/nm-cc.err"
- fail=$((fail + 1))
+ not_ok "nm-setup" "$work/nm-cc.err"
nm_obj_ok=0
fi
@@ -653,12 +553,11 @@ if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" nm "$work/nm-main.o" > "$work/nm-basic.out" 2> "$work/nm-basic.err" &&
grep -q "main" "$work/nm-basic.out" &&
grep -q "_start" "$work/nm-basic.out"; then
- printf 'PASS %s\n' "nm-basic"
- pass=$((pass + 1))
+ ok "nm-basic"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "nm-basic" "$work/nm-basic.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-basic"; sed 's/^/ | /' "$work/nm-basic.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-basic"
- fail=$((fail + 1))
+ not_ok "nm-basic"
fi
# nm -g: only global symbols.
@@ -676,23 +575,19 @@ if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" nm -g "$work/nm-global.o" > "$work/nm-global-g.out" 2>/dev/null &&
grep -q "visible" "$work/nm-global-g.out" &&
! grep -q "hidden" "$work/nm-global-g.out"; then
- printf 'PASS %s\n' "nm-global-only"
- pass=$((pass + 1))
+ ok "nm-global-only"
else
- [ "$nm_obj_ok" = 1 ] && printf 'FAIL %s\n' "nm-global-only"
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-global-only"
- fail=$((fail + 1))
+ not_ok "nm-global-only"
fi
# nm -u: undefined only (ELF objects may have no undefined)
if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" nm -u "$work/nm-main.o" > "$work/nm-undef.out" 2>"$work/nm-undef.err"; then
- printf 'PASS %s\n' "nm-undefined-only"
- pass=$((pass + 1))
+ ok "nm-undefined-only"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "nm-undefined-only" "$work/nm-undef.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-undefined-only"; sed 's/^/ | /' "$work/nm-undef.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-undefined-only"
- fail=$((fail + 1))
+ not_ok "nm-undefined-only"
fi
# nm on an archive
@@ -702,22 +597,19 @@ if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" nm "$work/libnmtest.a" > "$work/nm-archive.out" \
2> "$work/nm-archive.err" &&
grep -q "main" "$work/nm-archive.out"; then
- printf 'PASS %s\n' "nm-archive"
- pass=$((pass + 1))
+ ok "nm-archive"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "nm-archive" "$work/nm-archive.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-archive"; sed 's/^/ | /' "$work/nm-archive.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-archive"
- fail=$((fail + 1))
+ not_ok "nm-archive"
fi
# nm -h (help)
if "$CFREE" nm --help > "$work/nm-help.out" 2> "$work/nm-help.err" &&
grep -q "USAGE" "$work/nm-help.out"; then
- printf 'PASS %s\n' "nm-help"
- pass=$((pass + 1))
+ ok "nm-help"
else
- printf 'FAIL %s\n' "nm-help"
- fail=$((fail + 1))
+ not_ok "nm-help" "$work/nm-help.err"
fi
# ---- size ----
@@ -725,24 +617,22 @@ fi
if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" size "$work/nm-main.o" > "$work/size-basic.out" 2> "$work/size-basic.err" &&
grep -q "text" "$work/size-basic.out"; then
- printf 'PASS %s\n' "size-basic"
- pass=$((pass + 1))
+ ok "size-basic"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "size-basic" "$work/size-basic.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-basic"; sed 's/^/ | /' "$work/size-basic.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-basic"
- fail=$((fail + 1))
+ not_ok "size-basic"
fi
# size -A: SysV format
if [ "$nm_obj_ok" = 1 ] &&
"$CFREE" size -A "$work/nm-main.o" > "$work/size-sysv.out" 2> "$work/size-sysv.err" &&
grep -q "Total" "$work/size-sysv.out"; then
- printf 'PASS %s\n' "size-sysv"
- pass=$((pass + 1))
+ ok "size-sysv"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "size-sysv" "$work/size-sysv.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-sysv"; sed 's/^/ | /' "$work/size-sysv.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-sysv"
- fail=$((fail + 1))
+ not_ok "size-sysv"
fi
# size on an archive (uses libnmtest.a from nm test)
@@ -750,22 +640,19 @@ if [ "$nm_obj_ok" = 1 ] && [ -f "$work/libnmtest.a" ] &&
"$CFREE" size "$work/libnmtest.a" > "$work/size-archive.out" \
2> "$work/size-archive.err" &&
grep -q "text" "$work/size-archive.out"; then
- printf 'PASS %s\n' "size-archive"
- pass=$((pass + 1))
+ ok "size-archive"
+elif [ "$nm_obj_ok" = 1 ]; then
+ not_ok "size-archive" "$work/size-archive.err"
else
- [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-archive"; sed 's/^/ | /' "$work/size-archive.err"; }
- [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-archive"
- fail=$((fail + 1))
+ not_ok "size-archive"
fi
# size -h (help)
if "$CFREE" size --help > "$work/size-help.out" 2> "$work/size-help.err" &&
grep -q "USAGE" "$work/size-help.out"; then
- printf 'PASS %s\n' "size-help"
- pass=$((pass + 1))
+ ok "size-help"
else
- printf 'FAIL %s\n' "size-help"
- fail=$((fail + 1))
+ not_ok "size-help" "$work/size-help.err"
fi
# ---- addr2line ----
@@ -775,8 +662,8 @@ int main(void) { return calc(42); }
SRC
if [ "$nm_obj_ok" != 1 ]; then
- printf 'FAIL addr2line-basic (setup)\n'; fail=$((fail + 1))
- printf 'FAIL addr2line-nonzero (setup)\n'; fail=$((fail + 1))
+ not_ok "addr2line-basic"
+ not_ok "addr2line-nonzero"
elif "$CFREE" cc -g -target aarch64-linux -c "$work/a2l.c" -o "$work/a2l.o" \
> "$work/a2l-cc.out" 2> "$work/a2l-cc.err"; then
calc_addr=$("$CFREE" nm "$work/a2l.o" 2>/dev/null | \
@@ -785,26 +672,23 @@ elif "$CFREE" cc -g -target aarch64-linux -c "$work/a2l.c" -o "$work/a2l.o" \
"$CFREE" addr2line -e "$work/a2l.o" "$calc_addr" \
> "$work/a2l-hit.out" 2> "$work/a2l-hit.err" &&
grep -q "a2l.c" "$work/a2l-hit.out"; then
- printf 'PASS %s\n' "addr2line-basic"
- pass=$((pass + 1))
+ ok "addr2line-basic"
else
- printf 'FAIL %s (calc_addr=%s)\n' "addr2line-basic" "$calc_addr"
- sed 's/^/ | /' "$work/a2l-hit.err"
- fail=$((fail + 1))
+ { printf 'calc_addr=%s\n' "$calc_addr"
+ sed 's/^/ | /' "$work/a2l-hit.err"; } > "$work/a2l-hit.diag"
+ not_ok "addr2line-basic" "$work/a2l-hit.diag"
fi
# addr2line should produce output (not crash) on any address
if "$CFREE" addr2line -e "$work/a2l.o" 0x0 \
> "$work/a2l-miss.out" 2> "$work/a2l-miss.err" &&
[ -s "$work/a2l-miss.out" ]; then
- printf 'PASS %s\n' "addr2line-nonzero"
- pass=$((pass + 1))
+ ok "addr2line-nonzero"
else
- printf 'FAIL %s\n' "addr2line-nonzero"
- fail=$((fail + 1))
+ not_ok "addr2line-nonzero" "$work/a2l-miss.err"
fi
else
- printf 'FAIL addr2line-basic (setup compile failed)\n'; fail=$((fail + 1))
- printf 'FAIL addr2line-nonzero (setup)\n'; fail=$((fail + 1))
+ not_ok "addr2line-basic" "$work/a2l-cc.err"
+ not_ok "addr2line-nonzero"
fi
# addr2line over Mach-O DWARF: the producer emits .debug_* into the
@@ -819,15 +703,15 @@ if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2l.c" \
"$CFREE" addr2line -e "$work/a2l.macho.o" "$m_addr" \
> "$work/a2l-m.out" 2> "$work/a2l-m.err" &&
grep -q "a2l.c" "$work/a2l-m.out"; then
- printf 'PASS %s\n' "addr2line-macho"
- pass=$((pass + 1))
+ ok "addr2line-macho"
else
- printf 'FAIL %s (m_addr=%s)\n' "addr2line-macho" "$m_addr"
- sed 's/^/ | /' "$work/a2l-m.out" "$work/a2l-m.err"
- fail=$((fail + 1))
+ { printf 'm_addr=%s\n' "$m_addr"
+ sed 's/^/out| /' "$work/a2l-m.out"
+ sed 's/^/err| /' "$work/a2l-m.err"; } > "$work/a2l-m.diag"
+ not_ok "addr2line-macho" "$work/a2l-m.diag"
fi
else
- printf 'FAIL addr2line-macho (setup compile failed)\n'; fail=$((fail + 1))
+ not_ok "addr2line-macho" "$work/a2l-m-cc.err"
fi
# Debug-info retention through the Mach-O linker: link a self-contained
@@ -853,26 +737,22 @@ if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2lm.c" \
"$CFREE" addr2line -e "$work/a2lm.exe" "0x$cm_addr" \
> "$work/a2lm-a2l.out" 2> "$work/a2lm-a2l.err" &&
grep -q "a2lm.c" "$work/a2lm-a2l.out"; then
- printf 'PASS %s\n' "addr2line-macho-linked"
- pass=$((pass + 1))
+ ok "addr2line-macho-linked"
else
- printf 'FAIL %s (compute=%s: %s)\n' "addr2line-macho-linked" \
- "$cm_addr" "$(cat "$work/a2lm-a2l.out" 2>/dev/null)"
- fail=$((fail + 1))
+ printf 'compute=%s: %s\n' "$cm_addr" \
+ "$(cat "$work/a2lm-a2l.out" 2>/dev/null)" > "$work/a2lm.diag"
+ not_ok "addr2line-macho-linked" "$work/a2lm.diag"
fi
else
- printf 'FAIL addr2line-macho-linked (link failed)\n'; fail=$((fail + 1))
- sed 's/^/ | /' "$work/a2lm-ld.err"
+ not_ok "addr2line-macho-linked" "$work/a2lm-ld.err"
fi
# addr2line -h (help)
if "$CFREE" addr2line --help > "$work/a2l-help.out" 2> "$work/a2l-help.err" &&
grep -q "USAGE" "$work/a2l-help.out"; then
- printf 'PASS %s\n' "addr2line-help"
- pass=$((pass + 1))
+ ok "addr2line-help"
else
- printf 'FAIL %s\n' "addr2line-help"
- fail=$((fail + 1))
+ not_ok "addr2line-help" "$work/a2l-help.err"
fi
# --emit=ir: dump the semantic CG IR tape (requires -O1+).
@@ -889,31 +769,22 @@ if "$CFREE" cc -O1 --emit=ir -c "$work/ir.c" -o "$work/ir.out" \
grep -q "binop" "$work/ir.out" &&
grep -q "iadd" "$work/ir.out" &&
grep -q "ret values=\[" "$work/ir.out"; then
- printf 'PASS %s\n' "cc-emit-ir"
- pass=$((pass + 1))
+ ok "cc-emit-ir"
else
- printf 'FAIL %s\n' "cc-emit-ir"
- sed 's/^/ | /' "$work/ir-emit.err"
- fail=$((fail + 1))
+ not_ok "cc-emit-ir" "$work/ir-emit.err"
fi
# --emit=ir without -O1 must be rejected (no IR tape is recorded at -O0).
if "$CFREE" cc --emit=ir -c "$work/ir.c" -o "$work/ir-o0.out" \
> "$work/ir-o0.out.log" 2> "$work/ir-o0.err"; then
- printf 'FAIL %s (expected failure at -O0)\n' "cc-emit-ir-requires-opt"
- fail=$((fail + 1))
+ echo "expected failure at -O0" > "$work/ir-o0.diag"
+ not_ok "cc-emit-ir-requires-opt" "$work/ir-o0.diag"
elif grep -q "requires -O1" "$work/ir-o0.err"; then
- printf 'PASS %s\n' "cc-emit-ir-requires-opt"
- pass=$((pass + 1))
+ ok "cc-emit-ir-requires-opt"
else
- printf 'FAIL %s (wrong diagnostic)\n' "cc-emit-ir-requires-opt"
- sed 's/^/ | /' "$work/ir-o0.err"
- fail=$((fail + 1))
+ cp "$work/ir-o0.err" "$work/ir-o0-wrong.diag"
+ not_ok "cc-emit-ir-requires-opt" "$work/ir-o0-wrong.diag"
fi
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\ndriver: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\ndriver: %d/%d passed\n' "$pass" "$total"
+cf_summary driver-cc
+cf_exit
diff --git a/test/dwarf/dwarf_test.c b/test/dwarf/dwarf_test.c
@@ -22,6 +22,8 @@
#include <stdlib.h>
#include <string.h>
+#include "lib/cfree_unit.h"
+
/* This test reaches into the internal obj/ surface to construct a
* DWARF-bearing ELF without going through the parser/codegen path.
* That's deliberate: we want to test the *consumer* in isolation against
@@ -30,46 +32,10 @@
#include "core/pool.h"
#include "obj/obj.h"
-/* ---- env ---- */
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-static int g_fail;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- g_fail++; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fprintf(stderr, "\n"); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
/* ---- byte builders -------------------------------------------------- */
@@ -877,7 +843,7 @@ static void run_tests(CfreeDebugInfo* di) {
EXPECT(file.s && strstr(file.s, "test.c") != NULL,
"file should contain test.c, got %.*s", CFREE_SLICE_ARG(file));
} else {
- g_fail++;
+ g_u.fails++;
fprintf(stderr, "FAIL: addr_to_line(0x1000) returned no entry\n");
}
/* 2. line_to_addr round trip. */
@@ -894,7 +860,7 @@ static void run_tests(CfreeDebugInfo* di) {
EXPECT(pc == 0x1000, "expected pc 0x1000 for test.c:10, got 0x%llx",
(unsigned long long)pc);
} else {
- g_fail++;
+ g_u.fails++;
fprintf(stderr, "FAIL: line_to_addr could not find any test.c:10\n");
}
}
@@ -912,7 +878,7 @@ static void run_tests(CfreeDebugInfo* di) {
CfreeDwarfVarLoc loc;
EXPECT(cfree_dwarf_var_at(di, 0x1000, CFREE_SLICE_LIT("x"), &loc) == 0,
"var_at(0x1000, x) failed");
- if (g_fail == 0) {
+ if (g_u.fails == 0) {
EXPECT(loc.kind == CFREE_DLOC_FRAME_OFS,
"expected x.kind=FRAME_OFS, got %d", (int)loc.kind);
if (loc.kind == CFREE_DLOC_FRAME_OFS) {
@@ -966,7 +932,7 @@ static void run_tests(CfreeDebugInfo* di) {
if (cfree_dwarf_addr_to_line(di, 0x1004, &f2, &l2, &c2) == 0) {
EXPECT(l2 == 11, "expected line 11 at 0x1004, got %u", l2);
} else {
- g_fail++;
+ g_u.fails++;
fprintf(stderr, "FAIL: addr_to_line(0x1004) failed\n");
}
}
@@ -1037,7 +1003,7 @@ static void run_tests(CfreeDebugInfo* di) {
(int)it.kind);
}
} else {
- g_fail++;
+ g_u.fails++;
fprintf(stderr, "FAIL: var_at(my_int) returned nothing\n");
}
}
@@ -1103,20 +1069,13 @@ static void run_tests(CfreeDebugInfo* di) {
int main(void) {
CfreeTarget target;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
- CfreeContext ctx;
- memset(&ctx, 0, sizeof ctx);
- ctx.heap = &g_heap;
- ctx.diag = &g_diag;
- ctx.now = -1;
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
+ target =
+ cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
CfreeCompiler* cc = NULL;
- if (cfree_compiler_new(target, &ctx, &cc) != CFREE_OK || !cc) {
+ if (cfree_unit_compiler_new(&g_u, target, &cc) != CFREE_OK || !cc) {
fprintf(stderr, "compiler_new failed\n");
return 1;
}
@@ -1167,7 +1126,7 @@ int main(void) {
/* Emit ELF to memory. */
CfreeWriter* w = NULL;
- (void)cfree_writer_mem(&g_heap, &w);
+ (void)cfree_writer_mem(&g_u.heap, &w);
emit_elf(cc, ob, w);
size_t obj_len = 0;
const uint8_t* obj_bytes = cfree_writer_mem_bytes(w, &obj_len);
@@ -1194,13 +1153,13 @@ int main(void) {
in.data = obj_bytes;
in.len = obj_len;
CfreeObjFile* obj = NULL;
- EXPECT(
- cfree_obj_open(&ctx, CFREE_SLICE_LIT("test.o"), &in, &obj) == CFREE_OK &&
- obj != NULL,
- "cfree_obj_open failed");
+ EXPECT(cfree_obj_open(&g_u.ctx, CFREE_SLICE_LIT("test.o"), &in, &obj) ==
+ CFREE_OK &&
+ obj != NULL,
+ "cfree_obj_open failed");
if (obj) {
CfreeDebugInfo* di = NULL;
- EXPECT(cfree_dwarf_open(&ctx, obj, &di) == CFREE_OK && di != NULL,
+ EXPECT(cfree_dwarf_open(&g_u.ctx, obj, &di) == CFREE_OK && di != NULL,
"cfree_dwarf_open failed");
if (di) {
run_tests(di);
@@ -1221,8 +1180,8 @@ int main(void) {
cfree_compiler_free(cc);
- if (g_fail) {
- fprintf(stderr, "%d failure(s)\n", g_fail);
+ if (g_u.fails) {
+ fprintf(stderr, "%d failure(s)\n", g_u.fails);
return 1;
}
printf("OK\n");
diff --git a/test/elf/run.sh b/test/elf/run.sh
@@ -1,32 +1,40 @@
#!/usr/bin/env bash
-# test/elf/run.sh — ELF object-file fidelity tests.
+# test/elf/run.sh — ELF object-file fidelity tests, on the shared corpus
+# harness (test/lib/cf_corpus.sh).
#
# Scope: read/write/roundtrip of relocatable ELF .o files only. Linker
# and exe behavior live in test/link/ — do not duplicate them here.
#
-# Layers (each runs only when its prerequisites are present):
+# Layers — each is a SEQUENTIAL corpus (its own cf_corpus_run, single lane,
+# no opt axis), NOT a parallel lane within a case:
#
-# A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests.
-# Each .c links against build/libcfree.a, runs,
-# and exits 0 on success.
+# A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests. Each .c
+# links against build/libcfree.a, runs, exits 0
+# on success. (lane A)
# B. test/elf/cases/*.c clang-oracle structural roundtrip tests:
# clang -c case.c -> golden.o
# cfree-roundtrip golden.o -> rt.o
# normalized readelf diff golden.o vs rt.o.
-# C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip
-# to fail with a substring from <name>.expect.
+# Honors per-case NN_name.targets tuple
+# applicability (engine SKIP-NA) and per-case
+# NN_name.cflags extra compiler flags. (lane B)
+# C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip to
+# fail (nonzero, non-signal) with a substring from
+# <name>.expect on stderr. (lane C)
#
-# Tools detected: clang, llvm-readelf, python3. Missing tools cause the
-# dependent layer to be skipped, not failed. Set CFREE_TEST_ALLOW_SKIP=1
-# to allow the harness to exit 0 with skips; otherwise any skip makes
-# the run fail (so CI catches a silently degraded run).
+# Tools detected: clang, llvm-readelf/readelf, python3. Missing tools cause the
+# dependent layer's cases to be SKIPped, not failed. Set CFREE_TEST_ALLOW_SKIP=1
+# to allow the harness to exit 0 with skips; otherwise any skip makes the run
+# fail (so CI catches a silently degraded run).
#
# Parallelism:
# default run in parallel with a capped CPU-count default.
# CFREE_TEST_JOBS=N run up to N cases per layer concurrently.
# CFREE_TEST_JOBS=auto same as the default.
-# The console summary is replayed by the parent in deterministic order;
-# per-case command output remains in build/test/ for failure inspection.
+# CFREE_ELF_PARALLEL=0 force serial dispatch.
+# Every lane hook writes only under $CF_WORK and records only via cf_*, so the
+# console summary is identical serial or parallel; per-case command output for
+# a failure is surfaced by the engine in deterministic index order.
set -u
@@ -36,122 +44,14 @@ BUILD_DIR="$ROOT/build/test"
LIB_AR="$ROOT/build/libcfree.a"
NORMALIZE="$TEST_DIR/normalize.py"
-# shellcheck source=../lib/parallel.sh
-source "$ROOT/test/lib/parallel.sh"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
mkdir -p "$BUILD_DIR"
-TEST_JOBS="$(cfree_parallel_jobs)" || exit 2
-PARALLEL_DIR="$BUILD_DIR/elf.parallel/$$"
-mkdir -p "$PARALLEL_DIR"
-
-PASS=0
-FAIL=0
-SKIP=0
-FAIL_NAMES=()
-SKIP_NAMES=()
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-
-event_path() {
- printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2"
-}
-
-worker_stdout_path() {
- printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2"
-}
-
-worker_stderr_path() {
- printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2"
-}
-
-emit_event() {
- local file="$1" kind="$2" arg1="${3:-}" arg2="${4:-}"
- printf '%s\t%s\t%s\n' "$kind" "$arg1" "$arg2" >> "$file"
-}
-
-replay_events() {
- local event="$1" stdout_log="$2" stderr_log="$3"
- local kind arg1 arg2
-
- if [ ! -s "$event" ]; then
- note_fail "internal: missing worker result $event"
- if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi
- if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi
- return
- fi
-
- while IFS=$'\t' read -r kind arg1 arg2; do
- case "$kind" in
- PASS)
- note_pass "$arg1"
- ;;
- FAIL)
- note_fail "$arg1"
- ;;
- SKIP)
- note_skip "$arg1" "$arg2"
- ;;
- SKIP_NA)
- printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$arg1" "$arg2"
- ;;
- DUMP)
- if [ -f "$arg1" ]; then
- if [ -n "$arg2" ] && [ "$arg2" -gt 0 ] 2>/dev/null; then
- head -n "$arg2" "$arg1" | sed 's/^/ | /'
- else
- sed 's/^/ | /' "$arg1"
- fi
- else
- printf ' | (missing %s)\n' "$arg1"
- fi
- ;;
- *)
- note_fail "internal: malformed worker event in $event"
- ;;
- esac
- done < "$event"
-}
-
-run_parallel_items() {
- local layer="$1" worker="$2"
- shift 2
-
- local events=()
- local stdout_logs=()
- local stderr_logs=()
- local idx=0
- local item event stdout_log stderr_log
-
- for item in "$@"; do
- event="$(event_path "$layer" "$idx")"
- stdout_log="$(worker_stdout_path "$layer" "$idx")"
- stderr_log="$(worker_stderr_path "$layer" "$idx")"
- : > "$event"
- : > "$stdout_log"
- : > "$stderr_log"
- events+=("$event")
- stdout_logs+=("$stdout_log")
- stderr_logs+=("$stderr_log")
- cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \
- > "$stdout_log" 2> "$stderr_log"
- idx=$((idx+1))
- done
-
- cfree_parallel_wait_all || true
-
- idx=0
- while [ $idx -lt ${#events[@]} ]; do
- replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}"
- idx=$((idx+1))
- done
-}
+CFREE_BIN="${CFREE:-$ROOT/build/cfree}"
+PAR="${CFREE_ELF_PARALLEL:-1}"
# ----- tool detection ----------------------------------------------------
@@ -164,8 +64,6 @@ command -v llvm-readelf >/dev/null 2>&1 && have_llvm_readelf=1
command -v readelf >/dev/null 2>&1 && have_llvm_readelf=1
command -v python3 >/dev/null 2>&1 && have_python3=1
-READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || echo)"
-
# ----- locate cfree-roundtrip -------------------------------------------
# Built as a Make target (test/test.mk) so it picks up libcfree.a changes.
@@ -173,17 +71,7 @@ ROUNDTRIP_BIN="$BUILD_DIR/cfree-roundtrip"
roundtrip_ok=0
[ -x "$ROUNDTRIP_BIN" ] && roundtrip_ok=1
-# ----- header summary ----------------------------------------------------
-
-printf 'cfree ELF test harness\n'
-printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)"
-printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)"
-printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)"
-printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")"
-printf ' parallel jobs: %s\n' "$TEST_JOBS"
-printf '\n'
-
-# ----- Layer A: unit/*.c -------------------------------------------------
+# ----- Layer A toolchain wiring ------------------------------------------
UNIT_CC="${CC:-clang}"
UNIT_SYSROOT_FLAGS=()
@@ -200,53 +88,7 @@ if [ -n "${CFREE_ELF_UNIT_LDFLAGS:-}" ]; then
UNIT_EXTRA_LDFLAGS=(${CFREE_ELF_UNIT_LDFLAGS})
fi
-run_unit_case() {
- local _idx="$1" src="$2" event="$3"
- local name stem bin build_log out_log
- : "$_idx"
-
- name="unit/$(basename "$src" .c)"
- stem="$(basename "$src" .c)"
- bin="$BUILD_DIR/$stem"
- build_log="$BUILD_DIR/$stem.build.log"
- out_log="$BUILD_DIR/$stem.out"
-
- if [ ! -f "$LIB_AR" ]; then
- emit_event "$event" SKIP "$name" "build/libcfree.a missing"
- return 0
- fi
-
- if ! "$UNIT_CC" -std=c11 -Wall -Wextra -Werror \
- "${UNIT_EXTRA_CFLAGS[@]}" \
- "${UNIT_SYSROOT_FLAGS[@]}" \
- -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \
- "$src" "$LIB_AR" "${UNIT_EXTRA_LDFLAGS[@]}" -o "$bin" \
- 2> "$build_log"; then
- emit_event "$event" FAIL "$name (build failed; see $build_log)"
- return 0
- fi
-
- if "$bin" > "$out_log" 2>&1; then
- emit_event "$event" PASS "$name"
- else
- emit_event "$event" FAIL "$name"
- emit_event "$event" DUMP "$out_log"
- fi
- return 0
-}
-
-printf 'Layer A — unit tests\n'
-shopt -s nullglob
-unit_srcs=( "$TEST_DIR"/unit/*.c )
-if [ ${#unit_srcs[@]} -eq 0 ]; then
- printf ' (no unit tests yet)\n'
-else
- run_parallel_items "unit" run_unit_case "${unit_srcs[@]}"
-fi
-printf '\n'
-
-# ----- Layer B: cases/*.c ------------------------------------------------
-
+# ----- Layer B target wiring ---------------------------------------------
# Map (CFREE_TEST_ARCH, CFREE_TEST_OBJ) (defaults aa64+elf) to the clang
# `--target=` triple the Layer B golden objects are compiled against.
# cfree-roundtrip then detects the input's e_machine / Mach-O cputype and
@@ -273,158 +115,153 @@ esac
CUR_TUPLE="${CFREE_TEST_ARCH:-aa64}-${CFREE_TEST_OBJ:-elf}"
-run_oracle_case() {
- local _idx="$1" src="$2" event="$3"
- local name targets_file applicable tuple stem wd extra_cflags tok
- : "$_idx"
-
- name="cases/$(basename "$src" .c)"
-
- # Per-case applicability: a NN_name.targets file lists the
- # <arch>-<obj> tuples the case applies to (one per line, or
- # whitespace-separated). When the current tuple isn't listed
- # the case is silently filtered out — not a skip — because it
- # exercises target-specific features with no equivalent
- # elsewhere (e.g. AArch64 BTI/PAC notes have no RISC-V analogue;
- # ELF features have no Mach-O peer).
- targets_file="${src%.c}.targets"
- if [ -f "$targets_file" ]; then
- applicable=0
- for tuple in $(cat "$targets_file"); do
- [ "$tuple" = "$CUR_TUPLE" ] && applicable=1
- done
- if [ $applicable -eq 0 ]; then
- emit_event "$event" SKIP_NA "$name" "$CUR_TUPLE"
- return 0
- fi
+# ----- header summary ----------------------------------------------------
+
+printf 'cfree ELF test harness\n'
+printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)"
+printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)"
+printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)"
+printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")"
+printf ' target: %s (tuple %s)\n' "$CLANG_TARGET" "$CUR_TUPLE"
+printf '\n'
+
+# ----- Layer A lane: unit/*.c --------------------------------------------
+# Build a native binary against libcfree.a, run it, expect exit 0. All
+# artifacts (binary + logs) live under $CF_WORK for parallel safety.
+cf_lane_A() {
+ local name="unit/$CF_NAME"
+ if [ ! -f "$LIB_AR" ]; then
+ cf_skip "$name" "build/libcfree.a missing"; return
fi
- # Per-case skip reasons:
- if [ $roundtrip_ok -ne 1 ]; then emit_event "$event" SKIP "$name" "cfree-roundtrip not built"; return 0; fi
- if [ $have_clang -ne 1 ]; then emit_event "$event" SKIP "$name" "clang missing"; return 0; fi
- if [ $have_llvm_readelf -ne 1 ]; then emit_event "$event" SKIP "$name" "llvm-readelf missing"; return 0; fi
- if [ $have_python3 -ne 1 ]; then emit_event "$event" SKIP "$name" "python3 missing"; return 0; fi
-
- stem="$(basename "$src" .c)"
- wd="$BUILD_DIR/$stem"
- mkdir -p "$wd"
-
- # Per-case extra compiler flags: drop a NN_name.cflags file alongside
- # the .c to pass additional flags (e.g. -ffunction-sections, -x c++).
- extra_cflags=()
- if [ -f "${src%.c}.cflags" ]; then
- # Existing harness behavior tokenized this file on shell
- # whitespace; keep that convention.
- for tok in $(cat "${src%.c}.cflags"); do
+ local bin="$CF_WORK/$CF_BASE"
+ local build_log="$CF_WORK/build.log"
+ local out_log="$CF_WORK/out.log"
+
+ if ! "$UNIT_CC" -std=c11 -Wall -Wextra -Werror \
+ "${UNIT_EXTRA_CFLAGS[@]}" \
+ "${UNIT_SYSROOT_FLAGS[@]}" \
+ -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \
+ "$CF_SRC" "$LIB_AR" "${UNIT_EXTRA_LDFLAGS[@]}" -o "$bin" \
+ 2> "$build_log"; then
+ cf_fail "$name" "build failed; see $build_log"
+ return
+ fi
+
+ if "$bin" > "$out_log" 2>&1; then
+ cf_pass "$name"
+ else
+ cf_fail "$name"
+ sed 's/^/ | /' "$out_log"
+ fi
+}
+
+# ----- Layer B lane: cases/*.c -------------------------------------------
+# clang -c -> golden.o ; cfree-roundtrip -> rt.o ; normalized readelf diff.
+# Whole-case .targets applicability is handled by the engine (SKIP-NA) via
+# CF_TARGETS_EXT. Per-case .cflags are read here, CF_WORK-confined.
+cf_lane_B() {
+ local name="cases/$CF_NAME"
+
+ # Per-case skip reasons (missing tooling).
+ if [ $roundtrip_ok -ne 1 ]; then cf_skip "$name" "cfree-roundtrip not built"; return; fi
+ if [ $have_clang -ne 1 ]; then cf_skip "$name" "clang missing"; return; fi
+ if [ $have_llvm_readelf -ne 1 ]; then cf_skip "$name" "llvm-readelf missing"; return; fi
+ if [ $have_python3 -ne 1 ]; then cf_skip "$name" "python3 missing"; return; fi
+
+ # Per-case extra compiler flags: NN_name.cflags alongside the .c passes
+ # additional flags (e.g. -ffunction-sections, -x c++). Tokenized on shell
+ # whitespace, matching the original harness convention.
+ local extra_cflags=() cflags_file tok
+ cflags_file="${CF_SRC%.c}.cflags"
+ if [ -f "$cflags_file" ]; then
+ for tok in $(cat "$cflags_file"); do
[ -n "$tok" ] && extra_cflags+=("$tok")
done
fi
if ! clang --target="$CLANG_TARGET" -c -O0 "${extra_cflags[@]}" \
- "$src" -o "$wd/golden.o" 2> "$wd/clang.log"; then
- emit_event "$event" SKIP "$name" "clang -c failed (cross-compile not configured?)"
- return 0
+ "$CF_SRC" -o "$CF_WORK/golden.o" 2> "$CF_WORK/clang.log"; then
+ cf_skip "$name" "clang -c failed (cross-compile not configured?)"
+ return
fi
- if ! "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log"; then
- emit_event "$event" FAIL "$name (roundtrip failed)"
- emit_event "$event" DUMP "$wd/roundtrip.log"
- return 0
+ if ! "$ROUNDTRIP_BIN" "$CF_WORK/golden.o" "$CF_WORK/rt.o" 2> "$CF_WORK/roundtrip.log"; then
+ cf_fail "$name" "roundtrip failed"
+ sed 's/^/ | /' "$CF_WORK/roundtrip.log"
+ return
fi
# Structural diff: readelf normalized.
- python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true
- python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true
- if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then
- emit_event "$event" FAIL "$name (readelf diff)"
- emit_event "$event" DUMP "$wd/readelf.diff" 40
- return 0
+ python3 "$NORMALIZE" readelf "$CF_WORK/golden.o" > "$CF_WORK/golden.readelf" 2> /dev/null || true
+ python3 "$NORMALIZE" readelf "$CF_WORK/rt.o" > "$CF_WORK/rt.readelf" 2> /dev/null || true
+ if ! diff -u "$CF_WORK/golden.readelf" "$CF_WORK/rt.readelf" > "$CF_WORK/readelf.diff"; then
+ cf_fail "$name" "readelf diff"
+ head -n 40 "$CF_WORK/readelf.diff" | sed 's/^/ | /'
+ return
fi
- emit_event "$event" PASS "$name"
- return 0
+ cf_pass "$name"
}
-printf 'Layer B — clang-oracle cases\n'
-case_srcs=( "$TEST_DIR"/cases/*.c )
-if [ ${#case_srcs[@]} -eq 0 ]; then
- printf ' (no cases yet)\n'
-else
- run_parallel_items "cases" run_oracle_case "${case_srcs[@]}"
-fi
-printf '\n'
-
-# ----- Layer C: bad/*.elf ------------------------------------------------
-
-run_bad_case() {
- local _idx="$1" blob="$2" event="$3"
- local name stem wd rc expect_file expect
- : "$_idx"
-
- name="bad/$(basename "$blob" .elf)"
+# ----- Layer C lane: bad/*.elf -------------------------------------------
+# Expect cfree-roundtrip to fail (nonzero, non-signal) and emit a substring
+# from <name>.expect on stderr.
+cf_lane_C() {
+ local name="bad/$CF_NAME"
if [ $roundtrip_ok -ne 1 ]; then
- emit_event "$event" SKIP "$name" "cfree-roundtrip not built"
- return 0
+ cf_skip "$name" "cfree-roundtrip not built"; return
fi
- stem="$(basename "$blob" .elf)"
- wd="$BUILD_DIR/bad_$stem"
- mkdir -p "$wd"
-
- rc=0
- "$ROUNDTRIP_BIN" "$blob" "$wd/out.o" > "$wd/stdout.log" 2> "$wd/stderr.log" || rc=$?
+ local rc=0
+ "$ROUNDTRIP_BIN" "$CF_SRC" "$CF_WORK/out.o" \
+ > "$CF_WORK/stdout.log" 2> "$CF_WORK/stderr.log" || rc=$?
if [ $rc -eq 0 ]; then
- emit_event "$event" FAIL "$name (expected nonzero exit, got 0)"
- return 0
+ cf_fail "$name" "expected nonzero exit, got 0"; return
fi
if [ $rc -ge 128 ]; then
- emit_event "$event" FAIL "$name (terminated by signal $((rc - 128)) — segfault?)"
- return 0
+ cf_fail "$name" "terminated by signal $((rc - 128)) — segfault?"; return
fi
- expect_file="${blob%.elf}.expect"
+ local expect_file expect
+ expect_file="${CF_SRC%.elf}.expect"
if [ ! -f "$expect_file" ]; then
- emit_event "$event" FAIL "$name (missing $expect_file)"
- return 0
+ cf_fail "$name" "missing $expect_file"; return
fi
expect="$(cat "$expect_file")"
- if grep -qF -- "$expect" "$wd/stderr.log"; then
- emit_event "$event" PASS "$name"
+ if grep -qF -- "$expect" "$CF_WORK/stderr.log"; then
+ cf_pass "$name"
else
- emit_event "$event" FAIL "$name (stderr did not contain: $expect)"
- emit_event "$event" DUMP "$wd/stderr.log"
+ cf_fail "$name" "stderr did not contain: $expect"
+ sed 's/^/ | /' "$CF_WORK/stderr.log"
fi
- return 0
}
-printf 'Layer C — negative read_elf inputs\n'
-bad_files=( "$TEST_DIR"/bad/*.elf )
-if [ ${#bad_files[@]} -eq 0 ]; then
- printf ' (no bad inputs yet)\n'
-else
- run_parallel_items "bad" run_bad_case "${bad_files[@]}"
-fi
+# ----- drive the three layers --------------------------------------------
+
+printf 'Layer A — unit tests\n'
+CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/unit" \
+ CF_CORPUS_GLOBS="$TEST_DIR/unit/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/unit" \
+ CF_LANES="A" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
printf '\n'
-# ----- summary -----------------------------------------------------------
+printf 'Layer B — clang-oracle cases\n'
+CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/cases" \
+ CF_CORPUS_GLOBS="$TEST_DIR/cases/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/cases" \
+ CF_LANES="B" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT=".targets" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+printf '\n'
-if [ $FAIL -gt 0 ]; then
- printf 'Failures:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' - %s\n' "$n"; done
- printf '\n'
-fi
-if [ $SKIP -gt 0 ] && [ "${CFREE_TEST_ALLOW_SKIP:-}" != "1" ]; then
- printf 'Skips treated as failures (set CFREE_TEST_ALLOW_SKIP=1 to allow):\n'
- for n in "${SKIP_NAMES[@]}"; do printf ' - %s\n' "$n"; done
- printf '\n'
-fi
+printf 'Layer C — negative read_elf inputs\n'
+CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/bad" \
+ CF_CORPUS_GLOBS="$TEST_DIR/bad/*.elf" CF_CORPUS_EXT=elf CF_SIDECAR_DIR="$TEST_DIR/bad" \
+ CF_LANES="C" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+printf '\n'
-printf 'Summary: %s passed, %s failed, %s skipped\n' \
- "$(color_grn $PASS)" \
- "$([ $FAIL -eq 0 ] && echo $FAIL || color_red $FAIL)" \
- "$([ $SKIP -eq 0 ] && echo $SKIP || color_yel $SKIP)"
+# ----- summary -----------------------------------------------------------
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "${CFREE_TEST_ALLOW_SKIP:-}" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-elf
+cf_exit
diff --git a/test/emu/rv64_interp_smoke_test.c b/test/emu/rv64_interp_smoke_test.c
@@ -25,6 +25,9 @@
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
+
+#include "lib/cfree_unit.h"
+
#if defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_vm.h>
@@ -42,35 +45,10 @@
/* Not in the public header; provided by the linked library objects. */
EmuCPUState* emu_internal_cpu(CfreeEmu*);
-/* ---- Host heap / diag glue ---- */
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-static CfreeContext g_ctx;
+/* One shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites below are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
/* ---- Dual-mapped (W^X) exec memory for the per-block JIT image (still built
* in INTERP mode to resolve helper externs). Same shape as rv64_smoke_test. ---- */
@@ -213,45 +191,32 @@ static CfreeExecMem g_execmem = {
16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL,
};
-static int g_fail;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
-
static CfreeCompiler* new_host_compiler(void) {
+ CfreeArchKind arch;
+ CfreeOSKind os;
+ CfreeObjFmt obj;
CfreeTarget t;
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof t);
#if defined(__x86_64__) || defined(_M_X64)
- t.arch = CFREE_ARCH_X86_64;
+ arch = CFREE_ARCH_X86_64;
#elif defined(__aarch64__) || defined(_M_ARM64)
- t.arch = CFREE_ARCH_ARM_64;
+ arch = CFREE_ARCH_ARM_64;
#elif defined(__riscv) && __riscv_xlen == 64
- t.arch = CFREE_ARCH_RV64;
+ arch = CFREE_ARCH_RV64;
#else
return NULL;
#endif
#if defined(__APPLE__)
- t.os = CFREE_OS_MACOS;
- t.obj = CFREE_OBJ_MACHO;
+ os = CFREE_OS_MACOS;
+ obj = CFREE_OBJ_MACHO;
#elif defined(__linux__)
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
+ os = CFREE_OS_LINUX;
+ obj = CFREE_OBJ_ELF;
#else
return NULL;
#endif
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof g_ctx);
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ t = cfree_unit_target(arch, os, obj);
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "host compiler_new failed\n");
exit(2);
}
@@ -591,6 +556,8 @@ static void check_differential(const char* name, unsigned char* elf,
int main(void) {
size_t len = 0;
+ cfree_unit_init(&g_u);
+
/* (1) SD/LD/ecall: exercises the guest-memory helper path. */
check_differential("sd-ld-ecall", build_sd_ld_elf(&len, 99u), len, 99);
@@ -598,7 +565,7 @@ int main(void) {
* invalidation (a stale interp lookup would make INTERP diverge from JIT). */
check_differential("self-modifying-code", build_smc_elf(&len), len, 2);
- fprintf(stderr, "interp-emu-smoke: %d failure%s\n", g_fail,
- g_fail == 1 ? "" : "s");
- return g_fail ? 1 : 0;
+ fprintf(stderr, "interp-emu-smoke: %d failure%s\n", g_u.fails,
+ g_u.fails == 1 ? "" : "s");
+ return g_u.fails ? 1 : 0;
}
diff --git a/test/emu/rv64_smoke_test.c b/test/emu/rv64_smoke_test.c
@@ -48,37 +48,13 @@
* rv64_vm_unit_test.c, which links the library objects directly. */
#include "arch/rv64/isa.h"
#include "core/core.h"
+#include "lib/cfree_unit.h"
#include "obj/elf/elf.h"
-/* Host heap glue (same shape as test/api). */
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-static CfreeContext g_ctx;
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites below are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static int xm_to_posix(int p) {
int q = 0;
@@ -219,45 +195,32 @@ static CfreeExecMem g_execmem = {
16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL,
};
-static int g_fail;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
-
static CfreeCompiler* new_host_compiler(void) {
+ CfreeArchKind arch;
+ CfreeOSKind os;
+ CfreeObjFmt obj;
CfreeTarget t;
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof t);
#if defined(__x86_64__) || defined(_M_X64)
- t.arch = CFREE_ARCH_X86_64;
+ arch = CFREE_ARCH_X86_64;
#elif defined(__aarch64__) || defined(_M_ARM64)
- t.arch = CFREE_ARCH_ARM_64;
+ arch = CFREE_ARCH_ARM_64;
#elif defined(__riscv) && __riscv_xlen == 64
- t.arch = CFREE_ARCH_RV64;
+ arch = CFREE_ARCH_RV64;
#else
return NULL;
#endif
#if defined(__APPLE__)
- t.os = CFREE_OS_MACOS;
- t.obj = CFREE_OBJ_MACHO;
+ os = CFREE_OS_MACOS;
+ obj = CFREE_OBJ_MACHO;
#elif defined(__linux__)
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
+ os = CFREE_OS_LINUX;
+ obj = CFREE_OBJ_ELF;
#else
return NULL;
#endif
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof g_ctx);
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ t = cfree_unit_target(arch, os, obj);
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "host compiler_new failed\n");
exit(2);
}
@@ -1234,6 +1197,7 @@ static void signal_sigreturn_smoke(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
jit_vertical_smoke();
dynamic_import_tls_red();
host_import_bridge_smoke();
@@ -1242,8 +1206,8 @@ int main(void) {
signal_perms_red();
signal_load_fault_smoke();
signal_sigreturn_smoke();
- if (g_fail) {
- fprintf(stderr, "FAILED %d check(s)\n", g_fail);
+ if (g_u.fails) {
+ fprintf(stderr, "FAILED %d check(s)\n", g_u.fails);
return 1;
}
fprintf(stderr, "OK\n");
diff --git a/test/emu/rv64_vm_unit_test.c b/test/emu/rv64_vm_unit_test.c
@@ -19,61 +19,18 @@
#include "arch/rv64/isa.h"
#include "core/core.h"
#include "emu/emu.h"
+#include "lib/cfree_unit.h"
-/* ---- Host heap / diag glue ---- */
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-static CfreeContext g_ctx;
-
-static int g_fail;
-#define EXPECT(cond, ...) \
- do { \
- if (!(cond)) { \
- ++g_fail; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
static CfreeCompiler* new_compiler(void) {
- CfreeTarget t;
+ CfreeTarget t = cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX,
+ CFREE_OBJ_ELF);
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof t);
- t.arch = CFREE_ARCH_RV64;
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof g_ctx);
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "compiler_new failed\n");
exit(2);
}
@@ -81,33 +38,31 @@ static CfreeCompiler* new_compiler(void) {
}
static CfreeCompiler* new_host_compiler(void) {
+ CfreeArchKind arch;
+ CfreeOSKind os;
+ CfreeObjFmt obj;
CfreeTarget t;
CfreeCompiler* c = NULL;
- memset(&t, 0, sizeof t);
#if defined(__x86_64__) || defined(_M_X64)
- t.arch = CFREE_ARCH_X86_64;
+ arch = CFREE_ARCH_X86_64;
#elif defined(__aarch64__) || defined(_M_ARM64)
- t.arch = CFREE_ARCH_ARM_64;
+ arch = CFREE_ARCH_ARM_64;
#elif defined(__riscv) && __riscv_xlen == 64
- t.arch = CFREE_ARCH_RV64;
+ arch = CFREE_ARCH_RV64;
#else
return NULL;
#endif
#if defined(__APPLE__)
- t.os = CFREE_OS_MACOS;
- t.obj = CFREE_OBJ_MACHO;
+ os = CFREE_OS_MACOS;
+ obj = CFREE_OBJ_MACHO;
#elif defined(__linux__)
- t.os = CFREE_OS_LINUX;
- t.obj = CFREE_OBJ_ELF;
+ os = CFREE_OS_LINUX;
+ obj = CFREE_OBJ_ELF;
#else
return NULL;
#endif
- t.ptr_size = 8;
- t.ptr_align = 8;
- memset(&g_ctx, 0, sizeof g_ctx);
- g_ctx.heap = &g_heap;
- g_ctx.diag = &g_diag;
- if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) {
+ t = cfree_unit_target(arch, os, obj);
+ if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) {
fprintf(stderr, "host compiler_new failed\n");
exit(2);
}
@@ -328,13 +283,10 @@ static void linux_vm_syscall_smoke(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
decoder_smoke();
vm_unit_smoke();
linux_vm_syscall_smoke();
- if (g_fail) {
- fprintf(stderr, "FAILED %d check(s)\n", g_fail);
- return 1;
- }
- fprintf(stderr, "OK\n");
- return 0;
+ cfree_unit_summary(&g_u, "emu_rv64_unit_test");
+ return cfree_unit_status(&g_u);
}
diff --git a/test/interp/interp_smoke_test.c b/test/interp/interp_smoke_test.c
@@ -15,6 +15,7 @@
#include "cg/ir.h"
#include "interp/interp.h"
+#include "lib/cfree_unit.h"
#include "opt/opt.h"
#undef Operand
@@ -23,51 +24,13 @@
#undef CGCallDesc
#undef CGLocalStorage
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static int g_fails;
-static int g_checks;
-
-static void diag_sink(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_sink, NULL, 0, 0};
-
-#define EXPECT(cond, ...) \
- do { \
- ++g_checks; \
- if (!(cond)) { \
- ++g_fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. cfree_unit_init
+ * runs once in main (ctx.now is then set to -1 to match the original). */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
typedef struct TestCtx {
- CfreeContext ctx;
Compiler* c;
CfreeCgTypeId i32;
CfreeCgTypeId i64;
@@ -77,16 +40,9 @@ static void tc_init(TestCtx* tc) {
CfreeTarget target;
CfreeCgBuiltinTypes b;
memset(tc, 0, sizeof *tc);
- tc->ctx.heap = &g_heap;
- tc->ctx.diag = &g_diag;
- tc->ctx.now = -1;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_MACOS;
- target.obj = CFREE_OBJ_MACHO;
- target.ptr_size = 8;
- target.ptr_align = 8;
- if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != CFREE_OK ||
+ target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO);
+ if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) !=
+ CFREE_OK ||
!tc->c) {
fprintf(stderr, "fatal: compiler allocation failed\n");
abort();
@@ -239,12 +195,14 @@ static void interp_runs_branch(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
interp_runs_arithmetic();
interp_runs_branch();
- if (g_fails) {
- fprintf(stderr, "interp-smoke: %d/%d failed\n", g_fails, g_checks);
+ if (g_u.fails) {
+ fprintf(stderr, "interp-smoke: %d/%d failed\n", g_u.fails, g_u.checks);
return 1;
}
- printf("interp-smoke: %d checks, 0 failures\n", g_checks);
+ printf("interp-smoke: %d checks, 0 failures\n", g_u.checks);
return 0;
}
diff --git a/test/lib/cf_corpus.sh b/test/lib/cf_corpus.sh
@@ -0,0 +1,282 @@
+# test/lib/cf_corpus.sh — the unified corpus test harness engine.
+#
+# ONE harness for the lane-matrix corpus runners (parse/toy/asm/link/elf/wasm).
+# A runner declares its corpus + lanes and calls cf_corpus_run; the engine owns
+# discovery, the {case}x{opt}x{tuple} matrix, per-case workdirs, serial-or-
+# parallel dispatch, event replay, deferred cross-arch exec, and per-lane
+# timing. Reporting goes through cfree_sh_report.sh's mode-transparent verbs.
+#
+# Requires bash (indexed arrays). Sources, relative to $CF_LIB_DIR:
+# cfree_sh_report.sh (cf_pass/cf_fail/cf_skip/cf_skip_na/cf_time/summary)
+# cf_skip.sh (cf_skip_sidecar/cf_skip_diag/cf_tuple_applicable)
+# parallel.sh (cfree_parallel_jobs/run/wait_all)
+# exec_target.sh (exec_target_queue/flush/supported) [sourced by the runner if it uses path E]
+#
+# ============================================================================
+# RUNNER CONTRACT — set these before calling cf_corpus_run:
+# CFREE binary under test
+# CF_LABEL summary label (e.g. "toy")
+# CF_BUILD_DIR scratch root; per-item workdirs live under it
+# CF_CORPUS_GLOBS space-separated globs of case source files
+# CF_CORPUS_EXT extension stripped for the basename (e.g. "toy","c","s")
+# CF_SIDECAR_DIR dir holding sidecars (default: dir of the first glob)
+# CF_LANES active lane ids, space-separated (runner derives from CFREE_TEST_PATHS)
+# CF_OPT_LEVELS opt levels to expand (default "$CFREE_OPT_LEVELS" or "0 1");
+# set to "" for corpora with no opt axis (e.g. elf layers)
+# CF_TUPLES target tuples "<arch>-<obj>" (default "$CF_DEFAULT_TUPLE")
+# CF_OPT0ONLY lanes that run only at opt 0 (space list; e.g. "C W")
+# CF_PARALLELIZABLE 1 to allow parallel dispatch (default 1); 0 forces serial
+# CF_EXPECTED_EXT sidecar suffix for the expected exit code (default ".expected")
+# CF_TARGETS_EXT sidecar suffix for whole-case tuple applicability (default ".targets"; empty disables)
+# cf_lane_<ID>() per-lane hook (see below)
+# CF_READ_CASE optional fn, called per item after basics are set, to read
+# bespoke markers (may override CF_EXPECTED, set CF_SKIP_CASE/CF_SKIP_NA_CASE)
+# CF_FLUSH_VERIFY optional fn, called per queued-E item after flush: args
+# (label, payload, rc); return 0 to keep pass, 1 to fail
+#
+# Per-item vars the engine sets before each cf_lane_<ID> call:
+# CF_BASE CF_SRC CF_WORK CF_OPT CF_LANE CF_ARCH CF_OBJ CF_TUPLE CF_EXPECTED
+# CF_NAME (display label "base" or "base/Oopt") CF_SIDECAR_DIR
+# A hook ends by calling exactly one of: cf_pass/cf_fail/cf_skip/cf_skip_na, OR
+# cf_queue_e (deferred exec, resolved at flush). Hooks must write only under
+# CF_WORK and record only via these verbs — that is what makes a runner
+# parallel-safe by construction (flip CF_PARALLELIZABLE with no other change).
+# ============================================================================
+
+CF_LIB_DIR="${CF_LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
+. "$CF_LIB_DIR/cfree_sh_report.sh"
+. "$CF_LIB_DIR/cf_skip.sh"
+. "$CF_LIB_DIR/parallel.sh"
+
+# Corpus runners treat an unexplained SKIP as a soft failure (gated by
+# CFREE_TEST_ALLOW_SKIP); the driver-scenario harnesses leave this 0.
+CF_SKIP_IS_FAILURE=1
+
+# Millisecond clock for per-lane timing (python3 if present; else second res).
+cf_now_ms() {
+ if command -v python3 >/dev/null 2>&1; then
+ python3 -c 'import time;print(int(time.time()*1000))'
+ else
+ echo $(( $(date +%s) * 1000 ))
+ fi
+}
+
+# Deferred-exec bookkeeping (populated ONLY during serial execution / replay,
+# never inside a worker — workers merely emit QUEUE_E events).
+CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=()
+
+# cf_queue_e LABEL EXE OUT ERR RC_FILE EXPECTED EXEC_TAG [PAYLOAD]
+# Record a case whose execution is deferred to a batched exec_target flush.
+# In a worker: emit a QUEUE_E event (the parent does the actual queueing).
+# In serial/replay: enqueue with exec_target and remember it for the flush.
+cf_queue_e() {
+ if [ -n "${CF_EV:-}" ]; then
+ printf 'QUEUE_E\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+ "$1" "$2" "$3" "$4" "$5" "$6" "$7" "${8:-}" >> "$CF_EV"
+ return
+ fi
+ exec_target_queue "$7" "$1" "$2" "$3" "$4" "$5"
+ CFQ_LABELS+=("$1"); CFQ_RCS+=("$5"); CFQ_EXPS+=("$6"); CFQ_PAYLOADS+=("${8:-}")
+}
+
+# ---- discovery -------------------------------------------------------------
+cf_corpus_discover() {
+ CF_CASES=()
+ local g f base
+ shopt -s nullglob
+ for g in $CF_CORPUS_GLOBS; do
+ for f in $g; do
+ base=$(basename "$f")
+ [ -n "$CF_CORPUS_EXT" ] && base="${base%.$CF_CORPUS_EXT}"
+ if [ -n "${CFREE_TEST_FILTER:-}" ]; then
+ case "$base" in *"$CFREE_TEST_FILTER"*) ;; *) continue ;; esac
+ fi
+ CF_CASES+=("$f")
+ done
+ done
+}
+
+# ---- per-item driver (runs every active lane for one case,opt,tuple) -------
+# Item encoding: "src|base|opt|tuple" (src/base never contain '|').
+cf_corpus_item() {
+ local item="$1"
+ CF_SRC="${item%%|*}"; item="${item#*|}"
+ CF_BASE="${item%%|*}"; item="${item#*|}"
+ CF_OPT="${item%%|*}"; CF_TUPLE="${item#*|}"
+ CF_ARCH="${CF_TUPLE%-*}"; CF_OBJ="${CF_TUPLE#*-}"
+
+ if [ "$CF_OPT" = "-" ]; then
+ CF_NAME="$CF_BASE"; CF_WORK="$CF_BUILD_DIR/$CF_BASE"
+ else
+ CF_NAME="$CF_BASE/O$CF_OPT"; CF_WORK="$CF_BUILD_DIR/$CF_BASE/O$CF_OPT"
+ fi
+ rm -rf "$CF_WORK"; mkdir -p "$CF_WORK"
+
+ CF_EXPECTED=0
+ if [ -f "$CF_SIDECAR_DIR/$CF_BASE$CF_EXPECTED_EXT" ]; then
+ CF_EXPECTED=$(tr -d '[:space:]' < "$CF_SIDECAR_DIR/$CF_BASE$CF_EXPECTED_EXT")
+ fi
+
+ # Hook for bespoke per-case marker reading (link's expected/jit_only/targets,
+ # etc.). May set CF_EXPECTED, CF_SKIP_CASE (reason), CF_SKIP_NA_CASE (1).
+ CF_SKIP_CASE=; CF_SKIP_NA_CASE=
+ if [ -n "${CF_READ_CASE:-}" ] && command -v "$CF_READ_CASE" >/dev/null 2>&1; then
+ "$CF_READ_CASE"
+ fi
+ if [ -n "$CF_SKIP_CASE" ]; then cf_skip "$CF_NAME" "$CF_SKIP_CASE"; return; fi
+ if [ -n "$CF_SKIP_NA_CASE" ]; then cf_skip_na "$CF_NAME"; return; fi
+
+ # Whole-case sidecar skip (one event for the item, not per lane).
+ local reason
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "$CF_ARCH"); then
+ cf_skip "$CF_NAME" "$reason"; return
+ fi
+ # Whole-case tuple applicability (SKIP-NA, uncounted).
+ if [ -n "$CF_TARGETS_EXT" ] &&
+ ! cf_tuple_applicable "$CF_TUPLE" "$CF_SIDECAR_DIR/$CF_BASE$CF_TARGETS_EXT"; then
+ cf_skip_na "$CF_NAME"; return
+ fi
+
+ local lane
+ for lane in $CF_LANES; do
+ CF_LANE="$lane"
+ # opt-0-only lanes (C/W: backend ignores -O) run once, at opt 0.
+ case " $CF_OPT0ONLY " in
+ *" $lane "*) [ "$CF_OPT" = "0" ] || [ "$CF_OPT" = "-" ] || continue ;;
+ esac
+ # per-lane sidecar skip (.<lane>.skip)
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "$lane"); then
+ cf_skip "$CF_NAME/$lane" "$reason"; continue
+ fi
+ "cf_lane_$lane"
+ done
+}
+
+# ---- worker (parallel mode): isolate output to the event file --------------
+# Runs in a background subshell; the ONLY parent-visible channel is the event
+# file (+ captured stdout/stderr logs). Ends with a DONE sentinel so the parent
+# can tell "produced no records" (fine) from "crashed mid-case" (fail).
+cf_corpus_worker() {
+ CF_EV="$3"
+ cf_corpus_item "$2" > "$4" 2> "$5"
+ printf 'DONE\n' >> "$CF_EV"
+}
+
+# ---- replay one worker's event file through the counting verbs -------------
+cf_corpus_replay() {
+ local ev="$1" out="$2" err="$3" idx="$4"
+ if [ ! -s "$ev" ] || [ "$(tail -n1 "$ev" 2>/dev/null)" != "DONE" ]; then
+ cf_fail "internal/worker-$idx" "no DONE sentinel (worker crashed?)"
+ else
+ local kind a b c d e f g h
+ while IFS="$(printf '\t')" read -r kind a b c d e f g h; do
+ case "$kind" in
+ PASS) cf_pass "$a" ;;
+ FAIL) cf_fail "$a" "$b" ;;
+ SKIP) cf_skip "$a" "$b" ;;
+ SKIP_NA) cf_skip_na "$a" "$b" ;;
+ XFAIL) cf_xfail "$a" "$b" ;;
+ XPASS) cf_xpass "$a" ;;
+ TIME) cf_time "$a" "$b" ;;
+ QUEUE_E) cf_queue_e "$a" "$b" "$c" "$d" "$e" "$f" "$g" "$h" ;;
+ DONE) ;;
+ esac
+ done < "$ev"
+ fi
+ # Surface hook diagnostics (diffs/stderr) deterministically, in index order.
+ [ -s "$out" ] && cat "$out"
+ [ -s "$err" ] && cat "$err" >&2
+ return 0
+}
+
+# ---- deferred-exec flush + per-case verification ---------------------------
+cf_corpus_flush_e() {
+ [ "${#CFQ_LABELS[@]}" -eq 0 ] && return 0
+ exec_target_flush
+ local i rc ok
+ for i in "${!CFQ_LABELS[@]}"; do
+ rc=127; [ -f "${CFQ_RCS[$i]}" ] && rc=$(cat "${CFQ_RCS[$i]}")
+ rc=$((rc & 255))
+ ok=0; [ "$rc" -eq "$(( ${CFQ_EXPS[$i]} & 255 ))" ] && ok=1
+ if [ "$ok" -eq 1 ] && [ -n "${CF_FLUSH_VERIFY:-}" ] &&
+ command -v "$CF_FLUSH_VERIFY" >/dev/null 2>&1; then
+ "$CF_FLUSH_VERIFY" "${CFQ_LABELS[$i]}" "${CFQ_PAYLOADS[$i]}" "$rc" || ok=0
+ fi
+ if [ "$ok" -eq 1 ]; then cf_pass "${CFQ_LABELS[$i]}"
+ else cf_fail "${CFQ_LABELS[$i]}" "expected ${CFQ_EXPS[$i]} got $rc"; fi
+ done
+ CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=()
+}
+
+# ---- the entrypoint --------------------------------------------------------
+# Discovers + expands the matrix + dispatches (serial or parallel) + flushes
+# deferred exec. Accumulates into the shared counters (so a runner may call it
+# multiple times — e.g. elf's A/B/C layers — then summarize once). Does NOT
+# print the summary or exit; the runner calls cf_summary "$CF_LABEL"; cf_exit.
+cf_corpus_run() {
+ : "${CF_OPT_LEVELS=${CFREE_OPT_LEVELS:-0 1}}"
+ : "${CF_TUPLES:=${CF_DEFAULT_TUPLE:-aarch64-elf}}"
+ : "${CF_OPT0ONLY:=}"
+ : "${CF_PARALLELIZABLE:=1}"
+ : "${CF_EXPECTED_EXT:=.expected}"
+ : "${CF_TARGETS_EXT:=.targets}"
+ if [ -z "${CF_SIDECAR_DIR:-}" ]; then
+ set -- $CF_CORPUS_GLOBS; CF_SIDECAR_DIR=$(dirname "$1")
+ fi
+
+ cf_corpus_discover
+ if [ "${#CF_CASES[@]}" -eq 0 ]; then
+ echo "$CF_LABEL: no cases under $CF_CORPUS_GLOBS" >&2
+ exit 2
+ fi
+
+ # Build the flat work-item list = case x opt x tuple.
+ local opts="$CF_OPT_LEVELS"; [ -z "$opts" ] && opts="-"
+ local f base o t
+ CF_ITEMS=()
+ for f in "${CF_CASES[@]}"; do
+ base=$(basename "$f"); [ -n "$CF_CORPUS_EXT" ] && base="${base%.$CF_CORPUS_EXT}"
+ for o in $opts; do
+ for t in $CF_TUPLES; do
+ CF_ITEMS+=("$f|$base|$o|$t")
+ done
+ done
+ done
+
+ local jobs; jobs="$(cfree_parallel_jobs)" || jobs=1
+ if [ "$CF_PARALLELIZABLE" = "1" ] && [ "$jobs" -gt 1 ] && [ "${#CF_ITEMS[@]}" -gt 4 ]; then
+ cf_corpus_dispatch_parallel "$jobs"
+ else
+ local idx=0
+ for item in "${CF_ITEMS[@]}"; do
+ CF_EV=; cf_corpus_item "$item"
+ idx=$((idx + 1))
+ done
+ fi
+
+ cf_corpus_flush_e
+}
+
+cf_corpus_dispatch_parallel() {
+ local jobs="$1"
+ local pdir="$CF_BUILD_DIR/.parallel.$$"
+ rm -rf "$pdir"; mkdir -p "$pdir"
+ local evs=() outs=() errs=() idx=0 ev out err
+ for item in "${CF_ITEMS[@]}"; do
+ ev="$pdir/$idx.events"; out="$pdir/$idx.out"; err="$pdir/$idx.err"
+ : > "$ev"
+ evs+=("$ev"); outs+=("$out"); errs+=("$err")
+ cfree_parallel_run "$jobs" cf_corpus_worker "$idx" "$item" "$ev" "$out" "$err"
+ idx=$((idx + 1))
+ done
+ cfree_parallel_wait_all || true
+ # Serial replay in strict index order -> deterministic counts + output +
+ # exec_target queueing (which happens here, never in a worker).
+ CF_EV=
+ local i=0
+ while [ "$i" -lt "${#evs[@]}" ]; do
+ cf_corpus_replay "${evs[$i]}" "${outs[$i]}" "${errs[$i]}" "$i"
+ i=$((i + 1))
+ done
+ rm -rf "$pdir"
+}
diff --git a/test/lib/cf_corpus_selftest.sh b/test/lib/cf_corpus_selftest.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+# test/lib/cf_corpus_selftest.sh — hermetic proof of the cf_corpus.sh engine.
+#
+# No cfree binary, no podman/qemu: stubs exec_target_queue/flush and uses fake
+# lanes over a throwaway corpus. Asserts the properties that make the engine
+# safe to build runners on:
+# 1. serial and parallel runs produce IDENTICAL counts + failing-name order
+# + identical output (modulo timing) — determinism;
+# 2. SKIP-NA is uncounted and never gates;
+# 3. deferred-exec (cf_queue_e) flushes correctly and detects a wrong rc;
+# 4. exec_target_queue is called exactly once per E item DURING THE PARENT
+# (serial replay), never in a worker — if a worker queued, the parent
+# count would be 0 (background-subshell mutations are lost). This is the
+# invariant the whole parallel model rests on.
+#
+# Run: bash test/lib/cf_corpus_selftest.sh (exit 0 = ok)
+
+set -u
+SELF_DIR=$(cd "$(dirname "$0")" && pwd)
+export CF_LIB_DIR="$SELF_DIR"
+. "$SELF_DIR/cf_corpus.sh"
+
+WORK=$(mktemp -d "${TMPDIR:-/tmp}/cf-corpus-selftest.XXXXXX")
+trap 'rm -rf "$WORK"' EXIT
+CORPUS="$WORK/corpus"; mkdir -p "$CORPUS"
+i=1; while [ "$i" -le 12 ]; do printf 'x' > "$CORPUS/$(printf 'case%02d' "$i").t"; i=$((i+1)); done
+# case05 is not-applicable to the test tuple.
+printf 'other-z\n' > "$CORPUS/case05.targets"
+
+# ---- exec_target stubs (count parent-side queue calls) ---------------------
+ET_QUEUE_CALLS=0
+_et_pairs=
+exec_target_queue() { # TAG NAME EXE OUT ERR RC
+ ET_QUEUE_CALLS=$((ET_QUEUE_CALLS + 1))
+ _et_pairs="$_et_pairs $3:$6" # exe:rcfile (exe file content = rc to emit)
+}
+exec_target_flush() {
+ local pair exe rcf
+ for pair in $_et_pairs; do exe="${pair%%:*}"; rcf="${pair#*:}"; cat "$exe" > "$rcf"; done
+ _et_pairs=
+}
+
+# ---- fake lanes ------------------------------------------------------------
+cf_lane_P() { cf_pass "$CF_NAME/P"; cf_time P 1; }
+cf_lane_S() {
+ if [ "$CF_BASE" = "case03" ]; then cf_skip "$CF_NAME/S" "deliberate"; else cf_pass "$CF_NAME/S"; fi
+}
+cf_lane_E() {
+ local exe="$CF_WORK/run" rcf="$CF_WORK/rc"
+ if [ "$CF_BASE" = "case07" ]; then echo 9 > "$exe"; else echo 0 > "$exe"; fi
+ cf_queue_e "$CF_NAME/E" "$exe" "$CF_WORK/o" "$CF_WORK/e" "$rcf" "$CF_EXPECTED" "faketag"
+}
+
+# Run in the CURRENT shell (NOT $(...) — command substitution is a subshell
+# that would discard the counter globals); redirect output to a file instead.
+export CFREE_TEST_FILTER=
+run_corpus() { # $1 = CF_PARALLELIZABLE $2 = jobs $3 = outfile
+ cf_report_init
+ CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=()
+ ET_QUEUE_CALLS=0; _et_pairs=
+ CF_LABEL=selftest CF_BUILD_DIR="$WORK/b$1" CF_CORPUS_GLOBS="$CORPUS/*.t" \
+ CF_CORPUS_EXT=t CF_SIDECAR_DIR="$CORPUS" CF_LANES="P S E" CF_OPT_LEVELS="0" \
+ CF_TUPLES="aa64-elf" CF_TARGETS_EXT=.targets CF_PARALLELIZABLE="$1" \
+ CFREE_TEST_JOBS="$2" cf_corpus_run > "$3" 2>&1
+ cf_summary selftest >> "$3" 2>&1
+}
+
+NORM='s/[0-9]\{1,\}ms/Nms/g' # erase timing for output comparison
+
+run_corpus 0 1 "$WORK/ser.out"
+ser_out=$(cat "$WORK/ser.out")
+ser_counts="$CF_PASS/$CF_FAIL/$CF_SKIP/$CF_SKIP_NA"; ser_fails="$CF_FAILURES"; ser_q=$ET_QUEUE_CALLS
+
+run_corpus 1 4 "$WORK/par.out"
+par_out=$(cat "$WORK/par.out")
+par_counts="$CF_PASS/$CF_FAIL/$CF_SKIP/$CF_SKIP_NA"; par_fails="$CF_FAILURES"; par_q=$ET_QUEUE_CALLS
+
+fail=0
+check() { if [ "$2" = "$3" ]; then echo " ok $1 ($2)"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; }
+
+echo "cf_corpus selftest:"
+check "serial counts pass/fail/skip/na" "$ser_counts" "31/1/1/1"
+check "parallel counts pass/fail/skip/na" "$par_counts" "31/1/1/1"
+check "serial == parallel counts" "$ser_counts" "$par_counts"
+check "serial == parallel failing names" "$ser_fails" "$par_fails"
+check "failing name is case07/O0/E" "$(echo "$ser_fails" | tr -s ' ')" " case07/O0/E"
+check "serial queue calls (parent)" "$ser_q" "11"
+check "parallel queue calls (parent, not worker)" "$par_q" "11"
+# Determinism of full output (counts/order/summary), timing erased.
+if [ "$(printf '%s' "$ser_out" | sed "$NORM")" = "$(printf '%s' "$par_out" | sed "$NORM")" ]; then
+ echo " ok serial output == parallel output (timing-normalized)"
+else
+ echo " FAIL serial/parallel output differ:"; diff <(printf '%s' "$ser_out" | sed "$NORM") <(printf '%s' "$par_out" | sed "$NORM") | head -30; fail=1
+fi
+
+[ "$fail" -eq 0 ] && echo "cf_corpus selftest: OK" || echo "cf_corpus selftest: FAILED"
+exit $fail
diff --git a/test/lib/cf_differential.sh b/test/lib/cf_differential.sh
@@ -0,0 +1,49 @@
+# test/lib/cf_differential.sh — Type D (differential / agreement) helpers.
+#
+# Sourced AFTER cfree_sh_report.sh (a D harness sources both). Type D's oracle
+# is AGREEMENT, not a per-case fixed expectation:
+# - baseline mode: a freshly-generated, normalized report must equal a
+# checked-in baseline snapshot (which documents a known backlog).
+# CF_DIFF_UPDATE=1 regenerates the baseline instead of comparing.
+# - reference mode: two independent producers (e.g. cfree vs a third-party
+# tool) must agree byte-for-byte, with explicit equivalence-skips for
+# known-benign divergences (the caller cf_skip's those before comparing).
+# Both record exactly one verdict through cf_pass/cf_fail/cf_skip, so summary,
+# exit, color, and skip-gating match every other harness. A D harness still
+# calls cf_report_init at the top and cf_summary LABEL + cf_exit at the end.
+
+# cf_diff_baseline LABEL BASELINE ACTUAL
+# ACTUAL is a freshly produced + already-normalized report file; it must
+# equal the checked-in BASELINE. CF_DIFF_UPDATE=1 rewrites BASELINE from
+# ACTUAL and passes (the regen path). On mismatch, fail and show the unified
+# delta so the new asymmetries are visible.
+cf_diff_baseline() {
+ if [ "${CF_DIFF_UPDATE:-0}" = "1" ]; then
+ cp "$3" "$2"
+ cf_pass "$1 (baseline updated)"
+ return
+ fi
+ if [ ! -f "$2" ]; then
+ cf_fail "$1" "missing baseline $(basename "$2")"
+ return
+ fi
+ if diff -u "$2" "$3" >/dev/null 2>&1; then
+ cf_pass "$1"
+ else
+ cf_fail "$1" "report drifted from baseline (run with CF_DIFF_UPDATE=1 to refresh)"
+ diff -u "$2" "$3" || true
+ fi
+}
+
+# cf_diff_agree LABEL A B
+# A and B (two producers' outputs over the same input) must be byte-equal.
+# The caller is responsible for equivalence-skips: cf_skip the case BEFORE
+# calling this when a known-benign divergence applies, and skip the call.
+cf_diff_agree() {
+ if cmp -s "$2" "$3"; then
+ cf_pass "$1"
+ else
+ cf_fail "$1" "producers disagree"
+ cmp "$2" "$3" 2>&1 | head -3 || true
+ fi
+}
diff --git a/test/lib/cf_skip.sh b/test/lib/cf_skip.sh
@@ -0,0 +1,52 @@
+# test/lib/cf_skip.sh — unified skip detection for the corpus harness.
+#
+# Sourced (POSIX sh). Three independent skip sources the corpus runners all
+# reimplemented; each returns 0 and echoes a reason when it fires, else 1.
+
+# cf_skip_sidecar DIR BASE [ARCH] [LANE]
+# Looks, in priority order, for sidecar files next to a case:
+# DIR/BASE.skip — skip the whole case on every arch/lane
+# DIR/BASE.<ARCH>.skip — skip on this arch (when ARCH given)
+# DIR/BASE.<LANE>.skip — skip just this lane (when LANE given; covers
+# .cbackend.skip via LANE=cbackend, .wasm.skip
+# via LANE=wasm, toy's .link.skip via LANE=link)
+# Echoes the first line of the matching sidecar as the reason.
+cf_skip_sidecar() {
+ cf_sk_dir=$1; cf_sk_base=$2; cf_sk_arch=${3:-}; cf_sk_lane=${4:-}
+ for cf_sk_f in \
+ "$cf_sk_dir/$cf_sk_base.skip" \
+ ${cf_sk_arch:+"$cf_sk_dir/$cf_sk_base.$cf_sk_arch.skip"} \
+ ${cf_sk_lane:+"$cf_sk_dir/$cf_sk_base.$cf_sk_lane.skip"}; do
+ if [ -e "$cf_sk_f" ]; then
+ head -n1 "$cf_sk_f" 2>/dev/null
+ return 0
+ fi
+ done
+ return 1
+}
+
+# cf_skip_diag ERRFILE REGEX
+# Phased-rollout detector: if ERRFILE contains a line matching the extended
+# regex REGEX (a backend's "not yet implemented" diagnostic), echo the
+# matched text as the reason and return 0. Each backend passes its own regex
+# (e.g. 'interp: .*not supported', 'C target: .*not (implemented|yet supported)',
+# 'wasm.*not yet implemented'), keeping the panic format local to its lane.
+cf_skip_diag() {
+ [ -f "$1" ] || return 1
+ cf_sd_hit=$(grep -oE "$2" "$1" 2>/dev/null | head -n1 || true)
+ [ -n "$cf_sd_hit" ] || return 1
+ printf '%s' "$cf_sd_hit"
+ return 0
+}
+
+# cf_tuple_applicable TUPLE TARGETS_FILE
+# Returns 0 (applicable) if TARGETS_FILE is absent/empty, or lists TUPLE
+# (whitespace-separated <arch>-<obj> tuples). Returns 1 (not applicable ->
+# caller emits SKIP_NA) when the file exists and does NOT list TUPLE.
+cf_tuple_applicable() {
+ [ -s "$2" ] || return 0
+ for cf_ta_t in $(cat "$2"); do
+ [ "$cf_ta_t" = "$1" ] && return 0
+ done
+ return 1
+}
diff --git a/test/lib/cfree_sh_assert.sh b/test/lib/cfree_sh_assert.sh
@@ -0,0 +1,101 @@
+# test/lib/cfree_sh_assert.sh — Type-K mode-P procedural assert verbs + the
+# CAS object/tree helpers shared by cas/ and pkg/.
+#
+# Sourced AFTER cfree_sh_report.sh (cfree_sh_kit.sh does this). The verbs route
+# through cf_pass/cf_fail/cf_skip so counting, summary, and exit are unified —
+# a harness calls cf_report_init at the top and cf_summary/cf_exit at the end,
+# never its own counters.
+#
+# Contract: the caller provides a writable scratch dir $work; per-check
+# .out/.err/.diag files are written under it. Mode-P suites are SERIAL (they
+# share one $work and mutate fixtures), so these verbs do not emit CF_EV
+# events / participate in the corpus engine's parallel replay.
+
+# ---- result verbs (thin wrappers over the report layer) --------------------
+
+ok() { cf_pass "$1"; }
+
+# not_ok NAME [DIAGFILE] : record a failure; if DIAGFILE is given and non-empty,
+# show it indented after the FAIL line.
+not_ok() {
+ cf_fail "$1"
+ if [ "$#" -gt 1 ] && [ -s "$2" ]; then sed 's/^/ | /' "$2"; fi
+}
+
+skip_test() { cf_skip "$1" "${2:-}"; }
+
+# ---- command-result asserts ------------------------------------------------
+
+run_ok() {
+ name=$1; shift
+ if "$@" > "$work/$name.out" 2> "$work/$name.err"; then ok "$name"
+ else not_ok "$name" "$work/$name.err"; fi
+}
+
+run_fail() {
+ name=$1; shift
+ if "$@" > "$work/$name.out" 2> "$work/$name.err"; then
+ { echo "command unexpectedly succeeded"; sed 's/^/stdout: /' "$work/$name.out"; } > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ else ok "$name"; fi
+}
+
+# ---- content asserts -------------------------------------------------------
+
+contains() {
+ name=$1; file=$2; needle=$3
+ if grep -F "$needle" "$file" >/dev/null 2>&1; then ok "$name"
+ else
+ { printf 'missing text: %s\n' "$needle"; sed 's/^/file: /' "$file"; } > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ fi
+}
+
+same_file() {
+ name=$1; want=$2; got=$3
+ if cmp -s "$want" "$got"; then ok "$name"
+ else
+ { printf 'files differ:\n'; printf ' want: %s\n' "$want"; printf ' got: %s\n' "$got"; } > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ fi
+}
+
+is_executable() {
+ name=$1; file=$2
+ if [ -x "$file" ]; then ok "$name"
+ else echo "not executable: $file" > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi
+}
+
+assert_file_exists() {
+ name=$1; file=$2
+ if [ -f "$file" ]; then ok "$name"
+ else echo "missing file: $file" > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi
+}
+
+# ---- CAS object/tree helpers (shared by cas + pkg) -------------------------
+
+first_hex_id() {
+ sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p'
+}
+
+id_prefix() {
+ printf '%.2s' "$1"
+}
+
+cas_object_path() {
+ root=$1; kind=$2; id=$3
+ prefix=$(id_prefix "$id")
+ printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id"
+}
+
+tree_blob_for_path() {
+ tree_file=$1; want=$2
+ awk -v want="$want" '
+ $0 == "[file]" { in_file = 1; path = ""; blob = ""; next }
+ in_file && /^path = / { path = substr($0, 8); next }
+ in_file && /^blob = / {
+ blob = substr($0, 8);
+ if (path == want) { print blob; exit; }
+ }
+ ' "$tree_file"
+}
diff --git a/test/lib/cfree_sh_kit.sh b/test/lib/cfree_sh_kit.sh
@@ -0,0 +1,86 @@
+# test/lib/cfree_sh_kit.sh — the Type-K (scripted-shell) test kit.
+#
+# ONE source point for hand-written tool/driver tests. A K harness sources this
+# file and renders verdicts via ONE OF TWO oracle modes, both recording through
+# the single report layer (cfree_sh_report.sh):
+#
+# mode G (golden transcript): loop cases/<name>.sh and call
+# cf_scenario_case NAME SH EXPECTED — runs the case in a sandbox and
+# diffs stdout+stderr vs <name>.expected. (ar/strip/objcopy/strings/
+# objdump/dbg.) cf_scenario_case lives in cfree_sh_report.sh.
+# mode P (procedural asserts): set up $work and call ok / run_ok / run_fail /
+# contains / same_file / is_executable / assert_file_exists /
+# check_mode. (cas/pkg/driver/windows-smokes.) These come from
+# cfree_sh_assert.sh and route through cf_pass/cf_fail/cf_skip.
+#
+# Either way: cf_report_init at the top, cf_summary LABEL + cf_exit at the end.
+# No harness reimplements counters/summary. Mode-P suites are SERIAL by design
+# (cas/pkg/driver share one $work and mutate fixtures), so the assert verbs do
+# NOT participate in the corpus engine's parallel event-replay.
+
+# The caller sets CF_KIT_DIR to the test/lib directory before sourcing. (This
+# kit is sourced by /bin/sh harnesses, where BASH_SOURCE is unavailable, so we
+# do not self-resolve.)
+: "${CF_KIT_DIR:?cfree_sh_kit.sh: caller must set CF_KIT_DIR=<repo>/test/lib}"
+. "$CF_KIT_DIR/cfree_sh_report.sh" # counters + verbs + cf_summary/cf_exit
+. "$CF_KIT_DIR/cfree_sh_assert.sh" # mode-P procedural verbs (route through cf_*)
+
+# check_mode NAME FILE EXPECTED_OCTAL : assert FILE's permission bits, portably
+# (Darwin stat -f vs GNU stat -c). The stat invocation is the one host-specific
+# bit; the verdict goes through cf_pass/cf_fail.
+check_mode() {
+ cf_km_got=
+ if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then
+ cf_km_got=$(stat -f '%Lp' "$2" 2>/dev/null)
+ else
+ cf_km_got=$(stat -c '%a' "$2" 2>/dev/null)
+ fi
+ if [ "$cf_km_got" = "$3" ]; then cf_pass "$1"
+ else cf_fail "$1" "mode $cf_km_got want $3"; fi
+}
+
+# cf_scenario_case NAME SH EXPECTED [KEEP_DIR] : mode-G oracle. Run scenario
+# script SH in a sandbox under $CF_WORK and diff stdout+stderr against the
+# golden EXPECTED. Records exactly one pass/fail/skip; honors a leading "SKIP"
+# line when CF_SCENARIO_SKIP=1. (ar/strip/objcopy/strings/objdump; dbg extends
+# it with stdin/normalizer/xfail in its own migration.)
+cf_scenario_case() {
+ cf_sc_name=$1
+ cf_sc_sh=$2
+ cf_sc_exp=$3
+ cf_sc_keep=${4:-}
+ cf_sc_tag=$(printf '%s' "$cf_sc_name" | tr / -)
+ cf_sc_actual="$CF_WORK/$cf_sc_tag.actual"
+
+ if [ ! -e "$cf_sc_exp" ]; then
+ cf_fail "$cf_sc_name" "missing $(basename "$cf_sc_exp")"
+ return
+ fi
+
+ cf_sc_box="$CF_WORK/$cf_sc_tag"
+ mkdir -p "$cf_sc_box"
+ ( cd "$cf_sc_box" && sh "$cf_sc_sh" ) > "$cf_sc_actual" 2>&1
+ cf_sc_rc=$?
+
+ if [ "${CF_SCENARIO_SKIP:-0}" = "1" ] && [ "$cf_sc_rc" -eq 0 ] &&
+ head -n1 "$cf_sc_actual" 2>/dev/null | grep -q '^SKIP'; then
+ cf_skip "$cf_sc_name" "$(head -n1 "$cf_sc_actual" | sed 's/^SKIP[: ]*//')"
+ return
+ fi
+
+ if [ "$cf_sc_rc" -ne 0 ]; then
+ cf_fail "$cf_sc_name" "script exit=$cf_sc_rc"
+ diff -u "$cf_sc_exp" "$cf_sc_actual" || true
+ return
+ fi
+
+ if diff -u "$cf_sc_exp" "$cf_sc_actual" >/dev/null 2>&1; then
+ cf_pass "$cf_sc_name"
+ else
+ cf_fail "$cf_sc_name"
+ diff -u "$cf_sc_exp" "$cf_sc_actual" || true
+ if [ -n "$cf_sc_keep" ]; then
+ cp "$cf_sc_actual" "$cf_sc_keep/$cf_sc_name.actual" 2>/dev/null || true
+ fi
+ fi
+}
diff --git a/test/lib/cfree_sh_report.sh b/test/lib/cfree_sh_report.sh
@@ -0,0 +1,175 @@
+# test/lib/cfree_sh_report.sh — the single shared reporting layer for every
+# cfree shell test harness (driver-scenario AND corpus runners).
+#
+# Sourced. POSIX sh (works under /bin/sh and bash 3.2): counters are scalars,
+# name lists are space-joined strings, per-lane timing uses eval'd dynamic
+# vars — no bash arrays here, so the /bin/sh driver harnesses can source it.
+# (The corpus engine cf_corpus.sh adds the bash-only matrix/dispatch on top.)
+#
+# THE KEY SEAM — mode-transparent result verbs. cf_pass/cf_fail/cf_skip/
+# cf_skip_na/cf_time normally bump counters and print. But when CF_EV is set
+# (the harness ran the case in a background worker), they instead append a
+# tab-separated event to "$CF_EV"; the parent later replays those events in
+# index order through the SAME verbs (CF_EV unset during replay), so counts,
+# failing-name order, and printed output are deterministic regardless of
+# worker completion order. A lane hook calls cf_pass/... and never knows or
+# cares which mode it is in — that is what makes parallelism a pure flag flip.
+#
+# Color is emitted only to a TTY and honors NO_COLOR, so piped/CI output is
+# plain text.
+
+if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ _CF_RED=$(printf '\033[31m'); _CF_GRN=$(printf '\033[32m')
+ _CF_YEL=$(printf '\033[33m'); _CF_RST=$(printf '\033[0m')
+else
+ _CF_RED=; _CF_GRN=; _CF_YEL=; _CF_RST=
+fi
+
+# ---- state -----------------------------------------------------------------
+CF_PASS=0
+CF_FAIL=0
+CF_SKIP=0
+CF_SKIP_NA=0 # "not applicable" (e.g. arch tuple filtered) — tracked, never a failure
+CF_XFAIL=0 # expected failures (known-red cases that failed as expected)
+CF_XPASS=0 # xfail cases that unexpectedly passed (a real failure under CF_STRICT_XFAIL)
+CF_FAILURES=
+CF_SKIPS=
+CF_TIME_LANES= # space-joined lane ids that have accrued time
+# CF_EV — when set, verbs emit events here instead of counting
+# CF_SKIP_IS_FAILURE — corpus runners set =1 so SKIP gates the exit (unless CFREE_TEST_ALLOW_SKIP=1)
+
+cf_report_init() {
+ CF_PASS=0; CF_FAIL=0; CF_SKIP=0; CF_SKIP_NA=0; CF_XFAIL=0; CF_XPASS=0
+ CF_FAILURES=; CF_SKIPS=; CF_TIME_LANES=
+}
+
+# ---- mode-transparent result verbs ----------------------------------------
+
+cf_pass() {
+ if [ -n "${CF_EV:-}" ]; then printf 'PASS\t%s\n' "$1" >> "$CF_EV"; return; fi
+ CF_PASS=$((CF_PASS + 1))
+ printf ' %sPASS%s %s\n' "$_CF_GRN" "$_CF_RST" "$1"
+}
+
+# cf_fail NAME [DETAIL] : NAME is recorded in the failures list; DETAIL (single
+# line) is shown only on the printed line.
+cf_fail() {
+ if [ -n "${CF_EV:-}" ]; then printf 'FAIL\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi
+ CF_FAIL=$((CF_FAIL + 1))
+ CF_FAILURES="$CF_FAILURES $1"
+ if [ -n "${2:-}" ]; then
+ printf ' %sFAIL%s %s (%s)\n' "$_CF_RED" "$_CF_RST" "$1" "$2"
+ else
+ printf ' %sFAIL%s %s\n' "$_CF_RED" "$_CF_RST" "$1"
+ fi
+}
+
+cf_skip() {
+ if [ -n "${CF_EV:-}" ]; then printf 'SKIP\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi
+ CF_SKIP=$((CF_SKIP + 1))
+ CF_SKIPS="$CF_SKIPS $1"
+ if [ -n "${2:-}" ]; then
+ printf ' %sSKIP%s %s — %s\n' "$_CF_YEL" "$_CF_RST" "$1" "$2"
+ else
+ printf ' %sSKIP%s %s\n' "$_CF_YEL" "$_CF_RST" "$1"
+ fi
+}
+
+# cf_skip_na : "not applicable" — a case/lane that structurally does not apply
+# to the current target tuple. Counted separately and NEVER gates the exit.
+cf_skip_na() {
+ if [ -n "${CF_EV:-}" ]; then printf 'SKIP_NA\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi
+ CF_SKIP_NA=$((CF_SKIP_NA + 1))
+ # quiet by default; uncomment for verbose n/a tracing
+ [ -n "${CF_VERBOSE_NA:-}" ] && printf ' %sn/a%s %s — %s\n' "$_CF_YEL" "$_CF_RST" "$1" "${2:-}"
+ return 0
+}
+
+# cf_xfail NAME [REASON] : an expected failure (a known-red case that failed as
+# expected). Counted as xfail and does NOT gate the exit — UNLESS
+# CF_STRICT_XFAIL=1 (e.g. dbg's DBG_STRICT_XFAIL), under which even expected
+# failures are promoted to a hard failure (no known-red allowed in strict mode).
+cf_xfail() {
+ if [ -n "${CF_EV:-}" ]; then printf 'XFAIL\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi
+ if [ "${CF_STRICT_XFAIL:-0}" = "1" ]; then
+ cf_fail "$1" "expected failure not allowed under strict xfail${2:+ ($2)}"; return
+ fi
+ CF_XFAIL=$((CF_XFAIL + 1))
+ printf ' %sXFAIL%s %s%s\n' "$_CF_YEL" "$_CF_RST" "$1" "${2:+ — $2}"
+}
+
+# cf_xpass NAME : a case marked xfail that UNEXPECTEDLY passed — the xfail
+# marker is stale and should be removed. This is ALWAYS a failure (cf_exit
+# gates on CF_XPASS), matching the lit/DejaGnu convention and dbg's original
+# always-count-xpass-as-failure behavior. Tracked in its own bucket so the
+# summary distinguishes it from a plain FAIL.
+cf_xpass() {
+ if [ -n "${CF_EV:-}" ]; then printf 'XPASS\t%s\n' "$1" >> "$CF_EV"; return; fi
+ CF_XPASS=$((CF_XPASS + 1))
+ printf ' %sXPASS%s %s (unexpected pass — xfail marker is stale)\n' "$_CF_RED" "$_CF_RST" "$1"
+}
+
+# cf_time LANE MS : accumulate per-lane wall time for the summary's Time line.
+cf_time() {
+ if [ -n "${CF_EV:-}" ]; then printf 'TIME\t%s\t%s\n' "$1" "$2" >> "$CF_EV"; return; fi
+ case " $CF_TIME_LANES " in
+ *" $1 "*) ;;
+ *) CF_TIME_LANES="$CF_TIME_LANES $1" ;;
+ esac
+ eval "cf_t_$1=\$(( \${cf_t_$1:-0} + $2 ))"
+}
+
+# ---- summary + exit --------------------------------------------------------
+
+# cf_summary LABEL : unified summary — failing/skipped name lists, the
+# "P pass, F fail, S skip" line (+ " N n/a" when any), and a per-lane Time
+# line when timing was recorded.
+cf_summary() {
+ if [ -n "$CF_FAILURES" ]; then printf '\n%s: failures:%s\n' "$1" "$CF_FAILURES"; fi
+ if [ -n "$CF_SKIPS" ]; then printf '%s: skipped:%s\n' "$1" "$CF_SKIPS"; fi
+ cf_sm_extra=
+ [ "$CF_SKIP_NA" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_SKIP_NA n/a"
+ [ "$CF_XFAIL" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_XFAIL xfail"
+ [ "$CF_XPASS" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_XPASS xpass"
+ printf '\n%s: %d pass, %d fail, %d skip%s\n' "$1" "$CF_PASS" "$CF_FAIL" "$CF_SKIP" "$cf_sm_extra"
+ if [ -n "$CF_TIME_LANES" ]; then
+ cf_sm_t="$1: time"
+ for cf_sm_l in $CF_TIME_LANES; do
+ eval "cf_sm_v=\${cf_t_$cf_sm_l:-0}"
+ cf_sm_t="$cf_sm_t $cf_sm_l=${cf_sm_v}ms"
+ done
+ printf '%s\n' "$cf_sm_t"
+ fi
+}
+
+# cf_exit : exit 1 on any failure or any XPASS (stale xfail); also exit 1 on
+# skips when CF_SKIP_IS_FAILURE=1 unless CFREE_TEST_ALLOW_SKIP=1. SKIP_NA and
+# XFAIL never gate (XFAIL only via cf_xfail's strict promotion to FAIL).
+cf_exit() {
+ [ "$CF_FAIL" -gt 0 ] && exit 1
+ [ "${CF_XPASS:-0}" -gt 0 ] && exit 1
+ if [ "${CF_SKIP_IS_FAILURE:-0}" = "1" ] && [ "$CF_SKIP" -gt 0 ] &&
+ [ "${CFREE_TEST_ALLOW_SKIP:-0}" != "1" ]; then
+ exit 1
+ fi
+ exit 0
+}
+
+# ---- harness setup helpers -------------------------------------------------
+
+# cf_require_cfree LABEL : ensure $CFREE points at an executable, else exit 2.
+cf_require_cfree() {
+ if [ ! -x "${CFREE:-}" ]; then
+ echo "$1: cfree binary not found at ${CFREE:-<unset>}" >&2
+ exit 2
+ fi
+}
+
+# cf_workdir TOOL : mktemp -d under TMPDIR with the canonical name and install
+# an EXIT trap removing it. Echoes the path. NOTE: the EXIT trap is a singleton;
+# call once per harness.
+cf_workdir() {
+ cf_wd_dir=$(mktemp -d "${TMPDIR:-/tmp}/cfree-$1-test.XXXXXX")
+ trap 'rm -rf "$cf_wd_dir"' EXIT
+ printf '%s' "$cf_wd_dir"
+}
diff --git a/test/lib/cfree_unit.h b/test/lib/cfree_unit.h
@@ -0,0 +1,180 @@
+/* test/lib/cfree_unit.h — shared scaffolding for cfree C unit tests.
+ *
+ * The unit suite copy-pasted the same prologue into ~18 files: a
+ * malloc/realloc/free shim wrapping the host allocator into a CfreeHeap, a
+ * diagnostic sink that prints to stderr, an EXPECT/CHECK macro, a
+ * CfreeTarget + CfreeContext + cfree_compiler_new dance, and a pass/fail
+ * counter checked in main(). This header folds all of that into one
+ * stack-resident CfreeUnit context.
+ *
+ * Design constraints honored:
+ * - No global mutable state: every test threads a CfreeUnit it owns
+ * (matches the project's "everything hangs off a context struct" rule).
+ * - Public headers only (<cfree/core.h>), so the header compiles
+ * unchanged in BOTH linkage regimes: linking the public archive
+ * (LIB_AR, where ld -r has hidden internal symbols) and linking the raw
+ * objects (LIB_OBJS + -Isrc, internal symbols visible). It never names
+ * an internal symbol, so a test that additionally pulls internal
+ * headers (its own #include "abi/abi.h" etc.) is unaffected.
+ * - Every helper is `static inline` so a TU that uses only part of the
+ * API does not trip -Werror,-Wunused-function (verified: unused
+ * `static inline` is warning-clean; unused bare `static` is not).
+ *
+ * The build rule adds -Itest, so include as:
+ * #include "lib/cfree_unit.h"
+ */
+
+#ifndef CFREE_TEST_LIB_CFREE_UNIT_H
+#define CFREE_TEST_LIB_CFREE_UNIT_H
+
+#include <cfree/core.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* One context per test. Zero-initialize with cfree_unit_init. The diag sink
+ * captures the most recent message body into last_diag (before any
+ * suppression), which expected-panic tests strstr() against. */
+typedef struct CfreeUnit {
+ CfreeHeap heap;
+ CfreeDiagSink diag;
+ CfreeContext ctx;
+ char last_diag[256];
+ int suppress_fatal; /* when set, FATAL diagnostics are captured but not
+ * printed — used by tests that drive a deliberate
+ * panic and assert on last_diag. */
+ int checks; /* total CU_CHECK/CU_EXPECT evaluations */
+ int fails; /* subset that failed */
+} CfreeUnit;
+
+static inline void* cfree_unit_alloc(CfreeHeap* h, size_t n, size_t a) {
+ (void)h;
+ (void)a;
+ return n ? malloc(n) : NULL;
+}
+
+static inline void* cfree_unit_realloc(CfreeHeap* h, void* p, size_t old_n,
+ size_t n, size_t a) {
+ (void)h;
+ (void)old_n;
+ (void)a;
+ return realloc(p, n);
+}
+
+static inline void cfree_unit_free(CfreeHeap* h, void* p, size_t n) {
+ (void)h;
+ (void)n;
+ free(p);
+}
+
+static inline void cfree_unit_diag_emit(CfreeDiagSink* s, CfreeDiagKind k,
+ CfreeSrcLoc loc, const char* fmt,
+ va_list ap) {
+ static const char* const names[] = {"note", "warning", "error", "fatal"};
+ CfreeUnit* u = (CfreeUnit*)s->user;
+ va_list copy;
+ (void)loc;
+ /* Capture the message body first so it is available even when suppressed. */
+ if (u) {
+ va_copy(copy, ap);
+ vsnprintf(u->last_diag, sizeof u->last_diag, fmt, copy);
+ va_end(copy);
+ if (u->suppress_fatal && k == CFREE_DIAG_FATAL) return;
+ }
+ fprintf(stderr, "%s: ", (unsigned)k < 4u ? names[k] : "diag");
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+}
+
+/* Wire heap + diag + ctx onto u and zero the counters. ctx.now is set to 0
+ * (the value the memset-to-zero majority of the suite already used); a test
+ * that needs a specific clock may overwrite u->ctx.now afterward. */
+static inline void cfree_unit_init(CfreeUnit* u) {
+ memset(u, 0, sizeof *u);
+ u->heap.alloc = cfree_unit_alloc;
+ u->heap.realloc = cfree_unit_realloc;
+ u->heap.free = cfree_unit_free;
+ u->diag.emit = cfree_unit_diag_emit;
+ u->diag.user = u;
+ u->ctx.heap = &u->heap;
+ u->ctx.diag = &u->diag;
+ u->ctx.now = 0;
+}
+
+static inline CfreeTarget cfree_unit_target(CfreeArchKind arch, CfreeOSKind os,
+ CfreeObjFmt obj) {
+ CfreeTarget t;
+ memset(&t, 0, sizeof t);
+ t.arch = arch;
+ t.os = os;
+ t.obj = obj;
+ t.ptr_size = 8;
+ t.ptr_align = 8;
+ return t;
+}
+
+static inline CfreeStatus cfree_unit_compiler_new(CfreeUnit* u, CfreeTarget t,
+ CfreeCompiler** out) {
+ return cfree_compiler_new(t, &u->ctx, out);
+}
+
+/* Byte-substring search, for tests asserting emitted machine code contains a
+ * given encoding. Returns 1 if pat occurs in data (empty pat matches). */
+static inline int cfree_unit_contains(const uint8_t* data, size_t len,
+ const uint8_t* pat, size_t pat_len) {
+ size_t i;
+ if (pat_len == 0) return 1;
+ if (len < pat_len) return 0;
+ for (i = 0; i <= len - pat_len; ++i) {
+ if (memcmp(data + i, pat, pat_len) == 0) return 1;
+ }
+ return 0;
+}
+
+static inline void cfree_unit_summary(CfreeUnit* u, const char* name) {
+ if (u->fails) {
+ fprintf(stderr, "%s: %d/%d checks failed\n", name, u->fails, u->checks);
+ } else {
+ printf("%s: %d checks, 0 failures\n", name, u->checks);
+ }
+}
+
+static inline int cfree_unit_status(CfreeUnit* u) { return u->fails ? 1 : 0; }
+
+/* Record one check. `cond` is evaluated EXACTLY ONCE — it may have side
+ * effects (e.g. EXPECT(cfree_ar_iter_next(it, &m), ...) advances an
+ * iterator). Always bumps u->checks; on a miss bumps u->fails and prints a
+ * located FAIL line. Threads the context explicitly (no hidden global). */
+#define CU_CHECK(u, cond, ...) \
+ do { \
+ int cu_ok_ = (cond) ? 1 : 0; \
+ ++(u)->checks; \
+ if (!cu_ok_) { \
+ ++(u)->fails; \
+ fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); \
+ fputc('\n', stderr); \
+ } \
+ } while (0)
+
+/* Spelling preferred by most existing call sites. */
+#define CU_EXPECT(u, cond, ...) CU_CHECK(u, cond, __VA_ARGS__)
+
+/* CU_CHECK then `return 0;` on a miss — also evaluates `cond` exactly once.
+ * Preserves the per-test-function early-return shape used by table-of-tests
+ * harnesses (e.g. ar_test). */
+#define CU_CHECK_RET(u, cond, ...) \
+ do { \
+ int cu_ok_ = (cond) ? 1 : 0; \
+ ++(u)->checks; \
+ if (!cu_ok_) { \
+ ++(u)->fails; \
+ fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); \
+ fputc('\n', stderr); \
+ return 0; \
+ } \
+ } while (0)
+
+#endif /* CFREE_TEST_LIB_CFREE_UNIT_H */
diff --git a/test/lib/unit.mk b/test/lib/unit.mk
@@ -0,0 +1,77 @@
+# test/lib/unit.mk — build rules for the C unit-test binaries.
+#
+# Included by test/test.mk. Replaces the hand-written per-binary rules (each a
+# $(CC) ... SRC $(LIB_AR|LIB_OBJS) -o $@ triple) with two static-pattern rules,
+# one per regime. Registering a unit test is two lines: add its stem to a list
+# and set <stem>_SRC. Run-targets (test-isa, test-cg-api, ...) stay in test.mk
+# and invoke build/test/<stem> directly, so per-binary sequencing and skip
+# semantics (e.g. rv64-jit's exit-77 wrapper) are unchanged.
+#
+# Two regimes, by the interface under test. We prefer tests that exercise the
+# PUBLIC interface — keep a test public unless it genuinely needs internals.
+#
+# PUBLIC public headers only (-Iinclude), linked against the public
+# archive libcfree.a. Internal (visibility-hidden) symbols are
+# unreachable, so these prove the public surface is self-sufficient.
+# INTERNAL additionally sees src/ internal headers (-Isrc) and links the raw
+# objects (LIB_OBJS), exposing internal symbols. For units that have
+# no public API (ISA tables, ABI classifier, IR/opt internals, ...).
+#
+# Both add -Itest so `#include "lib/cfree_unit.h"` resolves; neither adds -Ilang
+# (no unit test needs the frontend-private headers). emu_rv64_test is a
+# deliberate exception kept in test.mk — see the note there.
+
+UNIT_HDR_DEPS := test/lib/cfree_unit.h test/arch/inline_public_test.h
+
+# Deferred (=) so they pick up HOST_CFLAGS regardless of include order.
+UNIT_CFLAGS_PUBLIC = $(HOST_CFLAGS) -Iinclude -Itest
+UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest
+
+# ---- registrations: stem lists + per-stem source ---------------------------
+
+UNIT_TESTS_PUBLIC := \
+ ar_test cg_api_test cg_switch_test rv64_jit_test \
+ aa64_inline_test rv64_inline_test x64_inline_test
+ar_test_SRC := test/ar/ar_test.c
+cg_api_test_SRC := test/api/cg_type_test.c
+cg_switch_test_SRC := test/api/cg_switch_test.c
+rv64_jit_test_SRC := test/link/rv64_jit_test.c
+aa64_inline_test_SRC := test/arch/aa64_inline_test.c
+rv64_inline_test_SRC := test/arch/rv64_inline_test.c
+x64_inline_test_SRC := test/arch/x64_inline_test.c
+
+UNIT_TESTS_INTERNAL := \
+ dwarf_test debug_roundtrip_unit debug_cfi_unit \
+ aa64_isa_test rv64_decode_test aa64_sweep_gen \
+ reloc_uleb128_unit emu_rv64_unit_test interp_smoke_test \
+ rv64_interp_smoke_test abi_classify_test ir_recorder_test \
+ native_direct_target_test x64_dbg_test cg_ir_lower_test tiny_inline_test
+dwarf_test_SRC := test/dwarf/dwarf_test.c
+debug_roundtrip_unit_SRC := test/debug/roundtrip_unit.c
+debug_cfi_unit_SRC := test/debug/cfi_unit.c
+aa64_isa_test_SRC := test/arch/aa64_isa_test.c
+rv64_decode_test_SRC := test/arch/rv64_decode_test.c
+aa64_sweep_gen_SRC := test/arch/aa64_sweep_gen.c
+reloc_uleb128_unit_SRC := test/link/reloc_uleb128_unit.c
+emu_rv64_unit_test_SRC := test/emu/rv64_vm_unit_test.c
+interp_smoke_test_SRC := test/interp/interp_smoke_test.c
+rv64_interp_smoke_test_SRC := test/emu/rv64_interp_smoke_test.c
+abi_classify_test_SRC := test/api/abi_classify_test.c
+ir_recorder_test_SRC := test/cg/ir_recorder_test.c
+native_direct_target_test_SRC := test/cg/native_direct_target_test.c
+x64_dbg_test_SRC := test/arch/x64_dbg_test.c
+cg_ir_lower_test_SRC := test/opt/cg_ir_lower_test.c
+tiny_inline_test_SRC := test/opt/tiny_inline_test.c
+
+# ---- build rules ------------------------------------------------------------
+# Secondary expansion lets a static-pattern prerequisite reference the
+# per-stem source via $$($$*_SRC).
+.SECONDEXPANSION:
+
+$(UNIT_TESTS_PUBLIC:%=build/test/%): build/test/%: $$($$*_SRC) $(UNIT_HDR_DEPS) $(LIB_AR)
+ @mkdir -p $(dir $@)
+ $(CC) $(UNIT_CFLAGS_PUBLIC) $($*_SRC) $(LIB_AR) -o $@
+
+$(UNIT_TESTS_INTERNAL:%=build/test/%): build/test/%: $$($$*_SRC) $(UNIT_HDR_DEPS) $(LIB_OBJS)
+ @mkdir -p $(dir $@)
+ $(CC) $(UNIT_CFLAGS_INTERNAL) $($*_SRC) $(LIB_OBJS) -o $@
diff --git a/test/libc/glibc/run.sh b/test/libc/glibc/run.sh
@@ -1,11 +1,13 @@
#!/usr/bin/env bash
-# test/libc/glibc/run.sh — drive cfree ld against a real glibc sysroot.
-# Dynamic-link only — static-linked glibc is officially discouraged
-# (libc.a relies on dlopen-loaded NSS modules, has its own entire reloc
-# surface area, and isn't a real-world deployment shape), so we don't
-# carry the variant. Each case in test/libc/cases/*.c is exercised once:
+# test/libc/glibc/run.sh — drive cfree ld against a real glibc sysroot, on the
+# shared corpus harness (test/lib/cf_corpus.sh).
#
-# dynamic — PIE object + libc.so.6, with explicit dynamic linker
+# Dynamic-link only — static-linked glibc is officially discouraged (libc.a
+# relies on dlopen-loaded NSS modules, has its own entire reloc surface area,
+# and isn't a real-world deployment shape), so we don't carry the variant.
+# Each case in test/libc/cases/*.c is exercised once, in a single lane:
+#
+# D dynamic — PIE object + libc.so.6, with explicit dynamic linker
# cfree ld -pie \
# -dynamic-linker <loader> \
# -o case.exe \
@@ -14,43 +16,48 @@
# $SYSROOT/lib/libc.so.6 $SYSROOT/lib/libc_nonshared.a $CFREE_RT \
# $SYSROOT/lib/crtn.o
#
-# Unlike musl, where ld-musl-<arch>.so.1 is the same file as libc,
-# glibc's loader is a separate ELF — cfree ld's default interp is musl,
-# so we override via -dynamic-linker. The per-arch loader path is:
+# Unlike musl, where ld-musl-<arch>.so.1 is the same file as libc, glibc's
+# loader is a separate ELF — cfree ld's default interp is musl, so we override
+# via -dynamic-linker. The per-arch loader path is:
# aa64 -> /lib/ld-linux-aarch64.so.1
# x64 -> /lib64/ld-linux-x86-64.so.2
+# rv64 -> /lib/ld-linux-riscv64-lp64d.so.1
# libc.so.6 carries SONAME=libc.so.6 so DT_NEEDED is correct without a
-# linker-script intermediary (the on-disk libc.so is a GROUP script
-# that cfree ld doesn't parse — we hand the SO directly).
-# libc_nonshared.a contributes the handful of non-shared callbacks
-# every glibc dyn-exe pulls in — atexit, __stack_chk_fail_local,
-# __libc_csu_init/fini on older glibc, etc. — and must follow
-# libc.so.6 in the demand chain.
+# linker-script intermediary (the on-disk libc.so is a GROUP script that cfree
+# ld doesn't parse — we hand the SO directly). libc_nonshared.a contributes the
+# handful of non-shared callbacks every glibc dyn-exe pulls in — atexit,
+# __stack_chk_fail_local, __libc_csu_init/fini on older glibc, etc. — and must
+# follow libc.so.6 in the demand chain.
#
-# Each case file may carry an `expected` companion (default 0) and an
-# optional `expected_stdout` file checked with substring match.
+# Each case file may carry an `expected` companion (default 0) and an optional
+# `expected_stdout` (.stdout) file checked with substring match.
#
-# Designed to fail fast and clearly: the *first* failure surface (compile
-# / link / run / output) is the gap to fix next. Run with
-# CFREE_GLIBC_KEEP=1 to leave intermediates in build/glibc/<arch>/<case>/.
+# The cross-exec is LANE-LOCAL: the lane runs the case either under qemu-user
+# with QEMU_LD_PREFIX=$sysroot (so the loader search resolves to the SYSROOT
+# copy), or under podman with an arch-pinned debian/glibc image (which ships the
+# loader + libc at the expected paths). This is deliberately NOT routed through
+# exec_target's default-image queue; the image + loader provenance + the
+# QEMU_LD_PREFIX requirement are glibc-specific and differ from musl.
#
-# Arch selection:
-# CFREE_LIBC_ARCHES (default "aa64") — space-separated list. Valid
-# values: aa64, x64, rv64. Each arch maps to:
-# aa64 -> build/glibc-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a
-# + --target=aarch64-linux-gnu
-# x64 -> build/glibc-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a
-# + --target=x86_64-linux-gnu
-# rv64 -> build/glibc-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a
-# + --target=riscv64-linux-gnu
-# Missing sysroot / rt for an enabled arch is reported as SKIP
-# (non-fatal); only test failures cause a nonzero exit.
+# Arch selection — CFREE_LIBC_ARCHES (default "aa64"), space-separated; each
+# token becomes an "<arch>-elf" corpus tuple:
+# aa64 -> build/glibc-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a
+# + --target=aarch64-linux-gnu
+# x64 -> build/glibc-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a
+# + --target=x86_64-linux-gnu
+# rv64 -> build/glibc-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a
+# + --target=riscv64-linux-gnu
+# A missing sysroot / rt / clang-target for an enabled arch is reported as a
+# non-gating SKIP-NA (never a failure); only test failures cause a nonzero exit.
set -u
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
CASES_DIR="$ROOT/test/libc/cases"
BUILD_DIR="$ROOT/build/glibc"
-CFREE="$ROOT/build/cfree"
+CFREE="${CFREE:-$ROOT/build/cfree}"
if [ ! -x "$CFREE" ]; then
echo "cfree driver missing at $CFREE — run 'make' first" >&2
@@ -59,14 +66,8 @@ fi
mkdir -p "$BUILD_DIR"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-PASS=0; FAIL=0; FAIL_NAMES=()
-SKIP_ARCHES=()
-
-# ---- arch lookup tables ---------------------------------------------------
+# ---- arch lookup tables (LANE-LOCAL config) -------------------------------
+# Keyed on the arch token (aa64/x64/rv64), which is the tuple's arch half.
arch_sysroot() {
case "$1" in
@@ -114,9 +115,9 @@ arch_extract_name() {
}
arch_loader() {
- # Dynamic-linker path baked into PT_INTERP. Both paths are the
- # canonical Linux glibc loader locations and match the layout the
- # extracted sysroots ship.
+ # Dynamic-linker path baked into PT_INTERP. All paths are the canonical
+ # Linux glibc loader locations and match the layout the extracted sysroots
+ # ship.
case "$1" in
aa64) echo "/lib/ld-linux-aarch64.so.1" ;;
x64) echo "/lib64/ld-linux-x86-64.so.2" ;;
@@ -125,7 +126,34 @@ arch_loader() {
esac
}
-# ---- per-arch runners ------------------------------------------------------
+# Container image carrying the glibc loader + libc at the expected paths, used
+# by the lane-local runner. Pinned to arch-specific repos (not multi-arch tags)
+# to dodge the cached-wrong-arch-manifest trap and avoid --platform (which would
+# force a registry manifest lookup on every run).
+arch_image() {
+ case "$1" in
+ # arm64v8/debian:bookworm-slim ships the matching glibc loader.
+ aa64) echo "docker.io/arm64v8/debian:bookworm-slim" ;;
+ # amd64-pinned debian peer; ships /lib64/ld-linux-x86-64.so.2 + libc.
+ x64) echo "docker.io/amd64/debian:bookworm-slim" ;;
+ # trixie ships the riscv64 glibc loader at
+ # /lib/ld-linux-riscv64-lp64d.so.1.
+ rv64) echo "docker.io/riscv64/debian:trixie-slim" ;;
+ *) echo "" ;;
+ esac
+}
+
+# qemu-user fallback binary per arch (used when no native exec is possible).
+arch_qemu() {
+ case "$1" in
+ aa64) echo "$QEMU_AA64" ;;
+ x64) echo "$QEMU_X64" ;;
+ rv64) echo "$QEMU_RV64" ;;
+ *) echo "" ;;
+ esac
+}
+
+# ---- host capability detection --------------------------------------------
#
# Native linux/<arch> hosts can exec ELFs directly under podman without
# binfmt; otherwise we fall back to qemu-<arch>-static.
@@ -141,52 +169,22 @@ QEMU_X64="$(command -v qemu-x86_64-static 2>/dev/null || command -v qemu-x86_64
QEMU_RV64="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)"
have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
+# ---- lane-local target runner ---------------------------------------------
# run_target <arch> <sysroot> <exe> <out> <err> -> sets RUN_RC
#
-# Dynamic-variant exes need /lib/ld-linux-<arch>.so.{1,2} + libc.so.6
-# to load. qemu-user resolves them relative to QEMU_LD_PREFIX; the
-# podman fallback uses a debian:bookworm image which ships them at the
-# expected paths.
+# Dynamic exes need /lib/ld-linux-<arch>.so.{1,2} + libc.so.6 to load.
+# qemu-user resolves them relative to QEMU_LD_PREFIX=$sysroot; the podman
+# fallback uses an arch-pinned debian image which ships them at the expected
+# paths.
run_target() {
local arch="$1" sysroot="$2" exe="$3" out="$4" err="$5"
- local qemu="" image=""
- case "$arch" in
- aa64)
- qemu="$QEMU_AA64"
- # Pin the image name to the arm64-specific repo
- # (docker.io/arm64v8/...) instead of the multi-arch
- # debian:bookworm-slim. Two reasons:
- # 1. Avoids the cached-amd64-manifest trap that
- # debian:bookworm-slim hits on arm64 hosts where an
- # amd64 pull happened earlier — podman silently uses
- # the wrong arch and the dyn-exe fails to load.
- # 2. Avoids passing --platform, which forces podman to
- # hit the registry on every run to verify the
- # manifest matches. Pinning the repo + relying on the
- # local cache keeps subsequent runs offline + fast.
- # arm64v8/debian:bookworm-slim ships the matching glibc
- # loader, so the dynamic variant resolves PT_INTERP
- # without extra mounts.
- image="docker.io/arm64v8/debian:bookworm-slim"
- ;;
- x64)
- qemu="$QEMU_X64"
- # amd64-pinned debian peer of the arm64v8 image above.
- # Same rationale: avoid --platform + multi-arch tag
- # cache traps. Ships /lib64/ld-linux-x86-64.so.2 + libc.
- image="docker.io/amd64/debian:bookworm-slim"
- ;;
- rv64)
- qemu="$QEMU_RV64"
- # riscv64-pinned Debian image. The trixie image ships the
- # riscv64 glibc loader at /lib/ld-linux-riscv64-lp64d.so.1.
- image="docker.io/riscv64/debian:trixie-slim"
- ;;
- esac
+ local qemu image
+ qemu="$(arch_qemu "$arch")"
+ image="$(arch_image "$arch")"
if [ -n "$qemu" ]; then
- # Point qemu-user at our extracted sysroot so the loader
- # search resolves to the SYSROOT copy rather than the
- # (possibly-absent) host one.
+ # Point qemu-user at our extracted sysroot so the loader search
+ # resolves to the SYSROOT copy rather than the (possibly-absent) host
+ # one.
QEMU_LD_PREFIX="$sysroot" \
"$qemu" "$exe" >"$out" 2>"$err"
RUN_RC=$?; return
@@ -203,175 +201,142 @@ run_target() {
RUN_RC=127
}
-# ---- case driver -----------------------------------------------------------
-
-# run_case <arch> <sysroot> <rt> <target> <loader> <triple_inc> <src>
-run_case() {
- local arch="$1" sysroot="$2" rt="$3" target="$4" loader="$5"
- local triple_inc="$6" src="$7"
- local name="$(basename "$src" .c)"
- local work="$BUILD_DIR/$arch/$name"
- local label="$arch/$name"
- mkdir -p "$work"
-
- local expected=0
- [ -f "$CASES_DIR/${name}.expected" ] && \
- expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')"
+# ---- per-case marker reading (CF_READ_CASE) --------------------------------
+# Reads the optional .stdout substring oracle and resolves this tuple's arch
+# config. The .expected sidecar (default 0) is read by the engine already
+# (CF_EXPECTED_EXT=.expected). A tuple whose sysroot/rt/clang-target is
+# unavailable is marked SKIP-NA (non-gating), mirroring the original's
+# SKIP_ARCHES informational handling.
+libc_read_case() {
+ LC_ARCH="$CF_ARCH"
+ LC_SYSROOT="$(arch_sysroot "$LC_ARCH")"
+ LC_RT="$(arch_rt "$LC_ARCH")"
+ LC_TARGET="$(arch_target "$LC_ARCH")"
+ LC_LOADER="$(arch_loader "$LC_ARCH")"
+ LC_TRIPLE_INC="$(arch_triple_include "$LC_ARCH")"
+
+ if [ -z "$LC_SYSROOT" ] || [ -z "$LC_RT" ] || [ -z "$LC_TARGET" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ if [ ! -d "$LC_SYSROOT" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ if [ ! -f "$LC_RT" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ # clang must understand --target=<target>. Every system path is overridden
+ # via --sysroot / -isystem so the host's headers / libraries are not
+ # consulted.
+ if ! clang --target="$LC_TARGET" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
+ CF_SKIP_NA_CASE=1; return
+ fi
- local expect_stdout=""
- if [ -f "$CASES_DIR/${name}.stdout" ]; then
- expect_stdout="$(cat "$CASES_DIR/${name}.stdout")"
+ LC_EXPECT_STDOUT=""
+ if [ -f "$CASES_DIR/${CF_BASE}.stdout" ]; then
+ LC_EXPECT_STDOUT="$(cat "$CASES_DIR/${CF_BASE}.stdout")"
fi
+}
+
+# ---- dynamic-link lane (compile -> link -> run -> stdout) ------------------
+# Emits exactly one cf_pass/cf_fail. All artifacts live under $CF_WORK
+# (parallel-safe).
+cf_lane_D() {
+ local label="$CF_ARCH/$CF_BASE"
+ local work="$CF_WORK"
# ---- compile ----
# Three -isystem layers, in order of precedence:
- # sysroot/include/ — glibc + linux-libc-dev
- # headers (top-level uapi).
+ # sysroot/include/ — glibc + linux-libc-dev headers
+ # (top-level uapi).
# sysroot/include/<triple> — glibc multi-arch (bits/*,
# gnu/stubs-lp64.h, ...);
# <features.h> reaches in.
# rt/include/ — cfree's freestanding overlay
# (stddef.h, stdarg.h, stdint.h).
- # glibc's stdio.h #include
- # <stddef.h> for size_t; glibc
- # doesn't ship compiler headers
- # so rt/include must be reachable.
- # -nostdinc strips clang's default include path so cross targets
- # don't accidentally pick up the host's compiler headers.
- local cc_flags=(--target="$target" --sysroot="$sysroot"
+ # glibc's stdio.h #include <stddef.h>
+ # for size_t; glibc doesn't ship
+ # compiler headers so rt/include must
+ # be reachable.
+ # -nostdinc strips clang's default include path so cross targets don't
+ # accidentally pick up the host's compiler headers.
+ local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT"
-nostdinc
- -isystem "$sysroot/include"
- -isystem "$sysroot/include/$triple_inc"
+ -isystem "$LC_SYSROOT/include"
+ -isystem "$LC_SYSROOT/include/$LC_TRIPLE_INC"
-isystem "$ROOT/rt/include"
-fPIE -fpic -O0)
- local obj="$work/${name}.o"
- if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then
- FAIL=$((FAIL+1))
- FAIL_NAMES+=("$label (compile)")
- printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)"
+ local obj="$work/${CF_BASE}.o"
+ if ! clang "${cc_flags[@]}" -c "$CF_SRC" -o "$obj" 2>"$work/cc.err"; then
+ cf_fail "$label (compile)"
sed 's/^/ cc| /' "$work/cc.err"
return
fi
# ---- link ----
- # PIE start file, libc.so.6 as the *shared* input (cfree ld
- # doesn't read the libc.so linker script, so we hand the actual
- # SO directly), with -dynamic-linker overriding the musl default.
- # Expects cfree ld to:
+ # PIE start file, libc.so.6 as the *shared* input (cfree ld doesn't read
+ # the libc.so linker script, so we hand the actual SO directly), with
+ # -dynamic-linker overriding the musl default. Expects cfree ld to:
# - accept ET_DYN ELF objects as input,
# - emit PT_INTERP "$loader",
# - emit PT_DYNAMIC with DT_NEEDED libc.so.6,
- # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt
- # so the loader can bind imported symbols at runtime.
- # libc_nonshared.a still links statically; libcfree_rt.a stays —
- # soft-float TF helpers are static-bound from our side.
- # crti/crtn are unchanged.
- local exe="$work/${name}.exe"
+ # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt so the loader
+ # can bind imported symbols at runtime.
+ # libc_nonshared.a still links statically; libcfree_rt.a stays — soft-float
+ # TF helpers are static-bound from our side. crti/crtn are unchanged.
+ local exe="$work/${CF_BASE}.exe"
local link_cmd=("$CFREE" "ld" -pie
- -dynamic-linker "$loader"
+ -dynamic-linker "$LC_LOADER"
-o "$exe"
- "$sysroot/lib/Scrt1.o" "$sysroot/lib/crti.o"
+ "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o"
"$obj"
- "$sysroot/lib/libc.so.6" "$sysroot/lib/libc_nonshared.a"
- "$rt"
- "$sysroot/lib/crtn.o")
+ "$LC_SYSROOT/lib/libc.so.6" "$LC_SYSROOT/lib/libc_nonshared.a"
+ "$LC_RT"
+ "$LC_SYSROOT/lib/crtn.o")
if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then
- FAIL=$((FAIL+1))
- FAIL_NAMES+=("$label (link)")
- printf ' %s %s\n' "$(color_red FAIL)" "$label (link)"
+ cf_fail "$label (link)"
sed 's/^/ ld| /' "$work/link.err" | head -10
return
fi
- # ---- run ----
- run_target "$arch" "$sysroot" "$exe" "$work/run.out" "$work/run.err"
- if [ "$RUN_RC" -ne "$expected" ]; then
- FAIL=$((FAIL+1))
- FAIL_NAMES+=("$label (run rc=$RUN_RC, want $expected)")
- printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \
- "$RUN_RC" "$expected"
+ # ---- run (lane-local cross-exec) ----
+ run_target "$LC_ARCH" "$LC_SYSROOT" "$exe" "$work/run.out" "$work/run.err"
+ if [ "$RUN_RC" -ne "$CF_EXPECTED" ]; then
+ cf_fail "$label (run rc=$RUN_RC, want $CF_EXPECTED)"
[ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5
[ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5
return
fi
- if [ -n "$expect_stdout" ]; then
- if ! grep -qF -- "$expect_stdout" "$work/run.out"; then
- FAIL=$((FAIL+1))
- FAIL_NAMES+=("$label (stdout)")
- printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label"
- printf ' expected substring: %s\n' "$expect_stdout"
+ if [ -n "$LC_EXPECT_STDOUT" ]; then
+ if ! grep -qF -- "$LC_EXPECT_STDOUT" "$work/run.out"; then
+ cf_fail "$label (stdout mismatch)"
+ printf ' expected substring: %s\n' "$LC_EXPECT_STDOUT"
sed 's/^/ got| /' "$work/run.out" | head -5
return
fi
fi
- PASS=$((PASS+1))
- printf ' %s %s\n' "$(color_grn PASS)" "$label"
-}
-
-# run_arch_cases <arch> <sysroot> <rt> <target>
-run_arch_cases() {
- local arch="$1" sysroot="$2" rt="$3" target="$4"
- local loader triple_inc
- loader="$(arch_loader "$arch")"
- triple_inc="$(arch_triple_include "$arch")"
-
- # clang must understand --target=<target>. Every system path is
- # overridden via --sysroot / -isystem so the host's headers /
- # libraries are not consulted.
- if ! clang --target="$target" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
- printf ' %s %s (clang does not accept --target=%s)\n' \
- "$(color_yel SKIP)" "$arch" "$target"
- SKIP_ARCHES+=("$arch (no clang --target=$target)")
- return
- fi
-
- printf 'Running glibc dynamic-link cases [%s]...\n' "$arch"
- for src in "$CASES_DIR"/*.c; do
- run_case "$arch" "$sysroot" "$rt" "$target" "$loader" "$triple_inc" "$src"
- done
- printf '\n'
+ cf_pass "$label"
}
-shopt -s nullglob
-
-ARCHES="${CFREE_LIBC_ARCHES:-aa64}"
-for arch in $ARCHES; do
- sysroot="$(arch_sysroot "$arch")"
- rt="$(arch_rt "$arch")"
- target="$(arch_target "$arch")"
- if [ -z "$sysroot" ] || [ -z "$rt" ] || [ -z "$target" ]; then
- printf ' %s %s (unknown arch)\n' "$(color_yel SKIP)" "$arch"
- SKIP_ARCHES+=("$arch (unknown)")
- continue
- fi
- if [ ! -d "$sysroot" ]; then
- printf ' %s %s (glibc sysroot missing at %s — run test/libc/glibc/extract.sh -a %s)\n' \
- "$(color_yel SKIP)" "$arch" "$sysroot" "$(arch_extract_name "$arch")"
- SKIP_ARCHES+=("$arch (sysroot)")
- continue
- fi
- if [ ! -f "$rt" ]; then
- printf ' %s %s (cfree rt missing at %s)\n' \
- "$(color_yel SKIP)" "$arch" "$rt"
- SKIP_ARCHES+=("$arch (rt)")
- continue
- fi
- run_arch_cases "$arch" "$sysroot" "$rt" "$target"
+# ---- drive the corpus ------------------------------------------------------
+# One cf_corpus_run per arch (single "<arch>-elf" tuple), so each arch gets a
+# distinct per-case $CF_WORK ($CF_BUILD_DIR/<arch>/<base>) and never collides
+# with another arch's under parallel dispatch. Results accumulate into the
+# shared counters; one summary at the end.
+printf 'test-libc-glibc arches=%s\n' "${CFREE_LIBC_ARCHES:-aa64}"
+
+PAR="${CFREE_GLIBC_PARALLEL:-1}"
+for arch in ${CFREE_LIBC_ARCHES:-aa64}; do
+ CF_LABEL=test-libc-glibc CF_BUILD_DIR="$BUILD_DIR/$arch" \
+ CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \
+ CF_LANES="D" CF_OPT_LEVELS="" CF_TUPLES="$arch-elf" \
+ CF_EXPECTED_EXT=.expected CF_TARGETS_EXT="" \
+ CF_READ_CASE=libc_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
done
-if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
- printf '\nFailed:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done
-fi
-
-printf '\nResults: %s pass, %s fail\n' "$PASS" "$FAIL"
-if [ ${#SKIP_ARCHES[@]} -gt 0 ]; then
- printf ' skipped: %s\n' "${SKIP_ARCHES[*]}"
-fi
-
-if [ ${#FAIL_NAMES[@]} -gt 0 ]; then exit 1; fi
-exit 0
+cf_summary test-libc-glibc
+cf_exit
diff --git a/test/libc/musl/run.sh b/test/libc/musl/run.sh
@@ -1,49 +1,56 @@
#!/usr/bin/env bash
-# test/libc/musl/run.sh — drive cfree ld against a real musl sysroot.
-# Each case in test/libc/cases/*.c is exercised in two variants:
+# test/libc/musl/run.sh — drive cfree ld against a real musl sysroot, on the
+# shared corpus harness (test/lib/cf_corpus.sh).
#
-# static — non-PIC object + libc.a, classic static-exe link
+# Cases under test/libc/cases/*.c are compiled, linked, and run against a
+# podman-pinned Alpine/musl sysroot, in two link variants (two lanes):
+#
+# S static — non-PIC object + libc.a, classic static-exe link
# cfree ld -static -o case.exe \
# $SYSROOT/lib/crt1.o $SYSROOT/lib/crti.o \
# case.o \
# $SYSROOT/lib/libc.a $CFREE_RT \
# $SYSROOT/lib/crtn.o
#
-# dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-<arch>.so.1
+# D dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-<arch>.so.1
# cfree ld -pie -o case.exe \
# $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \
# case.o \
# $SYSROOT/lib/libc.so $CFREE_RT \
# $SYSROOT/lib/crtn.o
-# (musl ships ld-musl-<arch>.so.1 *as* libc — same file. The
-# harness intentionally has no -dynamic-linker flag yet because
-# cfree ld currently doesn't accept one; this is one of the gaps
-# we expect the dynamic variant to surface.)
+# (musl ships ld-musl-<arch>.so.1 *as* libc — same file. The harness
+# intentionally has no -dynamic-linker flag yet because cfree ld
+# currently doesn't accept one; this is one of the gaps we expect the
+# dynamic variant to surface.)
#
-# Each case file may carry an `expected` companion (default 0) and an
-# optional `expected_stdout` file checked with substring match.
+# Each case file may carry an `expected` companion (default 0) and an optional
+# `expected_stdout` (.stdout) file checked with substring match.
#
-# Designed to fail fast and clearly: the *first* failure surface (compile
-# / link / run / output) is the gap to fix next. Run with
-# CFREE_MUSL_KEEP=1 to leave intermediates in build/musl/<arch>/<case>/.
+# The cross-exec is LANE-LOCAL: the lane runs the case under podman with musl's
+# own arch-pinned Alpine image, whose rootfs already carries the musl loader at
+# /lib/ld-musl-<arch>.so.1 — so NO QEMU_LD_PREFIX and no -dynamic-linker are
+# needed (unlike glibc). This is deliberately NOT routed through exec_target's
+# default-image queue; the image + loader provenance are musl-specific.
#
-# Arch selection:
-# CFREE_LIBC_ARCHES (default "aa64") — space-separated list. Valid
-# values: aa64, x64, rv64. Each arch maps to:
-# aa64 -> build/musl-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a
-# + --target=aarch64-linux-musl
-# x64 -> build/musl-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a
-# + --target=x86_64-linux-musl
-# rv64 -> build/musl-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a
-# + --target=riscv64-linux-musl
-# Missing sysroot / rt for an enabled arch is reported as SKIP
-# (non-fatal); only test failures cause a nonzero exit.
+# Arch selection — CFREE_LIBC_ARCHES (default "aa64"), space-separated; each
+# token becomes an "<arch>-elf" corpus tuple:
+# aa64 -> build/musl-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a
+# + --target=aarch64-linux-musl
+# x64 -> build/musl-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a
+# + --target=x86_64-linux-musl
+# rv64 -> build/musl-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a
+# + --target=riscv64-linux-musl
+# A missing sysroot / rt / clang-target for an enabled arch is reported as a
+# non-gating SKIP-NA (never a failure); only test failures cause a nonzero exit.
set -u
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
CASES_DIR="$ROOT/test/libc/cases"
BUILD_DIR="$ROOT/build/musl"
-CFREE="$ROOT/build/cfree"
+CFREE="${CFREE:-$ROOT/build/cfree}"
if [ ! -x "$CFREE" ]; then
echo "cfree driver missing at $CFREE — run 'make' first" >&2
@@ -52,19 +59,8 @@ fi
mkdir -p "$BUILD_DIR"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-# Per-variant counters so the dynamic-link surface is visible in its own
-# right rather than being averaged into one total. Counters are global
-# across arches; the arch tag is baked into each case label so failures
-# remain disambiguated in the summary.
-PASS_static=0; FAIL_static=0; FAIL_NAMES_static=()
-PASS_dynamic=0; FAIL_dynamic=0; FAIL_NAMES_dynamic=()
-SKIP_ARCHES=()
-
-# ---- arch lookup tables ---------------------------------------------------
+# ---- arch lookup tables (LANE-LOCAL config) -------------------------------
+# Keyed on the arch token (aa64/x64/rv64), which is the tuple's arch half.
arch_sysroot() {
case "$1" in
@@ -93,6 +89,23 @@ arch_target() {
esac
}
+# Container image carrying the musl loader at /lib/ld-musl-<arch>.so.1, used by
+# the lane-local runner. Pinned to arch-specific repos (not multi-arch tags) to
+# dodge the cached-wrong-arch-manifest trap and to avoid --platform (which would
+# force a registry manifest lookup on every run).
+arch_image() {
+ case "$1" in
+ # arm64v8/alpine ships the musl loader at /lib/ld-musl-aarch64.so.1.
+ aa64) echo "docker.io/arm64v8/alpine:latest" ;;
+ # amd64v2/alpine isn't a thing; amd64/alpine is the canonical pin.
+ # Ships /lib/ld-musl-x86_64.so.1.
+ x64) echo "docker.io/amd64/alpine:latest" ;;
+ # alpine:edge currently carries the riscv64 musl loader.
+ rv64) echo "docker.io/riscv64/alpine:edge" ;;
+ *) echo "" ;;
+ esac
+}
+
# Spelling extract.sh accepts for `-a`: aa64 -> aarch64; x64 -> x64.
arch_extract_name() {
case "$1" in
@@ -101,7 +114,17 @@ arch_extract_name() {
esac
}
-# ---- per-arch runners ------------------------------------------------------
+# qemu-user fallback binary per arch (used when no native exec is possible).
+arch_qemu() {
+ case "$1" in
+ aa64) echo "$QEMU_AA64" ;;
+ x64) echo "$QEMU_X64" ;;
+ rv64) echo "$QEMU_RV64" ;;
+ *) echo "" ;;
+ esac
+}
+
+# ---- host capability detection --------------------------------------------
#
# Native linux/<arch> hosts can exec ELFs directly under podman without
# binfmt; otherwise we fall back to qemu-<arch>-static.
@@ -117,40 +140,14 @@ QEMU_X64="$(command -v qemu-x86_64-static 2>/dev/null || command -v qemu-x86_64
QEMU_RV64="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)"
have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
+# ---- lane-local target runner ---------------------------------------------
# run_target <arch> <exe> <out> <err> -> sets RUN_RC
+# Musl: rootfs-baked loader, NO QEMU_LD_PREFIX.
run_target() {
local arch="$1" exe="$2" out="$3" err="$4"
- local qemu="" image="" platform=""
- case "$arch" in
- aa64)
- qemu="$QEMU_AA64"
- # Pin the image name to the arm64-specific repo
- # (docker.io/arm64v8/...) instead of the multi-arch
- # alpine:latest. Avoids the cached-wrong-arch-manifest
- # trap that bare alpine:latest hits when an unrelated
- # pull cached a different arch; also avoids --platform,
- # which would force a registry manifest lookup on every
- # run. arm64v8/alpine ships the musl loader at
- # /lib/ld-musl-aarch64.so.1 so the dynamic variant
- # resolves PT_INTERP without extra mounts.
- image="docker.io/arm64v8/alpine:latest"
- ;;
- x64)
- qemu="$QEMU_X64"
- # amd64v2/alpine isn't a thing on Docker Hub; the
- # canonical arch-pinned alpine for amd64 is amd64/alpine.
- # Same rationale as arm64v8/alpine above: pin the repo,
- # skip --platform, rely on the local cache. Ships the
- # musl loader at /lib/ld-musl-x86_64.so.1.
- image="docker.io/amd64/alpine:latest"
- ;;
- rv64)
- qemu="$QEMU_RV64"
- # riscv64-pinned Alpine image. alpine:edge currently carries
- # the riscv64 musl loader used by this sysroot.
- image="docker.io/riscv64/alpine:edge"
- ;;
- esac
+ local qemu image
+ qemu="$(arch_qemu "$arch")"
+ image="$(arch_image "$arch")"
if [ -n "$qemu" ]; then
"$qemu" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return
fi
@@ -166,188 +163,151 @@ run_target() {
RUN_RC=127
}
-# ---- case driver -----------------------------------------------------------
-#
-# run_case <arch> <sysroot> <rt> <target> <variant> <src>
-# variant ∈ {static, dynamic}
-run_case() {
- local arch="$1" sysroot="$2" rt="$3" target="$4" variant="$5" src="$6"
- local name="$(basename "$src" .c)"
- local work="$BUILD_DIR/$arch/$name/$variant"
- local label="$arch/$name [$variant]"
- mkdir -p "$work"
-
- local expected=0
- [ -f "$CASES_DIR/${name}.expected" ] && \
- expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')"
+# ---- per-case marker reading (CF_READ_CASE) --------------------------------
+# Reads the optional .stdout substring oracle and resolves this tuple's arch
+# config. The .expected sidecar (default 0) is read by the engine already
+# (CF_EXPECTED_EXT=.expected). A tuple whose sysroot/rt/clang-target is
+# unavailable is marked SKIP-NA (non-gating), mirroring the original's
+# SKIP_ARCHES informational handling.
+libc_read_case() {
+ LC_ARCH="$CF_ARCH"
+ LC_SYSROOT="$(arch_sysroot "$LC_ARCH")"
+ LC_RT="$(arch_rt "$LC_ARCH")"
+ LC_TARGET="$(arch_target "$LC_ARCH")"
+
+ if [ -z "$LC_SYSROOT" ] || [ -z "$LC_RT" ] || [ -z "$LC_TARGET" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ if [ ! -d "$LC_SYSROOT" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ if [ ! -f "$LC_RT" ]; then
+ CF_SKIP_NA_CASE=1; return
+ fi
+ # clang must understand --target=<target>. Recent clang ships linux-musl as
+ # a target alias of linux-gnu for our purposes (we override every system
+ # path via --sysroot).
+ if ! clang --target="$LC_TARGET" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
+ CF_SKIP_NA_CASE=1; return
+ fi
- local expect_stdout=""
- if [ -f "$CASES_DIR/${name}.stdout" ]; then
- expect_stdout="$(cat "$CASES_DIR/${name}.stdout")"
+ LC_EXPECT_STDOUT=""
+ if [ -f "$CASES_DIR/${CF_BASE}.stdout" ]; then
+ LC_EXPECT_STDOUT="$(cat "$CASES_DIR/${CF_BASE}.stdout")"
fi
+}
+
+# ---- shared per-variant pipeline (compile -> link -> run -> stdout) --------
+# libc_run_variant <variant> <label>
+# variant ∈ {static, dynamic}; emits exactly one cf_pass/cf_fail.
+# All artifacts live under $CF_WORK (parallel-safe).
+libc_run_variant() {
+ local variant="$1" label="$2"
+ local work="$CF_WORK/$variant"
+ mkdir -p "$work"
# ---- compile ----
# -nostdinc strips clang's default include path (resource dir +
- # /usr/include) so the sysroot's musl + linux-headers tree is the
- # sole source. -isystem $sysroot/include picks it up.
- local cc_flags=(--target="$target" --sysroot="$sysroot"
+ # /usr/include) so the sysroot's musl + linux-headers tree is the sole
+ # source. -isystem $sysroot/include picks it up.
+ local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT"
-nostdinc
- -isystem "$sysroot/include"
+ -isystem "$LC_SYSROOT/include"
-O0)
case "$variant" in
static) cc_flags+=(-fno-PIC -fno-pie) ;;
dynamic) cc_flags+=(-fPIE -fpic) ;;
esac
- local obj="$work/${name}.o"
- if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then
- eval "FAIL_${variant}=\$((FAIL_${variant}+1))"
- eval "FAIL_NAMES_${variant}+=(\"\$label (compile)\")"
- printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)"
+ local obj="$work/${CF_BASE}.o"
+ if ! clang "${cc_flags[@]}" -c "$CF_SRC" -o "$obj" 2>"$work/cc.err"; then
+ cf_fail "$label (compile)"
sed 's/^/ cc| /' "$work/cc.err"
return
fi
# ---- link ----
- local exe="$work/${name}.exe"
+ local exe="$work/${CF_BASE}.exe"
local link_cmd
case "$variant" in
static)
# Link order mirrors a typical static-musl invocation:
# crt1.o crti.o obj libc.a libcfree_rt.a crtn.o
- # libcfree_rt provides the TF / soft-float builtins
- # (__addtf3, __extenddftf2 etc.) that musl's libc.a calls
- # from printf's long-double formatting. Archive ingestion
- # iterates demand-load to a fixed point so one trailing
- # libcfree_rt.a is enough.
+ # libcfree_rt provides the TF / soft-float builtins (__addtf3,
+ # __extenddftf2 etc.) that musl's libc.a calls from printf's
+ # long-double formatting. Archive ingestion iterates demand-load
+ # to a fixed point so one trailing libcfree_rt.a is enough.
link_cmd=("$CFREE" "ld" -static -o "$exe"
- "$sysroot/lib/crt1.o" "$sysroot/lib/crti.o"
+ "$LC_SYSROOT/lib/crt1.o" "$LC_SYSROOT/lib/crti.o"
"$obj"
- "$sysroot/lib/libc.a" "$rt"
- "$sysroot/lib/crtn.o")
+ "$LC_SYSROOT/lib/libc.a" "$LC_RT"
+ "$LC_SYSROOT/lib/crtn.o")
;;
dynamic)
- # Dynamic-exe link: PIE start file, libc.so as a *shared*
- # input (not an archive), expects cfree ld to:
+ # Dynamic-exe link: PIE start file, libc.so as a *shared* input
+ # (not an archive), expects cfree ld to:
# - accept ET_DYN ELF objects as input,
# - emit PT_INTERP "/lib/ld-musl-<arch>.so.1",
# - emit PT_DYNAMIC with DT_NEEDED libc.so,
- # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt
- # so the loader can bind imported symbols at runtime.
+ # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt so the
+ # loader can bind imported symbols at runtime.
# libcfree_rt.a stays — soft-float TF helpers are still
# static-bound from our side. crti/crtn are unchanged.
link_cmd=("$CFREE" "ld" -pie -o "$exe"
- "$sysroot/lib/Scrt1.o" "$sysroot/lib/crti.o"
+ "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o"
"$obj"
- "$sysroot/lib/libc.so" "$rt"
- "$sysroot/lib/crtn.o")
+ "$LC_SYSROOT/lib/libc.so" "$LC_RT"
+ "$LC_SYSROOT/lib/crtn.o")
;;
esac
if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then
- eval "FAIL_${variant}=\$((FAIL_${variant}+1))"
- eval "FAIL_NAMES_${variant}+=(\"\$label (link)\")"
- printf ' %s %s\n' "$(color_red FAIL)" "$label (link)"
+ cf_fail "$label (link)"
sed 's/^/ ld| /' "$work/link.err" | head -10
return
fi
- # ---- run ----
- run_target "$arch" "$exe" "$work/run.out" "$work/run.err"
- if [ "$RUN_RC" -ne "$expected" ]; then
- eval "FAIL_${variant}=\$((FAIL_${variant}+1))"
- eval "FAIL_NAMES_${variant}+=(\"\$label (run rc=\$RUN_RC, want \$expected)\")"
- printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \
- "$RUN_RC" "$expected"
+ # ---- run (lane-local cross-exec) ----
+ run_target "$LC_ARCH" "$exe" "$work/run.out" "$work/run.err"
+ if [ "$RUN_RC" -ne "$CF_EXPECTED" ]; then
+ cf_fail "$label (run rc=$RUN_RC, want $CF_EXPECTED)"
[ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5
[ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5
return
fi
- if [ -n "$expect_stdout" ]; then
- if ! grep -qF -- "$expect_stdout" "$work/run.out"; then
- eval "FAIL_${variant}=\$((FAIL_${variant}+1))"
- eval "FAIL_NAMES_${variant}+=(\"\$label (stdout)\")"
- printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label"
- printf ' expected substring: %s\n' "$expect_stdout"
+ if [ -n "$LC_EXPECT_STDOUT" ]; then
+ if ! grep -qF -- "$LC_EXPECT_STDOUT" "$work/run.out"; then
+ cf_fail "$label (stdout mismatch)"
+ printf ' expected substring: %s\n' "$LC_EXPECT_STDOUT"
sed 's/^/ got| /' "$work/run.out" | head -5
return
fi
fi
- eval "PASS_${variant}=\$((PASS_${variant}+1))"
- printf ' %s %s\n' "$(color_grn PASS)" "$label"
-}
-
-# run_arch_cases <arch> <sysroot> <rt> <target>
-run_arch_cases() {
- local arch="$1" sysroot="$2" rt="$3" target="$4"
-
- # clang must understand --target=<target>. Recent clang ships
- # linux-musl as a target alias of linux-gnu for our purposes (we
- # override every system path via --sysroot).
- if ! clang --target="$target" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then
- printf ' %s %s (clang does not accept --target=%s)\n' \
- "$(color_yel SKIP)" "$arch" "$target"
- SKIP_ARCHES+=("$arch (no clang --target=$target)")
- return
- fi
-
- printf 'Running musl static-link cases [%s]...\n' "$arch"
- for src in "$CASES_DIR"/*.c; do
- run_case "$arch" "$sysroot" "$rt" "$target" static "$src"
- done
-
- printf '\nRunning musl dynamic-link cases [%s]...\n' "$arch"
- for src in "$CASES_DIR"/*.c; do
- run_case "$arch" "$sysroot" "$rt" "$target" dynamic "$src"
- done
- printf '\n'
+ cf_pass "$label"
}
-shopt -s nullglob
-
-ARCHES="${CFREE_LIBC_ARCHES:-aa64}"
-for arch in $ARCHES; do
- sysroot="$(arch_sysroot "$arch")"
- rt="$(arch_rt "$arch")"
- target="$(arch_target "$arch")"
- if [ -z "$sysroot" ] || [ -z "$rt" ] || [ -z "$target" ]; then
- printf ' %s %s (unknown arch)\n' "$(color_yel SKIP)" "$arch"
- SKIP_ARCHES+=("$arch (unknown)")
- continue
- fi
- if [ ! -d "$sysroot" ]; then
- printf ' %s %s (musl sysroot missing at %s — run test/libc/musl/extract.sh -a %s)\n' \
- "$(color_yel SKIP)" "$arch" "$sysroot" "$(arch_extract_name "$arch")"
- SKIP_ARCHES+=("$arch (sysroot)")
- continue
- fi
- if [ ! -f "$rt" ]; then
- printf ' %s %s (cfree rt missing at %s)\n' \
- "$(color_yel SKIP)" "$arch" "$rt"
- SKIP_ARCHES+=("$arch (rt)")
- continue
- fi
- run_arch_cases "$arch" "$sysroot" "$rt" "$target"
+# ---- lanes (static / dynamic variant axis) --------------------------------
+cf_lane_S() { libc_run_variant static "$CF_ARCH/$CF_BASE [static]"; }
+cf_lane_D() { libc_run_variant dynamic "$CF_ARCH/$CF_BASE [dynamic]"; }
+
+# ---- drive the corpus ------------------------------------------------------
+# One cf_corpus_run per arch (single "<arch>-elf" tuple), so each arch gets a
+# distinct per-case $CF_WORK ($CF_BUILD_DIR/<arch>/<base>) and the static/
+# dynamic lanes for one arch never collide with another arch's under parallel
+# dispatch. Results accumulate into the shared counters; one summary at the end.
+printf 'test-libc-musl arches=%s\n' "${CFREE_LIBC_ARCHES:-aa64}"
+
+PAR="${CFREE_MUSL_PARALLEL:-1}"
+for arch in ${CFREE_LIBC_ARCHES:-aa64}; do
+ CF_LABEL=test-libc-musl CF_BUILD_DIR="$BUILD_DIR/$arch" \
+ CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \
+ CF_LANES="S D" CF_OPT_LEVELS="" CF_TUPLES="$arch-elf" \
+ CF_EXPECTED_EXT=.expected CF_TARGETS_EXT="" \
+ CF_READ_CASE=libc_read_case CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
done
-if [ ${#FAIL_NAMES_static[@]} -gt 0 ]; then
- printf '\nFailed (static):\n'
- for n in "${FAIL_NAMES_static[@]}"; do printf ' %s\n' "$n"; done
-fi
-if [ ${#FAIL_NAMES_dynamic[@]} -gt 0 ]; then
- printf '\nFailed (dynamic):\n'
- for n in "${FAIL_NAMES_dynamic[@]}"; do printf ' %s\n' "$n"; done
-fi
-
-printf '\nResults:\n'
-printf ' static : %s pass, %s fail\n' "$PASS_static" "$FAIL_static"
-printf ' dynamic: %s pass, %s fail\n' "$PASS_dynamic" "$FAIL_dynamic"
-if [ ${#SKIP_ARCHES[@]} -gt 0 ]; then
- printf ' skipped: %s\n' "${SKIP_ARCHES[*]}"
-fi
-
-total_fail=$((FAIL_static + FAIL_dynamic))
-if [ $total_fail -gt 0 ]; then exit 1; fi
-exit 0
+cf_summary test-libc-musl
+cf_exit
diff --git a/test/link/reloc_uleb128_unit.c b/test/link/reloc_uleb128_unit.c
@@ -29,19 +29,15 @@
#include <stdio.h>
#include <string.h>
+#include "lib/cfree_unit.h"
#include "obj/obj.h"
#include "obj/reloc_apply.h"
-static int g_failures;
-#define CHECK(cond, ...) \
- do { \
- if (!(cond)) { \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- g_failures++; \
- } \
- } while (0)
+/* Shared test context replaces the per-file counter global; CHECK aliases
+ * CU_CHECK so the call sites are unchanged. This file builds no compiler/
+ * heap/diag — it only needs the check counters. */
+static CfreeUnit g_u;
+#define CHECK(cond, ...) CU_CHECK(&g_u, cond, __VA_ARGS__)
/* Decode a ULEB128 at p, returning value and (via *len_out) byte length. */
static uint64_t decode_uleb128(const uint8_t* p, uint32_t* len_out) {
@@ -86,6 +82,8 @@ static void expect_field(const char* label, const uint8_t* site,
}
int main(void) {
+ cfree_unit_init(&g_u);
+
/* ---- Case 1: real-object fixtures (1-byte fields) ---- */
{
/* off 0x12 of .debug_rnglists: pre-filled assembler value 0x18, a
@@ -156,8 +154,8 @@ int main(void) {
expect_field("set-only", buf, 0x2au, 1u, 0x9d, buf[1]);
}
- if (g_failures) {
- fprintf(stderr, "reloc_uleb128_unit: %d failure(s)\n", g_failures);
+ if (g_u.fails) {
+ fprintf(stderr, "reloc_uleb128_unit: %d failure(s)\n", g_u.fails);
return 1;
}
fputs("reloc_uleb128_unit: OK\n", stderr);
diff --git a/test/link/run.sh b/test/link/run.sh
@@ -1,22 +1,25 @@
#!/usr/bin/env bash
-# test/link/run.sh — linker and JIT test harness.
+# test/link/run.sh — linker and JIT test harness, on the shared corpus
+# harness (test/lib/cf_corpus.sh).
#
-# Three paths per case:
+# Cases are DIRECTORIES: test/link/cases/<name>/ and bad/<name>/, each holding
+# source(s) + marker files. Three lanes per case (CFREE_TEST_PATHS, default REJ):
#
# R test/link/cases/*/ clang-c → .o → cfree-roundtrip → structural diff.
# Validates obj reader + writer fidelity.
-# E clang-c → .o → link-exe-runner (cfree_link_exe) → AArch64 ELF
-# → qemu/podman → check exit code.
+# E clang-c → .o → link-exe-runner (cfree_link_exe) → exe → qemu/podman
+# → check exit code (DEFERRED batched exec, except
+# kernel_image cases which run a SYNCHRONOUS
+# qemu-system-* via exec_kernel.sh inline).
# Validates linker layout and reloc application.
# J clang-c → .o → jit-runner (cfree_link_jit) → call test_main()
-# → check return value.
-# Validates JIT path (runs on aarch64 host only).
+# → check return value (host-native arch only).
+# Validates JIT path.
#
# Negative tests live in test/link/bad/<name>/ — sources that compile
-# cleanly but should cause the linker (and JIT) to reject. Each bad/ case
-# requires an `expect` file containing a substring that must appear in
-# stderr. No special markers in test/link/cases/ trigger negative-test
-# behavior.
+# cleanly but should cause the linker (E) and JIT (J) to reject. Each bad/
+# case requires an `expect` file containing a substring that must appear in
+# stderr. The bad/ corpus is a SECOND cf_corpus_run with negative E/J lanes.
#
# Case markers (files in the case directory):
# expected — expected exit/return value (default 0)
@@ -25,41 +28,44 @@
# linker_flags — one flag per line; passed to link-exe-runner and jit-runner
# cflags — extra clang -c flags appended to every TU compile
# gc_absent — one symbol per line; verified absent post-link
-# (jit_runner --check-absent for J; readelf -s for E)
+# (jit_runner --check-absent for J; llvm-nm scan for E)
# gc_present — one symbol per line; verified present post-link
-# (jit_runner --check-present for J; readelf -s for E)
+# (jit_runner --check-present for J; llvm-nm scan for E)
# archive_b — package b.o into b.a; content "demand" or "whole"
# linker_script — basename of an .lds file in the case dir; passed via
-# --linker-script to both runners. The harness first
-# looks for a per-arch variant (foo.<arch>.lds) before
-# falling back to the literal name.
-# kernel_image — empty marker; case is a freestanding kernel image.
-# Skips paths R and J; on E, runs the linked exe via
-# a per-arch qemu-system-* invocation (semihosting on
-# aa64; SIFIVE_TEST MMIO exit on rv64).
-# j_targets — per-path applicability for J only. Listed tuples
-# (one per line) run J; others print SKIP-NA for J
-# and continue running R/E. For cases whose JIT
-# success criterion depends on an ABI feature with
-# no Mach-O analogue (ELF .fini_array destructors,
-# -ffunction-sections per-fn dead-strip).
+# --linker-script to both runners. The harness first looks
+# for a per-arch variant (foo.<arch>.lds) before the literal.
+# kernel_image — empty marker; case is a freestanding kernel image. Skips
+# R and J; on E runs the linked exe via a per-arch
+# qemu-system-* invocation (semihosting on aa64; SIFIVE_TEST
+# MMIO exit on rv64) SYNCHRONOUSLY.
+# targets — whole-case applicability: listed <arch>-<obj> tuples (one
+# per line) run; others print SKIP-NA and don't count.
+# j_targets — per-lane applicability for J only. Listed tuples run J;
+# others SKIP-NA for J and continue running R/E.
+# e_targets — peer of j_targets for lane E.
#
-# Per-arch source variants:
-# For each candidate source filename (entry.S, a.S, b.S, a.c, b.c, c.c),
-# the harness picks <name>.<TEST_ARCH>.<ext> if present, else falls back
-# to the bare <name>.<ext>. Same for any file referenced by linker_script.
-# Existing aa64-only cases keep their bare names; per-arch variants are
-# purely additive.
+# Per-arch source variants: for each candidate (entry.S, a.S, b.S, a.c, b.c,
+# c.c), prefer <name>.<TEST_ARCH>.<ext> if present, else the bare <name>.<ext>.
+# Same for any file referenced by linker_script.
#
# Filtering:
# ./run.sh [name_filter] [paths]
# name_filter substring match against case name (e.g. "02", "rodata")
# paths subset of "REJ" (default "REJ")
# Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS.
+#
+# All lane hooks write only under CF_WORK and record via cf_*, so the runner
+# is parallel-safe by construction; CFREE_LINK_PARALLEL flips dispatch. The
+# default is serial (0) because of the cached start.o + synchronous kernel
+# path; flipping to 1 is a one-line change since every hook is CF_WORK-confined.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
TEST_DIR="$ROOT/test/link"
BUILD_DIR="$ROOT/build/test"
LIB_AR="$ROOT/build/libcfree.a"
@@ -109,35 +115,21 @@ EXEC_TAG="${EXEC_ARCH}-${EXEC_OS}"
export CFREE_TEST_ARCH CFREE_TEST_OBJ
CLANG_TARGET="--target=$CLANG_TRIPLE"
-CC="${CC:-cc}"
-CFREE_CFLAGS="-I$ROOT/include -I$ROOT/test"
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
# Filters (env vars or positional args; args win):
-# $1 / CFREE_TEST_FILTER — substring match against case name (e.g. "02" or "rodata")
-# $2 / CFREE_TEST_PATHS — subset of "REJ" (default "REJ"); selects which paths run
+# $1 / CFREE_TEST_FILTER — substring match against case name (e.g. "02")
+# $2 / CFREE_TEST_PATHS — subset of "REJ" (default "REJ"); selects paths
FILTER="${1:-${CFREE_TEST_FILTER:-}}"
PATHS="${2:-${CFREE_TEST_PATHS:-REJ}}"
+# cf_corpus_discover filters discovery by CFREE_TEST_FILTER (substring vs base).
+export CFREE_TEST_FILTER="$FILTER"
case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac
case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
-T_R=0; T_E=0; T_J=0 # accumulated wall-clock seconds per path
-now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; }
mkdir -p "$BUILD_DIR"
mkdir -p "$BUILD_DIR/link"
-PASS=0; FAIL=0; SKIP=0
-FAIL_NAMES=(); SKIP_NAMES=()
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-
# ---- tool detection --------------------------------------------------------
have_clang_cross=0
@@ -160,17 +152,16 @@ have_readobj=0
command -v llvm-readobj >/dev/null 2>&1 && have_readobj=1
command -v readobj >/dev/null 2>&1 && have_readobj=1
# Path R needs the right dump tool for the obj format. ELF wants
-# llvm-readelf; Mach-O wants llvm-readobj. The harness exposes a
-# single have_dump flag so the per-case skip logic doesn't have to
-# branch on CFREE_TEST_OBJ.
+# llvm-readelf; Mach-O wants llvm-readobj. The harness exposes a single
+# have_dump flag so per-case skip logic doesn't have to branch on the obj.
have_dump=0
case "$CFREE_TEST_OBJ" in
elf) [ $have_readelf -eq 1 ] && have_dump=1 ;;
macho) [ $have_readobj -eq 1 ] && have_dump=1 ;;
esac
-# Prefer llvm-ar for archive creation: Apple's /usr/bin/ar requires
-# Mach-O members and silently drops ELF objects (leaving only a SYMDEF
-# stub), which breaks the cross-target archive cases here.
+# Prefer llvm-ar for archive creation: Apple's /usr/bin/ar requires Mach-O
+# members and silently drops ELF objects, breaking the cross-target archive
+# cases here.
AR_BIN="$(command -v llvm-ar 2>/dev/null || command -v ar 2>/dev/null || true)"
[ -n "$AR_BIN" ] && have_ar=1
[ -f "$ROUNDTRIP_BIN" ] && have_roundtrip=1
@@ -184,7 +175,7 @@ arch_raw="$(uname -m 2>/dev/null || true)"
{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
# is_native_target=1 when the cross-target arch matches the host arch.
-# Required for in-process JIT (path D) and the jit-runner (path J).
+# Required for the jit-runner (path J).
is_native_target=0
case "$TEST_ARCH" in
aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;;
@@ -193,24 +184,24 @@ case "$TEST_ARCH" in
esac
READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)"
-# llvm-nm works on both ELF and Mach-O; it's the format-agnostic tool for
-# the gc_present / gc_absent symbol-presence checks. Falls back to plain
-# `nm` which is also format-aware on most platforms.
+# llvm-nm works on both ELF and Mach-O; format-agnostic tool for the
+# gc_present / gc_absent symbol-presence checks. Falls back to plain `nm`.
NM_BIN="$(command -v llvm-nm 2>/dev/null || command -v nm 2>/dev/null || true)"
have_nm=0
[ -n "$NM_BIN" ] && have_nm=1
-# Shared per-arch exec helper. Path E queues each linked.exe and we
-# drain all cases in a single `podman run` per arch after the main
-# loop — amortizes the ~150 ms per-launch podman client overhead
-# across the whole suite.
+# Shared per-arch exec helpers. Path E queues each linked.exe and the engine
+# drains all cases in one batched flush per arch (amortizes the ~150 ms
+# per-launch podman client overhead). Kernel images run synchronously via
+# exec_kernel.sh.
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+. "$ROOT/test/lib/exec_target.sh"
# shellcheck source=../lib/exec_kernel.sh
-source "$ROOT/test/lib/exec_kernel.sh"
+. "$ROOT/test/lib/exec_kernel.sh"
-# ---- locate harness binaries ------------------------------------------------
+# ---- locate harness binaries -----------------------------------------------
# The Makefile's `test-link` target builds these as proper Make targets so
# they pick up libcfree.a changes. Running this script directly without
# `make test-link` is supported but requires the binaries to already exist.
@@ -227,24 +218,23 @@ have_jit_runner=0
if [ -x "$LINK_EXE_RUNNER" ]; then
have_exe_runner=1
- printf ' %s link-exe-runner\n' "$(color_grn found)"
+ printf ' found link-exe-runner\n'
else
- printf ' %s link-exe-runner missing (run "make %s")\n' \
- "$(color_yel warn)" "$LINK_EXE_RUNNER" >&2
+ printf ' warn link-exe-runner missing (run "make %s")\n' "$LINK_EXE_RUNNER" >&2
fi
if [ $is_native_target -eq 1 ]; then
if [ -x "$JIT_RUNNER" ]; then
have_jit_runner=1
- printf ' %s jit-runner\n' "$(color_grn found)"
+ printf ' found jit-runner\n'
else
- printf ' %s jit-runner missing (run "make %s")\n' \
- "$(color_yel warn)" "$JIT_RUNNER" >&2
+ printf ' warn jit-runner missing (run "make %s")\n' "$JIT_RUNNER" >&2
fi
fi
# Cached start.o — every case used to recompile this from the same source
-# (~40 ms × N cases). Build it once for the whole harness run.
+# (~40 ms × N cases). Build it once for the whole harness run. Hooks only
+# READ it, so this stays parallel-safe.
START_OBJ="$BUILD_DIR/link_start.$TEST_ARCH.$CFREE_TEST_OBJ.o"
have_start_obj=0
if [ $have_clang_cross -eq 1 ]; then
@@ -255,10 +245,10 @@ if [ $have_clang_cross -eq 1 ]; then
fi
fi
-# Mach-O Path E needs libSystem.tbd for the `exit` import in start.c (and
-# any libc calls user TUs make). Resolve it via xcrun on Darwin hosts.
-# On non-Darwin hosts there is no SDK to point at and Mach-O exec is SKIP
-# anyway (see exec_target.sh), so leaving these empty is fine.
+# Mach-O Path E needs libSystem.tbd for the `exit` import in start.c (and any
+# libc calls user TUs make). Resolve it via xcrun on Darwin hosts. On non-Darwin
+# hosts there is no SDK to point at and Mach-O exec is SKIP anyway, so leaving
+# these empty is fine.
MACHO_LIBSYSTEM=""
MACHO_DSO_ARGS=()
if [ "$CFREE_TEST_OBJ" = "macho" ]; then
@@ -271,575 +261,501 @@ if [ "$CFREE_TEST_OBJ" = "macho" ]; then
fi
fi
-printf 'Running cases...\n'
-
-# Path E result bookkeeping. We queue each linked.exe during the main loop
-# and drain them all in one podman invocation after the loop, then verify
-# the recorded exit codes / symbol checks here.
-#
-# bad/ cases are NOT queued: their linker is expected to fail, so the
-# podman runner never gets invoked.
-#
-# The kernel_image case is also not queued: it uses qemu-system-aarch64
-# rather than the user-mode runner and runs synchronously.
-E_NAMES=()
-E_WORK=()
-E_EXE=()
-E_LINK_MS=()
-E_EXPECTED=()
-E_GC_ABSENT_LIST=() # newline-joined per case (empty if none)
-E_GC_PRESENT_LIST=()
-
-# ---- per-case loop ---------------------------------------------------------
-
CUR_TUPLE="${TEST_ARCH}-${CFREE_TEST_OBJ}"
-for case_dir in "$TEST_DIR/cases"/*/; do
- [ -d "$case_dir" ] || continue
- name="$(basename "$case_dir")"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- # Per-case applicability: a `targets` file lists the <arch>-<obj>
- # tuples the case applies to (one per line, or whitespace-separated).
- # Cases with no `targets` file run on every tuple. Filtered cases
- # print a SKIP-NA line and don't count against pass/fail/skip — they
- # exercise target-specific features with no analogue elsewhere
- # (e.g. ELF __start_/__stop_ boundary syms have no Mach-O peer;
- # ELF TLS local-exec relocs differ fundamentally from Mach-O TLVP).
- if [ -f "$case_dir/targets" ]; then
+# ---- per-case marker reading (CF_READ_CASE) --------------------------------
+# Runs once per item, before lanes. Reads markers + per-arch source variants
+# into shell vars the lane hooks consume, then compiles the TUs (shared by all
+# lanes within the item) into $CF_WORK. On compile failure, records one FAIL
+# for the whole case and sets CF_SKIP_NA_CASE to suppress all lanes (mirrors
+# the original "note_fail; continue").
+link_read_case() {
+ CASE_DIR="${CF_SRC%/}" # CF_SRC is the glob match "cases/<name>/"
+ NAME="$CF_BASE" # case name (== dir basename)
+
+ # Whole-case applicability via `targets` (literal marker name, no base/ext).
+ if [ -f "$CASE_DIR/targets" ]; then
applicable=0
- for tuple in $(cat "$case_dir/targets"); do
+ for tuple in $(cat "$CASE_DIR/targets"); do
[ "$tuple" = "$CUR_TUPLE" ] && applicable=1
done
if [ $applicable -eq 0 ]; then
- printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE"
- continue
+ # SKIP-NA whole case (uncounted) — target-specific feature with no
+ # analogue on this tuple.
+ printf ' SKIP-NA %s — N/A on %s\n' "$NAME" "$CUR_TUPLE"
+ CF_SKIP_NA_CASE=1
+ return
fi
fi
- work="$BUILD_DIR/link/$name"
- mkdir -p "$work"
-
- # Read markers
- expected=0; [ -f "$case_dir/expected" ] && expected="$(cat "$case_dir/expected" | tr -d '[:space:]')"
- jit_only=0; [ -f "$case_dir/jit_only" ] && jit_only=1
- use_resolver=0; [ -f "$case_dir/use_resolver" ] && use_resolver=1
- # Per-path applicability — `j_targets` lists tuples on which the
- # J path can run. Used for cases that exercise an ELF-specific
- # ABI feature inapplicable to Mach-O at the test level (e.g. ELF
- # `.fini_array` destructors vs Mach-O `__StaticInit` + `atexit`;
- # `-ffunction-sections` per-function dead-strip vs Mach-O's single
- # `__TEXT,__text`). R and E still run — they don't depend on the
- # ABI feature the J path's pass/fail criterion does.
- j_applicable=1
- if [ -f "$case_dir/j_targets" ]; then
- j_applicable=0
- for tuple in $(cat "$case_dir/j_targets"); do
- [ "$tuple" = "$CUR_TUPLE" ] && j_applicable=1
+
+ # Markers
+ CF_EXPECTED=0
+ [ -f "$CASE_DIR/expected" ] && CF_EXPECTED="$(tr -d '[:space:]' < "$CASE_DIR/expected")"
+ JIT_ONLY=0; [ -f "$CASE_DIR/jit_only" ] && JIT_ONLY=1
+ USE_RESOLVER=0; [ -f "$CASE_DIR/use_resolver" ] && USE_RESOLVER=1
+
+ # Per-lane applicability — j_targets / e_targets list tuples on which the
+ # J / E lane can run. R and the other lane still run; only the gated lane
+ # prints SKIP-NA.
+ J_APPLICABLE=1
+ if [ -f "$CASE_DIR/j_targets" ]; then
+ J_APPLICABLE=0
+ for tuple in $(cat "$CASE_DIR/j_targets"); do
+ [ "$tuple" = "$CUR_TUPLE" ] && J_APPLICABLE=1
done
fi
- # `e_targets` — peer of `j_targets` for path E. Same shape, same
- # semantics: cases that exercise an ELF-specific runtime feature
- # the linked-exe-runner can't validate on Mach-O (ELF .fini_array
- # walk in start.c, --gc-sections per-function granularity that
- # Apple's clang doesn't emit, etc.) list the tuples on which E
- # runs. R and J still run.
- e_applicable=1
- if [ -f "$case_dir/e_targets" ]; then
- e_applicable=0
- for tuple in $(cat "$case_dir/e_targets"); do
- [ "$tuple" = "$CUR_TUPLE" ] && e_applicable=1
+ E_APPLICABLE=1
+ if [ -f "$CASE_DIR/e_targets" ]; then
+ E_APPLICABLE=0
+ for tuple in $(cat "$CASE_DIR/e_targets"); do
+ [ "$tuple" = "$CUR_TUPLE" ] && E_APPLICABLE=1
done
fi
- archive_mode="none"
- if [ -f "$case_dir/archive_b" ]; then
- archive_mode="$(cat "$case_dir/archive_b" | tr -d '[:space:]')"
- fi
- # Collect extra linker flags
- extra_flags=()
- if [ -f "$case_dir/linker_flags" ]; then
+ ARCHIVE_MODE="none"
+ [ -f "$CASE_DIR/archive_b" ] && ARCHIVE_MODE="$(tr -d '[:space:]' < "$CASE_DIR/archive_b")"
+
+ # Extra linker flags (one per line).
+ EXTRA_FLAGS=()
+ if [ -f "$CASE_DIR/linker_flags" ]; then
while IFS= read -r flag; do
- [ -n "$flag" ] && extra_flags+=("$flag")
- done < "$case_dir/linker_flags"
+ [ -n "$flag" ] && EXTRA_FLAGS+=("$flag")
+ done < "$CASE_DIR/linker_flags"
fi
- # Collect GC-absent / GC-present symbols
- gc_absent_syms=()
- if [ -f "$case_dir/gc_absent" ]; then
+ # GC-absent / GC-present symbols.
+ GC_ABSENT_SYMS=()
+ if [ -f "$CASE_DIR/gc_absent" ]; then
while IFS= read -r sym; do
- [ -n "$sym" ] && gc_absent_syms+=("$sym")
- done < "$case_dir/gc_absent"
+ [ -n "$sym" ] && GC_ABSENT_SYMS+=("$sym")
+ done < "$CASE_DIR/gc_absent"
fi
- gc_present_syms=()
- if [ -f "$case_dir/gc_present" ]; then
+ GC_PRESENT_SYMS=()
+ if [ -f "$CASE_DIR/gc_present" ]; then
while IFS= read -r sym; do
- [ -n "$sym" ] && gc_present_syms+=("$sym")
- done < "$case_dir/gc_present"
+ [ -n "$sym" ] && GC_PRESENT_SYMS+=("$sym")
+ done < "$CASE_DIR/gc_present"
fi
- # Per-case extra clang cflags (mirrors test/elf/cases/*.cflags but as a
- # file per case dir). One token per whitespace.
- case_cflags=()
- if [ -f "$case_dir/cflags" ]; then
+ # Per-case extra clang cflags (one token per whitespace).
+ CASE_CFLAGS=()
+ if [ -f "$CASE_DIR/cflags" ]; then
while IFS= read -r line; do
for tok in $line; do
- [ -n "$tok" ] && case_cflags+=("$tok")
+ [ -n "$tok" ] && CASE_CFLAGS+=("$tok")
done
- done < "$case_dir/cflags"
+ done < "$CASE_DIR/cflags"
fi
- # Collect source files (.c and .S; clang -c accepts both). For each
- # candidate, prefer the per-arch variant (entry.aa64.S beats entry.S
- # when TEST_ARCH=aa64) so cases can ship arch-specific entry stubs
- # alongside arch-agnostic shared sources. The bare name is the
- # fallback — existing aa64-only cases keep working.
- tu_srcs=()
- pick_variant() {
- local base="$1" ext="$2"
- if [ -f "$case_dir/${base}.${TEST_ARCH}.${ext}" ]; then
- echo "$case_dir/${base}.${TEST_ARCH}.${ext}"
- elif [ -f "$case_dir/${base}.${ext}" ]; then
- echo "$case_dir/${base}.${ext}"
- else
- echo ""
- fi
- }
+ KERNEL_IMAGE=0
+ [ -f "$CASE_DIR/kernel_image" ] && KERNEL_IMAGE=1
+
+ # Source files (.c and .S). For each candidate, prefer the per-arch variant
+ # (entry.aa64.S beats entry.S when TEST_ARCH=aa64); the bare name is the
+ # fallback.
+ TU_SRCS=()
for spec in entry:S a:S b:S a:c b:c c:c; do
base="${spec%%:*}"; ext="${spec##*:}"
- f="$(pick_variant "$base" "$ext")"
- [ -n "$f" ] && tu_srcs+=("$f")
+ if [ -f "$CASE_DIR/${base}.${TEST_ARCH}.${ext}" ]; then
+ TU_SRCS+=("$CASE_DIR/${base}.${TEST_ARCH}.${ext}")
+ elif [ -f "$CASE_DIR/${base}.${ext}" ]; then
+ TU_SRCS+=("$CASE_DIR/${base}.${ext}")
+ fi
done
- # Linker script + kernel-image markers. The marker file content is a
- # basename (e.g. "kernel.lds"); the harness derives a per-arch
- # variant (kernel.<arch>.lds) first, falling back to the literal.
- linker_script_file=""
- if [ -f "$case_dir/linker_script" ]; then
- ls_base="$(cat "$case_dir/linker_script" | tr -d '[:space:]')"
+ # Linker script (marker content is a basename; derive per-arch variant
+ # first, fall back to literal).
+ LINKER_SCRIPT_FILE=""
+ if [ -f "$CASE_DIR/linker_script" ]; then
+ ls_base="$(tr -d '[:space:]' < "$CASE_DIR/linker_script")"
ls_stem="${ls_base%.*}"
ls_ext="${ls_base##*.}"
- if [ -f "$case_dir/${ls_stem}.${TEST_ARCH}.${ls_ext}" ]; then
- linker_script_file="$case_dir/${ls_stem}.${TEST_ARCH}.${ls_ext}"
- elif [ -f "$case_dir/${ls_base}" ]; then
- linker_script_file="$case_dir/${ls_base}"
+ if [ -f "$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}" ]; then
+ LINKER_SCRIPT_FILE="$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}"
+ elif [ -f "$CASE_DIR/${ls_base}" ]; then
+ LINKER_SCRIPT_FILE="$CASE_DIR/${ls_base}"
fi
fi
- kernel_image=0
- [ -f "$case_dir/kernel_image" ] && kernel_image=1
-
- # kernel_image cases need an arch-specific entry stub. If the case
- # ships no entry.<arch>.S (and no bare entry.S that happens to
- # build), the case is structurally inapplicable to this arch and
- # the harness skips it.
- if [ $kernel_image -eq 1 ] && \
- [ ! -f "$case_dir/entry.${TEST_ARCH}.S" ] && \
- [ ! -f "$case_dir/entry.S" ]; then
- note_skip "$name" "kernel_image: no entry.${TEST_ARCH}.S in case"
- continue
+
+ # kernel_image cases need an arch-specific entry stub. If the case ships no
+ # entry.<arch>.S (and no bare entry.S), it's structurally inapplicable here.
+ if [ $KERNEL_IMAGE -eq 1 ] && \
+ [ ! -f "$CASE_DIR/entry.${TEST_ARCH}.S" ] && \
+ [ ! -f "$CASE_DIR/entry.S" ]; then
+ CF_SKIP_CASE="kernel_image: no entry.${TEST_ARCH}.S in case"
+ return
fi
- # ---- compile with clang cross ------------------------------------------
+ # ---- compile with clang cross -----------------------------------------
+ COMPILE_OK=1
if [ $have_clang_cross -eq 0 ]; then
- note_skip "$name/R" "no $TEST_ARCH clang"
- [ $jit_only -eq 0 ] && note_skip "$name/E" "no $TEST_ARCH clang"
- note_skip "$name/J" "no $TEST_ARCH clang"
- continue
+ # Original emits per-path SKIP lines (R, optionally E, J) and continues.
+ cf_skip "$NAME/R" "no $TEST_ARCH clang"
+ [ $JIT_ONLY -eq 0 ] && cf_skip "$NAME/E" "no $TEST_ARCH clang"
+ cf_skip "$NAME/J" "no $TEST_ARCH clang"
+ CF_SKIP_NA_CASE=1
+ return
fi
- obj_files=(); compile_ok=1
- for src in "${tu_srcs[@]}"; do
+ OBJ_FILES=()
+ for src in "${TU_SRCS[@]}"; do
base="$(basename "$src")"; base="${base%.c}"; base="${base%.S}"
- obj="$work/${base}.o"
+ obj="$CF_WORK/${base}.o"
if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \
-fno-PIC -fno-pie -fcommon \
- "${case_cflags[@]}" \
- -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then
- compile_ok=0; break
+ "${CASE_CFLAGS[@]}" \
+ -c "$src" -o "$obj" 2>"$CF_WORK/compile_${base}.err"; then
+ COMPILE_OK=0; break
fi
- obj_files+=("$obj")
+ OBJ_FILES+=("$obj")
done
- if [ $compile_ok -eq 0 ]; then
- note_fail "$name (compile failed)"
- continue
+ if [ $COMPILE_OK -eq 0 ]; then
+ cf_fail "$NAME (compile failed)"
+ CF_SKIP_NA_CASE=1
+ return
fi
- # ---- build archive from b.o if requested --------------------------------
+ # ---- build archive from b.o if requested ------------------------------
# Roundtrip always uses raw .o files; exec/JIT use the archive.
- rt_obj_files=("${obj_files[@]}") # for roundtrip: all .o files
- link_obj_files=() # for exec/JIT
- link_arc_flags=()
-
- for o in "${obj_files[@]}"; do
+ RT_OBJ_FILES=("${OBJ_FILES[@]}") # for roundtrip: all .o files
+ LINK_OBJ_FILES=() # for exec/JIT
+ LINK_ARC_FLAGS=()
+ for o in "${OBJ_FILES[@]}"; do
base="$(basename "$o" .o)"
- if [ "$base" = "b" ] && [ "$archive_mode" != "none" ]; then
+ if [ "$base" = "b" ] && [ "$ARCHIVE_MODE" != "none" ]; then
if [ $have_ar -eq 1 ]; then
- arc="$work/b.a"
+ arc="$CF_WORK/b.a"
"$AR_BIN" rcs "$arc" "$o" 2>/dev/null
- if [ "$archive_mode" = "whole" ]; then
- link_arc_flags+=(--whole-archive --archive "$arc")
+ if [ "$ARCHIVE_MODE" = "whole" ]; then
+ LINK_ARC_FLAGS+=(--whole-archive --archive "$arc")
else
- link_arc_flags+=(--archive "$arc")
+ LINK_ARC_FLAGS+=(--archive "$arc")
fi
else
- # no ar: just pass .o directly (archive test degrades to obj test)
- link_obj_files+=("$o")
+ LINK_OBJ_FILES+=("$o")
fi
else
- link_obj_files+=("$o")
+ LINK_OBJ_FILES+=("$o")
fi
done
-
- # ---- Path R: roundtrip --------------------------------------------------
- if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ] && [ $kernel_image -eq 0 ]; then
- if [ $have_roundtrip -eq 1 ] && [ $have_dump -eq 1 ] && [ $have_python3 -eq 1 ]; then
- t0=$(now_ms)
- r_ok=1
- for obj in "${rt_obj_files[@]}"; do
- base="$(basename "$obj" .o)"
- rt="$work/${base}_rt.o"
- if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt_${base}.err"; then
- r_ok=0; break
- fi
- # ELF: pipe `readelf -aW` through normalize.py filter.
- # Mach-O: normalize.py runs llvm-readobj on the file
- # itself (it knows the right flag set).
- if [ "$CFREE_TEST_OBJ" = "macho" ]; then
- python3 "$NORMALIZE" "$obj" \
- >"$work/${base}_golden.norm" 2>/dev/null
- python3 "$NORMALIZE" "$rt" \
- >"$work/${base}_rt.norm" 2>/dev/null
- else
- "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \
- >"$work/${base}_golden.norm" 2>/dev/null
- "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \
- >"$work/${base}_rt.norm" 2>/dev/null
- fi
- if ! diff -u "$work/${base}_golden.norm" \
- "$work/${base}_rt.norm" \
- >"$work/${base}_diff.txt" 2>&1; then
- r_ok=0; break
- fi
- done
- dt=$(( $(now_ms) - t0 )); T_R=$(( T_R + dt ))
- if [ $r_ok -eq 1 ]; then note_pass "$name/R (${dt}ms)"
- else note_fail "$name/R"; fi
- else
- note_skip "$name/R" "missing roundtrip/dump-tool/python3"
- fi
+}
+
+# ---- cases-corpus lanes ----------------------------------------------------
+
+# R — roundtrip + golden structural diff. Skipped (no record) for jit_only and
+# kernel_image cases, matching the original guards.
+cf_lane_R() {
+ [ $JIT_ONLY -eq 1 ] && return
+ [ $KERNEL_IMAGE -eq 1 ] && return
+ if [ $have_roundtrip -eq 1 ] && [ $have_dump -eq 1 ] && [ $have_python3 -eq 1 ]; then
+ local t0; t0=$(cf_now_ms)
+ local r_ok=1 obj base rt
+ for obj in "${RT_OBJ_FILES[@]}"; do
+ base="$(basename "$obj" .o)"
+ rt="$CF_WORK/${base}_rt.o"
+ if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$CF_WORK/rt_${base}.err"; then
+ r_ok=0; break
+ fi
+ if [ "$CFREE_TEST_OBJ" = "macho" ]; then
+ python3 "$NORMALIZE" "$obj" >"$CF_WORK/${base}_golden.norm" 2>/dev/null
+ python3 "$NORMALIZE" "$rt" >"$CF_WORK/${base}_rt.norm" 2>/dev/null
+ else
+ "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \
+ >"$CF_WORK/${base}_golden.norm" 2>/dev/null
+ "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \
+ >"$CF_WORK/${base}_rt.norm" 2>/dev/null
+ fi
+ if ! diff -u "$CF_WORK/${base}_golden.norm" "$CF_WORK/${base}_rt.norm" \
+ >"$CF_WORK/${base}_diff.txt" 2>&1; then
+ r_ok=0; break
+ fi
+ done
+ cf_time R "$(( $(cf_now_ms) - t0 ))"
+ if [ $r_ok -eq 1 ]; then cf_pass "$NAME/R"
+ else cf_fail "$NAME/R"; fi
+ else
+ cf_skip "$NAME/R" "missing roundtrip/dump-tool/python3"
+ fi
+}
+
+# E — link now; defer exec (batched) or run kernel synchronously. Branch
+# precedence mirrors the original if/elif/elif: the e_targets SKIP-NA check
+# (non-kernel, inapplicable tuple) fires first regardless of jit_only or runner
+# availability; then jit_only cases produce no E record; then the link path.
+cf_lane_E() {
+ # e_targets SKIP-NA (uncounted) for non-kernel cases on inapplicable tuples.
+ if [ $E_APPLICABLE -eq 0 ] && [ $KERNEL_IMAGE -eq 0 ]; then
+ printf ' SKIP-NA %s/E — N/A on %s\n' "$NAME" "$CUR_TUPLE"
+ cf_skip_na "$NAME/E"
+ return
+ fi
+ # jit_only cases skip E entirely (no record).
+ [ $JIT_ONLY -eq 1 ] && return
+ if [ $have_exe_runner -eq 0 ]; then
+ cf_skip "$NAME/E" "no link-exe-runner"
+ return
fi
- # ---- Path E: exec -------------------------------------------------------
- # Two stages: link now (per case), then run later in a single batched
- # podman invocation. Kernel-image cases are an exception — they use
- # qemu-system-aarch64 and run inline.
- if [ $RUN_E -eq 1 ] && [ $e_applicable -eq 0 ] && [ $kernel_image -eq 0 ]; then
- printf ' %s %s/E — N/A on %s\n' \
- "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE"
- elif [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ] && \
- [ $e_applicable -eq 1 ]; then
- t0=$(now_ms)
- script_flags=()
- if [ -n "$linker_script_file" ]; then
- script_flags=(--linker-script "$linker_script_file")
- fi
+ local t0; t0=$(cf_now_ms)
+ local script_flags=()
+ [ -n "$LINKER_SCRIPT_FILE" ] && script_flags=(--linker-script "$LINKER_SCRIPT_FILE")
+
+ local exe="$CF_WORK/linked.exe"
+ local link_cmd
+ if [ $KERNEL_IMAGE -eq 1 ]; then
+ # Freestanding kernel image: no harness start.o; the case's own entry.S
+ # is the program entry.
+ link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \
+ "${script_flags[@]}" -o "$exe" \
+ "${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}")
+ elif [ $have_start_obj -eq 1 ]; then
+ link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \
+ "${script_flags[@]}" -o "$exe")
+ [ ${#MACHO_DSO_ARGS[@]} -gt 0 ] && link_cmd+=("${MACHO_DSO_ARGS[@]}")
+ link_cmd+=("${LINK_OBJ_FILES[@]}" "$START_OBJ" "${LINK_ARC_FLAGS[@]}")
+ else
+ cf_skip "$NAME/E" "no cached start.o"
+ return
+ fi
- exe="$work/linked.exe"
- if [ $kernel_image -eq 1 ]; then
- # Freestanding kernel image: no harness start.o, no startup
- # crt; the case's own entry.S is the program entry.
- link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \
- "${script_flags[@]}" -o "$exe" \
- "${link_obj_files[@]}" "${link_arc_flags[@]}")
- elif [ $have_start_obj -eq 1 ]; then
- link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \
- "${script_flags[@]}" -o "$exe")
- [ ${#MACHO_DSO_ARGS[@]} -gt 0 ] && \
- link_cmd+=("${MACHO_DSO_ARGS[@]}")
- link_cmd+=("${link_obj_files[@]}" "$START_OBJ" \
- "${link_arc_flags[@]}")
+ if ! "${link_cmd[@]}" >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then
+ cf_time E "$(( $(cf_now_ms) - t0 ))"
+ cf_fail "$NAME/E" "link failed"
+ elif [ $KERNEL_IMAGE -eq 1 ]; then
+ if ! exec_kernel_supported "$TEST_ARCH"; then
+ cf_time E "$(( $(cf_now_ms) - t0 ))"
+ cf_skip "$NAME/E" "no qemu-system-* for $TEST_ARCH"
else
- note_skip "$name/E" "no cached start.o"
- continue
+ exec_kernel_run "$TEST_ARCH" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err"
+ cf_time E "$(( $(cf_now_ms) - t0 ))"
+ if [ "$RUN_RC" -eq "$CF_EXPECTED" ]; then cf_pass "$NAME/E"
+ else cf_fail "$NAME/E" "expected $CF_EXPECTED got $RUN_RC"; fi
fi
+ elif [ $have_runner -eq 1 ]; then
+ cf_time E "$(( $(cf_now_ms) - t0 ))"
+ # Defer to the batched flush. Payload carries the gc symbol-check sets,
+ # space-joined per list and '|'-separated (absent|present), verified
+ # post-flush by link_flush_verify.
+ local gca="" gcp="" s
+ for s in "${GC_ABSENT_SYMS[@]:-}"; do [ -n "$s" ] && gca="${gca:+$gca }$s"; done
+ for s in "${GC_PRESENT_SYMS[@]:-}"; do [ -n "$s" ] && gcp="${gcp:+$gcp }$s"; done
+ cf_queue_e "$NAME/E" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err" \
+ "$CF_WORK/exec.rc" "$CF_EXPECTED" "$EXEC_TAG" "${gca}|${gcp}|$exe"
+ else
+ cf_skip "$NAME/E" "no runner (qemu/podman)"
+ fi
+}
+
+# J — JIT runner + inline gc symbol checks. Skipped (no record) for kernel.
+cf_lane_J() {
+ [ $KERNEL_IMAGE -eq 1 ] && return
+ # j_targets SKIP-NA (uncounted) on inapplicable tuples.
+ if [ $J_APPLICABLE -eq 0 ]; then
+ printf ' SKIP-NA %s/J — N/A on %s\n' "$NAME" "$CUR_TUPLE"
+ cf_skip_na "$NAME/J"
+ return
+ fi
+ if [ $have_jit_runner -eq 0 ]; then
+ cf_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
+ return
+ fi
- if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- note_fail "$name/E (link failed, ${dt}ms)"
- elif [ $kernel_image -eq 1 ]; then
- if ! exec_kernel_supported "$TEST_ARCH"; then
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- note_skip "$name/E" "no qemu-system-* for $TEST_ARCH"
- else
- exec_kernel_run "$TEST_ARCH" "$exe" \
- "$work/exec.out" "$work/exec.err"
- dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt ))
- if [ "$RUN_RC" -eq "$expected" ]; then
- note_pass "$name/E (${dt}ms)"
- else
- note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"
- fi
- fi
- elif [ $have_runner -eq 1 ]; then
- # Queue for the post-loop batched flush. Per-case wall-clock
- # for the run portion is amortized; we record link-time only.
- link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt ))
- E_NAMES+=("$name")
- E_WORK+=("$work")
- E_EXE+=("$exe")
- E_LINK_MS+=("$link_dt")
- E_EXPECTED+=("$expected")
- # Newline-join the gc symbol lists so we can stash multi-element
- # data inside scalar array slots (bash arrays-of-arrays don't
- # exist). Empty string = no checks.
- gca=""
- for s in "${gc_absent_syms[@]:-}"; do [ -n "$s" ] && gca="${gca}${s}"$'\n'; done
- gcp=""
- for s in "${gc_present_syms[@]:-}"; do [ -n "$s" ] && gcp="${gcp}${s}"$'\n'; done
- E_GC_ABSENT_LIST+=("$gca")
- E_GC_PRESENT_LIST+=("$gcp")
- exec_target_queue "$EXEC_TAG" "$name" "$exe" \
- "$work/exec.out" "$work/exec.err" "$work/exec.rc"
+ local t0; t0=$(cf_now_ms)
+ local jit_cmd=("$JIT_RUNNER" "${EXTRA_FLAGS[@]}")
+ [ $USE_RESOLVER -eq 1 ] && jit_cmd+=(--use-resolver)
+ [ -n "$LINKER_SCRIPT_FILE" ] && jit_cmd+=(--linker-script "$LINKER_SCRIPT_FILE")
+ jit_cmd+=("${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}")
+
+ "${jit_cmd[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err"
+ local j_rc=$? sym
+ # gc_absent / gc_present checks via the jit_runner's --check-* flags.
+ for sym in "${GC_ABSENT_SYMS[@]:-}"; do
+ [ -z "$sym" ] && continue
+ if "${jit_cmd[@]}" --check-absent "$sym" \
+ >"$CF_WORK/jit_gc.out" 2>"$CF_WORK/jit_gc.err"; then
+ :
else
- note_skip "$name/E" "no runner (qemu/podman)"
+ j_rc=$?
+ break
fi
- elif [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $e_applicable -eq 1 ]; then
- note_skip "$name/E" "no link-exe-runner"
- fi
-
- # ---- Path J: JIT --------------------------------------------------------
- if [ $RUN_J -eq 1 ] && [ $j_applicable -eq 0 ] && [ $kernel_image -eq 0 ]; then
- printf ' %s %s/J — N/A on %s\n' \
- "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE"
- elif [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ] && [ $kernel_image -eq 0 ]; then
- t0=$(now_ms)
- jit_cmd=("$JIT_RUNNER" "${extra_flags[@]}")
- [ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver)
- [ -n "$linker_script_file" ] && jit_cmd+=(--linker-script "$linker_script_file")
- jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}")
-
- "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err"
- j_rc=$?
-
- # For gc_sections cases, additionally verify the absent and
- # present symbol sets via the jit_runner's --check-* flags.
- # Re-run jit_runner once per symbol; first failure wins.
- for sym in "${gc_absent_syms[@]}"; do
- if "${jit_cmd[@]}" --check-absent "$sym" \
- >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then
- : # absent check passed
+ done
+ if [ "$j_rc" -eq "$CF_EXPECTED" ]; then
+ for sym in "${GC_PRESENT_SYMS[@]:-}"; do
+ [ -z "$sym" ] && continue
+ if "${jit_cmd[@]}" --check-present "$sym" \
+ >"$CF_WORK/jit_gc.out" 2>"$CF_WORK/jit_gc.err"; then
+ :
else
j_rc=$?
break
fi
done
- if [ "$j_rc" -eq "$expected" ]; then
- for sym in "${gc_present_syms[@]}"; do
- if "${jit_cmd[@]}" --check-present "$sym" \
- >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then
- : # present check passed
- else
- j_rc=$?
- break
- fi
- done
+ fi
+ cf_time J "$(( $(cf_now_ms) - t0 ))"
+ if [ "$j_rc" -eq "$CF_EXPECTED" ]; then cf_pass "$NAME/J"
+ else cf_fail "$NAME/J" "expected $CF_EXPECTED got $j_rc"; fi
+}
+
+# ---- deferred-E flush verification (gc symbol presence/absence) ------------
+# Called per queued-E item after the batched exec flush, ONLY when rc==expected
+# (the engine's rc check passed). Runs the gc symbol presence/absence checks via
+# llvm-nm (format-agnostic). Payload = "<absent syms>|<present syms>|<exe>".
+# Returns 0 to keep the engine's PASS, 1 to flip it to FAIL — we do NOT call
+# cf_fail ourselves (the engine records the single verdict).
+link_flush_verify() {
+ local payload="$2"
+ local gca="${payload%%|*}"; payload="${payload#*|}"
+ local gcp="${payload%%|*}"; local exe="${payload#*|}"
+ { [ -z "$gca" ] && [ -z "$gcp" ]; } && return 0
+ [ $have_nm -eq 1 ] || return 0
+
+ # Write the symbol dump next to the exe (its dir is the persistent per-item
+ # CF_WORK; parallel-safe).
+ local syms="$exe.syms"
+ "$NM_BIN" "$exe" >"$syms" 2>/dev/null
+ [ -s "$syms" ] || return 0
+
+ # llvm-nm format: "<addr> <type> <name>". `U` = undefined. On Mach-O the
+ # asm form of source `name` is `_name`; accept either.
+ local sym
+ for sym in $gca; do
+ if awk -v s="$sym" -v u="_$sym" \
+ '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then
+ return 1 # gc_absent symbol unexpectedly present
fi
-
- dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt ))
- if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)"
- else note_fail "$name/J (expected $expected, got $j_rc, ${dt}ms)"; fi
- elif [ $RUN_J -eq 1 ] && [ $kernel_image -eq 0 ]; then
- note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
+ done
+ for sym in $gcp; do
+ if ! awk -v s="$sym" -v u="_$sym" \
+ '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then
+ return 1 # gc_present symbol missing
+ fi
+ done
+ return 0
+}
+
+# ---- bad/ negative lanes ---------------------------------------------------
+# Sources compile cleanly but the linker (E) and JIT (J) must reject: non-zero
+# exit with no signal AND stderr contains the substring from `expect`.
+
+bad_read_case() {
+ CASE_DIR="${CF_SRC%/}"
+ NAME="bad/$CF_BASE"
+
+ EXPECT_FILE="$CASE_DIR/expect"
+ if [ ! -f "$EXPECT_FILE" ]; then
+ cf_fail "$NAME (missing $EXPECT_FILE)"
+ CF_SKIP_NA_CASE=1
+ return
fi
+ EXPECT="$(cat "$EXPECT_FILE")"
-done
-
-# ---- bad/ — negative tests -------------------------------------------------
-# Sources that compile cleanly but should cause the linker (E) and JIT (J)
-# to reject. Pass = runner exits non-zero with no signal AND stderr
-# contains the substring from `expect`. No marker files; the bad/
-# directory itself is the marker.
-
-for case_dir in "$TEST_DIR/bad"/*/; do
- [ -d "$case_dir" ] || continue
- name="bad/$(basename "$case_dir")"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- work="$BUILD_DIR/link/$name"
- mkdir -p "$work"
-
- expect_file="$case_dir/expect"
- if [ ! -f "$expect_file" ]; then
- note_fail "$name (missing $expect_file)"; continue
+ if [ $have_clang_cross -eq 0 ]; then
+ cf_skip "$NAME" "no $TEST_ARCH clang"
+ CF_SKIP_NA_CASE=1
+ return
fi
- expect="$(cat "$expect_file")"
- if [ $have_clang_cross -eq 0 ]; then note_skip "$name" "no $TEST_ARCH clang"; continue; fi
-
- tu_srcs=()
- for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do
- [ -f "$f" ] && tu_srcs+=("$f")
+ TU_SRCS=()
+ for f in "$CASE_DIR/a.c" "$CASE_DIR/b.c" "$CASE_DIR/c.c"; do
+ [ -f "$f" ] && TU_SRCS+=("$f")
done
- obj_files=(); compile_ok=1
- for src in "${tu_srcs[@]}"; do
+ OBJ_FILES=(); COMPILE_OK=1
+ for src in "${TU_SRCS[@]}"; do
base="$(basename "$src" .c)"
- obj="$work/${base}.o"
+ obj="$CF_WORK/${base}.o"
if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \
-fno-PIC -fno-pie -fcommon \
- -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then
- compile_ok=0; break
+ -c "$src" -o "$obj" 2>"$CF_WORK/compile_${base}.err"; then
+ COMPILE_OK=0; break
fi
- obj_files+=("$obj")
+ OBJ_FILES+=("$obj")
done
- if [ $compile_ok -eq 0 ]; then
- note_fail "$name (compile failed; sources should compile, only link is expected to fail)"
- continue
+ if [ $COMPILE_OK -eq 0 ]; then
+ cf_fail "$NAME (compile failed; sources should compile, only link is expected to fail)"
+ CF_SKIP_NA_CASE=1
+ return
fi
+}
- # Path E (negative). Linker is expected to fail; no exe is run, so
- # this case never enters the podman queue.
- if [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then
- if [ $have_start_obj -eq 0 ]; then
- note_skip "$name/E" "no cached start.o"
+cf_lane_BE() { # bad/ Path E (negative)
+ if [ $have_exe_runner -eq 0 ]; then
+ cf_skip "$NAME/E" "no link-exe-runner"
+ return
+ fi
+ if [ $have_start_obj -eq 0 ]; then
+ cf_skip "$NAME/E" "no cached start.o"
+ return
+ fi
+ local exe="$CF_WORK/linked.exe"
+ if "$LINK_EXE_RUNNER" -o "$exe" "${OBJ_FILES[@]}" "$START_OBJ" \
+ >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then
+ cf_fail "$NAME/E" "linker succeeded; expected non-zero exit"
+ else
+ local rc=$?
+ if [ $rc -ge 128 ]; then
+ cf_fail "$NAME/E" "linker died via signal $((rc-128))"
+ elif ! grep -qF -- "$EXPECT" "$CF_WORK/link.err"; then
+ cf_fail "$NAME/E" "stderr did not contain: $EXPECT"
+ sed 's/^/ | /' "$CF_WORK/link.err"
else
- exe="$work/linked.exe"
- if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$START_OBJ" \
- >"$work/link.out" 2>"$work/link.err"; then
- note_fail "$name/E (linker succeeded; expected non-zero exit)"
- else
- rc=$?
- if [ $rc -ge 128 ]; then
- note_fail "$name/E (linker died via signal $((rc-128)))"
- elif ! grep -qF -- "$expect" "$work/link.err"; then
- note_fail "$name/E (stderr did not contain: $expect)"
- sed 's/^/ | /' "$work/link.err"
- else
- note_pass "$name/E"
- fi
- fi
+ cf_pass "$NAME/E"
fi
- elif [ $RUN_E -eq 1 ]; then
- note_skip "$name/E" "no link-exe-runner"
fi
+}
- # Path J
- if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ]; then
- if "$JIT_RUNNER" "${obj_files[@]}" >"$work/jit.out" 2>"$work/jit.err"; then
- note_fail "$name/J (jit-runner succeeded; expected non-zero exit)"
+cf_lane_BJ() { # bad/ Path J (negative)
+ if [ $have_jit_runner -eq 0 ]; then
+ cf_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
+ return
+ fi
+ if "$JIT_RUNNER" "${OBJ_FILES[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err"; then
+ cf_fail "$NAME/J" "jit-runner succeeded; expected non-zero exit"
+ else
+ local rc=$?
+ if [ $rc -ge 128 ]; then
+ cf_fail "$NAME/J" "jit-runner died via signal $((rc-128))"
+ elif ! grep -qF -- "$EXPECT" "$CF_WORK/jit.err"; then
+ cf_fail "$NAME/J" "stderr did not contain: $EXPECT"
+ sed 's/^/ | /' "$CF_WORK/jit.err"
else
- rc=$?
- if [ $rc -ge 128 ]; then
- note_fail "$name/J (jit-runner died via signal $((rc-128)))"
- elif ! grep -qF -- "$expect" "$work/jit.err"; then
- note_fail "$name/J (stderr did not contain: $expect)"
- sed 's/^/ | /' "$work/jit.err"
- else
- note_pass "$name/J"
- fi
+ cf_pass "$NAME/J"
fi
- elif [ $RUN_J -eq 1 ]; then
- note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)"
fi
-done
-
-# ---- batched path-E flush + verification -----------------------------------
-# Run every queued user case in a single podman invocation, then iterate
-# the queue to read each exit code and emit PASS/FAIL. R and J results
-# already printed inline above; E results print together here.
-
-T_E_BATCH=0
-if [ "$(exec_target_queue_size)" -gt 0 ]; then
- printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)"
- t0=$(now_ms)
- exec_target_flush
- T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
-
- i=0
- while [ $i -lt ${#E_NAMES[@]} ]; do
- name="${E_NAMES[$i]}"
- work="${E_WORK[$i]}"
- exe="${E_EXE[$i]}"
- link_dt="${E_LINK_MS[$i]}"
- expected="${E_EXPECTED[$i]}"
-
- if [ ! -f "$work/exec.rc" ]; then
- note_fail "$name/E (no rc; podman batch did not produce results)"
- i=$((i+1)); continue
- fi
- RUN_RC="$(cat "$work/exec.rc")"
-
- e_ok=1
- e_reported=0
- if [ "$RUN_RC" -ne "$expected" ]; then
- e_ok=0
- note_fail "$name/E (expected $expected, got $RUN_RC, link ${link_dt}ms)"
- e_reported=1
- fi
+}
- # Symbol presence/absence checks via llvm-nm — format-agnostic
- # (ELF + Mach-O). On Mach-O the on-disk name carries the
- # leading `_` mangle byte; the awk filter accepts either the
- # bare source-level name (ELF) or the `_`-prefixed form (Mach-O)
- # to match the same gc_present / gc_absent entry across formats.
- if [ $e_ok -eq 1 ] && [ $have_nm -eq 1 ] && \
- { [ -n "${E_GC_ABSENT_LIST[$i]}" ] || \
- [ -n "${E_GC_PRESENT_LIST[$i]}" ]; }; then
- "$NM_BIN" "$exe" >"$work/exec_syms.txt" 2>/dev/null
- if [ -s "$work/exec_syms.txt" ]; then
- # llvm-nm format: "<addr> <type> <name>". `U` = undefined.
- # On Mach-O, `_name` is the asm form of source `name`.
- while IFS= read -r sym; do
- [ -z "$sym" ] && continue
- if awk -v s="$sym" -v u="_$sym" \
- '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' \
- "$work/exec_syms.txt"; then
- e_ok=0
- note_fail "$name/E gc_absent: '$sym' present"
- e_reported=1
- break
- fi
- done <<< "${E_GC_ABSENT_LIST[$i]}"
- if [ $e_ok -eq 1 ]; then
- while IFS= read -r sym; do
- [ -z "$sym" ] && continue
- if ! awk -v s="$sym" -v u="_$sym" \
- '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' \
- "$work/exec_syms.txt"; then
- e_ok=0
- note_fail "$name/E gc_present: '$sym' missing"
- e_reported=1
- break
- fi
- done <<< "${E_GC_PRESENT_LIST[$i]}"
- fi
- fi
- fi
-
- if [ $e_ok -eq 1 ]; then
- note_pass "$name/E (link ${link_dt}ms)"
- elif [ $e_reported -eq 0 ]; then
- note_fail "$name/E (e_ok=0, no specific reason captured)"
- fi
- i=$((i+1))
- done
-fi
-
-# ---- summary ---------------------------------------------------------------
-
-if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
- printf '\nFailed:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done
-fi
+# ---- drive the corpora -----------------------------------------------------
+printf 'Running cases...\n'
-if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then
- printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n'
- for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done
+PAR="${CFREE_LINK_PARALLEL:-0}"
+
+# Active lanes for the cases corpus, in REJ order.
+CASE_LANES=
+[ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R"
+[ "$RUN_E" -eq 1 ] && CASE_LANES="$CASE_LANES E"
+[ "$RUN_J" -eq 1 ] && CASE_LANES="$CASE_LANES J"
+
+CF_LABEL=test-link CF_BUILD_DIR="$BUILD_DIR/link" \
+ CF_CORPUS_GLOBS="$TEST_DIR/cases/*/" CF_CORPUS_EXT="" CF_SIDECAR_DIR="$TEST_DIR/cases" \
+ CF_LANES="$CASE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_READ_CASE=link_read_case CF_FLUSH_VERIFY=link_flush_verify \
+ CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+# bad/ negative corpus — E and J only (no R).
+BAD_LANES=
+[ "$RUN_E" -eq 1 ] && BAD_LANES="$BAD_LANES BE"
+[ "$RUN_J" -eq 1 ] && BAD_LANES="$BAD_LANES BJ"
+
+if [ -n "$BAD_LANES" ]; then
+ CF_LABEL=test-link CF_BUILD_DIR="$BUILD_DIR/link/bad" \
+ CF_CORPUS_GLOBS="$TEST_DIR/bad/*/" CF_CORPUS_EXT="" CF_SIDECAR_DIR="$TEST_DIR/bad" \
+ CF_LANES="$BAD_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \
+ CF_TARGETS_EXT="" CF_READ_CASE=bad_read_case \
+ CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
fi
-printf '\n'
-printf 'Results: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: R=%dms E=%dms (batch %dms) J=%dms\n' \
- "$T_R" "$T_E" "$T_E_BATCH" "$T_J"
-
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-link
+cf_exit
diff --git a/test/link/rv64_jit_test.c b/test/link/rv64_jit_test.c
@@ -41,6 +41,8 @@
#include <sys/mman.h>
#include <unistd.h>
+#include "lib/cfree_unit.h"
+
/* Native execution requires the host CPU to be rv64 (any OS that gives
* us POSIX mmap + mprotect, which on rv64 means Linux today). Anywhere
* else we still build the JIT image but skip the call. */
@@ -50,34 +52,8 @@
#define RV64_HOST_NATIVE 0
#endif
-/* ---- host glue (heap + diag, copied from other test runners) ---- */
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- (void)s;
- (void)loc;
- fprintf(stderr, "diag %d: ", (int)k);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
+/* ---- host glue: heap + diag come from the shared CfreeUnit ---- */
+static CfreeUnit g_u;
/* ---- execmem with W^X dual-mapping (mirrors test/link/harness) ---- */
static int xm_to_posix(int p) {
@@ -215,22 +191,14 @@ int main(void) {
if (ps > 0) g_execmem.page_size = (size_t)ps;
}
- CfreeTarget target;
- memset(&target, 0, sizeof(target));
- target.arch = CFREE_ARCH_RV64;
- target.os = CFREE_OS_LINUX;
- target.obj = CFREE_OBJ_ELF;
- target.ptr_size = 8;
- target.ptr_align = 8;
+ CfreeTarget target =
+ cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX, CFREE_OBJ_ELF);
- CfreeContext ctx;
- memset(&ctx, 0, sizeof(ctx));
- ctx.heap = &g_heap;
- ctx.diag = &g_diag;
- ctx.now = -1;
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
CfreeCompiler* c = NULL;
- if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) {
+ if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) {
fprintf(stderr, "rv64_jit_test: compiler_new failed\n");
return 2;
}
diff --git a/test/objcopy/run.sh b/test/objcopy/run.sh
@@ -1,69 +1,26 @@
#!/bin/sh
-# Driver-level `cfree objcopy` test harness. Same shape as test/ar/run.sh.
+# Driver-level `cfree objcopy` test harness. Shared loop/reporting in
+# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and
+# diff stdout+stderr against cases/<name>.expected.
set -u
script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree objcopy-driver
-if [ ! -x "$CFREE" ]; then
- echo "objcopy-driver: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-objcopy-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
-
-pass=0
-fail=0
-failures=
-
+CF_WORK=$(cf_workdir objcopy)
+cf_report_init
for sh in "$cases_dir"/*.sh; do
[ -e "$sh" ] || continue
name=$(basename "${sh%.sh}")
- expected="${sh%.sh}.expected"
- actual="$work_root/$name.actual"
-
- if [ ! -e "$expected" ]; then
- printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- sandbox="$work_root/$name"
- mkdir -p "$sandbox"
- ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1
- case_rc=$?
-
- if [ "$case_rc" -ne 0 ]; then
- printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- if diff -u "$expected" "$actual" >/dev/null 2>&1; then
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s\n' "$name"
- diff -u "$expected" "$actual" || true
- cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true
- fail=$((fail + 1))
- failures="$failures $name"
- fi
+ cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir"
done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nobjcopy-driver: failures:%s\n' "$failures"
- printf 'objcopy-driver: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\nobjcopy-driver: %d/%d passed\n' "$pass" "$total"
+cf_summary objcopy-driver
+cf_exit
diff --git a/test/objdump/run.sh b/test/objdump/run.sh
@@ -1,93 +1,35 @@
#!/bin/sh
# Driver-level golden tests for linked-image inspection. Mostly `cfree
-# objdump`, plus the inherited tools (e.g. `size`) that read the same
-# image view; each case script picks its own subcommand.
+# objdump`, plus inherited tools (e.g. `size`) that read the same image view;
+# each case script picks its own subcommand.
#
-# Per-arch subdirectories (test/objdump/<arch>/cases/) hold:
-# <name>.sh — script invoked with CFREE and a per-case sandbox
-# <name>.expected — expected stdout
-#
-# Each script is run in its own work directory; stdout is diffed against
-# the .expected file. Mirrors the test/strip/, test/objcopy/, test/ar/
-# harness structure so failures are localized and goldens are diffable.
+# Per-arch subdirectories test/objdump/<arch>/cases/ hold <name>.sh +
+# <name>.expected. A case whose first output line begins with "SKIP" opts out
+# (e.g. missing llvm-mingw to build a PE fixture). Shared loop/reporting lives
+# in test/lib/cfree_sh_report.sh.
set -u
script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree objdump-driver
-if [ ! -x "$CFREE" ]; then
- echo "objdump-driver: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-objdump-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
-
-pass=0
-fail=0
-skip=0
-failures=
-
+CF_WORK=$(cf_workdir objdump)
+CF_SCENARIO_SKIP=1 # honor leading "SKIP" lines from cases
+cf_report_init
for arch_dir in "$script_dir"/*/; do
[ -d "$arch_dir/cases" ] || continue
arch=$(basename "$arch_dir")
for sh in "$arch_dir/cases"/*.sh; do
[ -e "$sh" ] || continue
name=$(basename "${sh%.sh}")
- expected="${sh%.sh}.expected"
- actual="$work_root/$arch-$name.actual"
-
- if [ ! -e "$expected" ]; then
- printf 'FAIL %s/%s (missing %s)\n' "$arch" "$name" "$(basename "$expected")"
- fail=$((fail + 1))
- failures="$failures $arch/$name"
- continue
- fi
-
- sandbox="$work_root/$arch-$name"
- mkdir -p "$sandbox"
- ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1
- case_rc=$?
-
- # A case that prints a leading "SKIP" line is opting out because a
- # prerequisite is missing (e.g. no llvm-mingw UCRT sysroot to build a
- # PE fixture). Honor it instead of diffing the SKIP text against the
- # golden — same skip-vs-fail convention as the other driver harnesses.
- if [ "$case_rc" -eq 0 ] && head -n1 "$actual" 2>/dev/null | grep -q '^SKIP'; then
- printf 'SKIP %s/%s (%s)\n' "$arch" "$name" \
- "$(head -n1 "$actual" | sed 's/^SKIP[: ]*//')"
- skip=$((skip + 1))
- continue
- fi
-
- if [ "$case_rc" -ne 0 ]; then
- printf 'FAIL %s/%s (script exit=%d)\n' "$arch" "$name" "$case_rc"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $arch/$name"
- continue
- fi
-
- if diff -u "$expected" "$actual" >/dev/null 2>&1; then
- printf 'PASS %s/%s\n' "$arch" "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s/%s\n' "$arch" "$name"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $arch/$name"
- fi
+ cf_scenario_case "$arch/$name" "$sh" "${sh%.sh}.expected"
done
done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nobjdump-driver: failures:%s\n' "$failures"
- printf 'objdump-driver: %d/%d passed (%d skipped)\n' "$pass" "$total" "$skip"
- exit 1
-fi
-printf '\nobjdump-driver: %d/%d passed (%d skipped)\n' "$pass" "$total" "$skip"
+cf_summary objdump-driver
+cf_exit
diff --git a/test/opt/cg_ir_lower_test.c b/test/opt/cg_ir_lower_test.c
@@ -5,6 +5,7 @@
#include <string.h>
#include "cg/ir.h"
+#include "lib/cfree_unit.h"
#include "opt/opt.h"
#undef Operand
@@ -13,54 +14,13 @@
#undef CGCallDesc
#undef CGLocalStorage
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static int g_fails;
-static int g_checks;
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-#define EXPECT(cond, ...) \
- do { \
- ++g_checks; \
- if (!(cond)) { \
- ++g_fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. ctx.now = -1 is
+ * preserved by setting it once after cfree_unit_init in main(). */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
typedef struct TestCtx {
- CfreeContext ctx;
Compiler* c;
CfreeCgTypeId i32;
} TestCtx;
@@ -69,16 +29,9 @@ static void tc_init(TestCtx* tc) {
CfreeTarget target;
CfreeCgBuiltinTypes b;
memset(tc, 0, sizeof *tc);
- tc->ctx.heap = &g_heap;
- tc->ctx.diag = &g_diag;
- tc->ctx.now = -1;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_MACOS;
- target.obj = CFREE_OBJ_MACHO;
- target.ptr_size = 8;
- target.ptr_align = 8;
- if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) !=
+ target =
+ cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO);
+ if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) !=
CFREE_OK ||
!tc->c) {
fprintf(stderr, "fatal: compiler allocation failed\n");
@@ -251,12 +204,14 @@ static void jump_cleanup_threads_empty_fallthrough_target(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
converter_builds_cfg_and_pregs();
jump_cleanup_threads_empty_fallthrough_target();
- if (g_fails) {
- fprintf(stderr, "cg-ir-lower: %d/%d failed\n", g_fails, g_checks);
+ if (g_u.fails) {
+ fprintf(stderr, "cg-ir-lower: %d/%d failed\n", g_u.fails, g_u.checks);
return 1;
}
- printf("cg-ir-lower: %d checks, 0 failures\n", g_checks);
+ printf("cg-ir-lower: %d checks, 0 failures\n", g_u.checks);
return 0;
}
diff --git a/test/opt/tiny_inline_test.c b/test/opt/tiny_inline_test.c
@@ -12,6 +12,8 @@
#include <stdlib.h>
#include <string.h>
+#include "lib/cfree_unit.h"
+
#include "cg/ir.h"
#include "cg/ir_recorder.h"
#include "opt/opt.h"
@@ -23,54 +25,13 @@
#undef CGCallDesc
#undef CGLocalStorage
-static void* h_alloc(CfreeHeap* h, size_t n, size_t a) {
- (void)h;
- (void)a;
- return n ? malloc(n) : NULL;
-}
-
-static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) {
- (void)h;
- (void)o;
- (void)a;
- return realloc(p, n);
-}
-
-static void h_free(CfreeHeap* h, void* p, size_t n) {
- (void)h;
- (void)n;
- free(p);
-}
-
-static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL};
-static int g_fails;
-static int g_checks;
-
-static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc,
- const char* fmt, va_list ap) {
- static const char* names[] = {"note", "warning", "error", "fatal"};
- (void)s;
- (void)loc;
- fprintf(stderr, "%s: ", names[k]);
- vfprintf(stderr, fmt, ap);
- fputc('\n', stderr);
-}
-
-static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0};
-
-#define EXPECT(cond, ...) \
- do { \
- ++g_checks; \
- if (!(cond)) { \
- ++g_fails; \
- fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
- fprintf(stderr, __VA_ARGS__); \
- fputc('\n', stderr); \
- } \
- } while (0)
+/* Shared test context replaces the per-file heap/diag/counter globals;
+ * EXPECT aliases CU_EXPECT so the call sites are unchanged. cfree_unit_init
+ * runs once in main(); tc_init reuses g_u's heap/diag/ctx per compiler. */
+static CfreeUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
typedef struct TestCtx {
- CfreeContext ctx;
Compiler* c;
CfreeCgTypeId i32;
CfreeCgTypeId ptr;
@@ -83,16 +44,8 @@ static void tc_init(TestCtx* tc) {
CfreeCgFuncSig sig;
CfreeCgFuncParam params[1];
memset(tc, 0, sizeof *tc);
- tc->ctx.heap = &g_heap;
- tc->ctx.diag = &g_diag;
- tc->ctx.now = -1;
- memset(&target, 0, sizeof target);
- target.arch = CFREE_ARCH_ARM_64;
- target.os = CFREE_OS_MACOS;
- target.obj = CFREE_OBJ_MACHO;
- target.ptr_size = 8;
- target.ptr_align = 8;
- if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) !=
+ target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO);
+ if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) !=
CFREE_OK ||
!tc->c) {
fprintf(stderr, "fatal: compiler allocation failed\n");
@@ -324,10 +277,13 @@ static void unknown_callee_is_skipped(void) {
}
int main(void) {
+ cfree_unit_init(&g_u);
+ g_u.ctx.now = -1;
tiny_callee_is_inlined();
over_budget_respects_policy();
never_policy_is_refused();
unknown_callee_is_skipped();
- fprintf(stderr, "tiny-inline: %d checks, %d failures\n", g_checks, g_fails);
- return g_fails ? 1 : 0;
+ fprintf(stderr, "tiny-inline: %d checks, %d failures\n", g_u.checks,
+ g_u.fails);
+ return cfree_unit_status(&g_u);
}
diff --git a/test/parse/run.sh b/test/parse/run.sh
@@ -1,21 +1,26 @@
#!/usr/bin/env bash
-# test/parse/run.sh — file-driven C-parser test harness.
+# test/parse/run.sh — file-driven C-parser test harness, on the shared corpus
+# harness (test/lib/cf_corpus.sh).
#
-# For each test/parse/cases/*.c, runs up to six paths (DWARF directives may
-# be added later via .dwarf sidecars):
+# For each test/parse/cases/*.c, runs up to six lanes (CFREE_TEST_PATHS,
+# default DREJ — C and W are opt-in):
#
# D in-process JIT — parse-runner --jit FILE.c → exit code matches
-# expected. No file I/O. aarch64 host only.
+# expected. No file I/O. Host arch must match cross
+# target.
# R ELF roundtrip — parse-runner --emit + cfree-roundtrip + readelf
# normalize diff. Validates emitter+reader fidelity.
# E exec via qemu — parse-runner --emit + start.o → link-exe-runner →
-# qemu/podman → exit code. Cross-host friendly.
-# J jit-via-file — parse-runner --emit + jit-runner. aarch64 host.
+# qemu/podman → exit code. Cross-host friendly. Deferred
+# to a batched exec_target flush (cf_queue_e).
+# J jit-via-file — parse-runner --emit + jit-runner. Host arch must match
+# cross target.
# C emit-c host — parse-runner --emit-c + host cc + test_main wrapper,
# run native. Validates the --emit=c C-source backend.
-# Host arch must match cross target. Cases that hit an
-# unimplemented C-target method are reported as SKIP
-# (not FAIL) so phased backend rollout is tolerated.
+# Host arch must match cross target. opt=0 only. Cases
+# that hit an unimplemented C-target method are reported
+# as SKIP (not FAIL) so phased backend rollout is
+# tolerated.
# W wasm roundtrip — cfree cc -target wasm32-none -c case.c -> .wasm, then
# cfree run -e test_main on it (the lang/wasm frontend
# re-lowers to native CG, JITs, calls test_main).
@@ -29,10 +34,13 @@
#
# Sidecar conventions (each missing file uses the documented default):
# <name>.expected — integer; default 0. Compared mod 256 to test_main.
-# <name>.skip — single-line reason. Treated as failure unless
-# CFREE_TEST_ALLOW_SKIP=1 (matching the rest of
-# the test suite).
-# <name>.wasm.skip — single-line reason; opts the case out of path W only.
+# <name>.skip — single-line reason. Whole-case skip on every arch.
+# <name>.<arch>.skip— single-line reason; whole-case skip on that arch only
+# (e.g. asm_01_grammar.rv64.skip).
+# <name>.cbackend.skip — single-line reason; opts the case out of lane C only.
+# <name>.wasm.skip — single-line reason; opts the case out of lane W only.
+# Skips are treated as failure unless CFREE_TEST_ALLOW_SKIP=1 (matching the
+# rest of the test suite).
#
# Filtering:
# ./run.sh [name_filter] [paths]
@@ -48,11 +56,16 @@
# Parallelism:
# default run in parallel with a capped CPU-count default.
# CFREE_TEST_JOBS=N run up to N cases concurrently.
-# CFREE_TEST_JOBS=auto same as the default.
+# CFREE_PARSE_PARALLEL=0 force serial dispatch.
+# All lane hooks write only under CF_WORK and record via cf_*, so the runner is
+# parallel-safe by construction.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
TEST_DIR="$ROOT/test/parse"
LINK_TEST_DIR="$ROOT/test/link"
BUILD_DIR="$ROOT/build/test"
@@ -65,9 +78,6 @@ LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner"
JIT_RUNNER="$BUILD_DIR/jit-runner"
NORMALIZE="$ROOT/test/elf/normalize.py"
-# shellcheck source=../lib/parallel.sh
-source "$ROOT/test/lib/parallel.sh"
-
# CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the
# pre-multiarch behavior. The C runners read the same env via
# test/lib/cfree_test_target.h.
@@ -92,10 +102,10 @@ fi
CLANG_TARGET="--target=$CLANG_TRIPLE"
CC="${CC:-cc}"
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
FILTER="${1:-${CFREE_TEST_FILTER:-}}"
PATHS="${2:-${CFREE_TEST_PATHS:-DREJ}}"
+export CFREE_TEST_FILTER="$FILTER"
if [ -n "${CFREE_OPT_LEVELS:-}" ]; then
OPT_LEVELS="$CFREE_OPT_LEVELS"
elif [ -n "${CFREE_OPT_LEVEL:-}" ]; then
@@ -115,137 +125,9 @@ case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac
case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac
-T_D=0; T_R=0; T_E=0; T_J=0; T_C=0; T_W=0
-now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; }
mkdir -p "$BUILD_DIR" "$BUILD_DIR/parse"
-TEST_JOBS="$(cfree_parallel_jobs)" || exit 2
-PARALLEL_DIR="$BUILD_DIR/parse.parallel/$$"
-mkdir -p "$PARALLEL_DIR"
-
-PASS=0; FAIL=0; SKIP=0
-FAIL_NAMES=(); SKIP_NAMES=()
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
-
-event_path() { printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2"; }
-worker_stdout_path() { printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2"; }
-worker_stderr_path() { printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2"; }
-
-emit_event() {
- local file="$1" kind="$2"
- shift 2
- printf '%s' "$kind" >> "$file"
- while [ $# -gt 0 ]; do
- printf '\t%s' "$1" >> "$file"
- shift
- done
- printf '\n' >> "$file"
-}
-
-replay_events() {
- local event="$1" stdout_log="$2" stderr_log="$3"
- local kind a b c d e f
-
- if [ ! -s "$event" ]; then
- note_fail "internal: missing worker result $event"
- if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi
- if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi
- return
- fi
-
- while IFS=$'\t' read -r kind a b c d e f; do
- case "$kind" in
- PASS) note_pass "$a" ;;
- FAIL) note_fail "$a" ;;
- SKIP) note_skip "$a" "$b" ;;
- TIME)
- case "$a" in
- D) T_D=$(( T_D + b )) ;;
- R) T_R=$(( T_R + b )) ;;
- E) T_E=$(( T_E + b )) ;;
- J) T_J=$(( T_J + b )) ;;
- C) T_C=$(( T_C + b )) ;;
- W) T_W=$(( T_W + b )) ;;
- esac
- ;;
- QUEUE_E)
- E_NAMES+=("$a")
- E_WORK+=("$b")
- E_LINK_MS+=("$c")
- E_EXPECTED+=("$d")
- T_E=$(( T_E + c ))
- exec_target_queue "$f" "$e" "$b/linked.exe" \
- "$b/exec.out" "$b/exec.err" "$b/exec.rc"
- ;;
- *)
- note_fail "internal: malformed worker event in $event"
- ;;
- esac
- done < "$event"
-}
-
-run_serial_items() {
- local layer="$1" worker="$2"
- shift 2
-
- local idx=0
- local item event stdout_log stderr_log
-
- for item in "$@"; do
- event="$(event_path "$layer" "$idx")"
- stdout_log="$(worker_stdout_path "$layer" "$idx")"
- stderr_log="$(worker_stderr_path "$layer" "$idx")"
- : > "$event"
- : > "$stdout_log"
- : > "$stderr_log"
- "$worker" "$idx" "$item" "$event" > "$stdout_log" 2> "$stderr_log"
- replay_events "$event" "$stdout_log" "$stderr_log"
- idx=$((idx+1))
- done
-}
-
-run_parallel_items() {
- local layer="$1" worker="$2"
- shift 2
-
- local events=()
- local stdout_logs=()
- local stderr_logs=()
- local idx=0
- local item event stdout_log stderr_log
-
- for item in "$@"; do
- event="$(event_path "$layer" "$idx")"
- stdout_log="$(worker_stdout_path "$layer" "$idx")"
- stderr_log="$(worker_stderr_path "$layer" "$idx")"
- : > "$event"
- : > "$stdout_log"
- : > "$stderr_log"
- events+=("$event")
- stdout_logs+=("$stdout_log")
- stderr_logs+=("$stderr_log")
- cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \
- > "$stdout_log" 2> "$stderr_log"
- idx=$((idx+1))
- done
-
- cfree_parallel_wait_all || true
-
- idx=0
- while [ $idx -lt ${#events[@]} ]; do
- replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}"
- idx=$((idx+1))
- done
-}
-
# ---- tool detection (mirrors test/cg/run.sh) -------------------------------
have_clang_cross=0
@@ -253,7 +135,6 @@ have_readelf=0
have_python3=0
have_qemu=0
have_podman=0
-have_runner=0
have_roundtrip=0
have_exe_runner=0
have_jit_runner=0
@@ -269,7 +150,6 @@ command -v python3 >/dev/null 2>&1 && have_python3=1
QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
[ -n "$QEMU_BIN" ] && have_qemu=1
command -v podman >/dev/null 2>&1 && have_podman=1
-{ [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; } && have_runner=1
arch_raw="$(uname -m 2>/dev/null || true)"
{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
@@ -295,6 +175,7 @@ READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/
# Shared per-arch exec helper — see test/lib/exec_target.sh.
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
source "$ROOT/test/lib/exec_target.sh"
@@ -309,39 +190,35 @@ fi
# parse-runner
if [ -x "$PARSE_RUNNER" ]; then
- printf ' %s parse-runner\n' "$(color_grn found)"
+ printf ' found parse-runner\n'
else
- printf ' %s parse-runner missing — run "make build/test/parse-runner"\n' \
- "$(color_red FATAL)" >&2
+ printf ' FATAL parse-runner missing — run "make build/test/parse-runner"\n' >&2
exit 1
fi
# cfree-roundtrip — for path R.
if [ -x "$ROUNDTRIP_BIN" ]; then
have_roundtrip=1
- printf ' %s cfree-roundtrip\n' "$(color_grn found)"
+ printf ' found cfree-roundtrip\n'
else
- printf ' %s cfree-roundtrip missing — path R will skip\n' \
- "$(color_yel warn)" >&2
+ printf ' warn cfree-roundtrip missing — path R will skip\n' >&2
fi
# link-exe-runner — for path E.
if [ -x "$LINK_EXE_RUNNER" ]; then
have_exe_runner=1
- printf ' %s link-exe-runner\n' "$(color_grn found)"
+ printf ' found link-exe-runner\n'
else
- printf ' %s link-exe-runner missing — path E will skip\n' \
- "$(color_yel warn)" >&2
+ printf ' warn link-exe-runner missing — path E will skip\n' >&2
fi
# jit-runner — for path J. Only when host arch matches the cross-target.
if [ $is_native_target -eq 1 ]; then
if [ -x "$JIT_RUNNER" ]; then
have_jit_runner=1
- printf ' %s jit-runner\n' "$(color_grn found)"
+ printf ' found jit-runner\n'
else
- printf ' %s jit-runner missing — path J will skip\n' \
- "$(color_yel warn)" >&2
+ printf ' warn jit-runner missing — path J will skip\n' >&2
fi
fi
@@ -372,9 +249,9 @@ EOF
fi
if $CC -std=c11 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" 2>/dev/null; then
have_c_wrapper=1
- printf ' %s c-wrapper\n' "$(color_grn built)"
+ printf ' built c-wrapper\n'
else
- printf ' %s c-wrapper (host CC failed)\n' "$(color_yel warn)" >&2
+ printf ' warn c-wrapper (host CC failed)\n' >&2
fi
# Probe whether the host C compiler treats `long double` as 128-bit
@@ -395,370 +272,287 @@ if $CC -std=c11 "$LDBL_PROBE_SRC" -o "$LDBL_PROBE_BIN" 2>/dev/null \
HOST_LDBL128=1
fi
-# ---- per-case loop ---------------------------------------------------------
+# ---- per-lane oracle hooks (CF_WORK-confined -> parallel-safe) -------------
+# Each hook records via cf_pass/cf_fail/cf_skip (or cf_queue_e for E). The
+# expected exit code is CF_EXPECTED (the engine read <name>.expected); compared
+# mod 256 to the program's exit code.
-CASES=()
-for src in "$TEST_DIR"/cases/*.c; do
- [ -e "$src" ] || continue
- CASES+=("$src")
-done
-
-# Path E result bookkeeping — same shape as test/cg.
-E_NAMES=()
-E_WORK=()
-E_LINK_MS=()
-E_EXPECTED=()
-
-FILTERED_CASES=()
-# Path C forces opt_level=0 internally — when it's the only enabled path,
-# higher opt levels duplicate identical work and (because no other path
-# emits anything) leave a zero-byte worker event file that the replay
-# loop flags as "missing worker result". Restrict to opt=0 in that case.
-case_opt_levels="$OPT_LEVELS"
-if [ $RUN_D -eq 0 ] && [ $RUN_R -eq 0 ] && [ $RUN_E -eq 0 ] && \
- [ $RUN_J -eq 0 ] && { [ $RUN_C -eq 1 ] || [ $RUN_W -eq 1 ]; }; then
- case_opt_levels="0"
-fi
-for src in "${CASES[@]}"; do
- name="$(basename "$src" .c)"
- [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue
- for opt in $case_opt_levels; do
- FILTERED_CASES+=("$opt:$src")
- done
-done
+# Build the .o the R/E/J lanes share, once per (case,opt). Sets PARSE_OBJ.
+# Returns 0 on success, 1 on failure.
+#
+# The original harness emitted the .o ONCE up front (before R/E/J) and, on a
+# failure, recorded a single FAIL "<name>/emit" and skipped R/E/J entirely. To
+# preserve that exact verdict (one FAIL, not one per lane), the first lane that
+# needs the object reports "<name>/emit"; a failure marker makes every later
+# lane in the same item return silently without re-reporting.
+_parse_emit_obj() {
+ PARSE_OBJ="$CF_WORK/$CF_BASE.o"
+ [ -f "$PARSE_OBJ" ] && return 0
+ [ -f "$CF_WORK/.emit.failed" ] && return 1
+ if ! CFREE_OPT_LEVEL="$CF_OPT" "$PARSE_RUNNER" --emit "$CF_SRC" "$PARSE_OBJ" \
+ 2>"$CF_WORK/emit.err"; then
+ : > "$CF_WORK/.emit.failed"
+ cf_fail "$CF_NAME/emit" "parse-runner --emit failed; see $CF_WORK/emit.err"
+ return 1
+ fi
+ return 0
+}
-run_parse_case() {
- local _idx="$1" item="$2" event="$3"
- local opt src base_name name work reason expected expected_byte obj t0 dt d_rc r_ok r_msg rt
- local exe link_dt j_rc
- local c_src c_bin c_rc missing run_c
- : "$_idx"
-
- opt="${item%%:*}"
- src="${item#*:}"
- base_name="$(basename "$src" .c)"
- name="$base_name/O$opt"
- work="$BUILD_DIR/parse/$base_name.O$opt"
- mkdir -p "$work"
-
- # Skip sidecar. `<name>.skip` skips on all arches; `<name>.<arch>.skip`
- # (e.g. asm_01_grammar.rv64.skip) skips only when CFREE_TEST_ARCH matches.
- if [ -e "$TEST_DIR/cases/$base_name.$TEST_ARCH.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/cases/$base_name.$TEST_ARCH.skip")
- emit_event "$event" SKIP "$name" "$reason"
- return 0
+cf_lane_D() {
+ if [ $is_native_target -eq 0 ]; then
+ cf_skip "$CF_NAME/D" "host arch != $TEST_ARCH (no native JIT)"
+ return
fi
- if [ -e "$TEST_DIR/cases/$base_name.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/cases/$base_name.skip")
- emit_event "$event" SKIP "$name" "$reason"
- return 0
+ local exp_byte t0 dt d_rc
+ exp_byte=$(( CF_EXPECTED & 0xff ))
+ t0=$(cf_now_ms)
+ CFREE_OPT_LEVEL="$CF_OPT" "$PARSE_RUNNER" --jit "$CF_SRC" \
+ >"$CF_WORK/d.out" 2>"$CF_WORK/d.err"
+ d_rc=$?
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time D "$dt"
+ if [ "$d_rc" -eq "$exp_byte" ]; then
+ cf_pass "$CF_NAME/D (${dt}ms)"
+ else
+ cf_fail "$CF_NAME/D" "expected $exp_byte got $d_rc, ${dt}ms"
fi
+}
- # Expected exit code (default 0)
- expected=0
- if [ -e "$TEST_DIR/cases/$base_name.expected" ]; then
- expected=$(head -n1 "$TEST_DIR/cases/$base_name.expected")
+cf_lane_R() {
+ if [ $have_roundtrip -ne 1 ] || [ $have_readelf -ne 1 ] || [ $have_python3 -ne 1 ]; then
+ cf_skip "$CF_NAME/R" "missing roundtrip/readelf/python3"
+ return
fi
- expected_byte=$(( expected & 0xff ))
-
- # ---- Path D: in-process JIT (only when host arch == cross-target) ----
- if [ $RUN_D -eq 1 ]; then
- if [ $is_native_target -eq 1 ]; then
- t0=$(now_ms)
- CFREE_OPT_LEVEL="$opt" "$PARSE_RUNNER" --jit "$src" \
- >"$work/d.out" 2>"$work/d.err"
- d_rc=$?
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME D "$dt"
- if [ "$d_rc" -eq "$expected_byte" ]; then
- emit_event "$event" PASS "$name/D (${dt}ms)"
- else
- emit_event "$event" FAIL "$name/D (expected $expected_byte got $d_rc, ${dt}ms)"
- fi
- else
- emit_event "$event" SKIP "$name/D" "host arch != $TEST_ARCH (no native JIT)"
- fi
+ _parse_emit_obj || return
+ local t0 dt rt r_ok r_msg
+ t0=$(cf_now_ms)
+ rt="$CF_WORK/$CF_BASE.rt.o"
+ r_ok=1; r_msg=""
+ if ! "$ROUNDTRIP_BIN" "$PARSE_OBJ" "$rt" 2>"$CF_WORK/rt.err"; then
+ r_ok=0; r_msg="roundtrip failed"
+ else
+ "$READELF_BIN" -aW "$PARSE_OBJ" | python3 "$NORMALIZE" >"$CF_WORK/golden.norm" 2>/dev/null
+ "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$CF_WORK/rt.norm" 2>/dev/null
+ diff -u "$CF_WORK/golden.norm" "$CF_WORK/rt.norm" >"$CF_WORK/r.diff" 2>&1 || r_ok=0
fi
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time R "$dt"
+ if [ $r_ok -eq 1 ]; then cf_pass "$CF_NAME/R (${dt}ms)"
+ else cf_fail "$CF_NAME/R" "${r_msg} ${dt}ms"; fi
+}
- # ---- emit (needed by R/E/J) ------------------------------------------
- obj="$work/$base_name.o"
- if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ]; then
- if ! CFREE_OPT_LEVEL="$opt" "$PARSE_RUNNER" --emit "$src" "$obj" \
- 2>"$work/emit.err"; then
- emit_event "$event" FAIL "$name/emit (parse-runner --emit failed; see $work/emit.err)"
- return 0
- fi
+cf_lane_E() {
+ if [ $have_exe_runner -ne 1 ] || [ $have_clang_cross -ne 1 ] || [ $have_start_obj -ne 1 ]; then
+ cf_skip "$CF_NAME/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"
+ return
fi
-
- # ---- Path R: ELF roundtrip -------------------------------------------
- if [ $RUN_R -eq 1 ]; then
- if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then
- t0=$(now_ms)
- rt="$work/$base_name.rt.o"
- r_ok=1; r_msg=""
- if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then
- r_ok=0; r_msg=" (roundtrip failed)"
- else
- "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null
- "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null
- diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0
- fi
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME R "$dt"
- if [ $r_ok -eq 1 ]; then emit_event "$event" PASS "$name/R (${dt}ms)"
- else emit_event "$event" FAIL "$name/R${r_msg} (${dt}ms)"; fi
- else
- emit_event "$event" SKIP "$name/R" "missing roundtrip/readelf/python3"
- fi
+ _parse_emit_obj || return
+ local t0 dt exe exp_byte
+ exp_byte=$(( CF_EXPECTED & 0xff ))
+ t0=$(cf_now_ms)
+ exe="$CF_WORK/linked.exe"
+ if ! "$LINK_EXE_RUNNER" -o "$exe" "$PARSE_OBJ" "$START_OBJ" "${RT_LINK_ARGS[@]}" \
+ >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time E "$dt"
+ cf_fail "$CF_NAME/E" "link failed, ${dt}ms"
+ elif exec_target_supported "$EXEC_ARCH"; then
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time E "$dt"
+ # Deferred batched exec: the engine flush runs the exe and verifies
+ # rc == exp_byte (both masked & 255) at the end.
+ cf_queue_e "$CF_NAME/E (link ${dt}ms)" "$exe" \
+ "$CF_WORK/exec.out" "$CF_WORK/exec.err" "$CF_WORK/exec.rc" \
+ "$exp_byte" "$EXEC_ARCH"
+ else
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time E "$dt"
+ cf_skip "$CF_NAME/E" "no runner for $EXEC_ARCH"
fi
+}
- # ---- Path E: link + (batched) qemu/podman ----------------------------
- if [ $RUN_E -eq 1 ]; then
- if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \
- && [ $have_start_obj -eq 1 ]; then
- t0=$(now_ms)
- exe="$work/linked.exe"
- if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" "${RT_LINK_ARGS[@]}" \
- >"$work/exec_link.out" 2>"$work/exec_link.err"; then
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME E "$dt"
- emit_event "$event" FAIL "$name/E (link failed, ${dt}ms)"
- elif exec_target_supported "$EXEC_ARCH"; then
- link_dt=$(( $(now_ms) - t0 ))
- emit_event "$event" QUEUE_E "$name" "$work" "$link_dt" "$expected_byte" "$name" "$EXEC_ARCH"
- else
- emit_event "$event" SKIP "$name/E" "no runner for $EXEC_ARCH"
- fi
- else
- emit_event "$event" SKIP "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"
- fi
+cf_lane_J() {
+ if [ $have_jit_runner -ne 1 ]; then
+ cf_skip "$CF_NAME/J" "no jit-runner (host arch != $TEST_ARCH)"
+ return
fi
-
- # ---- Path J: jit-via-file --------------------------------------------
- if [ $RUN_J -eq 1 ]; then
- if [ $have_jit_runner -eq 1 ]; then
- t0=$(now_ms)
- "$JIT_RUNNER" "$obj" "${RT_LINK_ARGS[@]}" >"$work/jit.out" 2>"$work/jit.err"
- j_rc=$?
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME J "$dt"
- if [ "$j_rc" -eq "$expected_byte" ]; then
- emit_event "$event" PASS "$name/J (${dt}ms)"
- else
- emit_event "$event" FAIL "$name/J (expected $expected_byte got $j_rc, ${dt}ms)"
- fi
- else
- emit_event "$event" SKIP "$name/J" "no jit-runner (host arch != $TEST_ARCH)"
- fi
+ _parse_emit_obj || return
+ local t0 dt j_rc exp_byte
+ exp_byte=$(( CF_EXPECTED & 0xff ))
+ t0=$(cf_now_ms)
+ "$JIT_RUNNER" "$PARSE_OBJ" "${RT_LINK_ARGS[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err"
+ j_rc=$?
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time J "$dt"
+ if [ "$j_rc" -eq "$exp_byte" ]; then
+ cf_pass "$CF_NAME/J (${dt}ms)"
+ else
+ cf_fail "$CF_NAME/J" "expected $exp_byte got $j_rc, ${dt}ms"
fi
+}
- # ---- Path C: --emit=c + host cc + run --------------------------------
- # Phase 1 of the C-source backend only handles a small slice of the
- # CGTarget vtable (see doc/CBACKEND.md). Cases that hit an
- # unimplemented method produce a panic that we surface as SKIP, so
- # the test pass/fail signal reflects the implemented surface rather
- # than churning while phases land.
- #
- # A per-case `<name>.cbackend.skip` sidecar opts the case out of
- # path C only (other paths still run), for surface gaps that don't
- # show up as a panic — e.g. emitted-C link errors that need Phase 4
- # work (aliases, asm definitions) to fix.
- run_c=$RUN_C
- # Path C forces opt_level=0 internally; running it at every requested
- # opt level would duplicate identical work. Mirror the toy runner.
- if [ $run_c -eq 1 ] && [ "$opt" != "0" ]; then
- run_c=0
- fi
- if [ $run_c -eq 1 ] && [ -e "$TEST_DIR/cases/$base_name.cbackend.skip" ]; then
- reason=$(head -n1 "$TEST_DIR/cases/$base_name.cbackend.skip")
- emit_event "$event" SKIP "$name/C" "$reason"
- run_c=0
+# Path C: --emit=c + host cc + run. Phase 1 of the C-source backend only
+# handles a slice of the CGTarget vtable (doc/CBACKEND.md). Cases that hit an
+# unimplemented method panic, surfaced here as SKIP so the pass/fail signal
+# reflects the implemented surface. The .cbackend.skip sidecar (handled by the
+# engine via the per-lane sidecar with LANE=cbackend) opts a case out of C only.
+cf_lane_C() {
+ # Per-case opt-out for path C: <name>.cbackend.skip. The engine's per-lane
+ # sidecar check keys on the lane id ("C"), so the cbackend-named sidecar is
+ # handled here instead.
+ local reason
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" cbackend); then
+ cf_skip "$CF_NAME/C" "$reason"
+ return
fi
# ldbl128_* tests assert 128-bit long-double semantics. Skip them on
# path C when the host C compiler doesn't provide 128-bit ldbl (the
# test's `if (__LDBL_MANT_DIG__ != 113) return 0;` early-out can't be
- # reconciled with a non-zero expected).
- if [ $run_c -eq 1 ] && [ $HOST_LDBL128 -eq 0 ] && \
- [[ "$base_name" == ldbl128_* ]] && \
- [[ "$base_name" != ldbl128_01_* ]]; then
- emit_event "$event" SKIP "$name/C" "host long double is not 128-bit"
- run_c=0
+ # reconciled with a non-zero expected). ldbl128_01_* are exempt.
+ if [ $HOST_LDBL128 -eq 0 ] && \
+ [[ "$CF_BASE" == ldbl128_* ]] && \
+ [[ "$CF_BASE" != ldbl128_01_* ]]; then
+ cf_skip "$CF_NAME/C" "host long double is not 128-bit"
+ return
fi
- # Mach-O's static linker rejects unresolved weak undef refs that
- # aren't backed by a dylib. ELF lets them resolve to 0 at link time,
- # which is what the test expects.
- if [ $run_c -eq 1 ] && [ "$HOST_OBJ_FMT" = "macho" ] && \
- [[ "$base_name" == attr_p2_08_weak_undef ]]; then
- emit_event "$event" SKIP "$name/C" \
- "Mach-O static link rejects weak undef ref without dylib"
- run_c=0
+ # Mach-O's static linker rejects unresolved weak undef refs that aren't
+ # backed by a dylib. ELF lets them resolve to 0 at link time, which is
+ # what the test expects.
+ if [ "$HOST_OBJ_FMT" = "macho" ] && [[ "$CF_BASE" == attr_p2_08_weak_undef ]]; then
+ cf_skip "$CF_NAME/C" "Mach-O static link rejects weak undef ref without dylib"
+ return
+ fi
+ if [ $have_c_wrapper -eq 0 ]; then
+ cf_skip "$CF_NAME/C" "no c-wrapper (host CC failed)"
+ return
fi
- if [ $run_c -eq 1 ]; then
- if [ $have_c_wrapper -eq 1 ] && [ $is_native_target -eq 1 ]; then
- t0=$(now_ms)
- c_src="$work/$base_name.cfree.c"
- c_bin="$work/$base_name.cbackend.bin"
- # Emitted C is target-locked, so we override CFREE_TEST_OBJ to
- # the host's object format for the --emit-c invocation —
- # otherwise ELF-only constructs like
- # __attribute__((alias("x"))) leak into source compiled by a
- # Mach-O-targeting host cc and fail at compile time.
- if ! CFREE_TEST_OBJ="$HOST_OBJ_FMT" "$PARSE_RUNNER" \
- --emit-c "$src" "$c_src" \
- >"$work/c.emit.out" 2>"$work/c.emit.err"; then
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME C "$dt"
- # Recognize "C target: ... not implemented" and
- # "C target: ... not yet supported" as phased-rollout
- # skips rather than regressions. Anything else is a real
- # failure that the harness flags so it can't hide.
- missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
- "$work/c.emit.err" 2>/dev/null | head -n1 || true)
- if [ -n "$missing" ]; then
- emit_event "$event" SKIP "$name/C" "$missing"
- else
- emit_event "$event" FAIL "$name/C (parse-runner --emit-c failed; see $work/c.emit.err)"
- fi
- elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
- >"$work/c.cc.out" 2>"$work/c.cc.err"; then
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME C "$dt"
- emit_event "$event" FAIL "$name/C (host cc rejected emitted source; see $work/c.cc.err)"
- else
- "$c_bin" >"$work/c.run.out" 2>"$work/c.run.err"
- c_rc=$?
- dt=$(( $(now_ms) - t0 ))
- emit_event "$event" TIME C "$dt"
- if [ "$c_rc" -eq "$expected_byte" ]; then
- emit_event "$event" PASS "$name/C (${dt}ms)"
- else
- emit_event "$event" FAIL "$name/C (expected $expected_byte got $c_rc, ${dt}ms)"
- fi
- fi
- elif [ $have_c_wrapper -eq 0 ]; then
- emit_event "$event" SKIP "$name/C" "no c-wrapper (host CC failed)"
+ if [ $is_native_target -eq 0 ]; then
+ cf_skip "$CF_NAME/C" "host arch != $TEST_ARCH (C target is target-locked)"
+ return
+ fi
+ local t0 dt c_src c_bin c_rc missing exp_byte
+ exp_byte=$(( CF_EXPECTED & 0xff ))
+ t0=$(cf_now_ms)
+ c_src="$CF_WORK/$CF_BASE.cfree.c"
+ c_bin="$CF_WORK/$CF_BASE.cbackend.bin"
+ # Emitted C is target-locked, so we override CFREE_TEST_OBJ to the host's
+ # object format for the --emit-c invocation — otherwise ELF-only constructs
+ # like __attribute__((alias("x"))) leak into source compiled by a
+ # Mach-O-targeting host cc and fail at compile time.
+ if ! CFREE_TEST_OBJ="$HOST_OBJ_FMT" "$PARSE_RUNNER" \
+ --emit-c "$CF_SRC" "$c_src" \
+ >"$CF_WORK/c.emit.out" 2>"$CF_WORK/c.emit.err"; then
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time C "$dt"
+ # Recognize "C target: ... not implemented" and "... not yet supported"
+ # as phased-rollout skips. Anything else is a real failure.
+ missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
+ "$CF_WORK/c.emit.err" 2>/dev/null | head -n1 || true)
+ if [ -n "$missing" ]; then
+ cf_skip "$CF_NAME/C" "$missing"
+ else
+ cf_fail "$CF_NAME/C" "parse-runner --emit-c failed; see $CF_WORK/c.emit.err"
+ fi
+ elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
+ >"$CF_WORK/c.cc.out" 2>"$CF_WORK/c.cc.err"; then
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time C "$dt"
+ cf_fail "$CF_NAME/C" "host cc rejected emitted source; see $CF_WORK/c.cc.err"
+ else
+ "$c_bin" >"$CF_WORK/c.run.out" 2>"$CF_WORK/c.run.err"
+ c_rc=$?
+ dt=$(( $(cf_now_ms) - t0 ))
+ cf_time C "$dt"
+ if [ "$c_rc" -eq "$exp_byte" ]; then
+ cf_pass "$CF_NAME/C (${dt}ms)"
else
- emit_event "$event" SKIP "$name/C" "host arch != $TEST_ARCH (C target is target-locked)"
+ cf_fail "$CF_NAME/C" "expected $exp_byte got $c_rc, ${dt}ms"
fi
fi
+}
- # ---- Path W: cc -target wasm32-none + cfree run ----------------------
- # Compile the case straight to a .wasm via the Wasm CGTarget, then run it
- # with `cfree run -e test_main` (the lang/wasm frontend re-lowers the
- # module to native CG, JITs it, and calls test_main). Mirrors the toy
- # runner's W path. Like path C it is target-agnostic, so it does not
- # depend on the cross-target arch; but the re-lowering JITs for the host,
- # so it only runs when the host arch matches the cross-target.
- #
- # The Wasm CGTarget ignores -O for opt purposes (the wasm frontend picks
- # its own native opt level when re-lowering), so W runs at opt=0 only.
- # Phased-rollout panics ("wasm: <feature> not yet implemented" and the
- # "unsupported"/"supported in v1" variants) surface as SKIP, not FAIL, so
- # the signal stays "real regressions". A `<name>.wasm.skip` sidecar opts a
- # case out of path W with a reason.
- run_w=$RUN_W
- if [ $run_w -eq 1 ] && [ "$opt" != "0" ]; then
- run_w=0
- fi
- if [ $run_w -eq 1 ] && [ -e "$TEST_DIR/cases/$base_name.wasm.skip" ]; then
- emit_event "$event" SKIP "$name/W" \
- "$(head -n1 "$TEST_DIR/cases/$base_name.wasm.skip")"
- run_w=0
+# Path W: cc -target wasm32-none + cfree run. Compile the case straight to a
+# .wasm via the Wasm CGTarget, then run it with `cfree run -e test_main` (the
+# lang/wasm frontend re-lowers to native CG, JITs it, and calls test_main).
+# Target-agnostic like C, but the re-lowering JITs for the host, so it only
+# runs when the host arch matches the cross target. opt=0 only. Phased-rollout
+# panics surface as SKIP. The .wasm.skip sidecar (engine per-lane, LANE=wasm)
+# opts a case out of W only.
+cf_lane_W() {
+ # Per-case opt-out for path W: <name>.wasm.skip. The engine's per-lane
+ # sidecar check keys on the lane id ("W"), so the wasm-named sidecar is
+ # handled here instead.
+ local reason
+ if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" wasm); then
+ cf_skip "$CF_NAME/W" "$reason"
+ return
fi
- if [ $run_w -eq 1 ] && [ $is_native_target -eq 0 ]; then
- emit_event "$event" SKIP "$name/W" "host arch != $TEST_ARCH (no native JIT for re-lowering)"
- run_w=0
+ if [ $is_native_target -eq 0 ]; then
+ cf_skip "$CF_NAME/W" "host arch != $TEST_ARCH (no native JIT for re-lowering)"
+ return
fi
- if [ $run_w -eq 1 ]; then
- local wasm w_cc_err w_run_err w_rc w_missing
- wasm="$work/$base_name.wasm"
- w_cc_err="$work/w.cc.err"
- w_run_err="$work/w.run.err"
- t0=$(now_ms)
- if ! "$CFREE" cc -O0 -target wasm32-none -c "$src" -o "$wasm" \
- >"$work/w.cc.out" 2>"$w_cc_err"; then
- dt=$(( $(now_ms) - t0 )); emit_event "$event" TIME W "$dt"
- w_missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \
- "$w_cc_err" 2>/dev/null | head -n1 || true)
- if [ -n "$w_missing" ]; then
- emit_event "$event" SKIP "$name/W" "$w_missing"
- else
- emit_event "$event" FAIL "$name/W (cc -target wasm32-none failed; see $w_cc_err)"
- fi
+ local t0 dt wasm w_cc_err w_run_err w_rc w_missing exp_byte
+ exp_byte=$(( CF_EXPECTED & 0xff ))
+ wasm="$CF_WORK/$CF_BASE.wasm"
+ w_cc_err="$CF_WORK/w.cc.err"
+ w_run_err="$CF_WORK/w.run.err"
+ t0=$(cf_now_ms)
+ if ! "$CFREE" cc -O0 -target wasm32-none -c "$CF_SRC" -o "$wasm" \
+ >"$CF_WORK/w.cc.out" 2>"$w_cc_err"; then
+ dt=$(( $(cf_now_ms) - t0 )); cf_time W "$dt"
+ w_missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \
+ "$w_cc_err" 2>/dev/null | head -n1 || true)
+ if [ -n "$w_missing" ]; then
+ cf_skip "$CF_NAME/W" "$w_missing"
else
- # Validate by exit code only (like paths D/J/C). cc stderr is not a
- # failure on its own: legitimate non-fatal diagnostics such as
- # `#warning` print there while compilation succeeds and the program
- # still runs. A real compile failure already took the branch above;
- # a real run failure shows up as an exit-code mismatch.
- "$CFREE" run -e test_main "$wasm" >"$work/w.run.out" 2>"$w_run_err"
- w_rc=$?
- dt=$(( $(now_ms) - t0 )); emit_event "$event" TIME W "$dt"
- if [ "$w_rc" -eq "$expected_byte" ]; then
- emit_event "$event" PASS "$name/W (${dt}ms)"
- else
- emit_event "$event" FAIL "$name/W (expected $expected_byte got $w_rc, ${dt}ms)"
- fi
+ cf_fail "$CF_NAME/W" "cc -target wasm32-none failed; see $w_cc_err"
fi
- fi
- return 0
-}
-
-if [ ${#FILTERED_CASES[@]} -gt 0 ]; then
- if [ ${#FILTERED_CASES[@]} -le 4 ]; then
- printf 'Running cases (serial, opt levels: %s)...\n' "$OPT_LEVELS"
- run_serial_items "cases" run_parse_case "${FILTERED_CASES[@]}"
else
- printf 'Running cases (%s jobs, opt levels: %s)...\n' "$TEST_JOBS" "$OPT_LEVELS"
- run_parallel_items "cases" run_parse_case "${FILTERED_CASES[@]}"
- fi
-fi
-
-# ---- batched path-E flush + verification -----------------------------------
-
-T_E_BATCH=0
-if [ "$(exec_target_queue_size)" -gt 0 ]; then
- printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)"
- t0=$(now_ms)
- exec_target_flush
- T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH ))
-
- i=0
- while [ $i -lt ${#E_NAMES[@]} ]; do
- name="${E_NAMES[$i]}"
- work="${E_WORK[$i]}"
- link_dt="${E_LINK_MS[$i]}"
- expected_byte="${E_EXPECTED[$i]}"
- if [ ! -f "$work/exec.rc" ]; then
- note_fail "$name/E (no rc; podman batch did not produce results)"
+ # Validate by exit code only (like D/J/C). cc stderr is not a failure
+ # on its own: legitimate non-fatal diagnostics such as `#warning` print
+ # there while compilation succeeds.
+ "$CFREE" run -e test_main "$wasm" >"$CF_WORK/w.run.out" 2>"$w_run_err"
+ w_rc=$?
+ dt=$(( $(cf_now_ms) - t0 )); cf_time W "$dt"
+ if [ "$w_rc" -eq "$exp_byte" ]; then
+ cf_pass "$CF_NAME/W (${dt}ms)"
else
- RUN_RC="$(cat "$work/exec.rc")"
- if [ "$RUN_RC" -eq "$expected_byte" ]; then
- note_pass "$name/E (link ${link_dt}ms)"
- else
- note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)"
- fi
+ cf_fail "$CF_NAME/W" "expected $exp_byte got $w_rc, ${dt}ms"
fi
- i=$((i+1))
- done
-fi
-
-# ---- summary ---------------------------------------------------------------
+ fi
+}
-if [ ${#FAIL_NAMES[@]} -gt 0 ]; then
- printf '\nFailed:\n'
- for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done
+# ---- drive the corpus ------------------------------------------------------
+
+# Active lanes in DREJCW order.
+LANES=
+[ $RUN_D -eq 1 ] && LANES="$LANES D"
+[ $RUN_R -eq 1 ] && LANES="$LANES R"
+[ $RUN_E -eq 1 ] && LANES="$LANES E"
+[ $RUN_J -eq 1 ] && LANES="$LANES J"
+[ $RUN_C -eq 1 ] && LANES="$LANES C"
+[ $RUN_W -eq 1 ] && LANES="$LANES W"
+
+# Paths C/W force opt_level=0 internally; running them at every requested opt
+# level would duplicate identical work. When C and/or W are the ONLY enabled
+# lanes, collapse the opt axis to "0" so the matrix isn't padded with items
+# that produce no records. (The engine already gates C/W to opt 0 via
+# CF_OPT0ONLY; this just trims the redundant opt=1 expansion.)
+CASE_OPT_LEVELS="$OPT_LEVELS"
+if [ $RUN_D -eq 0 ] && [ $RUN_R -eq 0 ] && [ $RUN_E -eq 0 ] && [ $RUN_J -eq 0 ] \
+ && { [ $RUN_C -eq 1 ] || [ $RUN_W -eq 1 ]; }; then
+ CASE_OPT_LEVELS="0"
fi
-if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then
- printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n'
- for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done
-fi
+PAR="${CFREE_PARSE_PARALLEL:-1}"
+
+printf 'test-parse-ok target=%s obj=%s arch=%s\n' "$CLANG_TRIPLE" "$HOST_OBJ_FMT" "$TEST_ARCH"
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms C=%dms W=%dms\n' \
- "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J" "$T_C" "$T_W"
+CF_LABEL=test-parse-ok CF_BUILD_DIR="$BUILD_DIR/parse" \
+ CF_CORPUS_GLOBS="$TEST_DIR/cases/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/cases" \
+ CF_LANES="$LANES" CF_OPT_LEVELS="$CASE_OPT_LEVELS" CF_TUPLES="$TEST_ARCH-noobj" \
+ CF_OPT0ONLY="C W" CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-parse-ok
+cf_exit
diff --git a/test/parse/run_errors.sh b/test/parse/run_errors.sh
@@ -1,26 +1,38 @@
-#!/bin/sh
-# test/parse/run_errors.sh — file-driven negative test runner.
+#!/usr/bin/env bash
+# test/parse/run_errors.sh — file-driven negative C-parser runner, on the
+# shared corpus harness (test/lib/cf_corpus.sh). Single negative lane.
#
-# For each test/parse/cases_err/*.c, runs `parse-runner --emit FILE.c
-# /dev/null` and expects a nonzero exit (the parser must diagnose the
-# constraint violation / syntax error). Mirrors test/pp/run_errors.sh.
+# For each test/parse/cases_err/*.c, runs `parse-runner --emit FILE.c /dev/null`
+# and expects a nonzero exit (the parser must diagnose the constraint violation
+# / syntax error). Kept as a separate runner from the positive parser harness
+# (test-parse-err vs test-parse-ok); reuses parse-runner, which test-parse-ok
+# builds.
#
-# Optional sidecar:
-# <name>.errpat — substring of stderr that must be present. If
-# missing, only exit status is checked.
+# Oracle (lane P): parse-runner must exit nonzero. Optional sidecar:
+# <name>.errpat — first line is a substring of stderr that must be present
+# (grep -qF). When absent, only exit status is checked.
#
-# Filtering:
-# ./run_errors.sh [name_filter]
-# name_filter substring match against case basename
-# Equivalent env var: CFREE_TEST_FILTER.
+# No opt axis. Filtering:
+# ./run_errors.sh [name_filter] (or CFREE_TEST_FILTER); substring match
+# against case basename.
+#
+# Parallelism: parallel by default; CFREE_TEST_JOBS=N caps concurrency;
+# CFREE_PARSE_PARALLEL=0 forces serial. The lane hook keeps all output under
+# $CF_WORK, so the summary is identical serial or parallel.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
TEST_DIR="$ROOT/test/parse"
BUILD_DIR="$ROOT/build/test"
+CASES_DIR="$TEST_DIR/cases_err"
FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
PARSE_RUNNER="$BUILD_DIR/parse-runner"
@@ -29,49 +41,48 @@ if [ ! -x "$PARSE_RUNNER" ]; then
exit 2
fi
-cd "$TEST_DIR/cases_err" 2>/dev/null || {
+# No cases_err directory -> nothing to test (graceful, like the original).
+if [ ! -d "$CASES_DIR" ]; then
echo "parse-err: no cases_err directory; nothing to test"
exit 0
-}
-
-pass=0
-fail=0
-failures=
+fi
-for src in *.c; do
- [ -e "$src" ] || continue
- name="${src%.c}"
- case "$name" in *"$FILTER"*) ;; *) [ -n "$FILTER" ] && continue ;; esac
- pat_file="$name.errpat"
- err_log="$BUILD_DIR/parse/$name.err.log"
- mkdir -p "$BUILD_DIR/parse"
+# ----- negative lane P: parse-runner --emit must fail (+ optional errpat) ----
+# parse-runner emits to /dev/null (we only care about exit status + stderr).
+# All logs stay under $CF_WORK for parallel safety. When <name>.errpat exists,
+# stderr must contain its first line (grep -qF), else only exit status checks.
+cf_lane_P() {
+ local name="$CF_BASE"
+ local err_log="$CF_WORK/parse.err"
- if "$PARSE_RUNNER" --emit "$src" /dev/null >/dev/null 2>"$err_log"; then
- printf 'FAIL %s (expected nonzero exit, got success)\n' "$name"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
+ if "$PARSE_RUNNER" --emit "$CF_SRC" /dev/null \
+ >"$CF_WORK/parse.out" 2>"$err_log"; then
+ cf_fail "$name" "expected nonzero exit, got success"
+ return
fi
+ local pat_file="$CF_SIDECAR_DIR/$CF_BASE.errpat"
if [ -e "$pat_file" ]; then
+ local pat
pat=$(head -n1 "$pat_file")
if ! grep -qF -- "$pat" "$err_log"; then
- printf 'FAIL %s (stderr missing pattern: %s)\n' "$name" "$pat"
- cat "$err_log"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
+ cf_fail "$name" "stderr missing pattern: $pat"
+ sed 's/^/ | /' "$err_log"
+ return
fi
fi
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
-done
+ cf_pass "$name"
+}
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nparse-err: failures:%s\n' "$failures"
- printf 'parse-err: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\nparse-err: %d/%d passed\n' "$pass" "$total"
+# ----- drive the corpus -----------------------------------------------------
+
+PAR="${CFREE_PARSE_PARALLEL:-1}"
+
+CF_LABEL=test-parse-err CF_BUILD_DIR="$BUILD_DIR/parse-err" \
+ CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \
+ CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+
+cf_summary test-parse-err
+cf_exit
diff --git a/test/pkg/run.sh b/test/pkg/run.sh
@@ -22,71 +22,15 @@ SOURCE_DATE_EPOCH=1
export HOME CFREE_TRUSTED_KEYS SOURCE_DATE_EPOCH
mkdir -p "$HOME" "$work/in" "$work/pkg" "$work/unpack" "$work/cas"
-pass=0
-fail=0
-skip=0
-failures=
-
artifacts="empty.dat one.bin dup-one.bin payload.txt chunk64.bin chunk64-copy.bin chunk64p1.bin chunk3.bin bin/tool.sh"
tree_id=
-ok() {
- printf 'PASS %s\n' "$1"
- pass=$((pass + 1))
-}
-
-not_ok() {
- printf 'FAIL %s\n' "$1"
- if [ "$#" -gt 1 ] && [ -s "$2" ]; then
- sed 's/^/ | /' "$2"
- fi
- fail=$((fail + 1))
- failures="$failures $1"
-}
-
-skip_test() {
- printf 'SKIP %s\n' "$1"
- skip=$((skip + 1))
-}
-
-run_ok() {
- name=$1
- shift
- if "$@" > "$work/$name.out" 2> "$work/$name.err"; then
- ok "$name"
- else
- not_ok "$name" "$work/$name.err"
- fi
-}
-
-run_fail() {
- name=$1
- shift
- if "$@" > "$work/$name.out" 2> "$work/$name.err"; then
- {
- echo "command unexpectedly succeeded"
- sed 's/^/stdout: /' "$work/$name.out"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- else
- ok "$name"
- fi
-}
-
-contains() {
- name=$1
- file=$2
- needle=$3
- if grep -F "$needle" "$file" >/dev/null 2>&1; then
- ok "$name"
- else
- {
- printf 'missing text: %s\n' "$needle"
- sed 's/^/file: /' "$file"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
+# Type-K mode-P kit: shared assert verbs + CAS helpers, all recording through
+# the unified cf_* counters over $work. pkg-specific helpers (not_contains,
+# same_artifacts, not_same_file, have_cmd) stay below.
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
+cf_report_init
not_contains() {
name=$1
@@ -103,22 +47,6 @@ not_contains() {
fi
}
-same_file() {
- name=$1
- want=$2
- got=$3
- if cmp -s "$want" "$got"; then
- ok "$name"
- else
- {
- printf 'files differ:\n'
- printf ' want: %s\n' "$want"
- printf ' got: %s\n' "$got"
- } > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
same_artifacts() {
name=$1
dir=$2
@@ -154,64 +82,10 @@ not_same_file() {
fi
}
-is_executable() {
- name=$1
- file=$2
- if [ -x "$file" ]; then
- ok "$name"
- else
- echo "not executable: $file" > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
-first_hex_id() {
- sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p'
-}
-
-id_prefix() {
- printf '%.2s' "$1"
-}
-
-cas_object_path() {
- root=$1
- kind=$2
- id=$3
- prefix=$(id_prefix "$id")
- printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id"
-}
-
-tree_blob_for_path() {
- tree_file=$1
- want=$2
- awk -v want="$want" '
- $0 == "[file]" { in_file = 1; path = ""; blob = ""; next }
- in_file && /^path = / { path = substr($0, 8); next }
- in_file && /^blob = / {
- blob = substr($0, 8);
- if (path == want) {
- print blob;
- exit;
- }
- }
- ' "$tree_file"
-}
-
-assert_file_exists() {
- name=$1
- file=$2
- if [ -f "$file" ]; then
- ok "$name"
- else
- echo "missing file: $file" > "$work/$name.diag"
- not_ok "$name" "$work/$name.diag"
- fi
-}
-
make_fixtures() {
: > "$work/in/empty.dat"
printf x > "$work/in/one.bin"
@@ -787,10 +661,5 @@ run_fail "pkg-inspect-malformed-fails" "$CFREE" pkg inspect "$work/not-a-package
run_fail "pkg-trust-add-missing-pubkey-fails" "$CFREE" pkg trust add
run_fail "pkg-trust-remove-bad-keyid-fails" "$CFREE" pkg trust remove xyz
-if [ "$fail" -ne 0 ]; then
- printf 'pkg: failures:%s\n' "$failures"
- printf 'pkg: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip"
- exit 1
-fi
-
-printf 'pkg: %d passed, %d skipped\n' "$pass" "$skip"
+cf_summary pkg
+cf_exit
diff --git a/test/pp/run.sh b/test/pp/run.sh
@@ -1,21 +1,37 @@
-#!/bin/sh
-# Data-driven preprocessor test runner.
+#!/usr/bin/env bash
+# test/pp/run.sh — data-driven preprocessor golden-diff runner, on the shared
+# corpus harness (test/lib/cf_corpus.sh). Single positive lane.
#
-# For each test/pp/cases/*.c, runs `cfree cc -E` (with -I pointing at the
-# cases dir so sibling headers resolve) and diffs the output against the
-# matching .expected file. Leaves .actual files behind on failure so they
-# can be reviewed and copied over the expected baseline once intentional
+# For each test/pp/cases/*.c, runs `cfree cc -E` (with -I pointing at the cases
+# dir so sibling headers resolve) and diffs the output against the matching
+# .expected file. Leaves <name>.actual files behind in the cases dir on failure
+# so they can be reviewed and copied over the expected baseline once intentional
# output changes are validated.
#
-# To make __DATE__/__TIME__/__FILE__ deterministic across runs and
-# checkouts, the runner:
-# - exports SOURCE_DATE_EPOCH=0 (1970-01-01T00:00:00Z), which fixes
-# __DATE__ to "Jan 1 1970" and __TIME__ to "00:00:00";
-# - cd's into the cases dir and passes the source as a basename, so
-# __FILE__ is the file name itself with no checkout-path prefix.
+# To make __DATE__/__TIME__/__FILE__ deterministic across runs and checkouts,
+# the runner:
+# - exports SOURCE_DATE_EPOCH=0 (1970-01-01T00:00:00Z), which fixes __DATE__
+# to "Jan 1 1970" and __TIME__ to "00:00:00";
+# - cd's into the cases dir and uses bare basenames as the corpus glob, so the
+# source path the preprocessor sees (and thus __FILE__) is the file name
+# itself with no checkout-path prefix.
#
-# Honors $CFREE for the binary path; defaults to build/cfree relative to the
-# repo root inferred from this script's location.
+# Oracle (lane P): cfree cc -E must succeed and its output, normalized to a
+# token sequence (any run of whitespace incl. newlines collapses to one space;
+# leading/trailing whitespace stripped), must equal the .expected file similarly
+# normalized. Line-position preservation, leading-space padding before
+# macro-expanded `#`, and embed-induced reflow are downstream/cosmetic concerns;
+# this runner checks only the token sequence (matching the original harness).
+#
+# No opt axis. Honors $CFREE for the binary path; defaults to build/cfree.
+#
+# Filtering: ./run.sh [name_filter] (or CFREE_TEST_FILTER); substring match
+# against case basename.
+#
+# Parallelism: parallel by default (capped CPU-count); CFREE_TEST_JOBS=N caps
+# concurrency; CFREE_PP_PARALLEL=0 forces serial. The lane hook reads/writes the
+# per-case .expected/.actual next to the case (distinct path per case) and keeps
+# all scratch under $CF_WORK, so the summary is identical serial or parallel.
set -u
@@ -23,6 +39,10 @@ script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+export CF_LIB_DIR="$repo_root/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$repo_root/test/lib/cf_corpus.sh"
+
CFREE="${CFREE:-$repo_root/build/cfree}"
if [ ! -x "$CFREE" ]; then
@@ -33,62 +53,63 @@ fi
# Reproducible-builds: pin __DATE__ and __TIME__ to the Unix epoch.
export SOURCE_DATE_EPOCH=0
+# cd into the cases dir so the corpus glob yields bare basenames: the path the
+# preprocessor sees becomes the file name itself (deterministic __FILE__), and
+# the .expected/.actual sidecars resolve relative to the same dir.
cd "$cases_dir" || exit 2
-pass=0
-fail=0
-failures=
+BUILD_DIR="$repo_root/build/test/pp"
+mkdir -p "$BUILD_DIR"
+
+FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
-for src in *.c; do
- [ -e "$src" ] || continue
- expected="${src%.c}.expected"
- actual="${src%.c}.actual"
- name="${src%.c}"
+# ----- positive lane P: cc -E + normalized golden diff ----------------------
+# CF_SRC is a bare basename (cwd == cases dir); the .expected/.actual sidecars
+# live alongside it. Scratch (the two normalized token streams) stays in
+# $CF_WORK; the reviewable .actual is left in the cases dir on failure, as the
+# original harness did (a deliberate baseline-update workflow aid).
+cf_lane_P() {
+ local name="$CF_BASE"
+ local expected="$CF_BASE.expected"
+ local actual="$CF_BASE.actual"
if [ ! -e "$expected" ]; then
- printf 'FAIL %s (missing %s)\n' "$name" "$expected"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
+ cf_fail "$name" "missing $expected"
+ return
fi
- if ! "$CFREE" cc -E -I . "$src" -o "$actual" >/dev/null 2>&1; then
- printf 'FAIL %s (cfree exit nonzero; see %s)\n' "$name" "$actual"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
+ if ! "$CFREE" cc -E -I . "$CF_SRC" -o "$actual" >"$CF_WORK/pp.out" 2>"$CF_WORK/pp.err"; then
+ cf_fail "$name" "cfree exit nonzero; see $actual"
+ return
fi
- # Compare token sequences only — any run of whitespace (including
- # newlines) collapses to a single space, and leading/trailing
- # whitespace is stripped. Line-position preservation across consumed
- # directives, leading-space padding clang inserts before
- # macro-expanded `#` tokens, and embed-induced reflow are downstream
- # / cosmetic concerns; this runner currently only checks the token
- # sequence.
- exp_strip=$(mktemp)
- act_strip=$(mktemp)
+ # Compare token sequences only — collapse every run of whitespace
+ # (including newlines) to a single space, strip leading/trailing space.
+ local exp_strip="$CF_WORK/exp.strip"
+ local act_strip="$CF_WORK/act.strip"
tr '\n' ' ' < "$expected" | tr -s '[:space:]' ' ' \
| sed -e 's/^ //' -e 's/ $//' > "$exp_strip" || true
tr '\n' ' ' < "$actual" | tr -s '[:space:]' ' ' \
| sed -e 's/^ //' -e 's/ $//' > "$act_strip" || true
+
if diff -u "$exp_strip" "$act_strip" >/dev/null 2>&1; then
- printf 'PASS %s\n' "$name"
+ cf_pass "$name"
rm -f "$actual"
- pass=$((pass + 1))
else
- printf 'FAIL %s\n' "$name"
- diff -u "$exp_strip" "$act_strip" || true
- fail=$((fail + 1))
- failures="$failures $name"
+ cf_fail "$name"
+ diff -u "$exp_strip" "$act_strip" | sed 's/^/ | /' || true
fi
- rm -f "$exp_strip" "$act_strip"
-done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\npp: failures:%s\n' "$failures"
- printf 'pp: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\npp: %d/%d passed\n' "$pass" "$total"
+}
+
+# ----- drive the corpus -----------------------------------------------------
+
+PAR="${CFREE_PP_PARALLEL:-1}"
+
+CF_LABEL=test-pp-ok CF_BUILD_DIR="$BUILD_DIR/cases" \
+ CF_CORPUS_GLOBS="*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$cases_dir" \
+ CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+
+cf_summary test-pp-ok
+cf_exit
diff --git a/test/pp/run_errors.sh b/test/pp/run_errors.sh
@@ -1,13 +1,24 @@
-#!/bin/sh
-# Data-driven preprocessor must-fail runner.
+#!/usr/bin/env bash
+# test/pp/run_errors.sh — data-driven preprocessor must-fail runner, on the
+# shared corpus harness (test/lib/cf_corpus.sh). Single negative lane.
#
-# For each test/pp/cases_err/*.c, runs `cfree cc -E` and expects a
-# nonzero exit (constraint violations from C11 §6.10 must be
-# diagnosed). Counts only exit status; stderr is captured but not
-# pattern-matched.
+# For each test/pp/cases_err/*.c, runs `cfree cc -E` and expects a nonzero exit
+# (constraint violations from C11 §6.10 must be diagnosed). The oracle checks
+# exit status only; stderr is captured under $CF_WORK but not pattern-matched
+# (these cases ship no diagnostic sidecar). Kept as a separate runner from the
+# positive pp golden-diff runner (test-pp vs test-pp-err).
#
-# Honors $CFREE for the binary path; defaults to build/cfree relative
-# to the repo root inferred from this script's location.
+# Like run.sh, exports SOURCE_DATE_EPOCH=0 and cd's into the cases dir so
+# __DATE__/__TIME__/__FILE__ are deterministic.
+#
+# No opt axis. Honors $CFREE for the binary path; defaults to build/cfree.
+#
+# Filtering: ./run_errors.sh [name_filter] (or CFREE_TEST_FILTER); substring
+# match against case basename.
+#
+# Parallelism: parallel by default; CFREE_TEST_JOBS=N caps concurrency;
+# CFREE_PP_PARALLEL=0 forces serial. The lane hook keeps all output under
+# $CF_WORK, so the summary is identical serial or parallel.
set -u
@@ -15,6 +26,10 @@ script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases_err"
+export CF_LIB_DIR="$repo_root/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$repo_root/test/lib/cf_corpus.sh"
+
CFREE="${CFREE:-$repo_root/build/cfree}"
if [ ! -x "$CFREE" ]; then
@@ -25,30 +40,38 @@ fi
# Match run.sh so __DATE__/__TIME__/__FILE__ are deterministic.
export SOURCE_DATE_EPOCH=0
+# cd into the cases dir so the corpus glob yields bare basenames (deterministic
+# __FILE__) and -I . resolves sibling headers.
cd "$cases_dir" || exit 2
-pass=0
-fail=0
-failures=
+BUILD_DIR="$repo_root/build/test/pp"
+mkdir -p "$BUILD_DIR"
-for src in *.c; do
- [ -e "$src" ] || continue
- name="${src%.c}"
+FILTER="${1:-${CFREE_TEST_FILTER:-}}"
+export CFREE_TEST_FILTER="$FILTER"
- if "$CFREE" cc -E -I . "$src" -o /dev/null >/dev/null 2>&1; then
- printf 'FAIL %s (expected nonzero exit, got success)\n' "$name"
- fail=$((fail + 1))
- failures="$failures $name"
+# ----- negative lane P: cc -E must fail (exit status only) -------------------
+# CF_SRC is a bare basename (cwd == cases dir). Output goes to /dev/null per the
+# original (the preprocessed text is irrelevant); stderr/stdout logs stay under
+# $CF_WORK for parallel safety.
+cf_lane_P() {
+ local name="$CF_BASE"
+ if "$CFREE" cc -E -I . "$CF_SRC" -o /dev/null \
+ >"$CF_WORK/pp.out" 2>"$CF_WORK/pp.err"; then
+ cf_fail "$name" "expected nonzero exit, got success"
else
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
+ cf_pass "$name"
fi
-done
+}
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\npp-err: failures:%s\n' "$failures"
- printf 'pp-err: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\npp-err: %d/%d passed\n' "$pass" "$total"
+# ----- drive the corpus -----------------------------------------------------
+
+PAR="${CFREE_PP_PARALLEL:-1}"
+
+CF_LABEL=test-pp-err CF_BUILD_DIR="$BUILD_DIR/cases_err" \
+ CF_CORPUS_GLOBS="*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$cases_dir" \
+ CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+
+cf_summary test-pp-err
+cf_exit
diff --git a/test/rt/run.sh b/test/rt/run.sh
@@ -1,7 +1,23 @@
#!/usr/bin/env bash
-# Runtime tests for rt/include headers and libcfree_rt.a. Each case is compiled
-# with cfree cc against rt/include, linked with the freestanding test _start and
-# the matching libcfree_rt.a, then executed on the target when a runner exists.
+# test/rt/run.sh — runtime tests for rt/include headers and libcfree_rt.a, on
+# the shared corpus harness (test/lib/cf_corpus.sh).
+#
+# Each case (test/rt/cases/*.c) is compiled with `cfree cc` against rt/include,
+# linked with the freestanding test _start and the matching libcfree_rt.a, then
+# executed on the target when a runner exists. A correct run exits 42.
+#
+# This is a single-lane (R), no-opt corpus swept across one tuple per arch.
+# Arch selection: CFREE_RT_RUNTIME_ARCHES (default "aa64 x64 rv64"). Each arch
+# maps to an <arch>-linux exec_target tag and a clang `--target=` triple; the
+# per-arch rt archive / clang start.o / extra clang flags are resolved
+# LANE-LOCAL in cf_lane_R (no shared sysroot driver), and every artifact is
+# written under $CF_WORK so the lane is parallel-safe by construction.
+#
+# Skip conditions (each a SKIP, not a failure):
+# - libcfree_rt.a for the arch missing
+# - no execution runner for the arch (podman/qemu/native)
+# - clang cannot build the freestanding start.o for the triple
+# Set CFREE_TEST_ALLOW_SKIP=1 to let the run exit 0 with skips.
set -u
@@ -12,20 +28,11 @@ CFREE="$ROOT/build/cfree"
LINK_EXE_RUNNER="$ROOT/build/test/link-exe-runner"
START_SRC="$ROOT/test/link/harness/start.c"
-mkdir -p "$BUILD_DIR"
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
-
-PASS=0
-FAIL=0
-SKIP=0
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
-note_pass() { PASS=$((PASS + 1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL + 1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP + 1)); printf ' %s %s -- %s\n' "$(color_yel SKIP)" "$1" "$2"; }
+mkdir -p "$BUILD_DIR"
if [ ! -x "$CFREE" ]; then
printf 'cfree driver missing at %s -- run `make bin` first\n' "$CFREE" >&2
@@ -37,6 +44,8 @@ if [ ! -x "$LINK_EXE_RUNNER" ]; then
exit 2
fi
+# exec_target wiring. The aarch64 helper expects these names regardless of
+# target arch — they describe host detection rather than the target.
have_qemu=0
QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
[ -n "$QEMU_BIN" ] && have_qemu=1
@@ -50,8 +59,11 @@ fi
export have_qemu QEMU_BIN have_podman is_aarch64
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+. "$ROOT/test/lib/exec_target.sh"
+
+# ---- per-arch wiring (LANE-LOCAL) ------------------------------------------
arch_triple() {
case "$1" in
@@ -78,73 +90,64 @@ clang_extra_flags() {
esac
}
-run_arch() {
- local arch="$1"
- local triple rtlib start_obj arch_dir extra
- triple="$(arch_triple "$arch")"
+# ---- lane R: compile (cfree cc) -> link (link-exe-runner) -> exec -----------
+# Synchronous exec via exec_target_run (matches the original per-case flow).
+# CF_ARCH is the corpus tuple's arch ("aa64"/"x64"/"rv64"); CF_OBJ is "linux".
+cf_lane_R() {
+ local arch="$CF_ARCH" name="$CF_ARCH/$CF_NAME"
+ local triple rtlib start_obj extra
+ triple="$(arch_triple "$arch")" || { cf_fail "$name" "unknown arch '$arch'"; return; }
rtlib="$(rt_archive "$arch")"
- arch_dir="$BUILD_DIR/$arch"
- start_obj="$arch_dir/start.o"
extra="$(clang_extra_flags "$arch")"
- mkdir -p "$arch_dir"
+ start_obj="$CF_WORK/start.o"
if [ ! -f "$rtlib" ]; then
- note_skip "$arch" "runtime archive missing at $rtlib"
- return 0
+ cf_skip "$name" "runtime archive missing at $rtlib"; return
fi
if ! exec_target_supported "$arch"; then
- note_skip "$arch" "no execution runner"
- return 0
+ cf_skip "$name" "no execution runner"; return
fi
if ! clang --target="$triple" $extra -O1 -ffreestanding -fno-stack-protector \
-fno-PIC -fno-pie -c "$START_SRC" -o "$start_obj" \
- >"$arch_dir/start.out" 2>"$arch_dir/start.err"; then
- note_skip "$arch" "clang cannot build start.o for $triple"
- return 0
+ >"$CF_WORK/start.out" 2>"$CF_WORK/start.err"; then
+ cf_skip "$name" "clang cannot build start.o for $triple"; return
fi
- local src name case_dir obj exe out err rc
- for src in "$CASES_DIR"/*.c; do
- [ -e "$src" ] || continue
- name="$(basename "$src" .c)"
- case_dir="$arch_dir/$name"
- obj="$case_dir/$name.o"
- exe="$case_dir/$name.exe"
- out="$case_dir/run.out"
- err="$case_dir/run.err"
- mkdir -p "$case_dir"
-
- if ! "$CFREE" cc -target "$triple" -Werror -c "$src" -o "$obj" \
- >"$case_dir/cc.out" 2>"$case_dir/cc.err"; then
- note_fail "$arch/$name compile (see $case_dir/cc.err)"
- continue
- fi
-
- if ! CFREE_TEST_ARCH="$arch" "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \
- --archive "$rtlib" >"$case_dir/link.out" 2>"$case_dir/link.err"; then
- note_fail "$arch/$name link (see $case_dir/link.err)"
- continue
- fi
-
- exec_target_run "$arch" "$exe" "$out" "$err"
- rc="$RUN_RC"
- if [ "$rc" -eq 42 ]; then
- note_pass "$arch/$name"
- else
- note_fail "$arch/$name run (expected 42 got $rc; see $err)"
- fi
- done
+ local obj="$CF_WORK/$CF_BASE.o"
+ local exe="$CF_WORK/$CF_BASE.exe"
+ if ! "$CFREE" cc -target "$triple" -Werror -c "$CF_SRC" -o "$obj" \
+ >"$CF_WORK/cc.out" 2>"$CF_WORK/cc.err"; then
+ cf_fail "$name" "compile (see $CF_WORK/cc.err)"; return
+ fi
+
+ if ! CFREE_TEST_ARCH="$arch" "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \
+ --archive "$rtlib" >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then
+ cf_fail "$name" "link (see $CF_WORK/link.err)"; return
+ fi
+
+ exec_target_run "$arch" "$exe" "$CF_WORK/run.out" "$CF_WORK/run.err"
+ if [ "$RUN_RC" -eq 42 ]; then
+ cf_pass "$name"
+ else
+ cf_fail "$name" "run (expected 42 got $RUN_RC; see $CF_WORK/run.err)"
+ fi
}
+# ---- drive the corpus ------------------------------------------------------
+# One tuple per requested arch (<arch>-linux). No opt axis; single lane R.
ARCHES="${CFREE_RT_RUNTIME_ARCHES:-aa64 x64 rv64}"
+TUPLES=
for arch in $ARCHES; do
case "$arch" in
- aa64|x64|rv64) run_arch "$arch" ;;
- *) note_fail "unknown arch '$arch'" ;;
+ aa64|x64|rv64) TUPLES="$TUPLES $arch-linux" ;;
+ *) cf_fail "unknown arch '$arch'" ;;
esac
done
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-if [ "$FAIL" -gt 0 ]; then exit 1; fi
-if [ "$SKIP" -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+CF_LABEL=test-rt-runtime CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \
+ CF_LANES="R" CF_OPT_LEVELS="" CF_TUPLES="$TUPLES" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="${CFREE_RT_PARALLEL:-1}" cf_corpus_run
+
+cf_summary test-rt-runtime
+cf_exit
diff --git a/test/smoke.c b/test/rt/smoke.c
diff --git a/test/smoke/rv64.sh b/test/smoke/rv64.sh
@@ -1,16 +1,18 @@
#!/usr/bin/env bash
-# test/smoke/rv64.sh — end-to-end smoke test for the rv64 podman/qemu path.
+# test/smoke/rv64.sh — end-to-end smoke test for the rv64 podman/qemu path, on
+# the shared Type-K mode-P kit (test/lib/cfree_sh_kit.sh).
#
# Phase-2 of the multi-arch bring-up: prove the test/lib/exec_target.sh helper
-# can build, queue, and run a riscv64-linux ELF before any cfree-emitted
-# rv64 bytes exist. Builds a tiny freestanding static executable with
+# can build, queue, and run a riscv64-linux ELF before any cfree-emitted rv64
+# bytes exist. Builds a tiny freestanding static executable with
# clang --target=riscv64-linux-gnu and pushes it through
-# exec_target_run / exec_target_queue+flush, asserting the expected
-# exit code on both paths.
+# exec_target_run / exec_target_queue+flush, asserting exit code 42 on both
+# paths. This is one inline program, so it is a single-scenario procedural
+# suite (build, exec, assert rc) rather than a file-glob corpus.
#
-# Skipped if clang lacks the riscv64-linux-gnu target or no runner
-# (podman or qemu-riscv64) is available. Mirrors test/cg's skip-vs-fail
-# convention: skip is treated as failure unless CFREE_TEST_ALLOW_SKIP=1.
+# Skipped if clang lacks the riscv64-linux-gnu target, ld.lld is missing, or no
+# runner (podman or qemu-riscv64) is available. Skip is treated as failure
+# unless CFREE_TEST_ALLOW_SKIP=1 (the shared cf_exit honors it).
set -u
@@ -18,11 +20,17 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
BUILD_DIR="$ROOT/build/test/smoke-rv64"
mkdir -p "$BUILD_DIR"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
+CF_KIT_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cfree_sh_kit.sh
+. "$ROOT/test/lib/cfree_sh_kit.sh"
+cf_report_init
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
+# This harness's convention (preserved from the original smoke runner): a SKIP
+# is a failure unless CFREE_TEST_ALLOW_SKIP=1, so CI catches a degraded run.
+CF_SKIP_IS_FAILURE=1
+
+# Mode-P suites are SERIAL and share one $work sandbox.
+work="$BUILD_DIR"
# ---- detect prerequisites --------------------------------------------------
#
@@ -32,7 +40,7 @@ ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
# READY/BLOCKED summary. The smoke script reuses those globals below
# and never re-implements the detection.
# shellcheck source=../lib/check_rv64_env.sh
-source "$(cd "$(dirname "$0")/.." && pwd)/lib/check_rv64_env.sh"
+. "$(cd "$(dirname "$0")/.." && pwd)/lib/check_rv64_env.sh"
check_rv64_env
have_clang_rv64="$RV64_HAVE_CLANG_TARGET"
@@ -56,41 +64,31 @@ is_aarch64=0
export have_qemu QEMU_BIN have_podman is_aarch64 QEMU_RV64_BIN
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
-
-PASS=0; FAIL=0; SKIP=0
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
+. "$ROOT/test/lib/exec_target.sh"
if [ "$have_clang_rv64" -eq 0 ]; then
- note_skip "build" "clang --target=riscv64-linux-gnu unavailable — install: $(_rv64_hint_clang)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "build" "clang --target=riscv64-linux-gnu unavailable — install: $(_rv64_hint_clang)"
+ cf_summary test-smoke-rv64; cf_exit
fi
if [ "$have_lld" -eq 0 ]; then
- note_skip "build" "ld.lld unavailable — install: $(_rv64_hint_lld)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "build" "ld.lld unavailable — install: $(_rv64_hint_lld)"
+ cf_summary test-smoke-rv64; cf_exit
fi
if ! exec_target_supported rv64; then
# No runner: pick the most actionable hint. qemu is the lightest
# to install on a contributor box; podman is the second-best.
- note_skip "exec" "no rv64 runner — easiest fix: $(_rv64_hint_qemu); or set up podman ($(_rv64_hint_podman_riscv64))"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "exec" "no rv64 runner — easiest fix: $(_rv64_hint_qemu); or set up podman ($(_rv64_hint_podman_riscv64))"
+ cf_summary test-smoke-rv64; cf_exit
fi
# ---- build a tiny freestanding riscv64 ELF ----------------------------------
# Direct syscall in _start: SYS_exit_group on rv64 is 94 (a7), exit
# code 42 (a0). No libc, no relocations, no PIE. The point is to
-# exercise the harness pipeline (clang cross-compile → podman/qemu
-# run → recorded rc), not to build a complete program.
+# exercise the harness pipeline (clang cross-compile -> podman/qemu
+# run -> recorded rc), not to build a complete program.
SRC="$BUILD_DIR/smoke.c"
cat >"$SRC" <<'EOF'
__attribute__((noreturn)) void _start(void) {
@@ -107,27 +105,33 @@ if ! clang $CLANG_TARGET -march=rv64gc -fuse-ld=lld \
-fno-PIC -fno-pie -nostdlib -static \
-Wl,-e,_start \
"$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then
- note_fail "build (see $BUILD_DIR/build.err)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- exit 1
+ not_ok "build" "$BUILD_DIR/build.err"
+ cf_summary test-smoke-rv64; cf_exit
fi
+ok "build"
+
+# Classify the recorded rc on a non-42 result. 125/126/127 are podman/shell
+# "couldn't execute" rcs — treat those as setup failures and run the podman
+# classifier so the contributor sees one line saying *which* podman issue it
+# is. ERRFILE / NAME / RC are passed in.
+note_run_fail() {
+ local name="$1" rc="$2" errfile="$3" diag="$BUILD_DIR/$4.diag"
+ if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \
+ { [ "$rc" -eq 125 ] || [ "$rc" -eq 126 ] || [ "$rc" -eq 127 ]; }; then
+ printf '(rc=%s) — %s\n' "$rc" "$(classify_podman_rv64_error "$errfile")" > "$diag"
+ else
+ printf 'expected 42 got %s; see %s\n' "$rc" "$errfile" > "$diag"
+ fi
+ not_ok "$name" "$diag"
+}
# ---- exec_target_run ---------------------------------------------------------
exec_target_run rv64 "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err"
if [ "$RUN_RC" -eq 42 ]; then
- note_pass "exec_target_run rv64 (rc=42)"
+ ok "exec_target_run rv64 (rc=42)"
else
- # 125/126/127 are podman/shell "couldn't execute" rcs — treat
- # those as setup failures and run the podman classifier so the
- # contributor sees one line saying *which* podman issue it is.
- if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \
- { [ "$RUN_RC" -eq 125 ] || [ "$RUN_RC" -eq 126 ] || [ "$RUN_RC" -eq 127 ]; }; then
- diag="$(classify_podman_rv64_error "$BUILD_DIR/run.err")"
- note_fail "exec_target_run rv64 (rc=$RUN_RC) — $diag"
- else
- note_fail "exec_target_run rv64 (expected 42 got $RUN_RC; see $BUILD_DIR/run.err)"
- fi
+ note_run_fail "exec_target_run rv64" "$RUN_RC" "$BUILD_DIR/run.err" "run"
fi
# ---- exec_target_queue + flush ----------------------------------------------
@@ -136,23 +140,16 @@ exec_target_queue rv64 smoke "$EXE" \
"$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc"
exec_target_flush
if [ ! -f "$BUILD_DIR/q.rc" ]; then
- note_fail "exec_target_flush rv64 (no rc file produced)"
+ echo "no rc file produced" > "$BUILD_DIR/q.diag"
+ not_ok "exec_target_queue+flush rv64" "$BUILD_DIR/q.diag"
else
Q_RC="$(cat "$BUILD_DIR/q.rc")"
if [ "$Q_RC" -eq 42 ]; then
- note_pass "exec_target_queue+flush rv64 (rc=42)"
+ ok "exec_target_queue+flush rv64 (rc=42)"
else
- if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \
- { [ "$Q_RC" -eq 125 ] || [ "$Q_RC" -eq 126 ] || [ "$Q_RC" -eq 127 ]; }; then
- diag="$(classify_podman_rv64_error "$BUILD_DIR/q.err")"
- note_fail "exec_target_queue+flush rv64 (rc=$Q_RC) — $diag"
- else
- note_fail "exec_target_queue+flush rv64 (expected 42 got $Q_RC; see $BUILD_DIR/q.err)"
- fi
+ note_run_fail "exec_target_queue+flush rv64" "$Q_RC" "$BUILD_DIR/q.err" "q"
fi
fi
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-smoke-rv64
+cf_exit
diff --git a/test/smoke/rv64_tls_link.sh b/test/smoke/rv64_tls_link.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
-# test/smoke/rv64_tls_link.sh — regression for rv64 TLS Local-Exec lowering.
+# test/smoke/rv64_tls_link.sh — regression for rv64 TLS Local-Exec lowering, on
+# the shared corpus harness (test/lib/cf_corpus.sh).
#
# rv64 used to emit R_RV_TLS_GOT_HI20 (Initial-Exec) for an extern
# _Thread_local symbol under -fPIE (the hosted default). The linker has no
@@ -8,42 +9,81 @@
# links whole-module/static, so the fix is to always emit Local-Exec
# (R_RV_TPREL_HI20/LO12_I), matching the aa64 and x64 backends.
#
-# This is link-only (no execution), so it runs on any host without qemu.
+# This is link-only (no execution), so it runs on any host without qemu. It is
+# a single synthetic structural case: one lane (L) that compiles two sources,
+# asserts the extern _Thread_local access lowered to TPREL (and NOT TLS_GOT) by
+# grepping `objdump -r` BEFORE linking, then asserts the whole-module link
+# succeeds. The L0 (reloc-presence) checks intentionally precede the link.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
CFREE="${CFREE:-$ROOT/build/cfree}"
-WORK="$ROOT/build/test/rv64-tls-link"
-mkdir -p "$WORK"
+BUILD_DIR="$ROOT/build/test/rv64-tls-link"
+mkdir -p "$BUILD_DIR"
+# Preserve the original pass-through skip: when cfree is not built, this
+# regression simply cannot run, and that is reported as a clean exit 0 (it is
+# not a degraded run — the binary just isn't there yet).
if [ ! -x "$CFREE" ]; then
echo "SKIP rv64_tls_link: $CFREE not built"
exit 0
fi
-cat > "$WORK/tls_def.c" <<'EOF'
+export CF_LIB_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cf_corpus.sh
+. "$ROOT/test/lib/cf_corpus.sh"
+
+# Single synthetic case: a marker file the engine can discover so the lane
+# fires exactly once. All real artifacts are generated under $CF_WORK.
+CASE_DIR="$BUILD_DIR/case"
+mkdir -p "$CASE_DIR"
+: > "$CASE_DIR/rv64_tls_link.case"
+
+# ---- lane L: compile -> objdump -r reloc presence/absence -> link ----------
+# Keeps the L0-before-link ordering: the reloc oracle runs on the compiled .o
+# before the whole-module link is attempted.
+cf_lane_L() {
+ cat > "$CF_WORK/tls_def.c" <<'EOF'
_Thread_local int g = 7;
EOF
-cat > "$WORK/tls_ie.c" <<'EOF'
+ cat > "$CF_WORK/tls_ie.c" <<'EOF'
extern _Thread_local int g;
int read_g(void) { return g; }
int *addr_g(void) { return &g; }
EOF
-fail() { printf 'FAIL rv64_tls_link: %s\n' "$1"; exit 1; }
+ if ! "$CFREE" cc -c -target riscv64-linux "$CF_WORK/tls_ie.c" \
+ -o "$CF_WORK/tls_ie.o" >"$CF_WORK/cc_ie.out" 2>"$CF_WORK/cc_ie.err"; then
+ cf_fail "$CF_NAME" "compile tls_ie.c"; return
+ fi
+ if ! "$CFREE" cc -c -target riscv64-linux "$CF_WORK/tls_def.c" \
+ -o "$CF_WORK/tls_def.o" >"$CF_WORK/cc_def.out" 2>"$CF_WORK/cc_def.err"; then
+ cf_fail "$CF_NAME" "compile tls_def.c"; return
+ fi
+
+ # The extern _Thread_local access must lower to TPREL, never TLS_GOT.
+ local relocs
+ relocs="$("$CFREE" objdump -r "$CF_WORK/tls_ie.o" 2>&1)"
+ if ! printf '%s\n' "$relocs" | grep -q 'RV_TPREL_HI20'; then
+ cf_fail "$CF_NAME" "expected RV_TPREL_HI20 reloc; got: $relocs"; return
+ fi
+ if printf '%s\n' "$relocs" | grep -q 'TLS_GOT'; then
+ cf_fail "$CF_NAME" "unexpected TLS_GOT reloc (Initial-Exec regressed): $relocs"; return
+ fi
-"$CFREE" cc -c -target riscv64-linux "$WORK/tls_ie.c" -o "$WORK/tls_ie.o" \
- || fail "compile tls_ie.c"
-"$CFREE" cc -c -target riscv64-linux "$WORK/tls_def.c" -o "$WORK/tls_def.o" \
- || fail "compile tls_def.c"
+ # And the whole-module link must succeed (previously: unsupported reloc 80).
+ if ! "$CFREE" ld --entry read_g "$CF_WORK/tls_ie.o" "$CF_WORK/tls_def.o" \
+ -o "$CF_WORK/tls_out" >"$CF_WORK/ld.out" 2>"$CF_WORK/ld.err"; then
+ cf_fail "$CF_NAME" "link extern _Thread_local"; return
+ fi
-# The extern _Thread_local access must lower to TPREL, never TLS_GOT.
-relocs="$("$CFREE" objdump -r "$WORK/tls_ie.o" 2>&1)"
-echo "$relocs" | grep -q 'RV_TPREL_HI20' || fail "expected RV_TPREL_HI20 reloc; got: $relocs"
-if echo "$relocs" | grep -q 'TLS_GOT'; then fail "unexpected TLS_GOT reloc (Initial-Exec regressed): $relocs"; fi
+ cf_pass "$CF_NAME"
+}
-# And the whole-module link must succeed (previously: unsupported reloc kind 80).
-"$CFREE" ld --entry read_g "$WORK/tls_ie.o" "$WORK/tls_def.o" -o "$WORK/tls_out" \
- || fail "link extern _Thread_local"
+CF_LABEL=rv64_tls_link CF_BUILD_DIR="$BUILD_DIR/work" \
+ CF_CORPUS_GLOBS="$CASE_DIR/*.case" CF_CORPUS_EXT=case CF_SIDECAR_DIR="$CASE_DIR" \
+ CF_LANES="L" CF_OPT_LEVELS="" CF_TUPLES="rv64-elf" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE=0 cf_corpus_run
-echo "OK rv64_tls_link"
+cf_summary rv64_tls_link
+cf_exit
diff --git a/test/smoke/x64.sh b/test/smoke/x64.sh
@@ -1,16 +1,18 @@
#!/usr/bin/env bash
-# test/smoke/x64.sh — end-to-end smoke test for the x64 podman/qemu path.
+# test/smoke/x64.sh — end-to-end smoke test for the x64 podman/qemu path, on
+# the shared Type-K mode-P kit (test/lib/cfree_sh_kit.sh).
#
# Phase-1 of the multi-arch bring-up: prove the test/lib/exec_target.sh helper
-# can build, queue, and run an x86_64-linux ELF before any cfree-emitted
-# x64 bytes exist. Builds a tiny freestanding static executable with
+# can build, queue, and run an x86_64-linux ELF before any cfree-emitted x64
+# bytes exist. Builds a tiny freestanding static executable with
# clang --target=x86_64-linux-gnu and pushes it through
-# exec_target_run / exec_target_queue+flush, asserting the expected
-# exit code on both paths.
+# exec_target_run / exec_target_queue+flush, asserting exit code 42 on both
+# paths. This is one inline program, so it is a single-scenario procedural
+# suite (build, exec, assert rc) rather than a file-glob corpus.
#
-# Skipped if clang lacks the x86_64-linux-gnu target or no runner
-# (podman or qemu-x86_64) is available. Mirrors test/cg's skip-vs-fail
-# convention: skip is treated as failure unless CFREE_TEST_ALLOW_SKIP=1.
+# Skipped if clang lacks the x86_64-linux-gnu target, ld.lld is missing, or no
+# runner (podman or qemu-x86_64) is available. Skip is treated as failure
+# unless CFREE_TEST_ALLOW_SKIP=1 (the shared cf_exit honors it).
set -u
@@ -18,11 +20,17 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
BUILD_DIR="$ROOT/build/test/smoke-x64"
mkdir -p "$BUILD_DIR"
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-color_yel() { printf '\033[33m%s\033[0m' "$1"; }
+CF_KIT_DIR="$ROOT/test/lib"
+# shellcheck source=../lib/cfree_sh_kit.sh
+. "$ROOT/test/lib/cfree_sh_kit.sh"
+cf_report_init
-ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}"
+# This harness's convention (preserved from the original smoke runner): a SKIP
+# is a failure unless CFREE_TEST_ALLOW_SKIP=1, so CI catches a degraded run.
+CF_SKIP_IS_FAILURE=1
+
+# Mode-P suites are SERIAL and share one $work sandbox.
+work="$BUILD_DIR"
# ---- detect prerequisites --------------------------------------------------
@@ -51,38 +59,28 @@ is_aarch64=0
export have_qemu QEMU_BIN have_podman is_aarch64
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+export EXEC_TARGET_MOUNT_ROOT
# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
-
-PASS=0; FAIL=0; SKIP=0
-note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; }
-note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; }
-note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; }
+. "$ROOT/test/lib/exec_target.sh"
if [ $have_clang_x64 -eq 0 ]; then
- note_skip "build" "clang --target=x86_64-linux-gnu unavailable"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "build" "clang --target=x86_64-linux-gnu unavailable"
+ cf_summary test-smoke-x64; cf_exit
fi
if [ $have_lld -eq 0 ]; then
- note_skip "build" "ld.lld unavailable (needed for ELF cross-link)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "build" "ld.lld unavailable (needed for ELF cross-link)"
+ cf_summary test-smoke-x64; cf_exit
fi
if ! exec_target_supported x64; then
- note_skip "exec" "no runner for x64 (podman or qemu-x86_64)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi
- exit 1
+ skip_test "exec" "no runner for x64 (podman or qemu-x86_64)"
+ cf_summary test-smoke-x64; cf_exit
fi
# ---- build a tiny freestanding x86_64 ELF -----------------------------------
# Direct syscall in _start: SYS_exit_group on x86_64 is 231, exit code
# 42. No libc, no relocations, no PIE. The point is to exercise the
-# harness pipeline (clang cross-compile → podman/qemu run → recorded
+# harness pipeline (clang cross-compile -> podman/qemu run -> recorded
# rc), not to build a complete program.
SRC="$BUILD_DIR/smoke.c"
cat >"$SRC" <<'EOF'
@@ -100,18 +98,19 @@ if ! clang $CLANG_TARGET -fuse-ld=lld \
-fno-PIC -fno-pie -nostdlib -static \
-Wl,-e,_start \
"$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then
- note_fail "build (see $BUILD_DIR/build.err)"
- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
- exit 1
+ not_ok "build" "$BUILD_DIR/build.err"
+ cf_summary test-smoke-x64; cf_exit
fi
+ok "build"
# ---- exec_target_run ---------------------------------------------------------
exec_target_run x64 "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err"
if [ "$RUN_RC" -eq 42 ]; then
- note_pass "exec_target_run x64 (rc=42)"
+ ok "exec_target_run x64 (rc=42)"
else
- note_fail "exec_target_run x64 (expected 42 got $RUN_RC; see $BUILD_DIR/run.err)"
+ echo "expected 42 got $RUN_RC; see $BUILD_DIR/run.err" > "$BUILD_DIR/run.diag"
+ not_ok "exec_target_run x64" "$BUILD_DIR/run.diag"
fi
# ---- exec_target_queue + flush ----------------------------------------------
@@ -120,17 +119,17 @@ exec_target_queue x64 smoke "$EXE" \
"$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc"
exec_target_flush
if [ ! -f "$BUILD_DIR/q.rc" ]; then
- note_fail "exec_target_flush x64 (no rc file produced)"
+ echo "no rc file produced" > "$BUILD_DIR/q.diag"
+ not_ok "exec_target_queue+flush x64" "$BUILD_DIR/q.diag"
else
Q_RC="$(cat "$BUILD_DIR/q.rc")"
if [ "$Q_RC" -eq 42 ]; then
- note_pass "exec_target_queue+flush x64 (rc=42)"
+ ok "exec_target_queue+flush x64 (rc=42)"
else
- note_fail "exec_target_queue+flush x64 (expected 42 got $Q_RC; see $BUILD_DIR/q.err)"
+ echo "expected 42 got $Q_RC; see $BUILD_DIR/q.err" > "$BUILD_DIR/q.diag"
+ not_ok "exec_target_queue+flush x64" "$BUILD_DIR/q.diag"
fi
fi
-printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP"
-if [ $FAIL -gt 0 ]; then exit 1; fi
-if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi
-exit 0
+cf_summary test-smoke-x64
+cf_exit
diff --git a/test/strings/run.sh b/test/strings/run.sh
@@ -1,69 +1,26 @@
#!/bin/sh
-# Driver-level `cfree strings` test harness. Same shape as test/strip/run.sh.
+# Driver-level `cfree strings` test harness. Shared loop/reporting in
+# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and
+# diff stdout+stderr against cases/<name>.expected.
set -u
script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree strings-driver
-if [ ! -x "$CFREE" ]; then
- echo "strings-driver: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-strings-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
-
-pass=0
-fail=0
-failures=
-
+CF_WORK=$(cf_workdir strings)
+cf_report_init
for sh in "$cases_dir"/*.sh; do
[ -e "$sh" ] || continue
name=$(basename "${sh%.sh}")
- expected="${sh%.sh}.expected"
- actual="$work_root/$name.actual"
-
- if [ ! -e "$expected" ]; then
- printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- sandbox="$work_root/$name"
- mkdir -p "$sandbox"
- ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1
- case_rc=$?
-
- if [ "$case_rc" -ne 0 ]; then
- printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- if diff -u "$expected" "$actual" >/dev/null 2>&1; then
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s\n' "$name"
- diff -u "$expected" "$actual" || true
- cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true
- fail=$((fail + 1))
- failures="$failures $name"
- fi
+ cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir"
done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nstrings-driver: failures:%s\n' "$failures"
- printf 'strings-driver: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\nstrings-driver: %d/%d passed\n' "$pass" "$total"
+cf_summary strings-driver
+cf_exit
diff --git a/test/strip/run.sh b/test/strip/run.sh
@@ -1,69 +1,26 @@
#!/bin/sh
-# Driver-level `cfree strip` test harness. Same shape as test/ar/run.sh.
+# Driver-level `cfree strip` test harness. Shared loop/reporting in
+# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and
+# diff stdout+stderr against cases/<name>.expected.
set -u
script_dir=$(cd "$(dirname "$0")" && pwd)
repo_root=$(cd "$script_dir/../.." && pwd)
cases_dir="$script_dir/cases"
+CF_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/cfree_sh_kit.sh"
CFREE="${CFREE:-$repo_root/build/cfree}"
export CFREE
+cf_require_cfree strip-driver
-if [ ! -x "$CFREE" ]; then
- echo "strip-driver: cfree binary not found at $CFREE" >&2
- exit 2
-fi
-
-work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-strip-test.XXXXXX")
-trap 'rm -rf "$work_root"' EXIT
-
-pass=0
-fail=0
-failures=
-
+CF_WORK=$(cf_workdir strip)
+cf_report_init
for sh in "$cases_dir"/*.sh; do
[ -e "$sh" ] || continue
name=$(basename "${sh%.sh}")
- expected="${sh%.sh}.expected"
- actual="$work_root/$name.actual"
-
- if [ ! -e "$expected" ]; then
- printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")"
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- sandbox="$work_root/$name"
- mkdir -p "$sandbox"
- ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1
- case_rc=$?
-
- if [ "$case_rc" -ne 0 ]; then
- printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc"
- diff -u "$expected" "$actual" || true
- fail=$((fail + 1))
- failures="$failures $name"
- continue
- fi
-
- if diff -u "$expected" "$actual" >/dev/null 2>&1; then
- printf 'PASS %s\n' "$name"
- pass=$((pass + 1))
- else
- printf 'FAIL %s\n' "$name"
- diff -u "$expected" "$actual" || true
- cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true
- fail=$((fail + 1))
- failures="$failures $name"
- fi
+ cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir"
done
-
-total=$((pass + fail))
-if [ "$fail" -gt 0 ]; then
- printf '\nstrip-driver: failures:%s\n' "$failures"
- printf 'strip-driver: %d/%d passed\n' "$pass" "$total"
- exit 1
-fi
-printf '\nstrip-driver: %d/%d passed\n' "$pass" "$total"
+cf_summary strip-driver
+cf_exit
diff --git a/test/test.mk b/test/test.mk
@@ -31,6 +31,7 @@
# and runs end-to-end so the wiring stays exercised. See doc/ASM.md.
TEST_TARGETS = \
+ test-cf-corpus-selftest \
test-aa64-inline \
test-abi-classify \
test-ar \
@@ -78,6 +79,7 @@ TEST_TARGETS = \
test-libc-musl \
test-libc-musl-rv64 \
test-link \
+ test-link-reloc-uleb128 \
test-macho \
test-native-direct-target \
test-opt \
@@ -106,6 +108,7 @@ TEST_TARGETS = \
test-x64-inline
DEFAULT_TEST_TARGETS = \
+ test-cf-corpus-selftest \
test-driver \
test-pp \
test-elf \
@@ -132,12 +135,45 @@ DEFAULT_TEST_TARGETS = \
test-x64-inline \
test-x64-dbg \
test-rt-headers \
- test-lib-deps
+ test-lib-deps \
+ test-cg-api \
+ test-abi-classify \
+ test-ir-recorder \
+ test-native-direct-target \
+ test-opt \
+ test-asm-symmetry \
+ test-link-reloc-uleb128 \
+ test-dbg \
+ test-disasm-complete \
+ test-macho \
+ test-interp-toy \
+ test-wasm-front
.PHONY: test $(TEST_TARGETS)
test: $(DEFAULT_TEST_TARGETS)
+# Unit-test binary build rules: two regimes (public vs internal interface).
+include test/lib/unit.mk
+
+# Provision the pinned per-arch container rootfs images that exec_target.sh runs
+# cfree-emitted binaries inside. This is the ONLY test step that touches the
+# network: the cross-arch exec harnesses (toy/parse/link ... path X/E/L) run with
+# `podman run --pull=never`, so without these images those paths SKIP. Run once,
+# and again only after the pin in test/lib/test_images.sh changes. FORCE=1
+# re-pulls. The images are pinned per-arch by content digest, so they coexist in
+# local storage and can never clobber one another.
+.PHONY: test-images
+test-images:
+ @bash test/lib/pull_test_images.sh
+
+# Hermetic self-test of the shared corpus harness engine (test/lib/cf_corpus.sh):
+# asserts serial==parallel determinism, SKIP-NA, and the parallel-safety
+# invariant (exec_target queued only on the parent, never in a worker). No
+# cfree binary / podman / qemu needed.
+test-cf-corpus-selftest:
+ @bash test/lib/cf_corpus_selftest.sh
+
test-driver: test-driver-cc test-driver-ar test-driver-cas test-driver-strip test-driver-objcopy test-driver-objdump test-driver-pkg test-driver-strings
test-driver-cc: bin
@@ -196,9 +232,6 @@ AR_TEST_BIN = build/test/ar_test
test-ar: $(AR_TEST_BIN)
$(AR_TEST_BIN)
-$(AR_TEST_BIN): test/ar_test.c $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) test/ar_test.c $(LIB_AR) -o $@
test-driver-ar: bin
@CFREE=$(abspath $(BIN)) test/ar/run.sh
@@ -230,9 +263,6 @@ DWARF_TEST_BIN = build/test/dwarf_test
test-dwarf: $(DWARF_TEST_BIN)
$(DWARF_TEST_BIN)
-$(DWARF_TEST_BIN): test/dwarf/dwarf_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/dwarf/dwarf_test.c $(LIB_OBJS) -o $@
# DWARF producer self-roundtrip unit test. Drives Debug directly, calls
# debug_emit, asserts the produced sections have valid DWARF 5 structure
@@ -246,15 +276,9 @@ test-debug: $(DEBUG_TEST_BIN) $(CFI_TEST_BIN)
$(DEBUG_TEST_BIN)
$(CFI_TEST_BIN)
-$(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/roundtrip_unit.c $(LIB_OBJS) -o $@
# CFI/.eh_frame producer roundtrip for aa64/rv64/x64 (validates the per-arch
# CIE template: code/data-align, return-address reg, CFA-init reg).
-$(CFI_TEST_BIN): test/debug/cfi_unit.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/cfi_unit.c $(LIB_OBJS) -o $@
test-dbg: bin
@CFREE=$(abspath $(BIN)) sh test/dbg/run.sh
@@ -274,21 +298,12 @@ test-isa: $(AA64_ISA_TEST_BIN) $(RV64_DECODE_TEST_BIN)
$(AA64_ISA_TEST_BIN)
$(RV64_DECODE_TEST_BIN)
-$(AA64_ISA_TEST_BIN): test/arch/aa64_isa_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_isa_test.c $(LIB_OBJS) -o $@
-$(RV64_DECODE_TEST_BIN): test/arch/rv64_decode_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/rv64_decode_test.c $(LIB_OBJS) -o $@
# aa64_sweep_gen: emits one representative encoding per disasm-table row for the
# asm<->disasm self-symmetry sweep (test/asm/symmetry.sh). Needs the internal
# arch/aa64/isa.h surface, so -Isrc + LIB_OBJS like the ISA unit test.
AA64_SWEEP_GEN = build/test/aa64_sweep_gen
-$(AA64_SWEEP_GEN): test/arch/aa64_sweep_gen.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_sweep_gen.c $(LIB_OBJS) -o $@
# test-emu: emulator end-to-end integration test. Builds tiny in-memory rv64
# ELFs and runs them to their exit syscall, asserting the exit code — entirely
@@ -300,9 +315,9 @@ EMU_RV64_TEST_BIN = build/test/emu_rv64_test
test-emu: $(EMU_RV64_TEST_BIN)
$(EMU_RV64_TEST_BIN)
-$(EMU_RV64_TEST_BIN): test/emu/rv64_smoke_test.c $(LIB_AR)
+$(EMU_RV64_TEST_BIN): test/emu/rv64_smoke_test.c $(UNIT_HDR_DEPS) $(LIB_AR)
@mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_smoke_test.c $(LIB_AR) -o $@
+ $(CC) $(HOST_CFLAGS) -Iinclude -Isrc -Itest test/emu/rv64_smoke_test.c $(LIB_AR) -o $@
# RISC-V ULEB128 diff-reloc application unit test. link_reloc_apply is an
# internal (hidden) symbol, so link the raw lib objects like the other
@@ -312,9 +327,6 @@ RELOC_ULEB128_TEST_BIN = build/test/reloc_uleb128_unit
test-link-reloc-uleb128: $(RELOC_ULEB128_TEST_BIN)
$(RELOC_ULEB128_TEST_BIN)
-$(RELOC_ULEB128_TEST_BIN): test/link/reloc_uleb128_unit.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/link/reloc_uleb128_unit.c $(LIB_OBJS) -o $@
# test-emu-unit: white-box unit tests for the emulator's INTERNAL units (rv64
# decoder, EmuAddrSpace, Linux syscall handler) that have no public API. Reaches
# internal symbols -> links $(LIB_OBJS) (mirrors test-interp), not the archive.
@@ -323,9 +335,6 @@ EMU_RV64_UNIT_TEST_BIN = build/test/emu_rv64_unit_test
test-emu-unit: $(EMU_RV64_UNIT_TEST_BIN)
$(EMU_RV64_UNIT_TEST_BIN)
-$(EMU_RV64_UNIT_TEST_BIN): test/emu/rv64_vm_unit_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_vm_unit_test.c $(LIB_OBJS) -o $@
# test-interp: threaded-bytecode interpreter unit smoke test. Builds tiny CG IR
# by hand, runs opt_run_o1_interp + interp_lower + the engine, asserts the
@@ -336,9 +345,6 @@ INTERP_SMOKE_TEST_BIN = build/test/interp_smoke_test
test-interp: $(INTERP_SMOKE_TEST_BIN)
$(INTERP_SMOKE_TEST_BIN)
-$(INTERP_SMOKE_TEST_BIN): test/interp/interp_smoke_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/interp/interp_smoke_test.c $(LIB_OBJS) -o $@
# test-interp-emu: differential test of the emulator's INTERP execution mode
# (doc/INTERPRETER.md Phase 4). Builds a tiny rv64 ELF with SD/LD/ecall and runs
@@ -350,9 +356,6 @@ INTERP_EMU_TEST_BIN = build/test/rv64_interp_smoke_test
test-interp-emu: $(INTERP_EMU_TEST_BIN)
$(INTERP_EMU_TEST_BIN)
-$(INTERP_EMU_TEST_BIN): test/emu/rv64_interp_smoke_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_interp_smoke_test.c $(LIB_OBJS) -o $@
# test-interp-toy: run the toy suite's interpreter (--no-jit) path only,
# asserting it matches the golden exit codes (and SKIPping unimplemented ops).
@@ -372,36 +375,20 @@ test-cg-api: $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN)
test-abi-classify: $(ABI_CLASSIFY_TEST_BIN)
$(ABI_CLASSIFY_TEST_BIN)
-$(CG_API_TEST_BIN): test/api/cg_type_test.c $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) test/api/cg_type_test.c $(LIB_AR) -o $@
-$(CG_SWITCH_TEST_BIN): test/api/cg_switch_test.c $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) test/api/cg_switch_test.c $(LIB_AR) -o $@
-$(ABI_CLASSIFY_TEST_BIN): test/api/abi_classify_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/api/abi_classify_test.c $(LIB_OBJS) -o $@
test-ir-recorder: $(IR_RECORDER_TEST_BIN)
$(IR_RECORDER_TEST_BIN)
-$(IR_RECORDER_TEST_BIN): test/cg/ir_recorder_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/cg/ir_recorder_test.c $(LIB_OBJS) -o $@
test-native-direct-target: $(NATIVE_DIRECT_TARGET_TEST_BIN)
$(NATIVE_DIRECT_TARGET_TEST_BIN)
-$(NATIVE_DIRECT_TARGET_TEST_BIN): test/cg/native_direct_target_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/cg/native_direct_target_test.c $(LIB_OBJS) -o $@
test-toy: bin
@CFREE=$(abspath $(BIN)) test/toy/run.sh
-INLINE_PUBLIC_TEST_HDR = test/arch/inline_public_test.h
# Public-API inline-asm backend tests. These emit a tiny function through CG,
# reopen the object through the public object reader, and assert the expected
@@ -411,18 +398,12 @@ AA64_INLINE_TEST_BIN = build/test/aa64_inline_test
test-aa64-inline: $(AA64_INLINE_TEST_BIN)
$(AA64_INLINE_TEST_BIN)
-$(AA64_INLINE_TEST_BIN): test/arch/aa64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_inline_test.c $(LIB_AR) -o $@
RV64_INLINE_TEST_BIN = build/test/rv64_inline_test
test-rv64-inline: $(RV64_INLINE_TEST_BIN)
$(RV64_INLINE_TEST_BIN)
-$(RV64_INLINE_TEST_BIN): test/arch/rv64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/rv64_inline_test.c $(LIB_AR) -o $@
# rv64 JIT smoke test. Builds a tiny rv64 ELF .o in memory, runs it
# through cfree_link_session in JIT-output mode, and skips native execution
@@ -438,9 +419,6 @@ test-rv64-jit: $(RV64_JIT_TEST_BIN)
exit $$rc; \
fi
-$(RV64_JIT_TEST_BIN): test/link/rv64_jit_test.c $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) test/link/rv64_jit_test.c $(LIB_AR) -o $@
# Link-only regression for rv64 TLS Local-Exec lowering (runs on any host).
test-rv64-tls-link: bin
@@ -451,9 +429,6 @@ X64_INLINE_TEST_BIN = build/test/x64_inline_test
test-x64-inline: $(X64_INLINE_TEST_BIN)
$(X64_INLINE_TEST_BIN)
-$(X64_INLINE_TEST_BIN): test/arch/x64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/x64_inline_test.c $(LIB_AR) -o $@
X64_DBG_TEST_BIN = build/test/x64_dbg_test
@@ -463,9 +438,6 @@ test-x64-dbg: $(X64_DBG_TEST_BIN)
# Reaches the internal arch/arch.h surface (arch_lookup, ArchDbgOps) -> links
# $(LIB_OBJS), not the archive, whose relocatable merge localizes non-public
# symbols (mirrors the aa64/rv64 arch unit tests above).
-$(X64_DBG_TEST_BIN): test/arch/x64_dbg_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/x64_dbg_test.c $(LIB_OBJS) -o $@
RT_HEADER_TEST_TARGETS = \
aarch64-linux-gnu \
@@ -479,7 +451,7 @@ test-rt-headers: bin
for target in $(RT_HEADER_TEST_TARGETS); do \
out="build/test/rt-headers/$$target/smoke.o"; \
mkdir -p "$$(dirname "$$out")"; \
- $(BIN) cc -target "$$target" -Werror -c test/smoke.c -o "$$out"; \
+ $(BIN) cc -target "$$target" -Werror -c test/rt/smoke.c -o "$$out"; \
done
test-rt-runtime: bin rt $(LINK_EXE_RUNNER)
@@ -637,19 +609,13 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R
OPT_TEST_BIN = build/test/cg_ir_lower_test
TINY_INLINE_TEST_BIN = build/test/tiny_inline_test
-test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail
+test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-prologue-tier
$(OPT_TEST_BIN)
-$(OPT_TEST_BIN): test/opt/cg_ir_lower_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/opt/cg_ir_lower_test.c $(LIB_OBJS) -o $@
test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN)
$(TINY_INLINE_TEST_BIN)
-$(TINY_INLINE_TEST_BIN): test/opt/tiny_inline_test.c $(LIB_OBJS)
- @mkdir -p $(dir $@)
- $(CC) $(TEST_HOST_CFLAGS) -Isrc test/opt/tiny_inline_test.c $(LIB_OBJS) -o $@
# Behavioral disasm check: tiny callee `bl` disappears from its caller at -O1.
test-opt-inline: bin
@@ -668,6 +634,12 @@ test-opt-static-prune-aa64: bin
test-opt-aa64-tail: bin
@CFREE=$(abspath $(BIN)) bash test/opt/aa64_tail_call.sh
+# Structural disasm check: the -O1 known-frame prologue cost-model tiers
+# (aa64 reference + ported x64 slim/red-zone and rv64 leaf shapes).
+.PHONY: test-opt-prologue-tier
+test-opt-prologue-tier: bin
+ @CFREE=$(abspath $(BIN)) bash test/opt/prologue_tier.sh
+
test-parse: test-parse-ok test-parse-err
test-parse-ok: lib $(TEST_RT_DEP) $(PARSE_RUNNER) $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER)
@@ -767,11 +739,11 @@ test-wasm-front: bin $(WASM_TOOL) $(LINK_EXE_RUNNER) $(JIT_RUNNER)
bash test/wasm/run.sh
# test-wasm-target: structural checks on `cfree cc -target wasm32-none`
-# output. Each check_*.sh under test/wasm-target compiles a tiny C or
-# toy fixture and asserts a property of the produced module bytes
-# (memory.copy/fill opcodes, exported "memory", (import ...) decls).
-# Opt-in: not in the default `test` target because the checks depend
-# on the bulk-memory + (import ...) backend work landing first.
+# output. test/wasm-target/run.sh is a Type C corpus harness whose lanes each
+# compile a tiny C or toy fixture and assert a property of the produced module
+# bytes (inline-asm opcodes, memory.copy/fill opcodes, exported "memory",
+# (import ...) decls). Opt-in: not in the default `test` target because the
+# checks depend on the bulk-memory + (import ...) backend work landing first.
test-wasm-target: bin
@CFREE=$(abspath $(BIN)) bash test/wasm-target/run.sh
@@ -915,8 +887,8 @@ test-lib-deps:
@$(MAKE) lib RELEASE=1
@mkdir -p $(dir $(LIB_DEPS_ACTUAL))
@python3 scripts/lib_external_deps.py $(LIB_DEPS_AR) > $(LIB_DEPS_ACTUAL)
- @diff -u test/lib_deps.allowlist $(LIB_DEPS_ACTUAL) \
- || { echo "libcfree.a external symbol set drifted from test/lib_deps.allowlist"; exit 1; }
+ @diff -u scripts/lib_deps.allowlist $(LIB_DEPS_ACTUAL) \
+ || { echo "libcfree.a external symbol set drifted from scripts/lib_deps.allowlist"; exit 1; }
@python3 scripts/lib_reloc_defined_prefixes.py $(LIB_DEPS_AR) \
--output $(LIB_RELOC) --ar $(AR) --cc $(CC) > $(LIB_RELOC_BAD)
@test ! -s $(LIB_RELOC_BAD) \
diff --git a/test/toy/run.sh b/test/toy/run.sh
@@ -1,33 +1,32 @@
#!/usr/bin/env bash
-# test/toy/run.sh — .toy frontend end-to-end tests.
-#
-# Paths per case:
-# R cfree run -O{level} case.toy
-# I cfree run --no-jit -O{level} case.toy -> execute via the IR interpreter
-# instead of JIT native code; asserts the same exit code as the golden.
+# test/toy/run.sh — .toy frontend end-to-end tests, on the shared corpus
+# harness (test/lib/cf_corpus.sh). Lanes (CFREE_TEST_PATHS, default RLCWI):
+# R cfree run -O{level} case.toy (JIT native)
+# I cfree run --no-jit -O{level} case.toy (IR interpreter)
# Ops the interpreter does not yet implement SKIP (greppable
# "interp: <feature> not supported"), like paths C/W.
-# L cfree cc -O{level} -c case.toy -> cfree ld case.o -> native executable
-# X cfree cc -O{level} -target -> cfree ld -> exec_target for Linux cross targets
+# L cfree cc -O{level} -c -> cfree ld -> native exec. Has a <name>.objdump
+# sidecar substring check (separate :objdump verdict) and a
+# <name>.link.skip sidecar.
+# X cross-arch: cfree cc -O{level} -target -> cfree ld -> exec_target for the
+# aa64/x64/rv64 Linux targets. Exec is deferred to the engine's batched
+# exec_target flush (cf_queue_e). Opt-in (not in the default paths).
# C cfree cc --emit=c case.toy -> host cc -> native exec. Exercises the
-# --emit=c C-source backend driven by a non-C frontend (validates that
-# the CGTarget seam is frontend-agnostic). Phased-rollout panics from
-# the C target report as SKIP. Host cc runs under
-# -Wall -Wextra -Werror; fixtures wrap their i64 main in a small i32
-# thunk so the emitted `int32_t main(void)` satisfies the standard.
-# W cfree cc -target wasm32-none -c case.toy -> .wasm; then cfree run on
-# the .wasm (routes through the lang/wasm frontend back to native CG,
-# JITs, and invokes main). Exercises the Wasm backend (Toy -> wasm) and
-# verifies the produced module still computes the expected result.
-# Disabled by default because most cases hit unimplemented Wasm
-# lowerings (aggregates, address-of, atomics, intrinsics, ...).
+# --emit=c C-source backend driven by a non-C frontend (validates that the
+# CGTarget seam is frontend-agnostic). Phased-rollout panics from the C
+# target report as SKIP. Host cc runs under -Wall -Wextra -Werror; fixtures
+# wrap their i64 main in a small i32 thunk so the emitted
+# `int32_t main(void)` satisfies the standard. opt-0 only.
+# W cfree cc -target wasm32-none -c case.toy -> .wasm; then cfree run on the
+# .wasm (routes through the lang/wasm frontend back to native CG, JITs, and
+# invokes main). Exercises the Wasm backend (Toy -> wasm). opt-0 only.
+# Most cases hit unimplemented Wasm lowerings -> phased-rollout SKIP.
#
# Sidecars:
# <name>.expected expected process exit code, default 0
# <name>.objdump fixed substrings expected in `cfree objdump -h -t`
-# after the linked-object compile path
-# <name>.cbackend.skip opts the case out of path C (with reason),
-# without affecting other paths
+# after the linked-object compile path (lane L)
+# <name>.cbackend.skip opts the case out of path C (with reason)
# <name>.wasm.skip opts the case out of path W (with reason)
# <name>.link.skip opts the case out of path L (with reason)
# err/<name>.expected expected diagnostic substring for compile-fail cases
@@ -39,10 +38,16 @@
# C and W run only at O0 even when included with other opt levels.
# Default paths are "RLCWI"; override with CFREE_TEST_PATHS.
# CFREE_OPT_LEVELS selects optimization levels.
+#
+# Every lane hook writes only under CF_WORK and records via cf_*, so the runner
+# is parallel-safe; CFREE_TOY_PARALLEL flips dispatch.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
TEST_DIR="$ROOT/test/toy"
BUILD_DIR="$ROOT/build/test/toy"
CFREE="${CFREE:-$ROOT/build/cfree}"
@@ -58,112 +63,89 @@ case "$PATHS" in *I*) RUN_I=1;; *) RUN_I=0;; esac
TOY_CROSS_ARCHS="${CFREE_TOY_CROSS_ARCHS:-aa64 x64 rv64}"
TOY_OPT_LEVELS="${CFREE_OPT_LEVELS:-0 1}"
HOST_CC="${CC:-cc}"
+PAR="${CFREE_TOY_PARALLEL:-1}"
-mkdir -p "$BUILD_DIR"
+# The engine's CFREE_TEST_FILTER drives discovery; honor the positional filter.
+export CFREE_TEST_FILTER="$FILTER"
-PASS=0
-FAIL=0
-SKIP=0
-FAIL_NAMES=()
-
-color_red() { printf '\033[31m%s\033[0m' "$1"; }
-color_grn() { printf '\033[32m%s\033[0m' "$1"; }
-
-note_pass() {
- PASS=$((PASS + 1))
- printf ' %s %s\n' "$(color_grn PASS)" "$1"
-}
-
-note_fail() {
- FAIL=$((FAIL + 1))
- FAIL_NAMES+=("$1")
- printf ' %s %s\n' "$(color_red FAIL)" "$1"
-}
-
-note_skip() {
- SKIP=$((SKIP + 1))
- printf ' SKIP %s (%s)\n' "$1" "$2"
-}
+shopt -s nullglob
+mkdir -p "$BUILD_DIR"
-expected_for() {
- local src="$1" exp="${src%.toy}.expected"
- if [ -f "$exp" ]; then
- tr -d '[:space:]' < "$exp"
- else
- printf '0'
- fi
-}
+if [ ! -x "$CFREE" ]; then
+ printf 'missing cfree binary: %s\n' "$CFREE" >&2
+ exit 2
+fi
-check_rc() {
+# ---- shared rc oracle (CF_WORK-confined -> parallel-safe) -------------------
+# Toy's pass rule is stricter than wasm's: rc must match AND stderr must be
+# empty. Diagnostics (expected/got + the stderr body) are surfaced on FAIL.
+tf_check_rc() { # LABEL GOT EXPECTED STDERR_FILE
local label="$1" got="$2" expected="$3" stderr_file="$4"
expected=$((expected & 255))
if [ "$got" -eq "$expected" ] && [ ! -s "$stderr_file" ]; then
- note_pass "$label"
+ cf_pass "$label"
else
- note_fail "$label"
- printf ' expected rc %d, got %d\n' "$expected" "$got"
+ cf_fail "$label" "expected rc $expected, got $got"
if [ -s "$stderr_file" ]; then
sed 's/^/ | /' "$stderr_file"
fi
fi
}
-run_case_run() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5"
- local out="$work/run.out" err="$work/run.err" rc
- "$CFREE" run "-O$opt" "$src" > "$out" 2> "$err"
+# ---- lanes (cases corpus) --------------------------------------------------
+cf_lane_R() {
+ local label="$CF_BASE/R-O$CF_OPT" rc
+ "$CFREE" run "-O$CF_OPT" "$CF_SRC" > "$CF_WORK/run.out" 2> "$CF_WORK/run.err"
rc=$?
- check_rc "$name/R-O$opt" "$rc" "$expected" "$err"
+ tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/run.err"
}
# Path I: cfree run --no-jit — execute through the IR interpreter instead of
-# JIT-compiled native code, and assert the same exit code as the golden (which
-# the JIT R-path also matches). Ops the interpreter does not yet implement emit
-# a greppable "interp: <feature> not supported" diagnostic and SKIP rather than
-# FAIL, mirroring how paths C/W treat phased-rollout panics.
-run_case_interp() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5"
- local out="$work/interp.out" err="$work/interp.err" rc missing
- local label="$name/I-O$opt"
- "$CFREE" run --no-jit "-O$opt" "$src" > "$out" 2> "$err"
+# JIT-compiled native code, and assert the same exit code as the golden. Ops the
+# interpreter does not yet implement emit a greppable
+# "interp: <feature> not supported" diagnostic and SKIP rather than FAIL.
+cf_lane_I() {
+ local label="$CF_BASE/I-O$CF_OPT" rc missing
+ "$CFREE" run --no-jit "-O$CF_OPT" "$CF_SRC" \
+ > "$CF_WORK/interp.out" 2> "$CF_WORK/interp.err"
rc=$?
- missing=$(grep -oE 'interp: .*not supported' "$err" 2>/dev/null | head -n1 || true)
+ missing=$(grep -oE 'interp: .*not supported' "$CF_WORK/interp.err" 2>/dev/null | head -n1 || true)
if [ -n "$missing" ]; then
- note_skip "$label" "$missing"
+ cf_skip "$label" "$missing"
return
fi
- check_rc "$label" "$rc" "$expected" "$err"
+ tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/interp.err"
}
-run_case_link() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5"
- local obj="$work/$name.o" exe="$work/$name.exe"
- local cc_err="$work/cc.err" ld_err="$work/ld.err" out="$work/exe.out"
- local err="$work/exe.err" dump="$work/objdump.out" dump_err="$work/objdump.err" rc
- local dump_exp="${src%.toy}.objdump"
- local link_skip="${src%.toy}.link.skip"
+cf_lane_L() {
+ local label="$CF_BASE/L-O$CF_OPT" rc
+ local obj="$CF_WORK/$CF_BASE.o" exe="$CF_WORK/$CF_BASE.exe"
+ local cc_err="$CF_WORK/cc.err" ld_err="$CF_WORK/ld.err"
+ local dump="$CF_WORK/objdump.out" dump_err="$CF_WORK/objdump.err"
+ local dump_exp="${CF_SRC%.toy}.objdump"
+ local link_skip="${CF_SRC%.toy}.link.skip"
+ local pattern missing
+
if [ -e "$link_skip" ]; then
- note_skip "$name/L-O$opt" "$(head -n1 "$link_skip")"
+ cf_skip "$label" "$(head -n1 "$link_skip")"
return
fi
- if ! "$CFREE" cc "-O$opt" -c "$src" -o "$obj" > "$work/cc.out" 2> "$cc_err"; then
- note_fail "$name/L-O$opt"
- printf ' cfree cc -O%s -c failed\n' "$opt"
+ if ! "$CFREE" cc "-O$CF_OPT" -c "$CF_SRC" -o "$obj" \
+ > "$CF_WORK/cc.out" 2> "$cc_err"; then
+ cf_fail "$label" "cfree cc -O$CF_OPT -c failed"
sed 's/^/ | /' "$cc_err"
return
fi
if [ -s "$cc_err" ]; then
- note_fail "$name/L-O$opt"
- printf ' cfree cc -O%s -c wrote stderr\n' "$opt"
+ cf_fail "$label" "cfree cc -O$CF_OPT -c wrote stderr"
sed 's/^/ | /' "$cc_err"
return
fi
if [ -f "$dump_exp" ]; then
if ! "$CFREE" objdump -h -t "$obj" > "$dump" 2> "$dump_err"; then
- note_fail "$name/L-O$opt:objdump"
- printf ' cfree objdump failed\n'
+ cf_fail "$label:objdump" "cfree objdump failed"
sed 's/^/ | /' "$dump_err"
return
fi
@@ -172,39 +154,38 @@ run_case_link() {
[ -z "$pattern" ] && continue
if ! grep -F -q -- "$pattern" "$dump"; then
missing=1
- printf '%s\n' "$pattern" >> "$work/objdump.missing"
+ printf '%s\n' "$pattern" >> "$CF_WORK/objdump.missing"
fi
done < "$dump_exp"
if [ "$missing" -eq 0 ]; then
- note_pass "$name/L-O$opt:objdump"
+ cf_pass "$label:objdump"
else
- note_fail "$name/L-O$opt:objdump"
+ cf_fail "$label:objdump" "missing objdump substring(s)"
printf ' missing objdump substring(s):\n'
- sed 's/^/ > /' "$work/objdump.missing"
+ sed 's/^/ > /' "$CF_WORK/objdump.missing"
printf ' actual objdump:\n'
sed 's/^/ | /' "$dump"
fi
fi
- if ! "$CFREE" ld "$obj" -o "$exe" > "$work/ld.out" 2> "$ld_err"; then
- note_fail "$name/L-O$opt"
- printf ' cfree ld failed\n'
+ if ! "$CFREE" ld "$obj" -o "$exe" > "$CF_WORK/ld.out" 2> "$ld_err"; then
+ cf_fail "$label" "cfree ld failed"
sed 's/^/ | /' "$ld_err"
return
fi
if [ -s "$ld_err" ]; then
- note_fail "$name/L-O$opt"
- printf ' cfree ld wrote stderr\n'
+ cf_fail "$label" "cfree ld wrote stderr"
sed 's/^/ | /' "$ld_err"
return
fi
chmod +x "$exe" 2>/dev/null || true
- "$exe" > "$out" 2> "$err"
+ "$exe" > "$CF_WORK/exe.out" 2> "$CF_WORK/exe.err"
rc=$?
- check_rc "$name/L-O$opt" "$rc" "$expected" "$err"
+ tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/exe.err"
}
+# ---- cross-arch (path X) helpers -------------------------------------------
cross_triple_for() {
case "$1" in
aa64|aarch64) printf 'aarch64-linux-gnu' ;;
@@ -263,185 +244,205 @@ EOF_START
printf '%s' "$start_o"
}
-run_case_cross_one() {
- local arch="$1" name="$2" src="$3" expected="$4" work="$5" opt="$6"
- local triple tag obj exe start_obj cc_err ld_err out err rc label
+cross_one() {
+ local arch="$1"
+ local triple tag obj exe start_obj cc_err ld_err out err label
triple="$(cross_triple_for "$arch")" || {
- note_skip "$name/X-O$opt:$arch" "unknown cross arch"
+ cf_skip "$CF_BASE/X-O$CF_OPT:$arch" "unknown cross arch"
return
}
tag="$(cross_tag_for "$arch")" || {
- note_skip "$name/X-O$opt:$arch" "unknown cross arch"
+ cf_skip "$CF_BASE/X-O$CF_OPT:$arch" "unknown cross arch"
return
}
- label="$name/X-O$opt:$arch"
+ label="$CF_BASE/X-O$CF_OPT:$arch"
if [ "$arch" != "aa64" ] && [ "$arch" != "aarch64" ] &&
- grep -q 'asmnop' "$src"; then
- note_skip "$label" "asmnop is target-specific before toy asm selectors"
+ grep -q 'asmnop' "$CF_SRC"; then
+ cf_skip "$label" "asmnop is target-specific before toy asm selectors"
return
fi
if ! exec_target_supported "$tag"; then
- note_skip "$label" "no runner for $tag"
+ cf_skip "$label" "no runner for $tag"
return
fi
- obj="$work/$name.O$opt.$arch.o"
- exe="$work/$name.O$opt.$arch.exe"
- cc_err="$work/$arch.cc.err"
- ld_err="$work/$arch.ld.err"
- out="$work/$arch.out"
- err="$work/$arch.err"
-
- if ! "$CFREE" cc "-O$opt" -target "$triple" -c "$src" -o "$obj" \
- > "$work/$arch.cc.out" 2> "$cc_err"; then
- note_fail "$label"
- printf ' cfree cc -O%s -target %s -c failed\n' "$opt" "$triple"
+ obj="$CF_WORK/$CF_BASE.O$CF_OPT.$arch.o"
+ exe="$CF_WORK/$CF_BASE.O$CF_OPT.$arch.exe"
+ cc_err="$CF_WORK/$arch.cc.err"
+ ld_err="$CF_WORK/$arch.ld.err"
+ out="$CF_WORK/$arch.out"
+ err="$CF_WORK/$arch.err"
+
+ if ! "$CFREE" cc "-O$CF_OPT" -target "$triple" -c "$CF_SRC" -o "$obj" \
+ > "$CF_WORK/$arch.cc.out" 2> "$cc_err"; then
+ cf_fail "$label" "cfree cc -O$CF_OPT -target $triple -c failed"
sed 's/^/ | /' "$cc_err"
return
fi
if [ -s "$cc_err" ]; then
- note_fail "$label"
- printf ' cfree cc -O%s -target %s -c wrote stderr\n' "$opt" "$triple"
+ cf_fail "$label" "cfree cc -O$CF_OPT -target $triple -c wrote stderr"
sed 's/^/ | /' "$cc_err"
return
fi
- start_obj="$(cross_make_start_obj "$arch" "$triple" "$work")" || {
- note_skip "$label" "clang --target=$triple unavailable for startup"
+ start_obj="$(cross_make_start_obj "$arch" "$triple" "$CF_WORK")" || {
+ cf_skip "$label" "clang --target=$triple unavailable for startup"
return
}
if ! "$CFREE" ld "$obj" "$start_obj" -o "$exe" \
- > "$work/$arch.ld.out" 2> "$ld_err"; then
- note_fail "$label"
- printf ' cfree ld failed\n'
+ > "$CF_WORK/$arch.ld.out" 2> "$ld_err"; then
+ cf_fail "$label" "cfree ld failed"
sed 's/^/ | /' "$ld_err"
return
fi
if [ -s "$ld_err" ]; then
- note_fail "$label"
- printf ' cfree ld wrote stderr\n'
+ cf_fail "$label" "cfree ld wrote stderr"
sed 's/^/ | /' "$ld_err"
return
fi
chmod +x "$exe" 2>/dev/null || true
- # Defer execution: queue for one batched container run per arch (drained
- # by exec_target_flush after every case is compiled+linked). The
- # platform-mismatch warning is filtered and the rc checked post-flush.
- XQ_LABELS+=("$label")
- XQ_EXPECTED+=("$expected")
- XQ_ERRS+=("$err")
- XQ_RCS+=("$work/$arch.rc")
- exec_target_queue "$tag" "$name" "$exe" "$out" "$err" "$work/$arch.rc"
+ # Defer execution: the engine batches one container run per arch and checks
+ # rc == expected after exec_target_flush; tf_flush_verify (CF_FLUSH_VERIFY)
+ # adds the empty-stderr check after filtering the platform-mismatch warning.
+ cf_queue_e "$label" "$exe" "$out" "$err" "$CF_WORK/$arch.rc" "$CF_EXPECTED" "$tag" "$err"
}
-run_case_cross() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5" arch
+cf_lane_X() {
+ local arch
for arch in $TOY_CROSS_ARCHS; do
- run_case_cross_one "$arch" "$name" "$src" "$expected" "$work" "$opt"
+ cross_one "$arch"
done
}
-run_case_wasm() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5"
- local label="$name/W-O$opt"
- local wasm_skip="${src%.toy}.wasm.skip"
- if [ -e "$wasm_skip" ]; then
- note_skip "$label" "$(head -n1 "$wasm_skip")"
+# CF_FLUSH_VERIFY: called per queued-X case after flush with (label, payload,
+# rc). The engine already verified rc == expected; here we replicate the
+# original's "stderr must be empty (after filtering the image-platform warning)"
+# rule. payload is the err file path. Returns 0 to keep pass, 1 to fail.
+tf_flush_verify() {
+ local errf="$2"
+ [ -n "$errf" ] || return 0
+ if [ -s "$errf" ]; then
+ grep -v '^WARNING: image platform .* does not match the expected platform' \
+ "$errf" > "$errf.clean" 2>/dev/null || true
+ mv "$errf.clean" "$errf" 2>/dev/null || true
+ fi
+ [ ! -s "$errf" ]
+}
+
+# ---- C-source backend (path C, opt-0 only) ---------------------------------
+cf_lane_C() {
+ local label="$CF_BASE/C-O$CF_OPT" rc missing
+ local cbackend_skip="${CF_SRC%.toy}.cbackend.skip"
+ if [ -e "$cbackend_skip" ]; then
+ cf_skip "$label" "$(head -n1 "$cbackend_skip")"
return
fi
- local wasm="$work/$name.wasm"
- local cc_err="$work/wasm.cc.err" cc_out="$work/wasm.cc.out"
- local run_err="$work/wasm.run.err" run_out="$work/wasm.run.out"
- local rc missing
- if ! "$CFREE" cc "-O$opt" -target wasm32-none -c "$src" -o "$wasm" \
- > "$cc_out" 2> "$cc_err"; then
- # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. Matches
- # the C target's pattern so the suite signal stays "real regressions".
- # The Wasm target emits "wasm: <feature> not yet implemented" plus a
- # handful of "unsupported X" / "too many X (max N supported in v1)"
- # / "max N supported in v1" variants — all of which mean the same
- # thing operationally: this case needs more wasm CGTarget work.
- missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \
- "$cc_err" 2>/dev/null | head -n1 || true)
+ local out_c="$CF_WORK/$CF_BASE.cfree.c"
+ local out_bin="$CF_WORK/$CF_BASE.cbackend.bin"
+ local emit_err="$CF_WORK/c.emit.err"
+ local cc_err="$CF_WORK/c.cc.err"
+ local run_err="$CF_WORK/c.run.err"
+
+ # --emit=c forces opt_level=0 internally; pass -O$CF_OPT anyway so the
+ # driver flag parsing stays exercised.
+ if ! "$CFREE" cc "-O$CF_OPT" --emit=c "$CF_SRC" -o "$out_c" \
+ > "$CF_WORK/c.emit.out" 2> "$emit_err"; then
+ # Phased-rollout panic from the C target -> SKIP, not FAIL.
+ missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
+ "$emit_err" 2>/dev/null | head -n1 || true)
if [ -n "$missing" ]; then
- note_skip "$label" "$missing"
+ cf_skip "$label" "$missing"
return
fi
- note_fail "$label"
- printf ' cfree cc -target wasm32-none failed\n'
- sed 's/^/ | /' "$cc_err"
+ cf_fail "$label" "cfree cc --emit=c failed"
+ sed 's/^/ | /' "$emit_err"
return
fi
- if [ -s "$cc_err" ]; then
- note_fail "$label"
- printf ' cfree cc -target wasm32-none wrote stderr\n'
+ # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the emitted
+ # `int32_t main(void)` satisfies the host C compiler's main-return-type check.
+ if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror "$out_c" -o "$out_bin" \
+ > "$CF_WORK/c.cc.out" 2> "$cc_err"; then
+ cf_fail "$label" "host cc rejected emitted source"
sed 's/^/ | /' "$cc_err"
return
fi
- # cfree run on the .wasm routes through the lang/wasm frontend, lowers
- # to native CG, links via the JIT, and invokes main.
- "$CFREE" run "$wasm" > "$run_out" 2> "$run_err"
+ "$out_bin" > "$CF_WORK/c.run.out" 2> "$run_err"
rc=$?
- check_rc "$label" "$rc" "$expected" "$run_err"
+ tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$run_err"
}
-run_case_emit_c() {
- local name="$1" src="$2" expected="$3" work="$4" opt="$5"
- local label="$name/C-O$opt"
- local cbackend_skip="${src%.toy}.cbackend.skip"
- if [ -e "$cbackend_skip" ]; then
- note_skip "$label" "$(head -n1 "$cbackend_skip")"
+# ---- Wasm roundtrip (path W, opt-0 only) -----------------------------------
+cf_lane_W() {
+ local label="$CF_BASE/W-O$CF_OPT" rc missing
+ local wasm_skip="${CF_SRC%.toy}.wasm.skip"
+ if [ -e "$wasm_skip" ]; then
+ cf_skip "$label" "$(head -n1 "$wasm_skip")"
return
fi
- local out_c="$work/$name.cfree.c"
- local out_bin="$work/$name.cbackend.bin"
- local emit_err="$work/c.emit.err"
- local cc_err="$work/c.cc.err"
- local run_out="$work/c.run.out" run_err="$work/c.run.err"
- local rc missing
-
- # --emit=c forces opt_level=0 internally; pass -O$opt anyway so the
- # driver flag parsing stays exercised.
- if ! "$CFREE" cc "-O$opt" --emit=c "$src" -o "$out_c" \
- > "$work/c.emit.out" 2> "$emit_err"; then
- # Phased-rollout panic from the C target → SKIP, not FAIL.
- missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
- "$emit_err" 2>/dev/null | head -n1 || true)
+ local wasm="$CF_WORK/$CF_BASE.wasm"
+ local cc_err="$CF_WORK/wasm.cc.err"
+ local run_err="$CF_WORK/wasm.run.err"
+ if ! "$CFREE" cc "-O$CF_OPT" -target wasm32-none -c "$CF_SRC" -o "$wasm" \
+ > "$CF_WORK/wasm.cc.out" 2> "$cc_err"; then
+ # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. The Wasm
+ # target emits "wasm: <feature> not yet implemented" plus a handful of
+ # "unsupported X" / "too many X (max N supported in v1)" /
+ # "max N supported in v1" variants — all meaning this case needs more
+ # wasm CGTarget work.
+ missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \
+ "$cc_err" 2>/dev/null | head -n1 || true)
if [ -n "$missing" ]; then
- note_skip "$label" "$missing"
+ cf_skip "$label" "$missing"
return
fi
- note_fail "$label"
- printf ' cfree cc --emit=c failed\n'
- sed 's/^/ | /' "$emit_err"
+ cf_fail "$label" "cfree cc -target wasm32-none failed"
+ sed 's/^/ | /' "$cc_err"
return
fi
- # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the
- # emitted `int32_t main(void)` satisfies the host C compiler's
- # main-return-type check.
- if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror "$out_c" -o "$out_bin" \
- > "$work/c.cc.out" 2> "$cc_err"; then
- note_fail "$label"
- printf ' host cc rejected emitted source\n'
+ if [ -s "$cc_err" ]; then
+ cf_fail "$label" "cfree cc -target wasm32-none wrote stderr"
sed 's/^/ | /' "$cc_err"
return
fi
- "$out_bin" > "$run_out" 2> "$run_err"
+ # cfree run on the .wasm routes through the lang/wasm frontend, lowers to
+ # native CG, links via the JIT, and invokes main.
+ "$CFREE" run "$wasm" > "$CF_WORK/wasm.run.out" 2> "$run_err"
rc=$?
- check_rc "$label" "$rc" "$expected" "$run_err"
+ tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$run_err"
}
-if [ ! -x "$CFREE" ]; then
- printf 'missing cfree binary: %s\n' "$CFREE" >&2
- exit 2
-fi
-
-printf 'toy: CFREE=%s\n' "$CFREE"
-printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS"
+# ---- err corpus (compile-fail cases) ---------------------------------------
+# cc MUST fail; the .expected file holds diagnostic substring(s) that must
+# appear in stderr (grep -F -f). cc is invoked native (no -target), matching the
+# original which used `cfree cc -c` on the host arch.
+cf_lane_ERR() {
+ local label="$CF_BASE/E"
+ local obj="$CF_WORK/$CF_BASE.o"
+ local err="$CF_WORK/cc.err"
+ local exp="${CF_SRC%.toy}.expected"
+ if "$CFREE" cc -c "$CF_SRC" -o "$obj" > "$CF_WORK/cc.out" 2> "$err"; then
+ cf_fail "$label" "expected compile failure, got success"
+ return
+ fi
+ if [ ! -f "$exp" ]; then
+ cf_fail "$label" "missing expected diagnostic: $exp"
+ return
+ fi
+ if grep -F -q -f "$exp" "$err"; then
+ cf_pass "$label"
+ else
+ cf_fail "$label" "expected diagnostic substring not found"
+ printf ' expected diagnostic substring:\n'
+ sed 's/^/ > /' "$exp"
+ printf ' actual stderr:\n'
+ sed 's/^/ | /' "$err"
+ fi
+}
-if [ $RUN_X -eq 1 ]; then
+# ---- exec_target wiring (path X uses the engine's deferred-exec flush) ------
+if [ "$RUN_X" -eq 1 ]; then
have_podman=0
command -v podman >/dev/null 2>&1 && have_podman=1
QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
@@ -452,137 +453,56 @@ if [ $RUN_X -eq 1 ]; then
*) is_aarch64=0 ;;
esac
export have_qemu QEMU_BIN have_podman is_aarch64
- # shellcheck source=../lib/exec_target.sh
- source "$ROOT/test/lib/exec_target.sh"
- # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it
- # once so a single batched container drains the whole path-X queue.
+ # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it once
+ # so a single batched container drains the whole path-X queue.
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
- # Deferred path-X bookkeeping, checked after exec_target_flush.
- XQ_LABELS=()
- XQ_EXPECTED=()
- XQ_ERRS=()
- XQ_RCS=()
+ export EXEC_TARGET_MOUNT_ROOT
+ # shellcheck source=../lib/exec_target.sh
+ . "$ROOT/test/lib/exec_target.sh"
fi
-shopt -s nullglob
-cases=("$TEST_DIR"/cases/*.toy)
-if [ ${#cases[@]} -eq 0 ]; then
- printf 'no toy cases found under %s/cases\n' "$TEST_DIR" >&2
- exit 2
+# ---- drive the corpora -----------------------------------------------------
+printf 'toy: CFREE=%s\n' "$CFREE"
+printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS"
+if [ "$RUN_X" -eq 1 ]; then
+ printf 'toy: cross archs=%s (path X)\n' "$TOY_CROSS_ARCHS"
fi
-for src in "${cases[@]}"; do
- name="$(basename "$src" .toy)"
- if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then
- continue
+# cases corpus — active lanes in PATHS canonical order R I L X C W.
+CASE_LANES=
+[ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R"
+[ "$RUN_I" -eq 1 ] && CASE_LANES="$CASE_LANES I"
+[ "$RUN_L" -eq 1 ] && CASE_LANES="$CASE_LANES L"
+[ "$RUN_X" -eq 1 ] && CASE_LANES="$CASE_LANES X"
+[ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C"
+[ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W"
+
+CF_LABEL=toy CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$TEST_DIR/cases/*.toy" CF_CORPUS_EXT=toy \
+ CF_SIDECAR_DIR="$TEST_DIR/cases" \
+ CF_LANES="$CASE_LANES" CF_OPT_LEVELS="$TOY_OPT_LEVELS" \
+ CF_TUPLES="host-host" CF_TARGETS_EXT="" CF_OPT0ONLY="C W" \
+ CF_PARALLELIZABLE="$PAR" CF_FLUSH_VERIFY=tf_flush_verify \
+ cf_corpus_run
+
+# err cases exercise compile-failure paths; they aren't relevant to path C/W
+# (which go through the same cc invocation). Only run them when at least one of
+# the native compile paths (R/L/X) is enabled.
+if [ "$RUN_R" -eq 1 ] || [ "$RUN_L" -eq 1 ] || [ "$RUN_X" -eq 1 ]; then
+ err_cases=("$TEST_DIR"/err/*.toy)
+ if [ "${#err_cases[@]}" -gt 0 ]; then
+ CF_LABEL=toy CF_BUILD_DIR="$BUILD_DIR/err" \
+ CF_CORPUS_GLOBS="$TEST_DIR/err/*.toy" CF_CORPUS_EXT=toy \
+ CF_SIDECAR_DIR="$TEST_DIR/err" \
+ CF_LANES="ERR" CF_OPT_LEVELS="" CF_TUPLES="host-host" \
+ CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
fi
- expected="$(expected_for "$src")"
- for opt in $TOY_OPT_LEVELS; do
- work="$BUILD_DIR/$name/O$opt"
- rm -rf "$work"
- mkdir -p "$work"
- if [ $RUN_R -eq 1 ]; then
- run_case_run "$name" "$src" "$expected" "$work" "$opt"
- fi
- if [ $RUN_I -eq 1 ]; then
- run_case_interp "$name" "$src" "$expected" "$work" "$opt"
- fi
- if [ $RUN_L -eq 1 ]; then
- run_case_link "$name" "$src" "$expected" "$work" "$opt"
- fi
- if [ $RUN_X -eq 1 ]; then
- run_case_cross "$name" "$src" "$expected" "$work" "$opt"
- fi
- # Path C forces opt_level=0 internally regardless of -O; running it
- # at multiple opt levels would duplicate identical work.
- if [ $RUN_C -eq 1 ] && [ "$opt" = "0" ]; then
- run_case_emit_c "$name" "$src" "$expected" "$work" "$opt"
- fi
- # Path W (wasm roundtrip) only exercises -O0 today; the Wasm CGTarget
- # ignores -O for opt-level purposes (the lang/wasm frontend chooses
- # its own native opt level when re-lowering the .wasm).
- if [ $RUN_W -eq 1 ] && [ "$opt" = "0" ]; then
- run_case_wasm "$name" "$src" "$expected" "$work" "$opt"
- fi
- done
-done
-
-# Drain the path-X queue in a single batched container per target arch, then
-# check each deferred case's exit code (the cross-compile + link already
-# passed/failed inline above; only the exec was deferred).
-if [ $RUN_X -eq 1 ] && [ "$(exec_target_queue_size)" -gt 0 ]; then
- printf 'Running path X (%d cases batched)...\n' "$(exec_target_queue_size)"
- exec_target_flush
- xi=0
- xn=${#XQ_LABELS[@]}
- while [ $xi -lt "$xn" ]; do
- xlabel="${XQ_LABELS[$xi]}"
- xexp="${XQ_EXPECTED[$xi]}"
- xerr="${XQ_ERRS[$xi]}"
- xrcf="${XQ_RCS[$xi]}"
- if [ -s "$xerr" ]; then
- grep -v '^WARNING: image platform .* does not match the expected platform' \
- "$xerr" > "$xerr.clean" 2>/dev/null || true
- mv "$xerr.clean" "$xerr" 2>/dev/null || true
- fi
- if [ -f "$xrcf" ]; then
- xrc="$(cat "$xrcf")"
- else
- xrc=127
- fi
- check_rc "$xlabel" "$xrc" "$xexp" "$xerr"
- xi=$((xi+1))
- done
fi
-# err cases exercise compile-failure paths; they aren't relevant to path C
-# (which goes through the same cc invocation). Only run them when at least
-# one of the native compile paths (R/L/X) is enabled.
-if [ $RUN_R -eq 1 ] || [ $RUN_L -eq 1 ] || [ $RUN_X -eq 1 ]; then
- err_cases=("$TEST_DIR"/err/*.toy)
-else
- err_cases=()
-fi
-# Bash 3.x trips on `${arr[@]}` when arr is empty under `set -u`; guard.
-for src in "${err_cases[@]:-}"; do
- [ -n "$src" ] || continue
- name="$(basename "$src" .toy)"
- if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then
- continue
- fi
- work="$BUILD_DIR/err/$name"
- rm -rf "$work"
- mkdir -p "$work"
- obj="$work/$name.o"
- out="$work/cc.out"
- err="$work/cc.err"
- exp="${src%.toy}.expected"
- if "$CFREE" cc -c "$src" -o "$obj" > "$out" 2> "$err"; then
- note_fail "$name/E"
- printf ' expected compile failure, got success\n'
- continue
- fi
- if [ ! -f "$exp" ]; then
- note_fail "$name/E"
- printf ' missing expected diagnostic: %s\n' "$exp"
- continue
- fi
- if grep -F -q -f "$exp" "$err"; then
- note_pass "$name/E"
- else
- note_fail "$name/E"
- printf ' expected diagnostic substring:\n'
- sed 's/^/ > /' "$exp"
- printf ' actual stderr:\n'
- sed 's/^/ | /' "$err"
- fi
-done
-
-printf '\nResults: %d pass, %d fail, %d skip\n' "$PASS" "$FAIL" "$SKIP"
-if [ $FAIL -ne 0 ]; then
- printf 'Failures:\n'
- for name in "${FAIL_NAMES[@]}"; do
- printf ' %s\n' "$name"
- done
- exit 1
-fi
+# toy treats skips (interp-unsupported ops, no cross-arch runner, the
+# intentionally-red musttail cases, ...) as non-fatal — matching the original
+# runner, which only ever exited nonzero on a real FAIL.
+CF_SKIP_IS_FAILURE=0
+cf_summary toy
+cf_exit
diff --git a/test/wasm-target/check_asm.sh b/test/wasm-target/check_asm.sh
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-# Structural test: confirm the wasm backend can lower an inline-asm block
-# whose template is a WAT instruction sequence. The fixture's snippet uses
-# i32.popcnt, so the produced module must contain the popcnt opcode byte
-# (0x69, the wasm-binary encoding of i32.popcnt).
-#
-# Exit status:
-# 0 expectations hold.
-# 1 fixture failed to compile, or popcnt opcode is missing.
-# 2 toolchain unavailable.
-set -u
-
-ROOT=$(cd "$(dirname "$0")/.." && pwd)/..
-ROOT=$(cd "$ROOT" && pwd)
-CFREE="${CFREE:-$ROOT/build/cfree}"
-SRC_DIR="$ROOT/test/wasm-target"
-BUILD_DIR="$ROOT/build/test/wasm-target"
-mkdir -p "$BUILD_DIR"
-
-if [ ! -x "$CFREE" ]; then
- echo "SKIP: cfree driver missing at $CFREE" >&2
- exit 2
-fi
-
-OUT="$BUILD_DIR/asm_popcnt.wasm"
-if ! "$CFREE" cc -target wasm32-none -c "$SRC_DIR/asm_popcnt.c" -o "$OUT" \
- >"$OUT.cc.out" 2>"$OUT.cc.err"; then
- echo "FAIL: cfree cc -target wasm32-none failed for asm_popcnt.c" >&2
- sed 's/^/ | /' "$OUT.cc.err" >&2
- exit 1
-fi
-
-# i32.popcnt encodes as 0x69. Look for the byte in the produced module.
-if ! od -An -tx1 -v "$OUT" | tr -d ' \n' | grep -q '69'; then
- echo "FAIL: asm_popcnt.wasm missing i32.popcnt opcode (0x69)" >&2
- exit 1
-fi
-
-# Negative: a snippet whose top-level `br 0` would escape the synthetic
-# frame must be rejected before emission. The driver should fail with a
-# "wasm target:" diagnostic.
-NEG="$BUILD_DIR/asm_branch_escape.wasm"
-if "$CFREE" cc -target wasm32-none -c "$SRC_DIR/asm_branch_escape.c" -o "$NEG" \
- >"$NEG.cc.out" 2>"$NEG.cc.err"; then
- echo "FAIL: asm_branch_escape.c compiled but should have been rejected" >&2
- exit 1
-fi
-if ! grep -F "wasm target:" "$NEG.cc.err" >/dev/null 2>&1; then
- echo "FAIL: asm_branch_escape rejection lacks 'wasm target:' diagnostic" >&2
- sed 's/^/ | /' "$NEG.cc.err" >&2
- exit 1
-fi
-
-echo "PASS: backend lowers WAT-template inline asm and rejects escaping br"
-exit 0
diff --git a/test/wasm-target/check_imports.sh b/test/wasm-target/check_imports.sh
@@ -1,74 +0,0 @@
-#!/usr/bin/env bash
-# Structural test: confirm that the wasm backend emits `(import ...)`
-# declarations for `extern` C symbols, honors the default `env` module,
-# and respects __attribute__((import_module, import_name)) overrides.
-#
-# Strategy: compile each fixture with -target wasm32-none -c and grep the
-# raw module bytes for the expected (module, field) UTF-8 strings. The
-# wasm import section encodes these as length-prefixed UTF-8 plain text
-# so a simple `grep -F` is enough.
-#
-# Exit status:
-# 0 every expectation holds.
-# 1 some fixture is missing the expected import.
-# 2 toolchain unavailable.
-set -u
-
-ROOT=$(cd "$(dirname "$0")/.." && pwd)/..
-ROOT=$(cd "$ROOT" && pwd)
-CFREE="${CFREE:-$ROOT/build/cfree}"
-SRC_DIR="$ROOT/test/wasm-target"
-BUILD_DIR="$ROOT/build/test/wasm-target"
-mkdir -p "$BUILD_DIR"
-
-if [ ! -x "$CFREE" ]; then
- echo "SKIP: cfree driver missing at $CFREE" >&2
- exit 2
-fi
-
-fail=0
-
-compile() {
- local src=$1
- local out=$2
- if ! "$CFREE" cc -target wasm32-none -c "$src" -o "$out" \
- >"$out.cc.out" 2>"$out.cc.err"; then
- echo "FAIL: cfree cc -target wasm32-none failed for $src" >&2
- sed 's/^/ | /' "$out.cc.err" >&2
- return 1
- fi
-}
-
-check_strings() {
- local label=$1
- local wasm=$2
- shift 2
- local s
- for s in "$@"; do
- if ! grep -F "$s" "$wasm" >/dev/null 2>&1; then
- echo "FAIL: $label: expected substring '$s' in $wasm" >&2
- fail=1
- fi
- done
-}
-
-# Default module/field: env / host_add.
-OUT="$BUILD_DIR/import_decl.wasm"
-if compile "$SRC_DIR/import_decl.c" "$OUT"; then
- check_strings "import_decl" "$OUT" "env" "host_add"
-else
- fail=1
-fi
-
-# Attribute-override module/field: custom / add.
-OUT="$BUILD_DIR/import_decl_attribute.wasm"
-if compile "$SRC_DIR/import_decl_attribute.c" "$OUT"; then
- check_strings "import_decl_attribute" "$OUT" "custom" "add"
-else
- fail=1
-fi
-
-if [ "$fail" -eq 0 ]; then
- echo "PASS: backend emits expected (import ...) declarations"
-fi
-exit "$fail"
diff --git a/test/wasm-target/check_memory_copy.sh b/test/wasm-target/check_memory_copy.sh
@@ -1,61 +0,0 @@
-#!/usr/bin/env bash
-# Structural test: confirm that the wasm backend lowers `copy_bytes` /
-# `set_bytes` to the bulk-memory opcodes `memory.copy` (0xfc 0x0a) and
-# `memory.fill` (0xfc 0x0b) rather than to byte-loops.
-#
-# Invocation:
-# test/wasm-target/check_memory_copy.sh
-#
-# Environment:
-# CFREE: path to the cfree driver (default: ./build/cfree).
-# TOY_FIXTURE: path to a .toy source whose lowering must use
-# copy_bytes/set_bytes. Default: test/toy/cases/131_memcpy_uses_bulk.toy.
-#
-# Exit status:
-# 0 both opcodes appear in the produced module.
-# 1 either opcode is missing.
-# 2 toolchain unavailable.
-set -u
-
-ROOT=$(cd "$(dirname "$0")/.." && pwd)/..
-ROOT=$(cd "$ROOT" && pwd)
-CFREE="${CFREE:-$ROOT/build/cfree}"
-TOY_FIXTURE="${TOY_FIXTURE:-$ROOT/test/toy/cases/131_memcpy_uses_bulk.toy}"
-BUILD_DIR="$ROOT/build/test/wasm-target"
-mkdir -p "$BUILD_DIR"
-
-if [ ! -x "$CFREE" ]; then
- echo "SKIP: cfree driver missing at $CFREE" >&2
- exit 2
-fi
-if [ ! -f "$TOY_FIXTURE" ]; then
- echo "SKIP: missing toy fixture $TOY_FIXTURE" >&2
- exit 2
-fi
-
-OUT="$BUILD_DIR/$(basename "$TOY_FIXTURE" .toy).wasm"
-if ! "$CFREE" cc -target wasm32-none -c "$TOY_FIXTURE" -o "$OUT" \
- >"$BUILD_DIR/cc.out" 2>"$BUILD_DIR/cc.err"; then
- echo "FAIL: cfree cc -target wasm32-none failed for $TOY_FIXTURE" >&2
- sed 's/^/ | /' "$BUILD_DIR/cc.err" >&2
- exit 1
-fi
-
-# Magic numbers in this test mirror the wasm spec opcode encoding:
-# memory.copy = 0xfc 0x0a
-# memory.fill = 0xfc 0x0b
-# Keep these in sync with WASM_INSN_MEMORY_{COPY,FILL} in src/wasm/insn.c.
-fail=0
-if ! xxd -p -c 256 "$OUT" | tr -d '\n' | grep -q 'fc0a'; then
- echo "FAIL: memory.copy (0xfc 0x0a) not present in $OUT" >&2
- fail=1
-fi
-if ! xxd -p -c 256 "$OUT" | tr -d '\n' | grep -q 'fc0b'; then
- echo "FAIL: memory.fill (0xfc 0x0b) not present in $OUT" >&2
- fail=1
-fi
-
-if [ "$fail" -eq 0 ]; then
- echo "PASS: $(basename "$OUT") contains memory.copy and memory.fill"
-fi
-exit "$fail"
diff --git a/test/wasm-target/check_memory_export.sh b/test/wasm-target/check_memory_export.sh
@@ -1,40 +0,0 @@
-#!/usr/bin/env bash
-# Structural test: confirm that the wasm backend exports the linear
-# memory under the conventional name `"memory"`. This is required for
-# browser, wasmtime, wasmer, and Node host runtimes.
-#
-# Strategy: compile a small toy fixture, then grep the produced module
-# bytes for the UTF-8 string "memory" appearing as an export name.
-# Combined with the test/wasm-target/import_decl.* fixtures, this gives
-# the minimum signal that cfree's output is loadable by standard hosts.
-set -u
-
-ROOT=$(cd "$(dirname "$0")/.." && pwd)/..
-ROOT=$(cd "$ROOT" && pwd)
-CFREE="${CFREE:-$ROOT/build/cfree}"
-TOY_FIXTURE="${TOY_FIXTURE:-$ROOT/test/toy/cases/01_return_const.toy}"
-BUILD_DIR="$ROOT/build/test/wasm-target"
-mkdir -p "$BUILD_DIR"
-
-if [ ! -x "$CFREE" ]; then
- echo "SKIP: cfree driver missing at $CFREE" >&2
- exit 2
-fi
-if [ ! -f "$TOY_FIXTURE" ]; then
- echo "SKIP: missing toy fixture $TOY_FIXTURE" >&2
- exit 2
-fi
-
-OUT="$BUILD_DIR/memory_export.wasm"
-if ! "$CFREE" cc -target wasm32-none -c "$TOY_FIXTURE" -o "$OUT" \
- >"$BUILD_DIR/me.cc.out" 2>"$BUILD_DIR/me.cc.err"; then
- echo "FAIL: cfree cc -target wasm32-none failed for $TOY_FIXTURE" >&2
- sed 's/^/ | /' "$BUILD_DIR/me.cc.err" >&2
- exit 1
-fi
-
-if ! grep -F "memory" "$OUT" >/dev/null 2>&1; then
- echo "FAIL: produced module does not contain export name 'memory'" >&2
- exit 1
-fi
-echo "PASS: produced module exports 'memory'"
diff --git a/test/wasm-target/run.sh b/test/wasm-target/run.sh
@@ -1,30 +1,182 @@
#!/usr/bin/env bash
-# Wasm-target structural test driver. Each check_*.sh script compiles a
-# small fixture with `cfree cc -target wasm32-none -c` and asserts a
-# structural property of the produced module (opcode bytes, export
-# names, import declarations).
+# test/wasm-target/run.sh — Wasm-target structural-grep tests, on the shared
+# corpus harness (test/lib/cf_corpus.sh).
#
-# Invoked by `make test-wasm-target`; runs every check_*.sh under this
-# directory and reports a pass/fail/skip summary.
+# Each check compiles a tiny C or toy fixture with `cfree cc -target
+# wasm32-none -c` and asserts a structural property of the produced module:
+# opcode bytes, export names, or import declarations. The oracle is a
+# structural grep over the raw module bytes; the opcode magic numbers and
+# import (module, field) names live as metadata inside the lane bodies.
+#
+# Lanes (each is one structural check — there is no opt/tuple axis):
+# ASM inline-asm WAT template lowers to i32.popcnt (0x69), and an
+# escaping `br 0` is rejected with a "wasm target:" diagnostic.
+# IMP extern C symbols emit (import ...) decls under the default `env`
+# module and honor import_module/import_name attribute overrides.
+# MCP copy_bytes/set_bytes lower to memory.copy (0xfc 0x0a) and
+# memory.fill (0xfc 0x0b), not byte-loops.
+# MEX the linear memory is exported under the conventional name "memory".
+#
+# All lane hooks write only under $CF_WORK and record via cf_*, so the runner
+# is parallel-safe by construction; CFREE_WASM_TARGET_PARALLEL flips dispatch.
set -u
-ROOT=$(cd "$(dirname "$0")/.." && pwd)/..
-ROOT=$(cd "$ROOT" && pwd)
-DIR="$ROOT/test/wasm-target"
-
-pass=0
-fail=0
-skip=0
-for script in "$DIR"/check_*.sh; do
- [ -e "$script" ] || continue
- name=$(basename "$script" .sh)
- "$script"
- rc=$?
- case "$rc" in
- 0) pass=$((pass + 1));;
- 2) skip=$((skip + 1));;
- *) fail=$((fail + 1));;
- esac
-done
-printf 'test-wasm-target: pass=%d fail=%d skip=%d\n' "$pass" "$fail" "$skip"
-[ "$fail" -eq 0 ]
+ROOT=$(cd "$(dirname "$0")/../.." && pwd)
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
+SRC_DIR="$ROOT/test/wasm-target"
+BUILD_DIR="$ROOT/build/test/wasm-target"
+CFREE_BIN="${CFREE:-$ROOT/build/cfree}"
+mkdir -p "$BUILD_DIR"
+
+# Preserve the original per-script semantics: a missing-toolchain/missing-fixture
+# SKIP (was a per-check exit 2 that the old run.sh counted as skip but did NOT
+# let fail the run) must not gate this harness's exit. Override the corpus
+# engine's default (CF_SKIP_IS_FAILURE=1) back to "skip is informational".
+CF_SKIP_IS_FAILURE=0
+
+# Toy fixtures live in the shared toy corpus; allow override (preserves the
+# original TOY_FIXTURE knob from check_memory_{copy,export}.sh).
+TOY_MEMCPY_FIXTURE="${TOY_FIXTURE_MEMCPY:-$ROOT/test/toy/cases/131_memcpy_uses_bulk.toy}"
+TOY_EXPORT_FIXTURE="${TOY_FIXTURE_EXPORT:-$ROOT/test/toy/cases/01_return_const.toy}"
+
+PAR="${CFREE_WASM_TARGET_PARALLEL:-1}"
+
+# Driver presence gates every check: a missing driver SKIPs (was exit 2 in the
+# per-script harnesses). Surfaced as a SKIP per lane via the hooks below.
+have_cfree=0
+[ -x "$CFREE_BIN" ] && have_cfree=1
+
+# wasm32-none -c <src> -> <out>; logs CF_WORK-confined. Returns nonzero on
+# compile failure (lane decides pass/skip/fail).
+wt_compile() { # SRC OUT TAG
+ "$CFREE_BIN" cc -target wasm32-none -c "$1" -o "$2" \
+ >"$CF_WORK/$3.cc.out" 2>"$CF_WORK/$3.cc.err"
+}
+
+# Hex-dump $1 with no whitespace, for opcode-byte greps. (od is universally
+# available; matches the original check_asm.sh / check_memory_copy.sh dumps.)
+wt_hex() { od -An -tx1 -v "$1" | tr -d ' \n'; }
+
+# ---- lane: inline-asm WAT template (check_asm.sh) --------------------------
+# i32.popcnt encodes as 0x69 in the wasm binary format; an escaping top-level
+# `br 0` must be rejected before emission with a "wasm target:" diagnostic.
+cf_lane_ASM() {
+ if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/asm" "cfree driver missing at $CFREE_BIN"; return; fi
+
+ local out="$CF_WORK/asm_popcnt.wasm"
+ if ! wt_compile "$SRC_DIR/asm_popcnt.c" "$out" asm_popcnt; then
+ cf_fail "wasm-target/asm-popcnt" "cfree cc -target wasm32-none failed for asm_popcnt.c"
+ sed 's/^/ | /' "$CF_WORK/asm_popcnt.cc.err" >&2
+ return
+ fi
+ if wt_hex "$out" | grep -q '69'; then cf_pass "wasm-target/asm-popcnt"
+ else cf_fail "wasm-target/asm-popcnt" "missing i32.popcnt opcode (0x69)"; fi
+
+ # Negative oracle: escaping `br 0` must be rejected (nonzero) with a
+ # "wasm target:" stderr diagnostic.
+ local neg="$CF_WORK/asm_branch_escape.wasm"
+ if wt_compile "$SRC_DIR/asm_branch_escape.c" "$neg" asm_branch_escape; then
+ cf_fail "wasm-target/asm-branch-escape" "compiled but should have been rejected"
+ elif grep -F "wasm target:" "$CF_WORK/asm_branch_escape.cc.err" >/dev/null 2>&1; then
+ cf_pass "wasm-target/asm-branch-escape"
+ else
+ cf_fail "wasm-target/asm-branch-escape" "rejection lacks 'wasm target:' diagnostic"
+ sed 's/^/ | /' "$CF_WORK/asm_branch_escape.cc.err" >&2
+ fi
+}
+
+# ---- lane: import declarations (check_imports.sh) --------------------------
+# Wasm import sections encode (module, field) as length-prefixed UTF-8 plain
+# text, so a `grep -F` over the raw bytes is enough.
+_wt_check_strings() { # LABEL WASM STR...
+ local label=$1 wasm=$2; shift 2
+ local s ok=1
+ for s in "$@"; do
+ if ! grep -F "$s" "$wasm" >/dev/null 2>&1; then
+ ok=0
+ printf 'expected substring %s in %s\n' "$s" "$wasm" >&2
+ fi
+ done
+ [ "$ok" -eq 1 ] && cf_pass "$label" || cf_fail "$label" "missing expected import substring(s)"
+}
+cf_lane_IMP() {
+ if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/imports" "cfree driver missing at $CFREE_BIN"; return; fi
+
+ # Default module/field: env / host_add.
+ local out="$CF_WORK/import_decl.wasm"
+ if wt_compile "$SRC_DIR/import_decl.c" "$out" import_decl; then
+ _wt_check_strings "wasm-target/import-default" "$out" "env" "host_add"
+ else
+ cf_fail "wasm-target/import-default" "cfree cc -target wasm32-none failed for import_decl.c"
+ sed 's/^/ | /' "$CF_WORK/import_decl.cc.err" >&2
+ fi
+
+ # Attribute-override module/field: custom / add.
+ out="$CF_WORK/import_decl_attribute.wasm"
+ if wt_compile "$SRC_DIR/import_decl_attribute.c" "$out" import_decl_attribute; then
+ _wt_check_strings "wasm-target/import-attribute" "$out" "custom" "add"
+ else
+ cf_fail "wasm-target/import-attribute" "cfree cc -target wasm32-none failed for import_decl_attribute.c"
+ sed 's/^/ | /' "$CF_WORK/import_decl_attribute.cc.err" >&2
+ fi
+}
+
+# ---- lane: bulk-memory opcodes (check_memory_copy.sh) ----------------------
+# Magic numbers mirror the wasm spec opcode encoding (keep in sync with
+# WASM_INSN_MEMORY_{COPY,FILL} in src/wasm/insn.c):
+# memory.copy = 0xfc 0x0a memory.fill = 0xfc 0x0b
+cf_lane_MCP() {
+ if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/memory-copy" "cfree driver missing at $CFREE_BIN"; return; fi
+ if [ ! -f "$TOY_MEMCPY_FIXTURE" ]; then cf_skip "wasm-target/memory-copy" "missing toy fixture $TOY_MEMCPY_FIXTURE"; return; fi
+
+ local out="$CF_WORK/$(basename "$TOY_MEMCPY_FIXTURE" .toy).wasm"
+ if ! wt_compile "$TOY_MEMCPY_FIXTURE" "$out" memory_copy; then
+ cf_fail "wasm-target/memory-copy" "cfree cc -target wasm32-none failed for $TOY_MEMCPY_FIXTURE"
+ sed 's/^/ | /' "$CF_WORK/memory_copy.cc.err" >&2
+ return
+ fi
+ local hex; hex=$(wt_hex "$out")
+ local ok=1
+ printf '%s' "$hex" | grep -q 'fc0a' || { ok=0; echo "memory.copy (0xfc 0x0a) not present in $out" >&2; }
+ printf '%s' "$hex" | grep -q 'fc0b' || { ok=0; echo "memory.fill (0xfc 0x0b) not present in $out" >&2; }
+ [ "$ok" -eq 1 ] && cf_pass "wasm-target/memory-copy" || cf_fail "wasm-target/memory-copy" "missing bulk-memory opcode(s)"
+}
+
+# ---- lane: memory export (check_memory_export.sh) --------------------------
+# The linear memory must be exported under the conventional name "memory" for
+# browser/wasmtime/wasmer/Node host runtimes.
+cf_lane_MEX() {
+ if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/memory-export" "cfree driver missing at $CFREE_BIN"; return; fi
+ if [ ! -f "$TOY_EXPORT_FIXTURE" ]; then cf_skip "wasm-target/memory-export" "missing toy fixture $TOY_EXPORT_FIXTURE"; return; fi
+
+ local out="$CF_WORK/memory_export.wasm"
+ if ! wt_compile "$TOY_EXPORT_FIXTURE" "$out" memory_export; then
+ cf_fail "wasm-target/memory-export" "cfree cc -target wasm32-none failed for $TOY_EXPORT_FIXTURE"
+ sed 's/^/ | /' "$CF_WORK/memory_export.cc.err" >&2
+ return
+ fi
+ if grep -F "memory" "$out" >/dev/null 2>&1; then cf_pass "wasm-target/memory-export"
+ else cf_fail "wasm-target/memory-export" "produced module does not contain export name 'memory'"; fi
+}
+
+# ---- drive the structural checks -------------------------------------------
+# A single synthetic case fans the checks out across lanes (no opt/tuple axis).
+# Use this dir's fixture .c files as the corpus glob (the engine needs >=1
+# match); CF_READ_CASE collapses to one driving case so each lane fires once.
+printf 'test-wasm-target target=wasm32-none\n'
+
+# Single-item corpus: pick exactly one fixture as the matrix driver so every
+# lane runs exactly once. The lane bodies reference fixtures by absolute path,
+# not via CF_SRC, since one check may touch several fixtures.
+wt_pick_one() { [ "$CF_BASE" = "asm_popcnt" ] || CF_SKIP_NA_CASE=1; }
+
+CF_LABEL=test-wasm-target CF_BUILD_DIR="$BUILD_DIR" \
+ CF_CORPUS_GLOBS="$SRC_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$SRC_DIR" \
+ CF_LANES="ASM IMP MCP MEX" CF_OPT_LEVELS="" CF_TUPLES="wasm32-none" \
+ CF_TARGETS_EXT="" CF_READ_CASE=wt_pick_one CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+cf_summary test-wasm-target
+cf_exit
diff --git a/test/wasm/run.sh b/test/wasm/run.sh
@@ -1,7 +1,22 @@
#!/usr/bin/env bash
+# test/wasm/run.sh — Wasm frontend end-to-end tests, on the shared corpus
+# harness (test/lib/cf_corpus.sh). Lanes (CFREE_TEST_PATHS, default WDNOJE):
+# W wat2wasm (wasm-tool)
+# D cfree run (JIT) — wat + wasm inputs
+# N cfree run --no-jit (interp; SKIPs ops it does not implement)
+# O cfree cc -c (object out) — wat + wasm inputs
+# J jit-runner against the obj — wat + wasm inputs
+# E link + native exec (synchronous exec_target_run)
+# C --emit=c + host cc + exec (opt-in; C-source backend, doc/CBACKEND.md)
+# Plus three diagnostic corpora (trap/err/meta) when any of W/D/O is enabled.
+# All lane hooks write only under CF_WORK and record via cf_*, so the runner is
+# parallel-safe; CFREE_WASM_PARALLEL flips dispatch.
set -u
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
+export CF_LIB_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/cf_corpus.sh"
+
BUILD_DIR="$ROOT/build/test/wasm"
CASES_DIR="$ROOT/test/wasm/cases"
ERR_DIR="$ROOT/test/wasm/err"
@@ -13,17 +28,9 @@ JIT_RUNNER="$ROOT/build/test/jit-runner"
LINK_EXE_RUNNER="$ROOT/build/test/link-exe-runner"
TEST_ARCH="${CFREE_TEST_ARCH:-aa64}"
TEST_OBJ="${CFREE_TEST_OBJ:-macho}"
+HOST_CC="${CC:-cc}"
+mkdir -p "$BUILD_DIR"
-# Path filtering. Default runs the legacy set (everything except C):
-# W wat2wasm
-# D cfree run (JIT)
-# N cfree run --no-jit (IR interpreter; SKIPs ops it does not implement)
-# O cfree cc -c (object output)
-# J jit-runner against the produced obj
-# E link + native exec
-# C --emit=c + host cc + native exec (C-source backend; see doc/CBACKEND.md)
-# C is opt-in because Phase 1 of the C backend skips most wasm cases; the
-# combined `make test-cbackend` invokes this runner with CFREE_TEST_PATHS=C.
PATHS="${CFREE_TEST_PATHS:-WDNOJE}"
case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac
case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac
@@ -32,91 +39,57 @@ case "$PATHS" in *O*) RUN_O=1;; *) RUN_O=0;; esac
case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac
case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac
case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac
-HOST_CC="${CC:-cc}"
-
-mkdir -p "$BUILD_DIR"
-
-pass=0
-fail=0
-skip=0
-
-note_pass() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); }
-note_fail() { printf ' FAIL %s\n' "$1"; fail=$((fail + 1)); }
-note_skip() { printf ' SKIP %s (%s)\n' "$1" "$2"; skip=$((skip + 1)); }
case "$TEST_ARCH" in
aa64|aarch64|arm64) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;;
- x64|x86_64|amd64) TEST_ARCH=x64; EXEC_ARCH="x64" ;;
- rv64|riscv64) TEST_ARCH=rv64; EXEC_ARCH="rv64" ;;
- *) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;;
+ x64|x86_64|amd64) TEST_ARCH=x64; EXEC_ARCH="x64" ;;
+ rv64|riscv64) TEST_ARCH=rv64; EXEC_ARCH="rv64" ;;
+ *) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;;
esac
-
-host_arch=$(uname -m)
-host_matches=0
+host_arch=$(uname -m); host_matches=0
case "$TEST_ARCH:$host_arch" in
aa64:arm64|aa64:aarch64) host_matches=1 ;;
x64:x86_64) host_matches=1 ;;
esac
-
case "$TEST_OBJ" in
- macho)
- EXEC_OS="macos"
+ macho) EXEC_OS="macos"
case "$TEST_ARCH" in
aa64) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;;
- x64) target_triple="x86_64-macos"; clang_triple="x86_64-apple-macos" ;;
- *) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;;
- esac
- ;;
- elf)
- EXEC_OS="linux"
+ x64) target_triple="x86_64-macos"; clang_triple="x86_64-apple-macos" ;;
+ *) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;;
+ esac ;;
+ elf) EXEC_OS="linux"
case "$TEST_ARCH" in
aa64) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;;
- x64) target_triple="x86_64-linux"; clang_triple="x86_64-linux-gnu" ;;
+ x64) target_triple="x86_64-linux"; clang_triple="x86_64-linux-gnu" ;;
rv64) target_triple="riscv64-linux"; clang_triple="riscv64-linux-gnu" ;;
- *) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;;
- esac
- ;;
+ *) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;;
+ esac ;;
*) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos"; EXEC_OS="macos" ;;
esac
EXEC_TAG="$EXEC_ARCH-$EXEC_OS"
export CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ"
-# Enable the runner's canned host-import resolver so fixtures like
-# host_import_add can use `(import "env" "host_add" ...)`.
-export CFREE_TEST_HOST_IMPORTS=1
+export CFREE_TEST_HOST_IMPORTS=1 # enable the runner's canned host-import resolver
-have_wasm_tool=0
-if [ -x "$WASM_TOOL" ]; then
- have_wasm_tool=1
-fi
+have_wasm_tool=0; [ -x "$WASM_TOOL" ] && have_wasm_tool=1
+have_jit_runner=0; [ -x "$JIT_RUNNER" ] && [ "$host_matches" -eq 1 ] && have_jit_runner=1
+have_link_runner=0;[ -x "$LINK_EXE_RUNNER" ] && have_link_runner=1
-have_jit_runner=0
-if [ -x "$JIT_RUNNER" ] && [ "$host_matches" -eq 1 ]; then
- have_jit_runner=1
-fi
-
-have_link_runner=0
-if [ -x "$LINK_EXE_RUNNER" ]; then
- have_link_runner=1
-fi
-
-have_qemu=0
-have_podman=0
-is_aarch64=0
+# exec_target wiring (lane E runs SYNCHRONOUSLY via exec_target_run).
+have_qemu=0; have_podman=0; is_aarch64=0
QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)"
[ -n "$QEMU_BIN" ] && have_qemu=1
command -v podman >/dev/null 2>&1 && have_podman=1
-arch_raw="$(uname -m 2>/dev/null || true)"
-{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
+{ [ "$host_arch" = "aarch64" ] || [ "$host_arch" = "arm64" ]; } && is_aarch64=1
EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
-# shellcheck source=../lib/exec_target.sh
-source "$ROOT/test/lib/exec_target.sh"
+export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT
+. "$ROOT/test/lib/exec_target.sh"
WASM_START_OBJ="$BUILD_DIR/start-wasm.o"
have_wasm_start_obj=0
if clang --target="$clang_triple" -ffreestanding -fno-stack-protector \
-fno-builtin -nostdlib -c "$ROOT/test/wasm/harness/start_wasm.c" \
- -o "$WASM_START_OBJ" >"$BUILD_DIR/start-wasm.out" \
- 2>"$BUILD_DIR/start-wasm.err"; then
+ -o "$WASM_START_OBJ" >"$BUILD_DIR/start-wasm.out" 2>"$BUILD_DIR/start-wasm.err"; then
have_wasm_start_obj=1
fi
@@ -128,329 +101,209 @@ if [ "$TEST_OBJ" = "macho" ] && command -v xcrun >/dev/null 2>&1; then
fi
fi
-# Path C wrapper: emitted C exports `test_main` (i32 → maps to int32_t in
-# the C target); bridge to `int main(void)` via a tiny host-cc-compiled
-# stub linked alongside.
+# Path C wrapper: bridges the emitted C's test_main(instance) to int main(void).
C_WRAPPER_SRC="$BUILD_DIR/wasm_c_wrapper.c"
C_WRAPPER_OBJ="$BUILD_DIR/wasm_c_wrapper.o"
have_c_wrapper=0
if [ "$RUN_C" -eq 1 ]; then
cat > "$C_WRAPPER_SRC" <<'EOF'
-/* Generated by test/wasm/run.sh — bridges main() to test_main().
- *
- * Wasm frontend exports use the internal instance ABI:
- * __cfree_wasm_init(instance)
- * test_main(instance)
- *
- * Mirrors test/wasm/harness/start_wasm.c but runs hosted (uses calloc),
- * since the C target is compiled with the host toolchain. */
#include <stdint.h>
#include <stdlib.h>
-
extern void __cfree_wasm_init(void *);
extern int32_t test_main(void *);
-
-typedef struct {
- unsigned char *data;
- unsigned long long pages;
- unsigned long long max_pages;
- unsigned int flags;
-} WasmStartMemoryPrefix;
-
+typedef struct { unsigned char *data; unsigned long long pages;
+ unsigned long long max_pages; unsigned int flags; } WasmStartMemoryPrefix;
#define WASM_START_MEMORY_PREFIX_COUNT 8u
#define WASM_START_INSTANCE_SIZE (64u * 1024u)
#define WASM_START_MEMORY_SIZE (16u * 1024u * 1024u)
-
int main(void) {
void *instance = calloc(1, WASM_START_INSTANCE_SIZE);
unsigned char *memory = calloc(1, WASM_START_MEMORY_SIZE);
if (!instance || !memory) return 1;
- for (unsigned int i = 0; i < WASM_START_MEMORY_PREFIX_COUNT; ++i) {
+ for (unsigned int i = 0; i < WASM_START_MEMORY_PREFIX_COUNT; ++i)
((WasmStartMemoryPrefix *)instance)[i].data =
memory + i * (WASM_START_MEMORY_SIZE / WASM_START_MEMORY_PREFIX_COUNT);
- }
__cfree_wasm_init(instance);
return (int)test_main(instance);
}
EOF
- if $HOST_CC -std=gnu99 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" \
- 2>"$BUILD_DIR/wasm_c_wrapper.err"; then
- have_c_wrapper=1
- fi
+ $HOST_CC -std=gnu99 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" \
+ 2>"$BUILD_DIR/wasm_c_wrapper.err" && have_c_wrapper=1
fi
-run_expect_rc() {
- local label=$1
- local expected=$2
- shift 2
- "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err"
- local rc=$?
- if [ "$rc" -eq "$expected" ]; then
- note_pass "$label"
- else
- note_fail "$label expected $expected got $rc"
- fi
+# ---- per-result oracle helpers (CF_WORK-confined -> parallel-safe) ---------
+_wf_base() { printf '%s/%s' "$CF_WORK" "$(printf '%s' "$1" | tr '/ ' '__')"; }
+wf_rc() { # LABEL EXPECTED CMD...
+ local label=$1 exp=$2 b; b=$(_wf_base "$1"); shift 2
+ "$@" >"$b.out" 2>"$b.err"; local rc=$?
+ if [ "$rc" -eq "$exp" ]; then cf_pass "$label"; else cf_fail "$label" "expected $exp got $rc"; fi
}
-
-# Like run_expect_rc, but for the --no-jit interpreter path: ops the interpreter
-# does not yet implement emit a greppable "interp: <feature> not supported"
-# diagnostic and SKIP rather than FAIL (mirrors the toy I-path).
-run_expect_rc_interp() {
- local label=$1
- local expected=$2
- shift 2
- local errf="$BUILD_DIR/${label//\//_}.err"
- "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$errf"
- local rc=$?
- local missing
- missing=$(grep -oE 'interp: .*not supported' "$errf" 2>/dev/null | head -n1 || true)
- if [ -n "$missing" ]; then
- note_skip "$label" "$missing"
- elif [ "$rc" -eq "$expected" ]; then
- note_pass "$label"
- else
- note_fail "$label expected $expected got $rc"
- fi
+wf_rc_interp() { # LABEL EXPECTED CMD... (--no-jit; SKIP on "interp: ..not supported")
+ local label=$1 exp=$2 b; b=$(_wf_base "$1"); shift 2
+ "$@" >"$b.out" 2>"$b.err"; local rc=$? miss
+ miss=$(grep -oE 'interp: .*not supported' "$b.err" 2>/dev/null | head -n1 || true)
+ if [ -n "$miss" ]; then cf_skip "$label" "$miss"
+ elif [ "$rc" -eq "$exp" ]; then cf_pass "$label"
+ else cf_fail "$label" "expected $exp got $rc"; fi
}
-
-run_expect_zero() {
- local label=$1
- shift
- if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err"; then
- note_pass "$label"
- else
- note_fail "$label"
- fi
+wf_zero() { # LABEL CMD...
+ local label=$1 b; b=$(_wf_base "$1"); shift
+ if "$@" >"$b.out" 2>"$b.err"; then cf_pass "$label"; else cf_fail "$label"; fi
+}
+wf_fail() { # LABEL CMD... (expect nonzero)
+ local label=$1 b; b=$(_wf_base "$1"); shift
+ if "$@" >"$b.out" 2>"$b.err"; then cf_fail "$label" "expected failure"; else cf_pass "$label"; fi
+}
+wf_fail_grep() { # LABEL SUBSTR CMD... (expect failure + stderr contains SUBSTR)
+ local label=$1 want=$2 b; b=$(_wf_base "$1"); shift 2
+ if "$@" >"$b.out" 2>"$b.err"; then cf_fail "$label" "expected failure"
+ elif grep -F "$want" "$b.err" >/dev/null 2>&1; then cf_pass "$label"
+ else cf_fail "$label" "missing diagnostic: $want"; fi
}
-run_expect_fail() {
- local label=$1
- shift
- if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err"; then
- note_fail "$label expected failure"
- else
- note_pass "$label"
+# Build the wat->wasm intermediate once per case (idempotent). Sets WASM.
+wasm_build() {
+ WASM="$CF_WORK/$CF_BASE.wasm"
+ [ -f "$WASM" ] && return 0
+ [ "$have_wasm_tool" -eq 1 ] || return 1
+ "$WASM_TOOL" --wat2wasm "$CF_SRC" "$WASM" >"$CF_WORK/_w.out" 2>"$CF_WORK/_w.err"
+}
+# Ensure the wat/wasm .o exists (O lane builds it; J/E build on demand if absent).
+_obj_wat() { local o="$CF_WORK/$CF_BASE.wat.o"; [ -f "$o" ] || "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$o" >/dev/null 2>&1; printf '%s' "$o"; }
+_obj_wasm() { local o="$CF_WORK/$CF_BASE.wasm.o"; wasm_build || return 1; [ -f "$o" ] || "$CFREE_BIN" cc -target "$target_triple" -c "$WASM" -o "$o" >/dev/null 2>&1; printf '%s' "$o"; }
+
+# ---- cases-corpus lanes ----------------------------------------------------
+cf_lane_W() {
+ if [ "$have_wasm_tool" -eq 1 ]; then wf_zero "$CF_NAME/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm"
+ else cf_skip "$CF_NAME/W" "no wasm-tool"; fi
+}
+cf_lane_D() {
+ wf_rc "$CF_NAME/D-wat" "$CF_EXPECTED" "$CFREE_BIN" run -e test_main "$CF_SRC"
+ if wasm_build; then wf_rc "$CF_NAME/D-wasm" "$CF_EXPECTED" "$CFREE_BIN" run -e test_main "$WASM"
+ else cf_skip "$CF_NAME/D-wasm" "no wasm-tool"; fi
+}
+cf_lane_N() {
+ wf_rc_interp "$CF_NAME/N-wat" "$CF_EXPECTED" "$CFREE_BIN" run --no-jit -e test_main "$CF_SRC"
+ if wasm_build; then wf_rc_interp "$CF_NAME/N-wasm" "$CF_EXPECTED" "$CFREE_BIN" run --no-jit -e test_main "$WASM"
+ else cf_skip "$CF_NAME/N-wasm" "no wasm-tool"; fi
+}
+cf_lane_O() {
+ wf_zero "$CF_NAME/O-wat" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$CF_WORK/$CF_BASE.wat.o"
+ if wasm_build; then wf_zero "$CF_NAME/O-wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$WASM" -o "$CF_WORK/$CF_BASE.wasm.o"
+ else cf_skip "$CF_NAME/O-wasm" "no wasm-tool"; fi
+}
+cf_lane_J() {
+ if [ "$have_jit_runner" -eq 0 ]; then cf_skip "$CF_NAME/J" "host arch does not match target or no jit-runner"; return; fi
+ wf_rc "$CF_NAME/J-wat-obj" "$CF_EXPECTED" env CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$(_obj_wat)"
+ local wo; if wo=$(_obj_wasm); then wf_rc "$CF_NAME/J-wasm-obj" "$CF_EXPECTED" env CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wo"
+ else cf_skip "$CF_NAME/J-wasm-obj" "no wasm-tool"; fi
+}
+cf_lane_E() {
+ if [ "$have_link_runner" -eq 0 ] || [ "$have_wasm_start_obj" -eq 0 ]; then
+ cf_skip "$CF_NAME/E" "requires link runner and wasm start.o"; return
fi
+ local exe="$CF_WORK/$CF_BASE.exe"
+ if "$LINK_EXE_RUNNER" -o "$exe" "${MACHO_DSO_ARGS[@]}" "$(_obj_wat)" "$WASM_START_OBJ" \
+ >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then
+ if exec_target_supported "$EXEC_TAG"; then
+ exec_target_run "$EXEC_TAG" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err"
+ if [ "$RUN_RC" -eq "$CF_EXPECTED" ]; then cf_pass "$CF_NAME/E"
+ else cf_fail "$CF_NAME/E" "expected $CF_EXPECTED got $RUN_RC"; fi
+ else cf_skip "$CF_NAME/E" "no execution support for $TEST_ARCH"; fi
+ else cf_fail "$CF_NAME/E" "link failed"; fi
}
-
-run_expect_fail_stderr_contains() {
- local label=$1
- local expected=$2
- local err="$BUILD_DIR/${label//\//_}.err"
- shift 2
- if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$err"; then
- note_fail "$label expected failure"
- elif grep -F "$expected" "$err" >/dev/null 2>&1; then
- note_pass "$label"
- else
- note_fail "$label missing diagnostic: $expected"
+cf_lane_C() {
+ if [ "$have_c_wrapper" -eq 0 ]; then cf_skip "$CF_NAME/C" "host CC wrapper unavailable"; return; fi
+ if [ "$host_matches" -eq 0 ]; then cf_skip "$CF_NAME/C" "host arch != $TEST_ARCH (C target is target-locked)"; return; fi
+ local c_src="$CF_WORK/$CF_BASE.cfree.c" c_bin="$CF_WORK/$CF_BASE.cbackend.bin" miss
+ if ! "$CFREE_BIN" cc -target "$target_triple" --emit=c "$CF_SRC" -o "$c_src" \
+ >"$CF_WORK/c.emit.out" 2>"$CF_WORK/c.emit.err"; then
+ miss=$(grep -oE 'C target: .*(not implemented|not yet supported)' "$CF_WORK/c.emit.err" 2>/dev/null | head -n1 || true)
+ if [ -n "$miss" ]; then cf_skip "$CF_NAME/C" "$miss"; else cf_fail "$CF_NAME/C" "cfree cc --emit=c failed"; fi
+ return
+ fi
+ if ! $HOST_CC -std=gnu99 -Wno-main-return-type "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \
+ >"$CF_WORK/c.cc.out" 2>"$CF_WORK/c.cc.err"; then
+ cf_fail "$CF_NAME/C" "host cc rejected emitted source"; return
fi
+ "$c_bin" >"$CF_WORK/c.run.out" 2>"$CF_WORK/c.run.err"; local rc=$?
+ if [ "$rc" -eq "$CF_EXPECTED" ]; then cf_pass "$CF_NAME/C"; else cf_fail "$CF_NAME/C" "expected $CF_EXPECTED got $rc"; fi
}
-run_expect_fail_quiet() {
- local label=$1
- local out="$BUILD_DIR/${label//\//_}.out"
- local err="$BUILD_DIR/${label//\//_}.err"
- shift
- if bash -c 'out=$1; err=$2; shift 2; "$@" >"$out" 2>"$err"' \
- bash "$out" "$err" "$@" >/dev/null 2>/dev/null; then
- note_fail "$label expected failure"
+# ---- diagnostic-corpus lanes ----------------------------------------------
+cf_lane_T() { # trap: build wasm (expect 0), then run expecting a trap (nonzero)
+ if [ "$have_wasm_tool" -eq 1 ]; then wf_zero "trap/$CF_BASE/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm"
+ else cf_skip "trap/$CF_BASE/W" "no wasm-tool"; return; fi
+ wf_fail "trap/$CF_BASE/D-wat" "$CFREE_BIN" run -e test_main "$CF_SRC"
+ wf_fail "trap/$CF_BASE/D-wasm" "$CFREE_BIN" run -e test_main "$CF_WORK/$CF_BASE.wasm"
+}
+cf_lane_ERR() { # err: cc must fail
+ wf_fail "err/$CF_BASE/cc" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$CF_WORK/$CF_BASE.o"
+}
+cf_lane_META() { # meta: build wasm, then native cc per the expect whitelist
+ wf_zero "meta/$CF_BASE/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm"
+ if [ "$CF_BASE" = "type_custom" ] || [ "$CF_BASE" = "import_table_global_start" ]; then
+ wf_zero "meta/$CF_BASE/native" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/$CF_BASE.wasm" -o "$CF_WORK/$CF_BASE.o"
else
- note_pass "$label"
+ wf_fail "meta/$CF_BASE/native" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/$CF_BASE.wasm" -o "$CF_WORK/$CF_BASE.o"
fi
}
+# ---- drive the corpora -----------------------------------------------------
printf 'test-wasm-front target=%s obj=%s\n' "$target_triple" "$TEST_OBJ"
-for wat in "$CASES_DIR"/*.wat; do
- [ -e "$wat" ] || continue
- name=$(basename "$wat" .wat)
- expected=$(tr -d '[:space:]' < "$CASES_DIR/$name.expect")
- work="$BUILD_DIR/$name"
- mkdir -p "$work"
- wasm="$work/$name.wasm"
- wat_obj="$work/$name.wat.o"
- wasm_obj="$work/$name.wasm.o"
-
- if [ "$RUN_W" -eq 1 ]; then
- if [ "$have_wasm_tool" -eq 1 ]; then
- run_expect_zero "$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm"
- else
- note_skip "$name/W" "no wasm-tool"
- continue
- fi
- elif [ "$have_wasm_tool" -eq 1 ]; then
- # Path C needs the .wasm only if it consumes binary input; we use the
- # .wat directly, so building .wasm is optional. Skip silently.
- "$WASM_TOOL" --wat2wasm "$wat" "$wasm" >/dev/null 2>&1 || true
- fi
-
- if [ "$RUN_D" -eq 1 ]; then
- run_expect_rc "$name/D-wat" "$expected" "$CFREE_BIN" run -e test_main "$wat"
- run_expect_rc "$name/D-wasm" "$expected" "$CFREE_BIN" run -e test_main "$wasm"
- fi
-
- if [ "$RUN_N" -eq 1 ]; then
- run_expect_rc_interp "$name/N-wat" "$expected" "$CFREE_BIN" run --no-jit \
- -e test_main "$wat"
- run_expect_rc_interp "$name/N-wasm" "$expected" "$CFREE_BIN" run --no-jit \
- -e test_main "$wasm"
- fi
-
- if [ "$RUN_O" -eq 1 ]; then
- run_expect_zero "$name/O-wat" "$CFREE_BIN" cc -target "$target_triple" -c \
- "$wat" -o "$wat_obj"
- run_expect_zero "$name/O-wasm" "$CFREE_BIN" cc -target "$target_triple" -c \
- "$wasm" -o "$wasm_obj"
- fi
-
- if [ "$RUN_J" -eq 1 ]; then
- if [ "$have_jit_runner" -eq 1 ]; then
- run_expect_rc "$name/J-wat-obj" "$expected" env CFREE_TEST_ARCH="$TEST_ARCH" \
- CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wat_obj"
- run_expect_rc "$name/J-wasm-obj" "$expected" env CFREE_TEST_ARCH="$TEST_ARCH" \
- CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wasm_obj"
- else
- note_skip "$name/J" "host arch does not match target or no jit-runner"
- fi
- fi
-
- if [ "$RUN_E" -eq 1 ]; then
- if [ "$have_link_runner" -eq 1 ] && [ "$have_wasm_start_obj" -eq 1 ]; then
- exe="$work/$name.exe"
- if "$LINK_EXE_RUNNER" -o "$exe" "${MACHO_DSO_ARGS[@]}" "$wat_obj" "$WASM_START_OBJ" \
- >"$work/link.out" 2>"$work/link.err"; then
- if exec_target_supported "$EXEC_TAG"; then
- exec_target_run "$EXEC_TAG" "$exe" "$work/exec.out" "$work/exec.err"
- rc=$RUN_RC
- if [ "$rc" -eq "$expected" ]; then
- note_pass "$name/E"
- else
- note_fail "$name/E expected $expected got $rc"
- fi
- else
- note_skip "$name/E" "no execution support for $TEST_ARCH"
- fi
- else
- note_fail "$name/E link failed"
- fi
- else
- note_skip "$name/E" "requires link runner and wasm start.o"
- fi
- fi
-
- if [ "$RUN_C" -eq 1 ]; then
- label="$name/C"
- if [ "$have_c_wrapper" -eq 0 ]; then
- note_skip "$label" "host CC wrapper unavailable"
- elif [ "$host_matches" -eq 0 ]; then
- note_skip "$label" "host arch != $TEST_ARCH (C target is target-locked)"
- else
- c_src="$work/$name.cfree.c"
- c_bin="$work/$name.cbackend.bin"
- c_emit_err="$work/c.emit.err"
- c_cc_err="$work/c.cc.err"
- # `cfree cc` uses the configured host triple by default. The wasm
- # frontend doesn't require -target, but pass it explicitly so the
- # emitted C is locked to the same arch as the host that will compile
- # it. Skip on phased-rollout panics from the C target.
- if ! "$CFREE_BIN" cc -target "$target_triple" --emit=c "$wat" \
- -o "$c_src" >"$work/c.emit.out" 2>"$c_emit_err"; then
- missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \
- "$c_emit_err" 2>/dev/null | head -n1 || true)
- if [ -n "$missing" ]; then
- note_skip "$label" "$missing"
- else
- note_fail "$label cfree cc --emit=c failed"
- sed 's/^/ | /' "$c_emit_err"
- fi
- elif ! $HOST_CC -std=gnu99 -Wno-main-return-type "$c_src" \
- "$C_WRAPPER_OBJ" -o "$c_bin" \
- >"$work/c.cc.out" 2>"$c_cc_err"; then
- note_fail "$label host cc rejected emitted source"
- sed 's/^/ | /' "$c_cc_err"
- else
- "$c_bin" >"$work/c.run.out" 2>"$work/c.run.err"
- rc=$?
- if [ "$rc" -eq "$expected" ]; then
- note_pass "$label"
- else
- note_fail "$label expected $expected got $rc"
- fi
- fi
- fi
- fi
-done
-
-# trap/err/meta corpora are diagnostics-focused and orthogonal to the
-# C-source emit path; skip them when only RUN_C is requested.
+# cases corpus — active lanes in PATHS order.
+CASE_LANES=
+[ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W"
+[ "$RUN_D" -eq 1 ] && CASE_LANES="$CASE_LANES D"
+[ "$RUN_N" -eq 1 ] && CASE_LANES="$CASE_LANES N"
+[ "$RUN_O" -eq 1 ] && CASE_LANES="$CASE_LANES O"
+[ "$RUN_J" -eq 1 ] && CASE_LANES="$CASE_LANES J"
+[ "$RUN_E" -eq 1 ] && CASE_LANES="$CASE_LANES E"
+[ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C"
+PAR="${CFREE_WASM_PARALLEL:-1}"
+CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/cases" \
+ CF_CORPUS_GLOBS="$CASES_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$CASES_DIR" \
+ CF_LANES="$CASE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" \
+ CF_EXPECTED_EXT=.expect CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \
+ cf_corpus_run
+
+# trap/err/meta are diagnostics corpora, orthogonal to the C-emit path.
RUN_DIAG=0
-[ "$RUN_W" -eq 1 ] || [ "$RUN_D" -eq 1 ] || [ "$RUN_O" -eq 1 ] && RUN_DIAG=1
+{ [ "$RUN_W" -eq 1 ] || [ "$RUN_D" -eq 1 ] || [ "$RUN_O" -eq 1 ]; } && RUN_DIAG=1
-for wat in "$TRAP_DIR"/*.wat; do
- [ "$RUN_DIAG" -eq 1 ] || break
- [ -e "$wat" ] || continue
- name=$(basename "$wat" .wat)
- work="$BUILD_DIR/trap-$name"
- mkdir -p "$work"
- wasm="$work/$name.wasm"
+if [ "$RUN_DIAG" -eq 1 ]; then
+ CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/trap" \
+ CF_CORPUS_GLOBS="$TRAP_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$TRAP_DIR" \
+ CF_LANES="T" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+
+ CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/err" \
+ CF_CORPUS_GLOBS="$ERR_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$ERR_DIR" \
+ CF_LANES="ERR" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
+
+ # Inline diagnostic cases (not file-glob corpora).
+ CF_WORK="$BUILD_DIR/err-inline"; rm -rf "$CF_WORK"; mkdir -p "$CF_WORK"
+ CF_NAME=err CF_BASE=err
+ wf_fail_grep "err/stack_underflow/loc" \
+ "$ERR_DIR/stack_underflow.wat:3:5: fatal: wasm: operand stack underflow" \
+ "$CFREE_BIN" cc -target "$target_triple" -c "$ERR_DIR/stack_underflow.wat" -o "$CF_WORK/su-loc.o"
if [ "$have_wasm_tool" -eq 1 ]; then
- run_expect_zero "trap/$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm"
+ CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/meta" \
+ CF_CORPUS_GLOBS="$META_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$META_DIR" \
+ CF_LANES="META" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \
+ CF_PARALLELIZABLE="$PAR" cf_corpus_run
else
- note_skip "trap/$name/W" "no wasm-tool"
- continue
+ cf_skip "meta" "no wasm-tool"
fi
- run_expect_fail_quiet "trap/$name/D-wat" "$CFREE_BIN" run -e test_main "$wat"
- run_expect_fail_quiet "trap/$name/D-wasm" "$CFREE_BIN" run -e test_main "$wasm"
-done
-
-for wat in "$ERR_DIR"/*.wat; do
- [ "$RUN_DIAG" -eq 1 ] || break
- [ -e "$wat" ] || continue
- name=$(basename "$wat" .wat)
- run_expect_fail "err/$name/cc" "$CFREE_BIN" cc -target "$target_triple" -c \
- "$wat" -o "$BUILD_DIR/err-$name.o"
-done
-
-if [ "$RUN_DIAG" -eq 1 ]; then
- run_expect_fail_stderr_contains "err/stack_underflow/loc" \
- "$ERR_DIR/stack_underflow.wat:3:5: fatal: wasm: operand stack underflow" \
- "$CFREE_BIN" cc -target "$target_triple" -c \
- "$ERR_DIR/stack_underflow.wat" -o "$BUILD_DIR/err-stack_underflow-loc.o"
-fi
-
-if [ "$RUN_DIAG" -eq 1 ] && [ "$have_wasm_tool" -eq 1 ]; then
- for wat in "$META_DIR"/*.wat; do
- [ -e "$wat" ] || continue
- name=$(basename "$wat" .wat)
- wasm="$BUILD_DIR/meta-$name.wasm"
- run_expect_zero "meta/$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm"
- if [ "$name" = "type_custom" ] ||
- [ "$name" = "import_table_global_start" ]; then
- run_expect_zero "meta/$name/native" "$CFREE_BIN" cc \
- -target "$target_triple" -c "$wasm" -o "$BUILD_DIR/meta-$name.o"
- else
- run_expect_fail "meta/$name/native" "$CFREE_BIN" cc \
- -target "$target_triple" -c "$wasm" -o "$BUILD_DIR/meta-$name.o"
- fi
- done
-elif [ "$RUN_DIAG" -eq 1 ]; then
- note_skip "meta" "no wasm-tool"
-fi
-
-if [ "$RUN_DIAG" -eq 1 ]; then
-bad_wasm="$BUILD_DIR/malformed-section.wasm"
-printf '\000asm\001\000\000\000\001\005\001\140' > "$bad_wasm"
-run_expect_fail "err/malformed-section/wasm" "$CFREE_BIN" cc \
- -target "$target_triple" -c "$bad_wasm" -o "$BUILD_DIR/bad-section.o"
-
-bad_leb="$BUILD_DIR/malformed-leb.wasm"
-printf '\000asm\001\000\000\000\001\200\200\200\200\020' > "$bad_leb"
-run_expect_fail "err/malformed-leb/wasm" "$CFREE_BIN" cc \
- -target "$target_triple" -c "$bad_leb" -o "$BUILD_DIR/bad-leb.o"
+ printf '\000asm\001\000\000\000\001\005\001\140' > "$CF_WORK/malformed-section.wasm"
+ wf_fail "err/malformed-section/wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/malformed-section.wasm" -o "$CF_WORK/bad-section.o"
+ printf '\000asm\001\000\000\000\001\200\200\200\200\020' > "$CF_WORK/malformed-leb.wasm"
+ wf_fail "err/malformed-leb/wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/malformed-leb.wasm" -o "$CF_WORK/bad-leb.o"
fi
-printf 'test-wasm-front: pass=%d fail=%d skip=%d\n' "$pass" "$fail" "$skip"
-[ "$fail" -eq 0 ]
+cf_summary test-wasm-front
+cf_exit