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:
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);
}