kit

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

commit 5194cb9d9957c73acaf9b0aec66baee5efa53e7c
parent 2eb08459e47cccd6938a04bb08f2f09cc68e7b09
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 20 May 2026 19:58:22 -0700

test: broaden switch corpus and seed jump-table red shape test

Lays the test groundwork before the O0/O1 switch-to-jump-table
lowering. Behavior is unchanged today: every new case passes under
the chain lowering and must keep passing once a table emitter lands.

Frontend (toy):
  125_switch_dense_boundaries — dense 10..20 with default, exercises
    vmin/vmax/below/above/interior-gap selectors a table lowering
    must bounds-check correctly.
  126_switch_sparse_chain — sparse {1,50,1000}, pins chain fallback
    so a tuning regression that relaxes density thresholds is caught.
  127_switch_forced_jump_table — forced .jump_table over a dense
    16-case no-default block; synthesized default arm must be reachable.

Frontend (C):
  6_8_29_switch_signed_negative — signed dense -3..3, exercises
    (sel - vmin) compute when vmin is negative.
  6_8_30_switch_sparse_chain — C-side equivalent of the toy sparse case.

API:
  test/api/cg_switch_test.c — drives cfree_cg_switch directly with
  hand-built descriptors across all hint variants, density shapes,
  signed/unsigned, default/no-default, single, and empty (default-only).
  Wired into test-cg-api alongside cg_type_test / abi_classify_test.

Red shape test:
  127_switch_forced_jump_table.objdump — asserts that the forced-hint
  case produces a __TEXT,__const section and a .Lcfree_jt symbol. Fails
  today (no table emitted yet); turns green when the emitter lands.

Diffstat:
Atest/api/cg_switch_test.c | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/parse/cases/6_8_29_switch_signed_negative.c | 26++++++++++++++++++++++++++
Atest/parse/cases/6_8_29_switch_signed_negative.expected | 1+
Atest/parse/cases/6_8_30_switch_sparse_chain.c | 22++++++++++++++++++++++
Atest/parse/cases/6_8_30_switch_sparse_chain.expected | 1+
Mtest/test.mk | 8+++++++-
Atest/toy/cases/125_switch_dense_boundaries.expected | 1+
Atest/toy/cases/125_switch_dense_boundaries.toy | 35+++++++++++++++++++++++++++++++++++
Atest/toy/cases/126_switch_sparse_chain.expected | 1+
Atest/toy/cases/126_switch_sparse_chain.toy | 26++++++++++++++++++++++++++
Atest/toy/cases/127_switch_forced_jump_table.expected | 1+
Atest/toy/cases/127_switch_forced_jump_table.objdump | 2++
Atest/toy/cases/127_switch_forced_jump_table.toy | 40++++++++++++++++++++++++++++++++++++++++
13 files changed, 478 insertions(+), 1 deletion(-)

diff --git a/test/api/cg_switch_test.c b/test/api/cg_switch_test.c @@ -0,0 +1,315 @@ +/* cg_switch_test — drives cfree_cg_switch with hand-built descriptors + * covering the shapes the lowering must handle. Structural: asserts that + * each shape lowers without panic and produces a defined function + * symbol. Behavior under different lowering strategies (chain vs table) + * is covered end-to-end by test/toy and test/parse — this file targets + * the API surface itself. + * + * Shapes covered: + * - hint = TARGET_DEFAULT / BRANCH_CHAIN / JUMP_TABLE + * - dense range, sparse range, signed dense range straddling zero + * - default present / default absent + * - single case, empty cases (default-only) + * - both opt levels (0 and 1) + * + * Run by: make test-cg-api + */ + +#include <cfree/cg.h> +#include <cfree/core.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "core/core.h" +#include "obj/obj.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; + +#define EXPECT(cond, ...) \ + do { \ + if (!(cond)) { \ + ++g_fail; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +/* ---- Helpers --------------------------------------------------------- */ + +typedef struct SwitchShape { + const char* name; + CfreeCgTypeId selector_type; + /* values[] interpreted by selector_type; arm i is reached when the + * selector equals values[i]. Each arm returns (uint32_t)results[i]. */ + const int64_t* values; + const int32_t* results; + uint32_t ncases; + int has_default; + int32_t default_result; /* used if has_default; otherwise arm falls past */ + CfreeCgSwitchHint hint; +} SwitchShape; + +/* Build: + * int32_t f(<selector_type> x) { + * switch (x) { + * case values[0]: return results[0]; + * ... + * default: return default_result; // if has_default + * } + * return -1; // fall-through past switch + * } + */ +static void build_switch_fn(CfreeCompiler* c, CfreeCgTypeId i32_ty, + const SwitchShape* sh, int opt_level) { + char fn_name[96]; + CfreeCodeOptions opts; + CfreeObjBuilder* ob; + CfreeCg* cg; + CfreeCgFuncParam param_desc; + CfreeCgFuncSig sig; + CfreeCgDecl decl; + CfreeCgSym sym; + CfreeCgLocalAttrs attrs; + CfreeCgLocal param; + CfreeCgLabel default_lbl; + CfreeCgLabel end_lbl; + CfreeCgLabel* case_lbls = NULL; + CfreeCgSwitchCase* cases = NULL; + CfreeCgSwitch sw; + uint32_t i; + + memset(&opts, 0, sizeof opts); + opts.opt_level = opt_level; + ob = (CfreeObjBuilder*)obj_new((Compiler*)c); + EXPECT(ob != NULL, "[%s/O%d] obj_new failed", sh->name, opt_level); + if (!ob) return; + cg = NULL; + (void)cfree_cg_new(c, ob, &opts, &cg); + EXPECT(cg != NULL, "[%s/O%d] cg_new failed", sh->name, opt_level); + if (!cg) { + obj_free((ObjBuilder*)ob); + return; + } + + memset(&param_desc, 0, sizeof param_desc); + param_desc.type = sh->selector_type; + memset(&sig, 0, sizeof sig); + sig.ret = i32_ty; + sig.params = &param_desc; + sig.nparams = 1; + sig.call_conv = CFREE_CG_CC_TARGET_C; + + snprintf(fn_name, sizeof fn_name, "switch_%s_o%d", sh->name, opt_level); + memset(&decl, 0, sizeof decl); + decl.kind = CFREE_CG_DECL_FUNC; + decl.linkage_name = cfree_sym_intern(c, fn_name); + decl.display_name = decl.linkage_name; + decl.type = cfree_cg_type_func(c, sig); + decl.sym.bind = CFREE_SB_GLOBAL; + decl.sym.visibility = CFREE_CG_VIS_DEFAULT; + sym = cfree_cg_decl(cg, decl); + EXPECT(sym != CFREE_CG_SYM_NONE, "[%s/O%d] decl failed", sh->name, + opt_level); + + cfree_cg_func_begin(cg, sym); + + memset(&attrs, 0, sizeof attrs); + attrs.name = cfree_sym_intern(c, "x"); + param = cfree_cg_param(cg, 0, sh->selector_type, attrs); + EXPECT(param != CFREE_CG_LOCAL_NONE, "[%s/O%d] param failed", sh->name, + opt_level); + + end_lbl = cfree_cg_label_new(cg); + default_lbl = sh->has_default ? cfree_cg_label_new(cg) : end_lbl; + if (sh->ncases) { + case_lbls = (CfreeCgLabel*)malloc(sh->ncases * sizeof *case_lbls); + cases = (CfreeCgSwitchCase*)malloc(sh->ncases * sizeof *cases); + EXPECT(case_lbls && cases, "[%s/O%d] alloc failed", sh->name, opt_level); + for (i = 0; i < sh->ncases; ++i) { + case_lbls[i] = cfree_cg_label_new(cg); + cases[i].value = (uint64_t)sh->values[i]; + cases[i].label = case_lbls[i]; + } + } + + /* Push selector, dispatch. */ + cfree_cg_push_local(cg, param); + cfree_cg_load(cg, (CfreeCgMemAccess){.type = sh->selector_type, + .align = cfree_cg_type_align( + c, sh->selector_type)}); + memset(&sw, 0, sizeof sw); + sw.selector_type = sh->selector_type; + sw.default_label = default_lbl; + sw.cases = cases; + sw.ncases = sh->ncases; + sw.hint = sh->hint; + cfree_cg_switch(cg, sw); + + /* Each arm: push result, jump to end_lbl. The cg API always materializes + * the return through `cfree_cg_ret` consuming the stack top; jump to a + * single ret epilogue at end_lbl. */ + for (i = 0; i < sh->ncases; ++i) { + cfree_cg_label_place(cg, case_lbls[i]); + cfree_cg_push_int(cg, (uint64_t)(int64_t)sh->results[i], i32_ty); + cfree_cg_ret(cg); + } + if (sh->has_default) { + cfree_cg_label_place(cg, default_lbl); + cfree_cg_push_int(cg, (uint64_t)(int64_t)sh->default_result, i32_ty); + cfree_cg_ret(cg); + } + cfree_cg_label_place(cg, end_lbl); + cfree_cg_push_int(cg, (uint64_t)(int64_t)-1, i32_ty); + cfree_cg_ret(cg); + cfree_cg_func_end(cg); + + EXPECT(g_fail == 0 || g_fail > 0, "shape-build sentinel"); /* no-op */ + + free(case_lbls); + free(cases); + cfree_cg_free(cg); + obj_free((ObjBuilder*)ob); +} + +/* ---- Shapes ---------------------------------------------------------- */ + +static void run_all_shapes(CfreeCompiler* c, CfreeCgTypeId i32_ty, + CfreeCgTypeId i64_ty, int opt_level) { + /* Dense unsigned range 10..15 + default. */ + static const int64_t dense_vals[] = {10, 11, 12, 13, 14, 15}; + static const int32_t dense_res[] = {100, 101, 102, 103, 104, 105}; + + /* Sparse: {1, 50, 1000}. */ + static const int64_t sparse_vals[] = {1, 50, 1000}; + static const int32_t sparse_res[] = {11, 22, 33}; + + /* Signed dense around zero: -3..3. */ + static const int64_t signed_vals[] = {-3, -2, -1, 0, 1, 2, 3}; + static const int32_t signed_res[] = {30, 31, 32, 33, 34, 35, 36}; + + /* Singleton. */ + static const int64_t single_vals[] = {42}; + static const int32_t single_res[] = {7}; + + SwitchShape shapes[] = { + /* TARGET_DEFAULT × shape × default-present/absent. */ + {"dense_def_target", i32_ty, dense_vals, dense_res, 6, 1, 999, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + {"dense_nodef_target", i32_ty, dense_vals, dense_res, 6, 0, 0, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + {"sparse_def_target", i32_ty, sparse_vals, sparse_res, 3, 1, 99, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + {"signed_def_target", i32_ty, signed_vals, signed_res, 7, 1, 999, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + {"signed64_def_target", i64_ty, signed_vals, signed_res, 7, 1, 999, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + + /* Forced hint variants. JUMP_TABLE is advisory; lowering may + * accept it or fall back to chain — both must produce a valid + * function. */ + {"dense_def_jump_table", i32_ty, dense_vals, dense_res, 6, 1, 999, + CFREE_CG_SWITCH_JUMP_TABLE}, + {"dense_nodef_jump_table", i32_ty, dense_vals, dense_res, 6, 0, 0, + CFREE_CG_SWITCH_JUMP_TABLE}, + {"sparse_def_jump_table", i32_ty, sparse_vals, sparse_res, 3, 1, 99, + CFREE_CG_SWITCH_JUMP_TABLE}, + {"signed_def_jump_table", i32_ty, signed_vals, signed_res, 7, 1, 999, + CFREE_CG_SWITCH_JUMP_TABLE}, + + {"dense_def_branch_chain", i32_ty, dense_vals, dense_res, 6, 1, 999, + CFREE_CG_SWITCH_BRANCH_CHAIN}, + {"sparse_def_branch_chain", i32_ty, sparse_vals, sparse_res, 3, 1, 99, + CFREE_CG_SWITCH_BRANCH_CHAIN}, + + /* Singleton: minimum case count. Both hints. */ + {"single_def_target", i32_ty, single_vals, single_res, 1, 1, 99, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + {"single_def_jump_table", i32_ty, single_vals, single_res, 1, 1, 99, + CFREE_CG_SWITCH_JUMP_TABLE}, + + /* Empty (default-only). cfree_cg_switch on an empty case array is + * tested for clean acceptance — frontends emit this from + * `switch (x) { default: ...; }`. */ + {"empty_def_target", i32_ty, NULL, NULL, 0, 1, 7, + CFREE_CG_SWITCH_TARGET_DEFAULT}, + }; + + size_t n = sizeof shapes / sizeof shapes[0]; + size_t i; + for (i = 0; i < n; ++i) { + build_switch_fn(c, i32_ty, &shapes[i], opt_level); + } +} + +/* ---- Entry ----------------------------------------------------------- */ + +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; + + if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) { + fprintf(stderr, "compiler_new failed\n"); + return 2; + } + + bi = cfree_cg_builtin_types(c); + i32_ty = bi.id[CFREE_CG_BUILTIN_I32]; + i64_ty = bi.id[CFREE_CG_BUILTIN_I64]; + EXPECT(i32_ty != CFREE_CG_TYPE_NONE, "i32 builtin id is none"); + EXPECT(i64_ty != CFREE_CG_TYPE_NONE, "i64 builtin id is none"); + + run_all_shapes(c, i32_ty, i64_ty, /*opt_level=*/0); + run_all_shapes(c, i32_ty, i64_ty, /*opt_level=*/1); + + cfree_compiler_free(c); + return g_fail ? 1 : 0; +} diff --git a/test/parse/cases/6_8_29_switch_signed_negative.c b/test/parse/cases/6_8_29_switch_signed_negative.c @@ -0,0 +1,26 @@ +/* Signed selector with negative case values. A dense range around zero + * (-3..3) exercises the (sel - vmin) compute in a jump-table lowering + * where vmin is negative; the bounds check must use unsigned compare or + * sign-extension correctly. */ +static int pick(int x) { + switch (x) { + case -3: return 30; + case -2: return 31; + case -1: return 32; + case 0: return 33; + case 1: return 34; + case 2: return 35; + case 3: return 36; + default: return 99; + } +} + +int test_main(void) { + int s = 0; + s += pick(-3); /* vmin -> 30 */ + s += pick(3); /* vmax -> 36 */ + s += pick(0); /* zero -> 33 */ + s += pick(-4); /* below vmin -> 99 */ + s += pick(4); /* above vmax -> 99 */ + return s - 197; /* 30+36+33+99+99 - 197 = 100 */ +} diff --git a/test/parse/cases/6_8_29_switch_signed_negative.expected b/test/parse/cases/6_8_29_switch_signed_negative.expected @@ -0,0 +1 @@ +100 diff --git a/test/parse/cases/6_8_30_switch_sparse_chain.c b/test/parse/cases/6_8_30_switch_sparse_chain.c @@ -0,0 +1,22 @@ +/* Sparse case values: three cases spread over a 1000-element range. The + * density-driven jump-table policy must reject this and keep a chain. The + * test pins correct behavior independent of which strategy was picked. */ +static int pick(int x) { + switch (x) { + case 1: return 11; + case 50: return 22; + case 1000: return 33; + default: return 99; + } +} + +int test_main(void) { + int s = 0; + s += pick(1); + s += pick(50); + s += pick(1000); + s += pick(2); + s += pick(999); + s += pick(1001); + return s - 263; /* 11+22+33+99+99+99 - 263 = 100 */ +} diff --git a/test/parse/cases/6_8_30_switch_sparse_chain.expected b/test/parse/cases/6_8_30_switch_sparse_chain.expected @@ -0,0 +1 @@ +100 diff --git a/test/test.mk b/test/test.mk @@ -124,16 +124,22 @@ $(AA64_ISA_TEST_BIN): test/arch/aa64_isa_test.c $(LIB_AR) $(CC) $(DRIVER_CFLAGS) -Isrc test/arch/aa64_isa_test.c $(LIB_AR) -o $@ CG_API_TEST_BIN = build/test/cg_api_test +CG_SWITCH_TEST_BIN = build/test/cg_switch_test ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test -test-cg-api: $(CG_API_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) +test-cg-api: $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) $(CG_API_TEST_BIN) + $(CG_SWITCH_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) $(CG_API_TEST_BIN): test/api/cg_type_test.c $(LIB_AR) @mkdir -p $(dir $@) $(CC) $(DRIVER_CFLAGS) -Isrc 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) $(DRIVER_CFLAGS) -Isrc test/api/cg_switch_test.c $(LIB_AR) -o $@ + $(ABI_CLASSIFY_TEST_BIN): test/api/abi_classify_test.c $(LIB_AR) @mkdir -p $(dir $@) $(CC) $(DRIVER_CFLAGS) -Isrc test/api/abi_classify_test.c $(LIB_AR) -o $@ diff --git a/test/toy/cases/125_switch_dense_boundaries.expected b/test/toy/cases/125_switch_dense_boundaries.expected @@ -0,0 +1 @@ +312 diff --git a/test/toy/cases/125_switch_dense_boundaries.toy b/test/toy/cases/125_switch_dense_boundaries.toy @@ -0,0 +1,35 @@ +// Dense case range with default; exercises the boundary selectors that a +// jump-table lowering must bounds-check correctly: vmin, vmax, vmin-1, +// vmax+1, an interior gap. Sum encodes which arm fired for each call. +// At O0 today this lowers to a chain; at O1 (once density-based promotion +// lands) it lowers to a table. Behavior must match. + +fn pick(x: i64): i64 { + return switch x { + 10 { 100 } + 11 { 101 } + 12 { 102 } + 13 { 103 } + 14 { 104 } + 15 { 105 } + // deliberate gap at 16 + 17 { 107 } + 18 { 108 } + 19 { 109 } + 20 { 110 } + default { 999 } + }; +} + +fn __user_main(): i64 { + var s: i64 = 0; + s = s + pick(10); // vmin -> 100 + s = s + pick(20); // vmax -> 110 + s = s + pick(9); // vmin - 1 -> 999 + s = s + pick(21); // vmax + 1 -> 999 + s = s + pick(16); // interior gap -> 999 + s = s + pick(15); // mid hit -> 105 + return s - 3000; // 100+110+999+999+999+105 - 3000 = 312 +} + +fn main(): i32 { return __user_main() as i32; } diff --git a/test/toy/cases/126_switch_sparse_chain.expected b/test/toy/cases/126_switch_sparse_chain.expected @@ -0,0 +1 @@ +363 diff --git a/test/toy/cases/126_switch_sparse_chain.toy b/test/toy/cases/126_switch_sparse_chain.toy @@ -0,0 +1,26 @@ +// Sparse case values: vmax - vmin = 999 with only 3 cases. Density +// thresholds must reject the jump table at every opt level and stay on +// the chain. Pins the no-promotion behavior so a tuning regression that +// relaxes thresholds too far is caught. + +fn pick(x: i64): i64 { + return switch x { + 1 { 11 } + 50 { 22 } + 1000 { 33 } + default { 99 } + }; +} + +fn __user_main(): i64 { + var s: i64 = 0; + s = s + pick(1); // 11 + s = s + pick(50); // 22 + s = s + pick(1000); // 33 + s = s + pick(2); // miss -> 99 + s = s + pick(999); // miss -> 99 + s = s + pick(1001); // miss -> 99 + return s; // 11+22+33+99+99+99 = 363 +} + +fn main(): i32 { return __user_main() as i32; } diff --git a/test/toy/cases/127_switch_forced_jump_table.expected b/test/toy/cases/127_switch_forced_jump_table.expected @@ -0,0 +1 @@ +3019 diff --git a/test/toy/cases/127_switch_forced_jump_table.objdump b/test/toy/cases/127_switch_forced_jump_table.objdump @@ -0,0 +1,2 @@ +__TEXT,__const +.Lcfree_jt diff --git a/test/toy/cases/127_switch_forced_jump_table.toy b/test/toy/cases/127_switch_forced_jump_table.toy @@ -0,0 +1,40 @@ +// Forced .jump_table over a dense 16-case range with no default. +// Stresses the table-emission path: when no default is given, every +// in-range value must reach a case, and out-of-range values must fall +// through past the switch (the synthesized default). + +fn pick(x: i64): i64 { + var r: i64 = -1; + switch @[.jump_table] x { + 0 { r = 1000; } + 1 { r = 1001; } + 2 { r = 1002; } + 3 { r = 1003; } + 4 { r = 1004; } + 5 { r = 1005; } + 6 { r = 1006; } + 7 { r = 1007; } + 8 { r = 1008; } + 9 { r = 1009; } + 10 { r = 1010; } + 11 { r = 1011; } + 12 { r = 1012; } + 13 { r = 1013; } + 14 { r = 1014; } + 15 { r = 1015; } + } + return r; +} + +fn __user_main(): i64 { + var s: i64 = 0; + s = s + pick(0); // vmin in-range -> 1000 + s = s + pick(15); // vmax in-range -> 1015 + s = s + pick(7); // interior -> 1007 + s = s + pick(-1); // below vmin -> -1 (synthesized default) + s = s + pick(16); // above vmax -> -1 (synthesized default) + s = s + pick(100); // far out -> -1 (synthesized default) + return s; // 1000+1015+1007 - 3 = 3019 +} + +fn main(): i32 { return __user_main() as i32; }