Skip to content

Commit 4e79cd5

Browse files
committed
fix 3 bugs, scope/block depth confusion over-unwinds defers, stmt-expr goto bypasses defer cleanup, bare orelse comma split escapes braceless control flow
1 parent 6bb82d1 commit 4e79cd5

File tree

3 files changed

+221
-24
lines changed

3 files changed

+221
-24
lines changed

.github/test.defer.c

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4577,6 +4577,127 @@ static void test_defer_shadow_braceless_for_ifelse(void) {
45774577
}
45784578
}
45794579

4580+
// BUG78: scope_depth/block_depth type confusion in emit_defers_ex.
4581+
// When non-SCOPE_BLOCK entries (SCOPE_CTRL_PAREN) are on the scope_stack,
4582+
// the scope_stack index `d` diverges from block_depth. The old code compared
4583+
// `d < stop_depth` where stop_depth is in block_depth units, causing
4584+
// over-unwinding of defers for same-scope gotos inside stmt-exprs.
4585+
static void test_scope_depth_overunwind(void) {
4586+
/* goto L is a forward jump WITHIN the same block inside a stmt-expr
4587+
* that lives inside if(...). The SCOPE_CTRL_PAREN shifts scope indices.
4588+
* The defer must fire exactly once (at scope exit), not at the goto. */
4589+
{
4590+
PrismResult r = prism_transpile_source(
4591+
"int counter = 0;\n"
4592+
"void f(void) {\n"
4593+
" if ( ({\n"
4594+
" {\n"
4595+
" defer { counter++; }\n"
4596+
" goto L;\n"
4597+
" L: ;\n"
4598+
" }\n"
4599+
" 42;\n"
4600+
" }) ) {\n"
4601+
" (void)0;\n"
4602+
" }\n"
4603+
"}\n",
4604+
"bug78a.c", prism_defaults());
4605+
CHECK_EQ(r.status, PRISM_OK,
4606+
"bug78-overunwind: transpiles OK");
4607+
if (r.status == PRISM_OK && r.output) {
4608+
/* The goto should NOT be wrapped with defer cleanup
4609+
* because it stays within the same scope. Count
4610+
* occurrences of "counter++" — should be exactly 1
4611+
* (the scope-exit defer), not 2 (goto + scope-exit). */
4612+
const char *fn = strstr(r.output, "void f(");
4613+
CHECK(fn != NULL, "bug78-overunwind: function found");
4614+
if (fn) {
4615+
int count = 0;
4616+
const char *p = fn;
4617+
while ((p = strstr(p, "counter++")) != NULL) {
4618+
count++;
4619+
p += 9;
4620+
}
4621+
CHECK_EQ(count, 1,
4622+
"bug78-overunwind: defer fires once (not over-unwound)");
4623+
}
4624+
}
4625+
prism_free(&r);
4626+
}
4627+
/* Backward same-scope goto with SCOPE_CTRL_PAREN on stack */
4628+
{
4629+
PrismResult r = prism_transpile_source(
4630+
"int counter = 0;\n"
4631+
"void f(void) {\n"
4632+
" if ( ({\n"
4633+
" {\n"
4634+
" defer { counter++; }\n"
4635+
" L: ;\n"
4636+
" static int j = 0;\n"
4637+
" if (!j) { j = 1; goto L; }\n"
4638+
" }\n"
4639+
" 42;\n"
4640+
" }) ) {\n"
4641+
" (void)0;\n"
4642+
" }\n"
4643+
"}\n",
4644+
"bug78b.c", prism_defaults());
4645+
CHECK_EQ(r.status, PRISM_OK,
4646+
"bug78-backward: transpiles OK");
4647+
if (r.status == PRISM_OK && r.output) {
4648+
const char *fn = strstr(r.output, "void f(");
4649+
CHECK(fn != NULL, "bug78-backward: function found");
4650+
if (fn) {
4651+
int count = 0;
4652+
const char *p = fn;
4653+
while ((p = strstr(p, "counter++")) != NULL) {
4654+
count++;
4655+
p += 9;
4656+
}
4657+
CHECK_EQ(count, 1,
4658+
"bug78-backward: defer fires once (not over-unwound)");
4659+
}
4660+
}
4661+
prism_free(&r);
4662+
}
4663+
}
4664+
4665+
// BUG79: emit_range_no_prep flat-loops tokens without routing stmt-exprs
4666+
// through walk_balanced. A goto inside a stmt-expr in an orelse LHS
4667+
// bypasses defer cleanup.
4668+
static void test_emit_range_no_prep_stmtexpr_defer(void) {
4669+
PrismResult r = prism_transpile_source(
4670+
"int counter = 0;\n"
4671+
"void f(void) {\n"
4672+
" int target = 0;\n"
4673+
" {\n"
4674+
" defer { counter++; }\n"
4675+
" *({ goto skip; &target; }) = 1 orelse 2;\n"
4676+
" (void)target;\n"
4677+
" }\n"
4678+
" return;\n"
4679+
"skip:\n"
4680+
" return;\n"
4681+
"}\n",
4682+
"bug79.c", prism_defaults());
4683+
CHECK_EQ(r.status, PRISM_OK,
4684+
"bug79-lhs-stmtexpr: transpiles OK");
4685+
if (r.status == PRISM_OK && r.output) {
4686+
/* The goto in the LHS stmt-expr must have defer cleanup.
4687+
* Find "goto skip" and check that "counter++" appears before it */
4688+
const char *fn = strstr(r.output, "void f(");
4689+
CHECK(fn != NULL, "bug79-lhs-stmtexpr: function found");
4690+
if (fn) {
4691+
const char *gs = strstr(fn, "goto skip");
4692+
const char *cleanup = strstr(fn, "counter++");
4693+
CHECK(gs != NULL, "bug79-lhs-stmtexpr: goto found");
4694+
CHECK(cleanup != NULL && cleanup < gs,
4695+
"bug79-lhs-stmtexpr: defer cleanup before goto");
4696+
}
4697+
}
4698+
prism_free(&r);
4699+
}
4700+
45804701
void run_defer_tests(void) {
45814702
printf("\n=== DEFER TESTS ===\n");
45824703
test_defer_in_comma_expr_bug();
@@ -4847,4 +4968,10 @@ void run_defer_tests(void) {
48474968

48484969
// BUG: braceless for-body if/else and do/while prematurely clears for_name_hid
48494970
test_defer_shadow_braceless_for_ifelse();
4971+
4972+
// BUG78: scope_depth/block_depth type confusion over-unwinds defers
4973+
test_scope_depth_overunwind();
4974+
4975+
// BUG79: emit_range_no_prep missing stmt-expr dispatch
4976+
test_emit_range_no_prep_stmtexpr_defer();
48504977
}

.github/test.orelse.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6021,6 +6021,47 @@ static void test_nested_bracket_orelse_no_quadratic(void) {
60216021
prism_free(&r);
60226022
}
60236023

6024+
// BUG80: bare orelse with comma operator in braceless control flow.
6025+
// The comma split produces two statements, but braceless if/for/while/else
6026+
// only captures the first — the orelse assignment leaks out of scope.
6027+
// Fix: emit_bare_orelse_impl wraps in { } when brace_wrap is set.
6028+
static int _bug80_if_helper(int cond) {
6029+
int status = -1;
6030+
if (cond)
6031+
(void)0, status = 0 orelse 1;
6032+
return status;
6033+
}
6034+
6035+
static int _bug80_while_helper(void) {
6036+
int x = -1;
6037+
int once = 1;
6038+
while (once)
6039+
once = 0, x = 5 orelse 99;
6040+
return x;
6041+
}
6042+
6043+
static int _bug80_else_helper(int cond) {
6044+
int status = -1;
6045+
if (cond)
6046+
status = 42;
6047+
else
6048+
(void)0, status = 0 orelse 1;
6049+
return status;
6050+
}
6051+
6052+
void test_bare_orelse_comma_braceless(void) {
6053+
// if: cond=1 → body runs, status = 0 orelse 1 = 1 (0 is falsy)
6054+
CHECK_EQ(_bug80_if_helper(1), 1, "BUG80: braceless if cond=1 orelse fires");
6055+
// if: cond=0 → body skipped, status stays -1
6056+
CHECK_EQ(_bug80_if_helper(0), -1, "BUG80: braceless if cond=0 body skipped");
6057+
// while: once=1 → body runs, x = 5 (truthy)
6058+
CHECK_EQ(_bug80_while_helper(), 5, "BUG80: braceless while comma orelse");
6059+
// else: cond=1 → if-branch, status=42
6060+
CHECK_EQ(_bug80_else_helper(1), 42, "BUG80: braceless else cond=1 if-branch");
6061+
// else: cond=0 → else-branch, status = 0 orelse 1 = 1
6062+
CHECK_EQ(_bug80_else_helper(0), 1, "BUG80: braceless else cond=0 orelse fires");
6063+
}
6064+
60246065
void run_orelse_tests(void) {
60256066
test_orelse_return_null();
60266067
test_orelse_return_cast();
@@ -6363,4 +6404,7 @@ void run_orelse_tests(void) {
63636404

63646405
// BUG77: O(N^2) nested bracket orelse scanning
63656406
test_nested_bracket_orelse_no_quadratic();
6407+
6408+
// BUG80: bare orelse comma operator in braceless control flow
6409+
test_bare_orelse_comma_braceless();
63666410
}

prism.c

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,8 +1201,13 @@ static void emit_range(Token *start, Token *end) {
12011201
// Like emit_range but skips TK_PREP_DIR tokens (for generated expressions
12021202
// where preprocessor directives would be invalid, e.g. inside if-conditions).
12031203
static void emit_range_no_prep(Token *start, Token *end) {
1204-
for (Token *t = start; t && t != end && t->kind != TK_EOF; t = tok_next(t))
1205-
if (t->kind != TK_PREP_DIR) emit_tok(t);
1204+
for (Token *t = start; t && t != end && t->kind != TK_EOF; t = tok_next(t)) {
1205+
if (t->kind == TK_PREP_DIR) continue;
1206+
if (match_ch(t, '(') && tok_next(t) && match_ch(tok_next(t), '{') && tok_match(t)) {
1207+
walk_balanced(t, true); t = tok_match(t); continue;
1208+
}
1209+
emit_tok(t);
1210+
}
12061211
}
12071212

12081213
// Like emit_range_no_prep but walks balanced () and [] groups through
@@ -1218,7 +1223,7 @@ static void emit_balanced_range(Token *start, Token *end) {
12181223
}
12191224

12201225
// Forward declarations — implementations below find_bare_orelse.
1221-
static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term);
1226+
static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term, bool brace_wrap);
12221227
static Token *emit_deferred_orelse(Token *t, Token *end);
12231228
static void emit_deferred_range(Token *start, Token *end);
12241229

@@ -1230,9 +1235,10 @@ static void emit_defers_ex(DeferEmitMode mode, int stop_depth) {
12301235
check_defer_shadow_at_exit(mode, stop_depth);
12311236

12321237
int current_defer = defer_count - 1;
1238+
int curr_bd = ctx->block_depth;
12331239
for (int d = ctx->scope_depth - 1; d >= 0; d--) {
12341240
if (scope_stack[d].kind != SCOPE_BLOCK) continue;
1235-
if (mode == DEFER_TO_DEPTH && d < stop_depth) break;
1241+
if (mode == DEFER_TO_DEPTH && curr_bd <= stop_depth) break;
12361242

12371243
ScopeNode *scope = &scope_stack[d];
12381244
for (int i = current_defer; i >= scope->defer_start_idx; i--) {
@@ -1241,6 +1247,7 @@ static void emit_defers_ex(DeferEmitMode mode, int stop_depth) {
12411247
out_char(';');
12421248
}
12431249
current_defer = scope->defer_start_idx - 1;
1250+
curr_bd--;
12441251

12451252
if (mode == DEFER_SCOPE) break;
12461253
if (mode == DEFER_BREAK && (scope->is_loop || scope->is_switch)) break;
@@ -1249,10 +1256,12 @@ static void emit_defers_ex(DeferEmitMode mode, int stop_depth) {
12491256
}
12501257

12511258
static bool has_defers_for(DeferEmitMode mode, int stop_depth) {
1259+
int curr_bd = ctx->block_depth;
12521260
for (int d = ctx->scope_depth - 1; d >= 0; d--) {
12531261
if (scope_stack[d].kind != SCOPE_BLOCK) continue;
1254-
if (mode == DEFER_TO_DEPTH && d < stop_depth) break;
1262+
if (mode == DEFER_TO_DEPTH && curr_bd <= stop_depth) break;
12551263
if (defer_count > scope_stack[d].defer_start_idx) return true;
1264+
curr_bd--;
12561265
if (mode == DEFER_BREAK && (scope_stack[d].is_loop || scope_stack[d].is_switch))
12571266
return false;
12581267
if (mode == DEFER_CONTINUE && scope_stack[d].is_loop) return false;
@@ -1486,11 +1495,13 @@ static void check_defer_shadow_at_exit(DeferEmitMode mode, int stop_depth) {
14861495
// Determine which defer indices will be pasted by this exit.
14871496
// Walk scopes the same way emit_defers_ex does, collecting the range.
14881497
int min_defer_idx = defer_count; // exclusive upper bound not needed; just track min
1498+
int curr_bd = ctx->block_depth;
14891499
for (int d = ctx->scope_depth - 1; d >= 0; d--) {
14901500
if (scope_stack[d].kind != SCOPE_BLOCK) continue;
1491-
if (mode == DEFER_TO_DEPTH && d < stop_depth) break;
1501+
if (mode == DEFER_TO_DEPTH && curr_bd <= stop_depth) break;
14921502
if (scope_stack[d].defer_start_idx < min_defer_idx)
14931503
min_defer_idx = scope_stack[d].defer_start_idx;
1504+
curr_bd--;
14941505
if (mode == DEFER_BREAK &&
14951506
(scope_stack[d].is_loop || scope_stack[d].is_switch)) break;
14961507
if (mode == DEFER_CONTINUE && scope_stack[d].is_loop) break;
@@ -6028,7 +6039,7 @@ static bool orelse_has_chain(Token *start, bool comma_term) {
60286039
// Returns the token after the statement, or NULL if no orelse was found.
60296040
// `comma_term`: also treat ',' at depth 0 as statement terminator.
60306041
// `end`: optional boundary (for deferred ranges).
6031-
static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term) {
6042+
static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term, bool brace_wrap) {
60326043
Token *orelse_tok = find_bare_orelse(t);
60336044
if (!orelse_tok || (end && tok_loc(orelse_tok) >= tok_loc(end)))
60346045
return NULL;
@@ -6038,33 +6049,26 @@ static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term) {
60386049

60396050
#define BARE_IS_END(s) (match_ch((s), ';') || (comma_term && match_ch((s), ',')))
60406051

6041-
// Skip past depth-0 commas before orelse — the comma operator separates
6042-
// independent sub-expressions and orelse only applies to the last one.
6043-
// Emit prefix tokens as a separate statement (comma → semicolon) because
6044-
// the orelse expansion may produce a block `{ ... }` which cannot appear
6045-
// after the comma operator.
6052+
// Find last depth-0 comma before orelse (search only — don't emit yet).
6053+
// Need to check is_bare_fallback before emitting, because if it's not
6054+
// bare we return NULL and the caller handles it differently.
6055+
Token *last_comma = NULL;
60466056
{
6047-
Token *last_comma = NULL;
60486057
int sd = 0;
60496058
for (Token *s = t; s != orelse_tok; s = tok_next(s)) {
60506059
if (s->flags & TF_OPEN) sd++;
60516060
else if (s->flags & TF_CLOSE) sd--;
6052-
else if (sd == 0 && match_ch(s, ',')) last_comma = s;
6053-
}
6054-
if (last_comma) {
6055-
for (Token *s = t; s != last_comma; s = tok_next(s))
6056-
emit_tok(s);
6057-
out_char(';'); // replace comma with semicolon
6058-
t = tok_next(last_comma);
6061+
else if (sd == 0 && comma_term && match_ch(s, ',')) last_comma = s;
60596062
}
60606063
}
6064+
Token *post_comma_t = last_comma ? tok_next(last_comma) : t;
60616065

60626066
// Find assignment target (= at depth 0 before orelse)
6063-
Token *bare_lhs_start = t;
6067+
Token *bare_lhs_start = post_comma_t;
60646068
Token *bare_assign_eq = NULL;
60656069
{
60666070
int sd = 0;
6067-
for (Token *s = t; s != orelse_tok; s = tok_next(s)) {
6071+
for (Token *s = post_comma_t; s != orelse_tok; s = tok_next(s)) {
60686072
if (s->flags & TF_OPEN) sd++;
60696073
else if (s->flags & TF_CLOSE) sd--;
60706074
else if (sd == 0 && is_assignment_operator_token(s)) {
@@ -6097,6 +6101,21 @@ static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term) {
60976101
if (!is_bare_fallback)
60986102
return NULL; // caller handles non-bare fallback
60996103

6104+
// Now we know this is bare — safe to emit.
6105+
// Wrap in braces for braceless control-flow bodies (if/for/while/else
6106+
// without braces) so the expansion stays as a single compound statement.
6107+
if (brace_wrap) OUT_LIT(" {");
6108+
6109+
// Emit comma prefix as a separate statement (comma → semicolon).
6110+
// The orelse expansion may produce a block which cannot appear after
6111+
// the comma operator.
6112+
if (last_comma) {
6113+
for (Token *s = t; s != last_comma; s = tok_next(s))
6114+
emit_tok(s);
6115+
out_char(';');
6116+
t = post_comma_t;
6117+
}
6118+
61006119
// Reject if the statement contains preprocessor conditionals (#ifdef/#else/etc.).
61016120
// emit_range_no_prep / emit_balanced_range skip TK_PREP_DIR tokens,
61026121
// producing concatenated code from ALL branches — silent miscompilation.
@@ -6263,6 +6282,7 @@ static Token *emit_bare_orelse_impl(Token *t, Token *end, bool comma_term) {
62636282
OUT_LIT(" }");
62646283
}
62656284
if (match_ch(t, ';') || (comma_term && match_ch(t, ','))) t = tok_next(t);
6285+
if (brace_wrap) OUT_LIT(" }");
62666286
#undef BARE_IS_END
62676287
return t;
62686288
}
@@ -6284,7 +6304,7 @@ static Token *emit_orelse_condition_wrap(Token *t, Token *orelse_tok) {
62846304
// Wrapper for defer blocks: handles both bare and non-bare orelse.
62856305
// Returns the token after the statement, or NULL if no orelse was found.
62866306
static Token *emit_deferred_orelse(Token *t, Token *end) {
6287-
Token *result = emit_bare_orelse_impl(t, end, false);
6307+
Token *result = emit_bare_orelse_impl(t, end, false, false);
62886308
if (result) return result;
62896309

62906310
// Check for non-bare orelse (block/control-flow action)
@@ -8370,8 +8390,14 @@ static int transpile_tokens(Token *tok, FILE *fp) {
83708390
tok = label_end;
83718391
}
83728392

8393+
// Braceless control flow: bare orelse can emit multiple
8394+
// statements (comma split + assignment), so needs braces.
8395+
// Non-bare (control-flow/block action) already wraps in
8396+
// { if (!(...)) ... } which is a single compound statement.
8397+
bool brace_wrap = ctrl_state.pending && ctrl_state.parens_just_closed;
8398+
83738399
// Try bare-fallback path (handled by shared impl)
8374-
Token *next = emit_bare_orelse_impl(tok, NULL, true);
8400+
Token *next = emit_bare_orelse_impl(tok, NULL, true, brace_wrap);
83758401
if (next) {
83768402
tok = next;
83778403
end_statement_after_semicolon();

0 commit comments

Comments
 (0)