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:
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(¶m, 0, sizeof param);
+ param.type = i32;
+ memset(&result, 0, sizeof result);
+ result.type = i32;
+ memset(&sig, 0, sizeof sig);
+ sig.result = result;
+ sig.params = ¶m;
+ 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);
+}