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:
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