kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mdoc/C11_CONFORMANCE_CHECKLIST.md | 60++++++++++++++++++++++++++++++++++++------------------------
Mlang/c/pp/pp.c | 35+++++++++++++++++++++++++++++++++++
Mrt/Makefile | 2++
Mrt/include/stdatomic.h | 13++++++++-----
Mrt/lib/atomic/atomic_common.inc | 13++++++++++++-
Mrt/lib/atomic/atomic_freestanding.c | 2+-
Msrc/api/cg.c | 35+++++++++++++++++++++++++++++++----
Msrc/arch/aa64/ops.c | 13++++++++++---
Dtest/parse/cases/asm_02_file_scope.skip | 1-
Atest/rt/cases/coro_runtime.c | 37+++++++++++++++++++++++++++++++++++++
Atest/rt/cases/setjmp_runtime.c | 13+++++++++++++
Atest/rt/cases/stdarg_runtime.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/rt/cases/stdatomic_runtime.c | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/rt/run.sh | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 23+++++++++++++++++++++--
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.