kit

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

commit 1ad968848968d8f5138df151b65883bb9d9b0005
parent 9d10a93d9c5f01d50072556ba9034a96dae14a27
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 20 May 2026 16:33:33 -0700

cg,c_target: wire cfree_cg_switch through a target hook; real C `switch`

The CG layer already had a value-keyed switch primitive (`cfree_cg_switch`)
but native arches lowered it inline to a cmp_branch chain — there was no
seam for a target to do better. Add an optional `CGTarget.switch_branch`
vtable method: when set, CG hands the target the parallel (label[],
value[]) arrays and lets it pick the lowering; when NULL, CG runs the
existing cmp-chain default. Behavior on native arches is unchanged.

The C-source target overrides `switch_branch` to emit a real C
`switch (val) { case V: goto L_V; …; default: goto L_def; }`. The host
gcc/clang then picks jump-table vs cmp-tree itself, so dense and sparse
case sets both lower well without cfree having to second-guess the
host. For `case` constants matching an int32_t selector the bare
integer literal is emitted (no `(int32_t)` cast) since it's already the
right type; wider selectors keep the cast.

Frontends migrated to `cfree_cg_switch` for their switch-shaped dispatch:
  - lang/c/parse/parse_stmt.c: `parse_switch_stmt` now packs cases and
    calls `cfree_cg_switch` once at the dispatch point instead of
    emitting a manual cmp chain.
  - lang/toy/parser.c: both `toy_parse_switch_initializer` (expression
    switch) and `toy_parse_switch_stmt_named` (statement switch) use
    `cfree_cg_switch`. The `@[.branch_chain]` / `@[.jump_table]`
    strategy hints now flow through to the new `CfreeCgSwitchHint`.
  - lang/wasm/cg.c: `BR_TABLE` already called `cfree_cg_switch`; its
    hint flipped from BRANCH_CHAIN to JUMP_TABLE since wasm br_table is
    dense by construction (case values 0..N-1).

One scope-ordering bug fixed in toy_parse_switch_stmt_named: the
`jump(dispatch_label)` was emitted before `cfree_cg_scope_begin`, which
left the scope's entry block unreachable in opt's CFG. Opt pruned it,
`scope_map[scope_id]` stayed 0, and aa64 scope_end panicked under -O1.
Moving scope_begin before the jump puts the entry block on a
straight-line predecessor edge and the CFG analysis behaves.

Other frontend branch sites surveyed and left alone — none are
switch-shaped (binary if/else, short-circuit logic, range/overflow
validation, linear scans by name).

Verified: parse 3584/0/0, toy 610/0/0, toy path-C 124/0/3, parse
path-C 419/0/29.

Diffstat:
Minclude/cfree/cg.h | 9+++++++--
Mlang/c/parse/parse_stmt.c | 52++++++++++++++++++++++++++++++++++++++++++----------
Mlang/toy/parser.c | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mlang/wasm/cg.c | 6+++++-
Msrc/arch/arch.h | 14++++++++++++++
Msrc/arch/c_target/emit.c | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/c_target/target.c | 3+++
Msrc/cg/control.c | 43++++++++++++++++++++++++++++++++++++-------
8 files changed, 256 insertions(+), 43 deletions(-)

diff --git a/include/cfree/cg.h b/include/cfree/cg.h @@ -475,8 +475,13 @@ typedef struct CfreeCgSwitch { CfreeCgSwitchHint hint; } CfreeCgSwitch; -/* Pops an integer selector and branches to the matching case or default. The - * target may ignore hint when another lowering is required for correctness. */ +/* Pops an integer selector and branches to the case whose value matches + * it, or to default_label if none does. Mirrors the shape of C's + * `switch (val) { case V1: ...; default: ...; }`. Frontends that want + * jump-table dispatch (wasm br_table, computed-goto-style direct + * threading) pass dense case values 0..N-1 — backends can detect the + * shape and emit a real table; the C target lets the host compiler + * decide. The hint is advisory; targets may ignore it. */ void cfree_cg_switch(CfreeCg*, CfreeCgSwitch sw); /* Pushes the address of a label in the current function. Label addresses are diff --git a/lang/c/parse/parse_stmt.c b/lang/c/parse/parse_stmt.c @@ -312,7 +312,11 @@ static void parse_switch_stmt(Parser* p) { /* Wrap the whole switch in a structured scope so the C-source target * renders `break;` as the keyword. Continue isn't applicable to * switch (C `continue` skips switches and targets the enclosing loop), - * so cur_continue is left alone. */ + * so cur_continue is left alone. The dispatch itself goes through + * `cfree_cg_switch_value`, which native arches lower to a cmp_branch + * chain (unchanged behaviour) and which the C target overrides to + * emit a real `switch (sel) { case V: goto L_V; …; default: goto + * L_def; }`. */ CfreeCgScope scope = cfree_cg_scope_begin(p->cg, CFREE_CG_TYPE_NONE); CGLabel L_dispatch = cg_label_new(p->cg); CGLabel L_end = cfree_cg_scope_break_label(p->cg, scope); @@ -359,6 +363,8 @@ static void parse_switch_stmt(Parser* p) { cg_jump(p->cg, L_end); cg_label_place(p->cg, L_dispatch); + /* Reverse cases into source order; CaseEntry list grows at the head + * during parsing so iteration here is LIFO without the flip. */ prev = NULL; head = ctx.cases; while (head) { @@ -367,15 +373,41 @@ static void parse_switch_stmt(Parser* p) { prev = head; head = nxt; } - for (it = prev; it; it = it->next) { - cg_push_local_typed(p->cg, ctx.value_slot, vty); - cg_load(p->cg); - cg_push_int(p->cg, it->value, vty); - cg_cmp(p->cg, CMP_EQ); - cg_branch_true(p->cg, it->label); - } - if (ctx.default_label) { - cg_jump(p->cg, ctx.default_label); + /* Count and pack into the value/label arrays the public API expects. */ + { + u32 ncases = 0; + for (it = prev; it; it = it->next) ncases++; + if (ncases == 0 && ctx.default_label == 0) { + /* `switch (x) {}` — no cases, no default. Nothing to dispatch. + * Fall through to scope_end which terminates the loop. */ + } else { + CfreeCgSwitchCase* cases = + ncases ? arena_array(p->pool->arena, CfreeCgSwitchCase, ncases) + : NULL; + if (ncases && !cases) perr(p, "out of memory in parse_switch_stmt"); + { + u32 i = 0; + for (it = prev; it; it = it->next) { + cases[i].value = (uint64_t)it->value; + cases[i].label = (CfreeCgLabel)it->label; + i++; + } + } + cg_push_local_typed(p->cg, ctx.value_slot, vty); + cg_load(p->cg); + { + CfreeCgSwitch sw; + memset(&sw, 0, sizeof sw); + sw.selector_type = pcg_tid(p, vty); + sw.default_label = + ctx.default_label ? (CfreeCgLabel)ctx.default_label + : (CfreeCgLabel)L_end; + sw.cases = cases; + sw.ncases = ncases; + sw.hint = CFREE_CG_SWITCH_TARGET_DEFAULT; + cfree_cg_switch(p->cg, sw); + } + } } cfree_cg_scope_end(p->cg, scope); } diff --git a/lang/toy/parser.c b/lang/toy/parser.c @@ -387,7 +387,9 @@ static int toy_parse_value_block_to_local(ToyParser* p, CfreeCgLocal slot, result_toy_type); } -static int toy_parse_switch_strategy_hint(ToyParser* p) { +static int toy_parse_switch_strategy_hint(ToyParser* p, + CfreeCgSwitchHint* hint_out) { + *hint_out = CFREE_CG_SWITCH_TARGET_DEFAULT; if (p->cur.kind != TOK_AT || toy_lexer_peek(&p->lex).kind != TOK_LBRACKET) return 1; toy_parser_advance(p); @@ -398,8 +400,11 @@ static int toy_parse_switch_strategy_hint(ToyParser* p) { while (p->cur.kind != TOK_RBRACKET && p->cur.kind != TOK_EOF) { CfreeSym name; if (!toy_parse_attr_dot_name(p, &name)) return 0; - if (!toy_sym_is(p, name, "branch_chain") && - !toy_sym_is(p, name, "jump_table")) { + if (toy_sym_is(p, name, "branch_chain")) { + *hint_out = CFREE_CG_SWITCH_BRANCH_CHAIN; + } else if (toy_sym_is(p, name, "jump_table")) { + *hint_out = CFREE_CG_SWITCH_JUMP_TABLE; + } else { toy_error(p, p->cur.loc, "unknown switch strategy"); return 0; } @@ -418,14 +423,22 @@ static int toy_parse_switch_initializer(ToyParser* p, CfreeCgLocal slot, CfreeCgTypeId selector_ty; CfreeCgLocal selector_slot; CfreeCgLabel end_label; + CfreeCgLabel dispatch_label; + CfreeCgLabel default_arm_label = CFREE_CG_LABEL_NONE; + CfreeCgLabel unreachable_label = CFREE_CG_LABEL_NONE; ToyNamedType* selector_enum; unsigned char* enum_seen = NULL; size_t enum_seen_size = 0; int saw_default = 0; size_t enum_seen_count = 0; + CfreeCgSwitchCase* cases = NULL; + size_t ncases = 0; + size_t cap_cases = 0; + CfreeCgSwitchHint hint; + CfreeCgSwitch sw; if (!toy_parser_match(p, TOK_SWITCH)) return 0; - if (!toy_parse_switch_strategy_hint(p)) return 0; + if (!toy_parse_switch_strategy_hint(p, &hint)) return 0; selector_ty = toy_parse_expr(p); if (selector_ty == CFREE_CG_TYPE_NONE) return 0; if (!toy_type_is_intlike(p, selector_ty)) { @@ -437,6 +450,9 @@ static int toy_parse_switch_initializer(ToyParser* p, CfreeCgLocal slot, cfree_cg_swap(p->cg); cfree_cg_store(p->cg, toy_mem_access(p, selector_ty)); end_label = cfree_cg_label_new(p->cg); + dispatch_label = cfree_cg_label_new(p->cg); + /* Skip the arm bodies on entry; come back through the dispatch label. */ + cfree_cg_jump(p->cg, dispatch_label); selector_enum = toy_find_named_type_by_type(p, selector_ty); if (!selector_enum || selector_enum->kind != TOY_NAMED_ENUM) selector_enum = NULL; @@ -457,17 +473,17 @@ static int toy_parse_switch_initializer(ToyParser* p, CfreeCgLocal slot, } while (p->cur.kind != TOK_RBRACE && p->cur.kind != TOK_EOF) { CfreeCgLabel arm_label = cfree_cg_label_new(p->cg); - CfreeCgLabel next_label = cfree_cg_label_new(p->cg); if (p->cur.kind == TOK_DEFAULT) { saw_default = 1; + default_arm_label = arm_label; toy_parser_advance(p); - cfree_cg_jump(p->cg, arm_label); } else { for (;;) { int64_t value; size_t i; if (!toy_parse_switch_label_value(p, selector_ty, &value)) { toy_parser_free_mem(p, enum_seen, enum_seen_size); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 0; } if (selector_enum) { @@ -481,36 +497,61 @@ static int toy_parse_switch_initializer(ToyParser* p, CfreeCgLocal slot, } } } - cfree_cg_push_local(p->cg, selector_slot); - cfree_cg_load(p->cg, toy_mem_access(p, selector_ty)); - cfree_cg_push_int(p->cg, (uint64_t)value, selector_ty); - cfree_cg_int_cmp(p->cg, CFREE_CG_INT_EQ); - cfree_cg_branch_true(p->cg, arm_label); + if (!toy_parser_reserve(p, (void**)&cases, &cap_cases, ncases + 1u, + sizeof *cases, "switch cases")) { + toy_parser_free_mem(p, enum_seen, enum_seen_size); + return 0; + } + cases[ncases].value = (uint64_t)value; + cases[ncases].label = arm_label; + ncases++; if (!toy_parser_match(p, TOK_COMMA)) break; } - cfree_cg_jump(p->cg, next_label); } cfree_cg_label_place(p->cg, arm_label); if (!toy_parse_value_block_to_local(p, slot, result_ty, result_toy_type)) { toy_parser_free_mem(p, enum_seen, enum_seen_size); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 0; } cfree_cg_jump(p->cg, end_label); - cfree_cg_label_place(p->cg, next_label); } if (!toy_parser_expect(p, TOK_RBRACE)) { toy_error(p, p->cur.loc, "expected '}' after switch expression"); toy_parser_free_mem(p, enum_seen, enum_seen_size); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 0; } if (!saw_default && (!selector_enum || enum_seen_count != selector_enum->nenum_values)) { toy_error(p, p->cur.loc, "expression switch requires default"); toy_parser_free_mem(p, enum_seen, enum_seen_size); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 0; } + cfree_cg_label_place(p->cg, dispatch_label); + cfree_cg_push_local(p->cg, selector_slot); + cfree_cg_load(p->cg, toy_mem_access(p, selector_ty)); + if (saw_default) { + sw.default_label = default_arm_label; + } else { + /* Enum-exhaustive switch with no source default: any value outside the + * covered set is undefined behavior — emit an unreachable landing pad. */ + unreachable_label = cfree_cg_label_new(p->cg); + sw.default_label = unreachable_label; + } + sw.selector_type = selector_ty; + sw.cases = cases; + sw.ncases = (uint32_t)ncases; + sw.hint = hint; + cfree_cg_switch(p->cg, sw); + if (unreachable_label != CFREE_CG_LABEL_NONE) { + cfree_cg_label_place(p->cg, unreachable_label); + cfree_cg_unreachable(p->cg); + } cfree_cg_label_place(p->cg, end_label); toy_parser_free_mem(p, enum_seen, enum_seen_size); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 1; } @@ -988,10 +1029,17 @@ static int toy_parse_switch_stmt_named(ToyParser* p, CfreeSym label_name) { CfreeCgTypeId selector_ty; CfreeCgLocal selector_slot; CfreeCgLabel end_label; + CfreeCgLabel dispatch_label; + CfreeCgLabel default_arm_label = CFREE_CG_LABEL_NONE; CfreeCgScope scope; + CfreeCgSwitchCase* cases = NULL; + size_t ncases = 0; + size_t cap_cases = 0; + CfreeCgSwitchHint hint; + CfreeCgSwitch sw; toy_parser_advance(p); /* switch */ - if (!toy_parse_switch_strategy_hint(p)) return 0; + if (!toy_parse_switch_strategy_hint(p, &hint)) return 0; selector_ty = toy_parse_expr(p); if (selector_ty == CFREE_CG_TYPE_NONE) return 0; if (!toy_type_is_intlike(p, selector_ty)) { @@ -1003,10 +1051,15 @@ static int toy_parse_switch_stmt_named(ToyParser* p, CfreeSym label_name) { cfree_cg_swap(p->cg); cfree_cg_store(p->cg, toy_mem_access(p, selector_ty)); end_label = cfree_cg_label_new(p->cg); + dispatch_label = cfree_cg_label_new(p->cg); if (!toy_parser_reserve(p, (void**)&p->scopes, &p->cap_scopes, p->nscopes + 1u, sizeof *p->scopes, "scopes")) { return 0; } + /* scope_begin must precede the jump-to-dispatch so the scope's entry + * block is reachable from the straight-line predecessor. With the + * jump before scope_begin, opt's CFG sees the scope_begin block as + * unreachable and prunes it; scope_end then can't find its handle. */ scope = cfree_cg_scope_begin(p->cg, CFREE_CG_TYPE_NONE); p->scopes[p->nscopes].name = label_name; p->scopes[p->nscopes].kind = TOY_SCOPE_SWITCH; @@ -1014,6 +1067,8 @@ static int toy_parse_switch_stmt_named(ToyParser* p, CfreeSym label_name) { p->scopes[p->nscopes].result_type = CFREE_CG_TYPE_NONE; p->scopes[p->nscopes].result_toy_type = TOY_TYPE_NONE; p->nscopes++; + /* Skip the arm bodies on entry; come back through the dispatch label. */ + cfree_cg_jump(p->cg, dispatch_label); if (!toy_parser_expect(p, TOK_LBRACE)) { toy_error(p, p->cur.loc, "expected '{' after switch selector"); p->nscopes--; @@ -1021,42 +1076,57 @@ static int toy_parse_switch_stmt_named(ToyParser* p, CfreeSym label_name) { } while (p->cur.kind != TOK_RBRACE && p->cur.kind != TOK_EOF) { CfreeCgLabel arm_label = cfree_cg_label_new(p->cg); - CfreeCgLabel next_label = cfree_cg_label_new(p->cg); if (p->cur.kind == TOK_DEFAULT) { + default_arm_label = arm_label; toy_parser_advance(p); - cfree_cg_jump(p->cg, arm_label); } else { for (;;) { int64_t value; if (!toy_parse_switch_label_value(p, selector_ty, &value)) { + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); p->nscopes--; return 0; } - cfree_cg_push_local(p->cg, selector_slot); - cfree_cg_load(p->cg, toy_mem_access(p, selector_ty)); - cfree_cg_push_int(p->cg, (uint64_t)value, selector_ty); - cfree_cg_int_cmp(p->cg, CFREE_CG_INT_EQ); - cfree_cg_branch_true(p->cg, arm_label); + if (!toy_parser_reserve(p, (void**)&cases, &cap_cases, ncases + 1u, + sizeof *cases, "switch cases")) { + p->nscopes--; + return 0; + } + cases[ncases].value = (uint64_t)value; + cases[ncases].label = arm_label; + ncases++; if (!toy_parser_match(p, TOK_COMMA)) break; } - cfree_cg_jump(p->cg, next_label); } cfree_cg_label_place(p->cg, arm_label); if (!toy_parse_block(p)) { + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); p->nscopes--; return 0; } cfree_cg_jump(p->cg, end_label); - cfree_cg_label_place(p->cg, next_label); } if (!toy_parser_expect(p, TOK_RBRACE)) { toy_error(p, p->cur.loc, "expected '}' after switch"); + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); p->nscopes--; return 0; } + cfree_cg_label_place(p->cg, dispatch_label); + cfree_cg_push_local(p->cg, selector_slot); + cfree_cg_load(p->cg, toy_mem_access(p, selector_ty)); + sw.selector_type = selector_ty; + sw.default_label = default_arm_label != CFREE_CG_LABEL_NONE + ? default_arm_label + : end_label; + sw.cases = cases; + sw.ncases = (uint32_t)ncases; + sw.hint = hint; + cfree_cg_switch(p->cg, sw); cfree_cg_label_place(p->cg, end_label); cfree_cg_scope_end(p->cg, scope); p->nscopes--; + toy_parser_free_mem(p, cases, cap_cases * sizeof *cases); return 1; } diff --git a/lang/wasm/cg.c b/lang/wasm/cg.c @@ -1187,7 +1187,11 @@ void wasm_emit_cg(CfreeCompiler* c, const CfreeCodeOptions* code_opts, WASM_INSN_LOOP ? control[ncontrol - 1u - in.targets[in.ntargets - 1u]].start : control[ncontrol - 1u - in.targets[in.ntargets - 1u]].end; - sw.hint = CFREE_CG_SWITCH_BRANCH_CHAIN; + /* br_table is dense-by-construction (case values 0..N-1); + * tell targets that care that a jump table is the natural + * lowering. Targets that don't honour the hint fall back to + * a cmp chain, which is still correct. */ + sw.hint = CFREE_CG_SWITCH_JUMP_TABLE; cfree_cg_switch(cg, sw); break; } diff --git a/src/arch/arch.h b/src/arch/arch.h @@ -699,6 +699,20 @@ struct CGTarget { * synthesize cmp_branch(CMP_NE, val, IMM_ZERO, label). */ void (*cmp_branch)(CGTarget*, CmpOp, Operand a, Operand b, Label); + /* Switch dispatch. Optional: if NULL, cg falls back to a chain of + * cmp_branch calls + jump-to-default — the same lowering native arches + * have always used. The C-source target overrides this to emit + * `switch (val) { case V: goto L_V; … default: goto L_def; }` so the + * host C compiler picks the best lowering (jump table / branch tree). + * `values[i]` is the constant the case matches; `labels[i]` is where + * to branch when it does. Both arrays have length `ncases`. For + * jump-table-shaped use (wasm br_table, direct threading) frontends + * pass dense values 0..ncases-1 and a non-NONE default_label; the + * arch backend can detect the dense shape and emit a real table. */ + void (*switch_branch)(CGTarget*, Operand selector, const Label* labels, + const u64* values, u32 ncases, Label default_label, + u8 hint /* CfreeCgSwitchHint */); + /* ---- structured control flow ---- * Mirrors CG's scope ops. CG passes explicit break/continue targets so C * `for` continues can land on the increment expression rather than the loop diff --git a/src/arch/c_target/emit.c b/src/arch/c_target/emit.c @@ -1550,6 +1550,62 @@ void c_continue_to(CGTarget* T, CGScope s) { c_jump(T, t->scopes[s - 1u].continue_label); } +/* ===== switch dispatch ===== */ + +/* Emit `case <value>:`. For an int32_t selector the bare literal is + * already the right type, so we skip the cast; for wider/narrower + * integers we wrap in `(T)` so the case constant matches the switch + * value's promoted type (avoids -Wswitch warnings on narrower + * selectors). */ +static void c_emit_case_value(CTarget* t, CfreeCgTypeId sel_ty, u64 v) { + u32 w = c_int_width_for_signedness(t, sel_ty); + cbuf_puts(&t->body, " case "); + if (w != 0 && w != 32) { + cbuf_putc(&t->body, '('); + c_emit_type(t, &t->body, sel_ty); + cbuf_puts(&t->body, ")"); + } + cbuf_put_u64(&t->body, v); + cbuf_puts(&t->body, ":"); +} + +void c_switch_branch(CGTarget* T, Operand selector, const Label* labels, + const u64* values, u32 ncases, Label default_label, + u8 hint) { + CTarget* t = (CTarget*)T; + (void)hint; /* gcc/clang ignore strategy hints and pick their own. */ + if (t->last_was_terminator) return; + cbuf_puts(&t->body, " switch ("); + c_emit_operand(t, selector); + cbuf_puts(&t->body, ") {\n"); + for (u32 i = 0; i < ncases; ++i) { + char buf[24]; + c_label_name(labels[i], buf, sizeof buf); + c_emit_case_value(t, selector.type, values[i]); + cbuf_puts(&t->body, " goto "); + cbuf_puts(&t->body, buf); + cbuf_puts(&t->body, ";\n"); + } + cbuf_puts(&t->body, " default: "); + if (default_label != (Label)LABEL_NONE) { + char buf[24]; + c_label_name(default_label, buf, sizeof buf); + cbuf_puts(&t->body, "goto "); + cbuf_puts(&t->body, buf); + cbuf_puts(&t->body, ";\n"); + } else { + /* No default supplied — the cfree IR's contract for that case is + * "if no case matches, fall through." `break;` does exactly that + * inside the for-wrapper around structured scopes. */ + cbuf_puts(&t->body, "break;\n"); + } + cbuf_puts(&t->body, " }\n"); + /* The switch always transfers control (every arm jumps or breaks). + * Mark as terminator so any frontend-emitted defensive jump after + * dispatch is dropped. */ + t->last_was_terminator = 1; +} + /* ===== local, local_addr ===== */ CGLocalStorage c_local(CGTarget* T, const CGLocalDesc* d) { diff --git a/src/arch/c_target/target.c b/src/arch/c_target/target.c @@ -36,6 +36,8 @@ Label c_label_new(CGTarget*); void c_label_place(CGTarget*, Label); void c_jump(CGTarget*, Label); void c_cmp_branch(CGTarget*, CmpOp, Operand, Operand, Label); +void c_switch_branch(CGTarget*, Operand, const Label*, const u64*, u32, + Label, u8); CGScope c_scope_begin(CGTarget*, const CGScopeDesc*); void c_scope_else(CGTarget*, CGScope); void c_scope_end(CGTarget*, CGScope); @@ -212,6 +214,7 @@ CGTarget* c_cgtarget_new(Compiler* c, ObjBuilder* o, CfreeWriter* w) { t->label_place = c_label_place; t->jump = c_jump; t->cmp_branch = c_cmp_branch; + t->switch_branch = c_switch_branch; t->scope_begin = c_scope_begin; t->scope_else = c_scope_else; t->scope_end = c_scope_end; diff --git a/src/cg/control.c b/src/cg/control.c @@ -83,13 +83,42 @@ void cfree_cg_switch(CfreeCg* g, CfreeCgSwitch sw) { ty = resolve_type(g->c, sw.selector_type); if (!ty) ty = api_sv_type(&selector); sel = api_force_reg_unless_imm(g, &selector, ty); - for (u32 i = 0; i < sw.ncases; ++i) { - Operand imm = api_op_imm((i64)sw.cases[i].value, ty); - g->target->cmp_branch(g->target, CMP_EQ, sel, imm, - (Label)sw.cases[i].label); - } - if (sw.default_label != CFREE_CG_LABEL_NONE) { - g->target->jump(g->target, (Label)sw.default_label); + if (g->target->switch_branch) { + /* Materialize parallel label[] / value[] arrays — the public-side + * CfreeCgSwitchCase keeps them paired per-entry for caller + * convenience, but the vtable wants them split so the target can + * scan values for density and pick chain vs jump table without + * per-element indirection. */ + Heap* h = g->c->ctx->heap; + Label* labels = NULL; + u64* values = NULL; + if (sw.ncases) { + labels = (Label*)h->alloc(h, sw.ncases * sizeof(Label), _Alignof(Label)); + values = (u64*)h->alloc(h, sw.ncases * sizeof(u64), _Alignof(u64)); + if (!labels || !values) { + compiler_panic(g->c, g->cur_loc, "cfree_cg_switch: out of memory"); + } + for (u32 i = 0; i < sw.ncases; ++i) { + labels[i] = (Label)sw.cases[i].label; + values[i] = sw.cases[i].value; + } + } + g->target->switch_branch(g->target, sel, labels, values, sw.ncases, + (Label)sw.default_label, (u8)sw.hint); + if (labels) h->free(h, labels, sw.ncases * sizeof(Label)); + if (values) h->free(h, values, sw.ncases * sizeof(u64)); + } else { + /* Default lowering: cmp-and-branch chain — same behaviour native + * arches have always had. Targets that can do better (real C + * `switch`, machine jump table) override `switch_branch`. */ + for (u32 i = 0; i < sw.ncases; ++i) { + Operand imm = api_op_imm((i64)sw.cases[i].value, ty); + g->target->cmp_branch(g->target, CMP_EQ, sel, imm, + (Label)sw.cases[i].label); + } + if (sw.default_label != CFREE_CG_LABEL_NONE) { + g->target->jump(g->target, (Label)sw.default_label); + } } api_release(g, &selector); }