kit

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

commit 80ba21ae01667332bf937b70d209a0e73d690938
parent 106b8cb6c48dabf72df6804e4900cbdb39e836ca
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon,  8 Jun 2026 12:52:35 -0700

Track 1c: test conditional control ops + reject continue on block scope

break_true / continue_true / continue_false have zero callers in any frontend
(toy uses only break / break_false / continue), so they were unexercised. Add
test/api/cg_control_test.c: builds `int f(int n)` returning sum(0..n-1) via each
conditional break/continue, runs all four through the in-process interpreter
across a spread of n, emits each across aa64/x64/rv64 at -O0/-O1, and checks the
new block-scope rejection.

Audit fix: kit_cg_continue/_true/_false jumped to s->continue_lbl unconditionally,
which is LABEL_NONE for a forward-only block scope (no loop header) — a jump to a
nonexistent label. Now rejected with a clean diagnostic via api_require_loop_scope.

Green: test-cg-api (cg_control_test 85/0), toy 1382/0, parse + parse-err.

Diffstat:
Mmk/test.mk | 5++++-
Mmk/test_unit.mk | 4+++-
Msrc/cg/control.c | 16++++++++++++++++
Atest/api/cg_control_test.c | 347+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 370 insertions(+), 2 deletions(-)

diff --git a/mk/test.mk b/mk/test.mk @@ -432,6 +432,7 @@ test-interp-toy: bin CG_API_TEST_BIN = build/test/cg_api_test CG_SWITCH_TEST_BIN = build/test/cg_switch_test CG_FP_CMP_TEST_BIN = build/test/cg_fp_cmp_test +CG_CONTROL_TEST_BIN = build/test/cg_control_test STRENGTH_REDUCE_TEST_BIN = build/test/strength_reduce_test TARGET_TEST_BIN = build/test/target_test HASH_TEST_BIN = build/test/hash_test @@ -441,12 +442,14 @@ IR_RECORDER_TEST_BIN = build/test/ir_recorder_test NATIVE_DIRECT_TARGET_TEST_BIN = build/test/native_direct_target_test test-cg-api: $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) \ - $(CG_FP_CMP_TEST_BIN) $(STRENGTH_REDUCE_TEST_BIN) \ + $(CG_FP_CMP_TEST_BIN) $(CG_CONTROL_TEST_BIN) \ + $(STRENGTH_REDUCE_TEST_BIN) \ $(PANIC_RECOVERY_TEST_BIN) $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) $(CG_FP_CMP_TEST_BIN) + $(CG_CONTROL_TEST_BIN) $(STRENGTH_REDUCE_TEST_BIN) $(PANIC_RECOVERY_TEST_BIN) diff --git a/mk/test_unit.mk b/mk/test_unit.mk @@ -30,7 +30,8 @@ UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest # ---- registrations: stem lists + per-stem source --------------------------- UNIT_TESTS_PUBLIC := \ - ar_test target_test cg_api_test cg_switch_test cg_fp_cmp_test hash_test \ + ar_test target_test cg_api_test cg_switch_test cg_fp_cmp_test \ + cg_control_test hash_test \ panic_recovery_test \ rv64_jit_test rv32_jit_test aa64_inline_test rv64_inline_test x64_inline_test \ strength_reduce_test @@ -41,6 +42,7 @@ panic_recovery_test_SRC := test/api/panic_recovery_test.c cg_api_test_SRC := test/api/cg_type_test.c cg_switch_test_SRC := test/api/cg_switch_test.c cg_fp_cmp_test_SRC := test/api/cg_fp_cmp_test.c +cg_control_test_SRC := test/api/cg_control_test.c strength_reduce_test_SRC := test/cg/strength_reduce_test.c rv64_jit_test_SRC := test/link/rv64_jit_test.c rv32_jit_test_SRC := test/link/rv32_jit_test.c diff --git a/src/cg/control.c b/src/cg/control.c @@ -640,9 +640,23 @@ void kit_cg_break_false(KitCg* g, KitCgScope scope) { } } +/* continue jumps to the loop header, which a forward-only block scope does not + * have (its continue_lbl is LABEL_NONE). Reject it with a clean diagnostic + * rather than emitting a jump to a nonexistent label. */ +static int api_require_loop_scope(KitCg* g, const ApiCgScope* s, + const char* op) { + if (s->continue_lbl == LABEL_NONE) { + compiler_panic(g->c, g->cur_loc, + "KitCg: %s is not valid on a forward-only block scope", op); + return 0; + } + return 1; +} + void kit_cg_continue(KitCg* g, KitCgScope scope) { ApiCgScope* s = api_scope_from_handle(g, scope, 0, "KitCg: continue"); if (!s) return; + if (!api_require_loop_scope(g, s, "continue")) return; api_local_const_control_boundary(g); g->target->jump(g->target, s->continue_lbl); } @@ -653,6 +667,7 @@ void kit_cg_continue_true(KitCg* g, KitCgScope scope) { if (!g || scope == 0) return; s = api_scope_from_handle(g, scope, 0, "KitCg: continue_true"); if (!s) return; + if (!api_require_loop_scope(g, s, "continue_true")) return; v = api_pop(g); api_branch_if(g, &v, 1, s->continue_lbl); } @@ -663,6 +678,7 @@ void kit_cg_continue_false(KitCg* g, KitCgScope scope) { if (!g || scope == 0) return; s = api_scope_from_handle(g, scope, 0, "KitCg: continue_false"); if (!s) return; + if (!api_require_loop_scope(g, s, "continue_false")) return; v = api_pop(g); api_branch_if(g, &v, 0, s->continue_lbl); } diff --git a/test/api/cg_control_test.c b/test/api/cg_control_test.c @@ -0,0 +1,347 @@ +/* cg_control_test — drives the conditional structured control-flow ops + * (break_true / break_false / continue_true / continue_false) through the + * public KitCg API. break_true / continue_true / continue_false have zero + * callers in any frontend (toy uses only break / break_false / continue), so + * like the unordered FP compares they are reachable *only* through this entry + * point — this is the guard against an advertise-but-ignore gap in the + * structured-control surface. + * + * Each variant builds the same loop, `int f(int n) { return 0+1+..+(n-1); }`, + * spelled with a different conditional break/continue, and: + * + * Execution — captures each into the in-process interpreter (target- + * independent IR) and asserts the sum for a spread of n. + * + * Emission — builds each for aarch64 / x86-64 / riscv64 at -O0 and -O1 and + * finalizes; a backend that mishandles the lowered branches panics, which + * fails the test. + * + * Rejection — confirms `continue` on a forward-only block scope (which has no + * loop header) is rejected with a clean diagnostic rather than emitting a + * jump to a nonexistent label. + * + * Run by: make test-cg-api + */ + +#include <kit/cg.h> +#include <kit/core.h> +#include <kit/frontend.h> +#include <kit/interp.h> +#include <kit/object.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +#include "lib/kit_unit.h" + +static KitUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) + +typedef enum { + V_BREAK_TRUE, + V_BREAK_FALSE, + V_CONTINUE_TRUE, + V_CONTINUE_FALSE +} Variant; + +typedef struct { + Variant variant; + const char* name; +} VariantDesc; + +static const VariantDesc VARIANTS[] = { + {V_BREAK_TRUE, "break_true"}, + {V_BREAK_FALSE, "break_false"}, + {V_CONTINUE_TRUE, "continue_true"}, + {V_CONTINUE_FALSE, "continue_false"}, +}; +enum { NVAR = (int)(sizeof VARIANTS / sizeof VARIANTS[0]) }; + +/* Reference: sum 0 + 1 + ... + (n-1), with n clamped at 0. */ +static int64_t loop_expected(int64_t n) { + int64_t acc = 0, i; + for (i = 0; i < n; ++i) acc += i; + return acc; +} + +/* Build `int <name>(int n)` computing sum(0..n-1) via `variant`. + * + * break_* : cond-at-top loop — test, conditionally exit, else body, continue. + * continue_*: cond-at-bottom loop — body, test, conditionally re-loop. + * All four are equivalent; they exercise the four conditional ops plus plain + * `continue` (in the break_* shapes). */ +static void build_loop_fn(KitCompiler* c, KitCg* cg, const char* name, + Variant variant) { + KitCgBuiltinTypes bi = kit_cg_builtin_types(c); + KitCgTypeId i32 = bi.id[KIT_CG_BUILTIN_I32]; + KitCgFuncParam param; + KitCgFuncResult result; + KitCgFuncSig sig; + KitCgDecl decl; + KitCgSym sym; + KitCgLocalAttrs la; + KitCgLocal n_param, i_local, acc_local; + KitCgMemAccess ma; + KitCgScope scope; + + memset(&param, 0, sizeof param); + param.type = i32; + memset(&result, 0, sizeof result); + result.type = i32; + memset(&sig, 0, sizeof sig); + sig.result = result; + sig.params = &param; + sig.nparams = 1; + sig.call_conv = KIT_CG_CC_TARGET_C; + + memset(&decl, 0, sizeof decl); + decl.kind = KIT_CG_DECL_FUNC; + decl.linkage_name = kit_sym_intern(c, kit_slice_cstr(name)); + decl.display_name = decl.linkage_name; + decl.type = kit_cg_type_func(c, sig); + decl.sym.bind = KIT_SB_GLOBAL; + decl.sym.visibility = KIT_CG_VIS_DEFAULT; + sym = kit_cg_decl(cg, decl); + EXPECT(sym != KIT_CG_SYM_NONE, "%s: decl failed", name); + + kit_cg_func_begin(cg, sym); + memset(&la, 0, sizeof la); + n_param = kit_cg_param(cg, 0, i32, la); + i_local = kit_cg_local(cg, i32, la); + acc_local = kit_cg_local(cg, i32, la); + + memset(&ma, 0, sizeof ma); + ma.type = i32; + ma.align = kit_cg_type_align(c, i32); + + /* i = 0; acc = 0; */ + kit_cg_push_int(cg, 0, i32); + kit_cg_local_write(cg, i_local, ma); + kit_cg_push_int(cg, 0, i32); + kit_cg_local_write(cg, acc_local, ma); + + scope = kit_cg_scope_begin(cg, KIT_CG_TYPE_NONE); + + if (variant == V_BREAK_TRUE || variant == V_BREAK_FALSE) { + /* test at top: break out when i has reached n */ + kit_cg_local_read(cg, i_local, ma); + kit_cg_local_read(cg, n_param, ma); + if (variant == V_BREAK_TRUE) { + kit_cg_int_cmp(cg, KIT_CG_INT_GE_S); /* i >= n */ + kit_cg_break_true(cg, scope); /* exit when true */ + } else { + kit_cg_int_cmp(cg, KIT_CG_INT_LT_S); /* i < n */ + kit_cg_break_false(cg, scope); /* exit when false (i >= n) */ + } + /* acc += i; i += 1; */ + kit_cg_local_read(cg, acc_local, ma); + kit_cg_local_read(cg, i_local, ma); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, acc_local, ma); + kit_cg_local_read(cg, i_local, ma); + kit_cg_push_int(cg, 1, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, i_local, ma); + kit_cg_continue(cg, scope); + } else { + /* body first, test at bottom: re-loop while i < n */ + kit_cg_local_read(cg, acc_local, ma); + kit_cg_local_read(cg, i_local, ma); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, acc_local, ma); + kit_cg_local_read(cg, i_local, ma); + kit_cg_push_int(cg, 1, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, i_local, ma); + kit_cg_local_read(cg, i_local, ma); + kit_cg_local_read(cg, n_param, ma); + if (variant == V_CONTINUE_TRUE) { + kit_cg_int_cmp(cg, KIT_CG_INT_LT_S); /* i < n */ + kit_cg_continue_true(cg, scope); /* re-loop while true */ + } else { + kit_cg_int_cmp(cg, KIT_CG_INT_GE_S); /* i >= n */ + kit_cg_continue_false(cg, scope); /* re-loop while false (i < n) */ + } + } + + kit_cg_scope_end(cg, scope); + kit_cg_local_read(cg, acc_local, ma); + kit_cg_ret(cg); + kit_cg_func_end(cg); +} + +/* ---- Execution coverage (in-process interpreter) -------------------- */ + +static const int64_t NS[] = {0, 1, 2, 5, 10}; +enum { NNS = (int)(sizeof NS / sizeof NS[0]) }; + +static void run_exec(void) { + KitTargetSpec tgt = + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF); + KitCompiler* c = NULL; + KitInterpProgram* pp; + KitObjBuilder* ob = NULL; + KitCg* cg = NULL; + KitCodeOptions opts; + int i, j; + char nm[32]; + + if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { + EXPECT(0, "exec: compiler_new failed"); + return; + } + pp = kit_interp_program_new(c); + EXPECT(pp != NULL, "exec: interp_program_new failed"); + kit_interp_program_attach(pp, c); + + EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "exec: obj_new"); + EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "exec: cg_new"); + memset(&opts, 0, sizeof opts); + opts.opt_level = 1; /* interp capture requires the optimizer pass */ + kit_cg_begin(cg, ob, &opts); + + for (i = 0; i < NVAR; ++i) { + snprintf(nm, sizeof nm, "loop_%s", VARIANTS[i].name); + build_loop_fn(c, cg, nm, VARIANTS[i].variant); + } + EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "exec: finish"); + EXPECT(kit_cg_detach(cg) == KIT_OK, "exec: detach"); + + for (i = 0; i < NVAR; ++i) { + KitInterpFunc* fn; + snprintf(nm, sizeof nm, "loop_%s", VARIANTS[i].name); + fn = kit_interp_lookup(pp, kit_slice_cstr(nm)); + EXPECT(fn != NULL, "exec: %s not captured", VARIANTS[i].name); + if (!fn) continue; + for (j = 0; j < NNS; ++j) { + uint64_t args[1] = {(uint64_t)NS[j]}; + int64_t ret = -1; + int64_t want = loop_expected(NS[j]); + KitInterpStatus s = kit_interp_call_args(pp, fn, args, 1, &ret); + EXPECT(s == KIT_INTERP_DONE && ret == want, + "%s(%lld): want %lld got %lld (status %d)", VARIANTS[i].name, + (long long)NS[j], (long long)want, (long long)ret, (int)s); + } + } + + kit_cg_free(cg); + kit_obj_builder_free(ob); + kit_interp_program_free(pp); + kit_compiler_free(c); +} + +/* ---- Emission coverage (every native backend, no execution) --------- */ + +static void run_emit(KitArchKind arch, KitOSKind os, KitObjFmt fmt, + const char* tag, int opt_level) { + KitTargetSpec tgt = kit_unit_target(arch, os, fmt); + KitCompiler* c = NULL; + KitObjBuilder* ob = NULL; + KitCg* cg = NULL; + KitCodeOptions opts; + int i; + char nm[48]; + + if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { + EXPECT(0, "%s/O%d: compiler_new failed", tag, opt_level); + return; + } + EXPECT(kit_obj_builder_new(c, &ob) == KIT_OK && ob, "%s: obj_new", tag); + EXPECT(kit_cg_new(c, &cg) == KIT_OK && cg, "%s: cg_new", tag); + memset(&opts, 0, sizeof opts); + opts.opt_level = opt_level; + kit_cg_begin(cg, ob, &opts); + + for (i = 0; i < NVAR; ++i) { + snprintf(nm, sizeof nm, "emit_%s_o%d_%s", tag, opt_level, VARIANTS[i].name); + build_loop_fn(c, cg, nm, VARIANTS[i].variant); + } + EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s/O%d: finish failed", tag, + opt_level); + EXPECT(kit_cg_detach(cg) == KIT_OK, "%s/O%d: detach failed", tag, opt_level); + + kit_cg_free(cg); + kit_obj_builder_free(ob); + kit_compiler_free(c); +} + +/* ---- Rejection: continue on a forward-only block scope -------------- */ + +typedef struct { + KitObjBuilder* ob; + KitCg* cg; +} RejectCtx; + +static KitStatus reject_body(KitCompiler* c, void* user) { + RejectCtx* r = (RejectCtx*)user; + KitCgBuiltinTypes bi = kit_cg_builtin_types(c); + KitCgFuncSig sig; + KitCgDecl decl; + KitCgSym sym; + KitCgScope blk; + (void)bi; + memset(&sig, 0, sizeof sig); + sig.call_conv = KIT_CG_CC_TARGET_C; /* void() */ + memset(&decl, 0, sizeof decl); + decl.kind = KIT_CG_DECL_FUNC; + decl.linkage_name = kit_sym_intern(c, kit_slice_cstr("reject_fn")); + decl.display_name = decl.linkage_name; + decl.type = kit_cg_type_func(c, sig); + decl.sym.bind = KIT_SB_GLOBAL; + decl.sym.visibility = KIT_CG_VIS_DEFAULT; + sym = kit_cg_decl(r->cg, decl); + kit_cg_func_begin(r->cg, sym); + blk = kit_cg_block_begin(r->cg, KIT_CG_TYPE_NONE); + kit_cg_continue(r->cg, blk); /* <- must panic: blocks have no loop header */ + kit_cg_scope_end(r->cg, blk); + kit_cg_ret(r->cg); + kit_cg_func_end(r->cg); + return KIT_OK; +} + +static void run_reject(void) { + KitTargetSpec tgt = + kit_unit_target(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF); + KitCompiler* c = NULL; + RejectCtx r; + KitCodeOptions opts; + KitStatus st; + memset(&r, 0, sizeof r); + if (kit_unit_compiler_new(&g_u, tgt, &c) != KIT_OK || !c) { + EXPECT(0, "reject: compiler_new failed"); + return; + } + EXPECT(kit_obj_builder_new(c, &r.ob) == KIT_OK && r.ob, "reject: obj_new"); + EXPECT(kit_cg_new(c, &r.cg) == KIT_OK && r.cg, "reject: cg_new"); + memset(&opts, 0, sizeof opts); + kit_cg_begin(r.cg, r.ob, &opts); + + g_u.suppress_fatal = 1; + st = kit_frontend_run(c, reject_body, &r); + g_u.suppress_fatal = 0; + + EXPECT(st == KIT_ERR, "continue on block scope should be rejected"); + EXPECT(strstr(g_u.last_diag, "forward-only block scope") != NULL, + "rejection diagnostic should name the block scope (got: %s)", + g_u.last_diag); + + kit_compiler_free(c); +} + +int main(void) { + int opt; + kit_unit_init(&g_u); + + run_exec(); + + for (opt = 0; opt <= 1; ++opt) { + run_emit(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF, "aa64", opt); + run_emit(KIT_ARCH_X86_64, KIT_OS_LINUX, KIT_OBJ_ELF, "x64", opt); + run_emit(KIT_ARCH_RV64, KIT_OS_LINUX, KIT_OBJ_ELF, "rv64", opt); + } + + run_reject(); + + kit_unit_summary(&g_u, "cg_control_test"); + return kit_unit_status(&g_u); +}