kit

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

commit 08c1a50a4f0995d70a5b05429fe4e3405982d9ae
parent f29ca291d13f441b4116feb9766984e80a6874f4
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 11:42:26 -0700

fix(c): give goto labels first seen in a constant-false region a real id

A goto label whose first mention is inside a constant-false (codegen-
suppressed) `if` was allocated its CG-label id with pcg_label_new, which
returns a suppression sentinel while codegen is suppressed. The sentinel
then aliased the function's first real label, so the -O0 native emitter
aborted with "MCEmitter: label N placed twice" — the defect that kept
yyjson red at -O0 in the ecosystem gate (Bug A).

A label's placement and its `goto`s can straddle a suppressed region, so
its id must be real whenever the function emits at all, not keyed on the
momentary suppress depth. parse_function_body records cur_func_emits
(= pcg_emit_enabled at func_begin) and label_get_or_create allocates a
real kit_cg_label_new id when the function emits, saving/restoring the
flag across nested definitions.

Regression: test/parse/cases/6_8_06_01_goto_label_in_dead_branch.c.

Diffstat:
Mlang/c/parse/parse.c | 7+++++++
Mlang/c/parse/parse_priv.h | 8++++++++
Mlang/c/parse/parse_stmt.c | 8+++++++-
Atest/parse/cases/6_8_06_01_goto_label_in_dead_branch.c | 32++++++++++++++++++++++++++++++++
Atest/parse/cases/6_8_06_01_goto_label_in_dead_branch.expected | 1+
5 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -1285,6 +1285,11 @@ static void parse_function_body(Parser* p, ObjSymId fsym, const Type* fn_ty, p->cur_switch = NULL; p->computed_goto_emitted = 0; pcg_set_loc(p, fname_loc); + /* Record whether this body emits before func_begin (which is itself gated on + * emit-enabled): goto-label allocation keys off this, not the momentary + * suppress_codegen depth, so a label first referenced inside a constant-false + * region still gets a real CG-label id rather than the suppression sentinel. */ + p->cur_func_emits = (u8)pcg_emit_enabled(p); pcg_func_begin(p, &fd); for (u16 i = 0; i < nparams; ++i) { @@ -1405,6 +1410,7 @@ static void parse_external_decl(Parser* p) { fent->decl_state = DSTATE_FUNC_DEFINED; Sym saved_func_name = p->cur_func_name; const Type* saved_func_ret = p->cur_func_ret; + u8 saved_func_emits = p->cur_func_emits; p->cur_func_name = name; p->cur_func_ret = fn_ty->fn.ret; if (suppress_body_codegen) pcg_codegen_suppress_push(p); @@ -1413,6 +1419,7 @@ static void parse_external_decl(Parser* p) { if (suppress_body_codegen) pcg_codegen_suppress_pop(p); p->cur_func_name = saved_func_name; p->cur_func_ret = saved_func_ret; + p->cur_func_emits = saved_func_emits; return; } if (accept_punct(p, ';')) { diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -283,6 +283,14 @@ typedef struct Parser { Sym sym_pretty_func_gcc; /* __PRETTY_FUNCTION__ */ Sym cur_func_name; /* name of the function whose body we're in, * 0 at file scope */ + u8 cur_func_emits; /* the current function body is being emitted (a CG + * func was begun); 0 for whole-body-suppressed + * `extern inline` definitions. Distinct from the + * momentary `suppress_codegen` counter: goto labels + * persist across the whole body, so their CG-label + * ids must be real whenever the function emits, even + * if the first reference is in a transiently + * suppressed (constant-false) region. */ const Type* cur_func_ret; Sym sym_b_expect; Sym sym_b_offsetof; diff --git a/lang/c/parse/parse_stmt.c b/lang/c/parse/parse_stmt.c @@ -251,7 +251,13 @@ GotoLabel* label_get_or_create(Parser* p, Sym name, SrcLoc loc) { if (!gl) perr(p, "out of memory in label_get_or_create"); memset(gl, 0, sizeof *gl); gl->name = name; - gl->label = pcg_label_new(p); + /* A goto label's CG-label id outlives any momentary codegen suppression: the + * `LABEL:` placement and its `goto LABEL` references can straddle a + * constant-false (suppressed) region. Key allocation off whether the function + * emits at all, not the transient suppress depth — otherwise a label first + * mentioned inside a suppressed `goto` would cache pcg_label_new's suppression + * sentinel and later alias the function's first real label ("placed twice"). */ + gl->label = p->cur_func_emits ? kit_cg_label_new(p->cg) : pcg_label_new(p); gl->placed = 0; gl->first_use = loc; gl->min_forward_vla_mark = p->vla_mark; diff --git a/test/parse/cases/6_8_06_01_goto_label_in_dead_branch.c b/test/parse/cases/6_8_06_01_goto_label_in_dead_branch.c @@ -0,0 +1,32 @@ +/* Regression: a goto label whose FIRST mention is inside a constant-false + * (codegen-suppressed) `if` must still receive a real CG-label id, not the + * suppression sentinel. Previously the sentinel aliased the function's first + * real label — here the `switch` dispatch — and the -O0 native emitter aborted + * with "MCEmitter: label N placed twice". Mirrors yyjson's check_str_len / + * fail_alloc shape (`if ((sizeof(...) < 8) && rt) goto fail;`). */ + +static int classify(int sel, unsigned long n) { + switch (sel) { /* dispatch is the function's first real label */ + case 1: + /* sizeof(char) is 1 on every target, so the && folds to constant + * false: this `goto fail` is parsed suppressed and is the first + * reference to `fail`. */ + if ((sizeof(char) > 1) && (n >= 1234)) goto fail; + return 10; + case 2: + if (!n) goto fail; /* emit-enabled goto to the same label */ + return 20; + default: + goto fail; + } +fail: + return -1; +} + +int test_main(void) { + int acc = 0; + acc += classify(1, 5); /* normal case-1 path: 10 */ + acc += (classify(2, 0) == -1) ? 100 : 0; /* if(!n) goto fail: +100 */ + acc += (classify(9, 0) == -1) ? 1 : 0; /* default -> fail: +1 */ + return acc; /* 111 */ +} diff --git a/test/parse/cases/6_8_06_01_goto_label_in_dead_branch.expected b/test/parse/cases/6_8_06_01_goto_label_in_dead_branch.expected @@ -0,0 +1 @@ +111