commit 22ad0e948be159d0c73851107fc3ac6534b3936e
parent cec5aeab9c5d70fee51c0e90ae8cf9f673fade85
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 19 May 2026 06:37:16 -0700
Complete freestanding runtime conformance gate
Diffstat:
15 files changed, 509 insertions(+), 41 deletions(-)
diff --git a/doc/C11_CONFORMANCE_CHECKLIST.md b/doc/C11_CONFORMANCE_CHECKLIST.md
@@ -1,6 +1,6 @@
# C11 conformance checklist
-Status snapshot: 2026-05-18.
+Status snapshot: 2026-05-19.
Ground truth should be the implementation plus targeted tests, not README.md.
Keep this checklist red-green: add or unskip the smallest case first, then
@@ -9,15 +9,17 @@ make the implementation pass it.
## Current signal
- [x] `make test-lex` passes: 16/16.
-- [x] `make test-pp test-pp-err` passes: 82/82 and 15/15.
+- [x] `make test-pp test-pp-err` passes: 83/83 and 15/15.
- [x] `make test-parse-err` passes with expanded C11 constraint coverage:
currently 57/57 pass.
-- [ ] `make test-parse` passes without skips: currently 2528 pass, 0 fail,
- 4 skip. Skips are `long double` and file-scope `asm`.
+- [ ] `make test-parse` passes without skips: currently 2680 pass, 0 fail,
+ 2 skip. The remaining skip is `long double`.
- [x] `make test-cg-api test-opt test-dwarf test-debug` passes.
-- [ ] `make rt` builds the default runtime archives. Currently fails in
- `rt/lib/atomic/atomic_common.inc` because exported `__atomic_*`
- functions conflict with clang builtin declarations.
+- [x] `make rt` builds the default runtime archives.
+- [x] `make test-rt-headers` passes for the default runtime targets:
+ AArch64/x86-64/RV64 Linux and AArch64/x86-64 Darwin.
+- [x] `make test-rt-runtime` passes for the default execution targets:
+ AArch64/x86-64/RV64 Linux.
- [x] `make test-lib-deps` passes.
## First conformance gate: required diagnostics
@@ -84,7 +86,7 @@ Suggested cadence:
make test-parse-err > /tmp/cfree_parse_err.log 2>&1 || tail -n 80 /tmp/cfree_parse_err.log
```
-## Positive parse skips
+## Positive parse skips and recently unskipped cases
Goal: `make test-parse` is green with `CFREE_TEST_ALLOW_SKIP` unset.
@@ -92,11 +94,11 @@ Goal: `make test-parse` is green with `CFREE_TEST_ALLOW_SKIP` unset.
Current skipped case: `test/parse/cases/6_7_2_12_long_double.c`.
Skip reason: binary128 literal/convert needs `rt/lib/fp_tf` wiring
through CG.
-- [ ] Enable file-scope `asm`.
- Current skipped case: `test/parse/cases/asm_02_file_scope.c`.
- Parser currently parses and then deliberately errors in
- `parse_file_scope_asm` because the C frontend is isolated from assembler
- internals.
+- [x] Enable file-scope `asm`.
+ Covered case: `test/parse/cases/asm_02_file_scope.c`.
+ The parser decodes the file-scope string literal and submits it through
+ `cfree_cg_file_scope_asm`, which reuses the standalone asm parser over
+ the current object emitter.
Focused run:
@@ -237,19 +239,28 @@ CFREE_TEST_FILTER=asm_02_file_scope make test-parse
C11 freestanding requires at least `<float.h>`, `<iso646.h>`, `<limits.h>`,
`<stdalign.h>`, `<stdarg.h>`, `<stdbool.h>`, `<stddef.h>`, `<stdint.h>`, and
-`<stdnoreturn.h>`. This tree also ships `assert.h`, `setjmp.h`, and
-`stdatomic.h`, plus cfree extensions.
+`<stdnoreturn.h>`. This tree also ships `assert.h` and `stdatomic.h`.
+`setjmp.h` and `cfree/coro.h` are advertised freestanding extensions: they
+depend on target register context, not hosted OS services.
-- [ ] Add header compile smoke tests per supported target for every
- freestanding header.
-- [ ] Add macro/value tests for `limits.h`, `stdint.h`, `stddef.h`, and
+Status: complete for the current freestanding C11 profile. Keep this gate
+green with `make rt`, `make test-rt-headers`, `make test-rt-runtime`, and
+`make test-lib-deps`.
+
+- [x] Add header compile smoke tests for every freestanding header across the
+ default runtime targets.
+ Test: `make test-rt-headers` / `test/smoke.c`.
+- [x] Add macro/value tests for `limits.h`, `stdint.h`, `stddef.h`, and
`float.h` against target ABI expectations.
-- [ ] Add `stdarg.h` runtime tests for AArch64, x86-64, and RV64.
-- [ ] Get `stdatomic.h` tests passing against both parser builtins and
+ Test: `make test-rt-headers` / `test/smoke.c`.
+- [x] Add `stdarg.h` runtime tests for AArch64, x86-64, and RV64.
+ Test: `make test-rt-runtime`.
+- [x] Get `stdatomic.h` tests passing against both parser builtins and
`libcfree_rt.a`.
-- [ ] Fix `make rt` before treating atomics as conforming.
-- [ ] Decide whether `setjmp.h` remains an advertised extension or is part of
- a hosted profile only.
+ Test: `make test-rt-runtime`.
+- [x] Fix `make rt` before treating atomics as conforming.
+- [x] Keep `setjmp.h` as an advertised freestanding extension; classify
+ `cfree/coro.h` the same way.
## Strict mode and extensions
@@ -282,4 +293,5 @@ conformance needs a mode story.
static initializer constant path when adding new initializer forms.
6. Unskip `long double` or explicitly narrow the supported C profile until
runtime/CG support exists.
-7. Bring `rt` and freestanding header tests into the default conformance gate.
+7. Keep the completed freestanding runtime/header gate green while expanding
+ target coverage.
diff --git a/lang/c/pp/pp.c b/lang/c/pp/pp.c
@@ -343,6 +343,8 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, "__SIZE_TYPE__", lp64 ? "unsigned long" : "unsigned int");
pp_define(pp, "__PTRDIFF_TYPE__", lp64 ? "long" : "int");
pp_define(pp, "__WCHAR_TYPE__", "int");
+ pp_define(pp, "__CHAR16_TYPE__", "unsigned short");
+ pp_define(pp, "__CHAR32_TYPE__", "unsigned int");
/* stdint.h exact-width aliases (widths <= 32 are model-independent) */
pp_define(pp, "__INT8_TYPE__", "signed char");
@@ -402,6 +404,7 @@ static void pp_register_target_predefined(Pp* pp) {
/* Pointer-holding integers + ptrdiff/size maxes */
if (lp64) {
+ pp_define(pp, "__LONG_MAX__", "9223372036854775807L");
pp_define(pp, "__INTPTR_TYPE__", "long");
pp_define(pp, "__UINTPTR_TYPE__", "unsigned long");
pp_define(pp, "__INTPTR_MAX__", "9223372036854775807L");
@@ -409,6 +412,7 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, "__PTRDIFF_MAX__", "9223372036854775807L");
pp_define(pp, "__SIZE_MAX__", "18446744073709551615UL");
} else {
+ pp_define(pp, "__LONG_MAX__", "2147483647L");
pp_define(pp, "__INTPTR_TYPE__", "int");
pp_define(pp, "__UINTPTR_TYPE__", "unsigned int");
pp_define(pp, "__INTPTR_MAX__", "2147483647");
@@ -445,6 +449,37 @@ static void pp_register_target_predefined(Pp* pp) {
pp_define(pp, "__WINT_MIN__", "(-__WINT_MAX__ - 1)");
pp_define(pp, "__SIG_ATOMIC_MAX__", "2147483647");
pp_define(pp, "__SIG_ATOMIC_MIN__", "(-__SIG_ATOMIC_MAX__ - 1)");
+
+ /* C11 <stdatomic.h> lock-free macros. The currently supported primary
+ * targets have naturally lock-free scalar and pointer atomics through the
+ * machine-word sizes used by these typedefs. */
+ pp_define(pp, "__ATOMIC_BOOL_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_CHAR_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_CHAR16_T_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_CHAR32_T_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_WCHAR_T_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_SHORT_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_INT_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_LONG_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_LLONG_LOCK_FREE", "2");
+ pp_define(pp, "__ATOMIC_POINTER_LOCK_FREE", "2");
+
+ /* The C frontend currently lowers long double as binary64. Keep the
+ * compiler-predefined floating macros aligned with that implementation. */
+ pp_define(pp, "__FLT_EVAL_METHOD__", "0");
+ pp_define(pp, "__LDBL_HAS_DENORM__", "1");
+ pp_define(pp, "__LDBL_MANT_DIG__", "53");
+ pp_define(pp, "__LDBL_DECIMAL_DIG__", "17");
+ pp_define(pp, "__LDBL_DIG__", "15");
+ pp_define(pp, "__LDBL_MIN_EXP__", "(-1021)");
+ pp_define(pp, "__LDBL_MIN_10_EXP__", "(-307)");
+ pp_define(pp, "__LDBL_MAX_EXP__", "1024");
+ pp_define(pp, "__LDBL_MAX_10_EXP__", "308");
+ pp_define(pp, "__LDBL_MAX__", "0x1.fffffffffffffp+1023L");
+ pp_define(pp, "__LDBL_EPSILON__", "0x1p-52L");
+ pp_define(pp, "__LDBL_MIN__", "0x1p-1022L");
+ pp_define(pp, "__LDBL_DENORM_MIN__", "0x1p-1074L");
+ pp_define(pp, "__DECIMAL_DIG__", "17");
}
Pp* pp_new(Compiler* c) {
diff --git a/rt/Makefile b/rt/Makefile
@@ -211,6 +211,8 @@ $$(RT_BUILD_DIR)/$(1)/%.S.o: rt/lib/%.S | $$(BIN)
$$(RT_BUILD_DIR)/$(1)/%.o: rt/lib/% | $$(BIN)
@mkdir -p $$(dir $$@)
$$(RT_CC) $$(RT_CFLAGS_$(1)) -c $$< -o $$@
+
+$$(RT_BUILD_DIR)/$(1)/atomic/atomic_freestanding.c.o: rt/lib/atomic/atomic_common.inc
endef
$(foreach variant,$(RT_VARIANTS),$(eval $(call RT_VARIANT_template,$(variant))))
diff --git a/rt/include/stdatomic.h b/rt/include/stdatomic.h
@@ -153,17 +153,20 @@ typedef _Atomic uintmax_t atomic_uintmax_t;
/* 7.17.8 Atomic flag */
/* ------------------------------------------------------------------ */
typedef struct atomic_flag {
- _Atomic _Bool _Value;
+ /* Opaque flag storage may be wider than _Bool; use a word-sized atomic so
+ every primary backend can implement test-and-set with native RMW width. */
+ _Atomic unsigned int _Value;
} atomic_flag;
#define ATOMIC_FLAG_INIT {0}
#define atomic_flag_test_and_set(obj) \
- __atomic_test_and_set(&(obj)->_Value, __ATOMIC_SEQ_CST)
+ __atomic_exchange_n(&(obj)->_Value, 1, __ATOMIC_SEQ_CST)
#define atomic_flag_test_and_set_explicit(obj, order) \
- __atomic_test_and_set(&(obj)->_Value, (order))
-#define atomic_flag_clear(obj) __atomic_clear(&(obj)->_Value, __ATOMIC_SEQ_CST)
+ __atomic_exchange_n(&(obj)->_Value, 1, (order))
+#define atomic_flag_clear(obj) \
+ __atomic_store_n(&(obj)->_Value, 0, __ATOMIC_SEQ_CST)
#define atomic_flag_clear_explicit(obj, order) \
- __atomic_clear(&(obj)->_Value, (order))
+ __atomic_store_n(&(obj)->_Value, 0, (order))
#endif
diff --git a/rt/lib/atomic/atomic_common.inc b/rt/lib/atomic/atomic_common.inc
@@ -29,6 +29,17 @@ static inline Lock *lock_for_pointer(void *ptr) {
return locks + (hash & SPINLOCK_MASK);
}
+static inline int bytes_equal(const void *a, const void *b, int size) {
+ const unsigned char *p = (const unsigned char *)a;
+ const unsigned char *q = (const unsigned char *)b;
+ for (int i = 0; i < size; ++i) {
+ unsigned char pa = p[i];
+ unsigned char qb = q[i];
+ if (pa != qb) return 0;
+ }
+ return 1;
+}
+
#define ATOMIC_ALWAYS_LOCK_FREE_OR_ALIGNED_LOCK_FREE(size, p) \
(__atomic_always_lock_free(size, p) || \
(__atomic_always_lock_free(size, 0) && ((uintptr_t)p % size) == 0))
@@ -112,7 +123,7 @@ int __atomic_compare_exchange(int size, void *ptr, void *expected,
(void)failure;
Lock *l = lock_for_pointer(ptr);
lock(l);
- if (__builtin_memcmp(ptr, expected, size) == 0) {
+ if (bytes_equal(ptr, expected, size)) {
__builtin_memcpy(ptr, desired, size);
unlock(l);
return 1;
diff --git a/rt/lib/atomic/atomic_freestanding.c b/rt/lib/atomic/atomic_freestanding.c
@@ -12,7 +12,7 @@
#include <stddef.h>
#include <stdint.h>
-#define SPINLOCK_COUNT (1 << 10)
+enum { SPINLOCK_COUNT = 1 << 10 };
static const long SPINLOCK_MASK = SPINLOCK_COUNT - 1;
// HAS_INT128 is defined by the build system: -DHAS_INT128=1 on 64-bit targets,
diff --git a/src/api/cg.c b/src/api/cg.c
@@ -407,6 +407,15 @@ static CfreeCgTypeId resolve_type(Compiler *c, CfreeCgTypeId id) {
return cg_type_get(c, id) ? id : CFREE_CG_TYPE_NONE;
}
+static CfreeCgTypeId api_unalias_type(Compiler *c, CfreeCgTypeId id) {
+ const CgType *ty = cg_type_get(c, id);
+ while (ty && ty->kind == CFREE_CG_TYPE_ALIAS) {
+ id = ty->alias.base;
+ ty = cg_type_get(c, id);
+ }
+ return ty ? id : CFREE_CG_TYPE_NONE;
+}
+
static CfreeCgFuncParam *copy_cg_params(Compiler *c, const CfreeCgFuncParam *src,
u32 n) {
CfreeCgFuncParam *dst;
@@ -2027,9 +2036,15 @@ static void api_ensure_reg(CfreeCg *g, ApiSValue *sv) {
static Operand api_force_reg(CfreeCg *g, ApiSValue *v, CfreeCgTypeId ty) {
CGTarget *T = g->target;
+ ty = api_unalias_type(g->c, ty);
api_ensure_reg(g, v);
- if (v->op.kind == OPK_REG)
+ if (v->op.kind == OPK_REG) {
+ if (ty) {
+ v->op.type = ty;
+ v->type = ty;
+ }
return v->op;
+ }
Reg r = api_alloc_reg_or_spill(g, api_type_class(ty), ty);
Operand dst = api_op_reg(r, ty);
if (v->op.kind == OPK_IMM) {
@@ -4025,8 +4040,15 @@ static void api_cg_convert_kind(CfreeCg *g, CfreeCgTypeId dst_type,
if (!dty)
return;
v = api_pop(g);
- sty = v.type ? v.type : v.op.type;
+ dty = api_unalias_type(g->c, dty);
+ sty = api_unalias_type(g->c, v.type ? v.type : v.op.type);
+ if (!sty) {
+ api_release(g, &v);
+ return;
+ }
if (sty == dty) {
+ v.type = dty;
+ v.op.type = dty;
api_push(g, v);
return;
}
@@ -5952,7 +5974,12 @@ void cfree_cg_data_begin(CfreeCg *g, CfreeCgSym cg_sym,
if (!attrs.section && decl_attrs.as.object.section) {
attrs.section = decl_attrs.as.object.section;
}
- if (attrs.flags & CFREE_CG_DATADEF_ZERO_FILL) {
+ if ((decl_attrs.as.object.flags & CFREE_CG_OBJ_TLS) &&
+ (attrs.flags & CFREE_CG_DATADEF_ZERO_FILL)) {
+ sec_kind = SEC_BSS;
+ sec_flags = SF_ALLOC | SF_WRITE | SF_TLS;
+ sec_name_sym = attrs.section ? (Sym)attrs.section : obj_secname_tbss(c);
+ } else if (attrs.flags & CFREE_CG_DATADEF_ZERO_FILL) {
sec_kind = SEC_BSS;
sec_flags = SF_ALLOC | SF_WRITE;
sec_name_sym = attrs.section ? (Sym)attrs.section
@@ -5974,7 +6001,7 @@ void cfree_cg_data_begin(CfreeCg *g, CfreeCgSym cg_sym,
} else if (decl_attrs.as.object.flags & CFREE_CG_OBJ_TLS) {
sec_kind = SEC_DATA;
sec_flags = SF_ALLOC | SF_WRITE | SF_TLS;
- sec_name_sym = pool_intern_cstr(c->global, ".tdata");
+ sec_name_sym = obj_secname_tdata(c);
} else {
sec_kind = SEC_DATA;
sec_flags = SF_ALLOC | SF_WRITE;
diff --git a/src/arch/aa64/ops.c b/src/arch/aa64/ops.c
@@ -684,7 +684,12 @@ static void aa_convert(CGTarget* t, ConvKind k, Operand dst, Operand src) {
compiler_panic(t->c, a->loc, "aarch64 convert SEXT: bad classes");
}
u32 src_bits = type_byte_size(src.type) * 8u;
+ u32 dst_bits = type_byte_size(dst.type) * 8u;
u32 sf_dst = type_is_64(dst.type) ? 1u : 0u;
+ if (src_bits >= dst_bits) {
+ aa64_emit32(mc, aa64_mov_reg(sf_dst, rd, rn));
+ return;
+ }
aa64_emit32(mc, aa64_sbfm(sf_dst, rd, rn, /*immr=*/0, /*imms=*/src_bits - 1u));
return;
}
@@ -693,10 +698,12 @@ static void aa_convert(CGTarget* t, ConvKind k, Operand dst, Operand src) {
compiler_panic(t->c, a->loc, "aarch64 convert ZEXT: bad classes");
}
u32 src_bits = type_byte_size(src.type) * 8u;
- if (src_bits == 32u) {
- aa64_emit32(mc, aa64_mov_reg(0, rd, rn));
+ u32 dst_bits = type_byte_size(dst.type) * 8u;
+ u32 sf_dst = type_is_64(dst.type) ? 1u : 0u;
+ if (src_bits >= dst_bits || src_bits == 32u) {
+ aa64_emit32(mc, aa64_mov_reg(src_bits == 32u ? 0u : sf_dst, rd, rn));
} else {
- aa64_emit32(mc, aa64_ubfm(0, rd, rn, /*immr=*/0, /*imms=*/src_bits - 1u));
+ aa64_emit32(mc, aa64_ubfm(sf_dst, rd, rn, /*immr=*/0, /*imms=*/src_bits - 1u));
}
return;
}
diff --git a/test/parse/cases/asm_02_file_scope.skip b/test/parse/cases/asm_02_file_scope.skip
@@ -1 +0,0 @@
-file-scope asm is disabled while the C frontend is isolated from assembler internals
diff --git a/test/rt/cases/coro_runtime.c b/test/rt/cases/coro_runtime.c
@@ -0,0 +1,37 @@
+#include <cfree/coro.h>
+#include <stdint.h>
+
+struct CoroState {
+ int phase;
+ int saw_self;
+};
+
+static uintptr_t coro_body(uintptr_t value) {
+ struct CoroState* st = (struct CoroState*)value;
+ st->saw_self = coro_self() != 0;
+ st->phase = 1;
+ st = (struct CoroState*)coro_yield(0);
+ if (coro_self() == 0) st->phase = 101;
+ st->phase = 2;
+ return 0;
+}
+
+int test_main(void) {
+ unsigned char* stack = __builtin_alloca(1024);
+ struct CoroState st;
+ coro_t co;
+
+ st.phase = 0;
+ st.saw_self = 0;
+ coro_init(&co, coro_body, stack, 1024);
+ if (coro_status(&co) != CORO_INIT) return 1;
+
+ coro_resume(&co, (uintptr_t)&st);
+ if (coro_status(&co) != CORO_SUSPENDED) return 2;
+ if (st.phase != 1 || !st.saw_self) return 3;
+
+ coro_resume(&co, (uintptr_t)&st);
+ if (coro_status(&co) != CORO_DEAD) return 4;
+ if (st.phase != 2) return 5;
+ return 42;
+}
diff --git a/test/rt/cases/setjmp_runtime.c b/test/rt/cases/setjmp_runtime.c
@@ -0,0 +1,13 @@
+#include <setjmp.h>
+
+int test_main(void) {
+ jmp_buf env;
+ volatile int marker = 11;
+ int rc = setjmp(env);
+ if (rc == 0) {
+ marker = 31;
+ longjmp(env, 1);
+ }
+ if (marker != 31 || rc != 1) return 1;
+ return 42;
+}
diff --git a/test/rt/cases/stdarg_runtime.c b/test/rt/cases/stdarg_runtime.c
@@ -0,0 +1,57 @@
+#include <stdarg.h>
+
+static int sum_ints_twice(int n, ...) {
+ va_list ap;
+ va_list copy;
+ va_start(ap, n);
+ va_copy(copy, ap);
+
+ int local = 0;
+ for (int i = 0; i < n; ++i) local += va_arg(ap, int);
+
+ int copied = 0;
+ for (int i = 0; i < n; ++i) copied += va_arg(copy, int);
+
+ va_end(copy);
+ va_end(ap);
+ return local + copied;
+}
+
+static long sum_longs(int n, ...) {
+ va_list ap;
+ va_start(ap, n);
+ long total = 0;
+ for (int i = 0; i < n; ++i) total += va_arg(ap, long);
+ va_end(ap);
+ return total;
+}
+
+static int sum_ptrs(int n, ...) {
+ va_list ap;
+ va_start(ap, n);
+ int total = 0;
+ for (int i = 0; i < n; ++i) total += *va_arg(ap, int*);
+ va_end(ap);
+ return total;
+}
+
+static int sum_doubles(int n, ...) {
+ va_list ap;
+ va_start(ap, n);
+ double total = 0.0;
+ for (int i = 0; i < n; ++i) total += va_arg(ap, double);
+ va_end(ap);
+ return (int)total;
+}
+
+int test_main(void) {
+ int a = 10;
+ int b = 20;
+ int c = 12;
+
+ if (sum_ints_twice(3, 5, 7, 9) != 42) return 1;
+ if (sum_longs(3, 10L, 20L, 12L) != 42L) return 2;
+ if (sum_ptrs(3, &a, &b, &c) != 42) return 3;
+ if (sum_doubles(3, 10.0, 20.0, 12.0) != 42) return 4;
+ return 42;
+}
diff --git a/test/rt/cases/stdatomic_runtime.c b/test/rt/cases/stdatomic_runtime.c
@@ -0,0 +1,95 @@
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+struct Blob {
+ int a;
+ int b;
+ int c;
+};
+
+void __atomic_load(int size, void* src, void* dest, int model);
+void __atomic_store(int size, void* dest, void* src, int model);
+void __atomic_exchange(int size, void* ptr, void* val, void* old, int model);
+int __atomic_compare_exchange(int size, void* ptr, void* expected,
+ void* desired, int success, int failure);
+
+static int same_blob(const struct Blob* a, const struct Blob* b) {
+ return a->a == b->a && a->b == b->b && a->c == b->c;
+}
+
+static int header_ops(void) {
+ atomic_int x = ATOMIC_VAR_INIT(0);
+ atomic_init(&x, 1);
+ atomic_store_explicit(&x, 10, memory_order_relaxed);
+ if (atomic_load(&x) != 10) return 0;
+ if (atomic_exchange(&x, 20) != 10) return 0;
+
+ int expected = 20;
+ if (!atomic_compare_exchange_strong(&x, &expected, 30)) return 0;
+ if (expected != 20 || atomic_load(&x) != 30) return 0;
+ if (atomic_compare_exchange_strong(&x, &expected, 40)) return 0;
+ if (expected != 30 || atomic_load(&x) != 30) return 0;
+
+ if (atomic_fetch_add(&x, 5) != 30) return 0;
+ if (atomic_fetch_sub(&x, 3) != 35) return 0;
+ if (atomic_fetch_or(&x, 1) != 32) return 0;
+ if (atomic_fetch_xor(&x, 3) != 33) return 0;
+ if (atomic_fetch_and(&x, 15) != 34) return 0;
+
+ atomic_flag f = ATOMIC_FLAG_INIT;
+ if (atomic_flag_test_and_set(&f)) return 0;
+ if (!atomic_flag_test_and_set_explicit(&f, memory_order_acquire)) return 0;
+ atomic_flag_clear_explicit(&f, memory_order_release);
+ if (atomic_flag_test_and_set(&f)) return 0;
+ return 21;
+}
+
+static int runtime_fallback_ops(void) {
+ struct Blob obj;
+ struct Blob out;
+ struct Blob val;
+ struct Blob old;
+ struct Blob expected;
+ struct Blob desired;
+
+ obj.a = 1;
+ obj.b = 2;
+ obj.c = 3;
+ __atomic_load((int)sizeof(obj), &obj, &out, __ATOMIC_SEQ_CST);
+ if (!same_blob(&out, &obj)) return 1;
+
+ val.a = 4;
+ val.b = 5;
+ val.c = 6;
+ __atomic_store((int)sizeof(obj), &obj, &val, __ATOMIC_RELEASE);
+ if (!same_blob(&obj, &val)) return 2;
+
+ desired.a = 7;
+ desired.b = 8;
+ desired.c = 9;
+ __atomic_exchange((int)sizeof(obj), &obj, &desired, &old, __ATOMIC_ACQ_REL);
+ if (!same_blob(&old, &val) || !same_blob(&obj, &desired)) return 3;
+
+ expected = desired;
+ val.a = 10;
+ val.b = 11;
+ val.c = 12;
+ if (!__atomic_compare_exchange((int)sizeof(obj), &obj, &expected, &val,
+ __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
+ return 4;
+ }
+ if (!same_blob(&obj, &val)) return 5;
+
+ expected.a = 0;
+ expected.b = 0;
+ expected.c = 0;
+ if (__atomic_compare_exchange((int)sizeof(obj), &obj, &expected, &desired,
+ __ATOMIC_SEQ_CST, __ATOMIC_RELAXED)) {
+ return 6;
+ }
+ if (!same_blob(&expected, &val) || !same_blob(&obj, &val)) return 7;
+ return 21;
+}
+
+int test_main(void) { return header_ops() + runtime_fallback_ops(); }
diff --git a/test/rt/run.sh b/test/rt/run.sh
@@ -0,0 +1,151 @@
+#!/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.
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+CASES_DIR="$ROOT/test/rt/cases"
+BUILD_DIR="$ROOT/build/test/rt-runtime"
+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}"
+
+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"; }
+
+if [ ! -x "$CFREE" ]; then
+ printf 'cfree driver missing at %s -- run `make bin` first\n' "$CFREE" >&2
+ exit 2
+fi
+if [ ! -x "$LINK_EXE_RUNNER" ]; then
+ printf 'link-exe-runner missing at %s -- run `make test-rt-runtime` from make\n' \
+ "$LINK_EXE_RUNNER" >&2
+ exit 2
+fi
+
+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
+have_podman=0
+command -v podman >/dev/null 2>&1 && have_podman=1
+arch_raw="$(uname -m 2>/dev/null || true)"
+is_aarch64=0
+if [ "$(uname -s 2>/dev/null)" = "Linux" ]; then
+ { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1
+fi
+export have_qemu QEMU_BIN have_podman is_aarch64
+
+EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"
+# shellcheck source=../lib/exec_target.sh
+source "$ROOT/test/lib/exec_target.sh"
+
+arch_triple() {
+ case "$1" in
+ aa64) echo "aarch64-linux-gnu" ;;
+ x64) echo "x86_64-linux-gnu" ;;
+ rv64) echo "riscv64-linux-gnu" ;;
+ *) return 1 ;;
+ esac
+}
+
+rt_archive() {
+ case "$1" in
+ aa64) echo "$ROOT/rt/build/aarch64-linux/libcfree_rt.a" ;;
+ x64) echo "$ROOT/rt/build/x86_64-linux/libcfree_rt.a" ;;
+ rv64) echo "$ROOT/rt/build/riscv64-linux/libcfree_rt.a" ;;
+ *) return 1 ;;
+ esac
+}
+
+clang_extra_flags() {
+ case "$1" in
+ rv64) echo "-march=rv64gc" ;;
+ *) echo "" ;;
+ esac
+}
+
+run_arch() {
+ local arch="$1"
+ local triple rtlib start_obj arch_dir extra
+ triple="$(arch_triple "$arch")"
+ rtlib="$(rt_archive "$arch")"
+ arch_dir="$BUILD_DIR/$arch"
+ start_obj="$arch_dir/start.o"
+ extra="$(clang_extra_flags "$arch")"
+ mkdir -p "$arch_dir"
+
+ if [ ! -f "$rtlib" ]; then
+ note_skip "$arch" "runtime archive missing at $rtlib"
+ return 0
+ fi
+ if ! exec_target_supported "$arch"; then
+ note_skip "$arch" "no execution runner"
+ return 0
+ 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
+ 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" -isystem "$ROOT/rt/include" \
+ -isystem "$ROOT/rt/include/libc" -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
+}
+
+ARCHES="${CFREE_RT_RUNTIME_ARCHES:-aa64 x64 rv64}"
+for arch in $ARCHES; do
+ case "$arch" in
+ aa64|x64|rv64) run_arch "$arch" ;;
+ *) note_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
diff --git a/test/test.mk b/test/test.mk
@@ -27,9 +27,9 @@
# asm_parse / cfree_disasm_iter_* are still stubs; the harness builds
# and runs end-to-end so the wiring stays exercised. See doc/ASM.md.
-.PHONY: test test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-libc test-musl test-glibc test-lib-deps test-smoke-x64 test-smoke-rv64
+.PHONY: test test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-rt-headers test-rt-runtime test-libc test-musl test-glibc test-lib-deps test-smoke-x64 test-smoke-rv64
-test: test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-toy test-dwarf test-debug test-parse test-parse-err test-asm test-isa test-aa64-inline test-lib-deps
+test: test-driver test-lex test-pp test-pp-err test-elf test-ar test-ar-driver test-link test-toy test-dwarf test-debug test-parse test-parse-err test-asm test-isa test-aa64-inline test-rt-headers test-lib-deps
test-driver: bin
@CFREE=$(abspath $(BIN)) sh test/driver/run.sh
@@ -129,6 +129,25 @@ $(AA64_INLINE_TEST_BIN): test/arch/aa64_inline_test.c $(LIB_AR)
@mkdir -p $(dir $@)
$(CC) $(DRIVER_CFLAGS) -Isrc test/arch/aa64_inline_test.c $(LIB_AR) -o $@
+RT_HEADER_TEST_TARGETS = \
+ aarch64-linux-gnu \
+ x86_64-linux-gnu \
+ riscv64-linux-gnu \
+ aarch64-apple-darwin \
+ x86_64-apple-darwin
+
+test-rt-headers: bin
+ @set -e; \
+ for target in $(RT_HEADER_TEST_TARGETS); do \
+ out="build/test/rt-headers/$$target/smoke.o"; \
+ mkdir -p "$$(dirname "$$out")"; \
+ $(BIN) cc -target "$$target" -isystem rt/include -isystem rt/include/libc \
+ -Werror -c test/smoke.c -o "$$out"; \
+ done
+
+test-rt-runtime: bin rt $(LINK_EXE_RUNNER)
+ @bash test/rt/run.sh
+
# Test harness binaries shared by test-elf and test-link.
# Declared as Make targets (not built by the run.sh scripts) so they pick
# up libcfree.a changes deterministically.