commit 24d477f4ce2d0de89590909ce1fdbfb2c84f0ffa
parent f7ac167dc65d43ed7b189508db8a2f6a4a82f14a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 18 May 2026 15:39:38 -0700
Fix macro hideset recursion in fp runtime probe
Diffstat:
17 files changed, 115 insertions(+), 18 deletions(-)
diff --git a/doc/RT_CFREERT_CHECKLIST.md b/doc/RT_CFREERT_CHECKLIST.md
@@ -76,13 +76,19 @@ does not build yet because `rt/lib/fp/fp.c` still fails.
rewriting the token to an undeclared plain identifier.
- [x] Added parser support for `__atomic_fetch_nand`, mapped through the
existing target-independent atomic NAND RMW operation.
+- [x] Fixed the `rt/lib/fp/fp.c` preprocessor crash. Function-like macro
+ argument prescan now preserves raw-token hidesets, so self-referential
+ suffix-renaming macros such as `rep_t -> _FP_NAME(rep_t)` do not recurse
+ until stack exhaustion.
## Remaining Blockers
-- [ ] Fix the `rt/lib/fp/fp.c` compiler crash.
- Current symptom: cfree segfaults while compiling this file; lldb stops in
- `cfree_arena_alloc`. Reduce to the smallest function or included helper that
- reproduces the stack growth / allocator crash.
+- [ ] Finish `rt/lib/fp/fp.c`.
+ Current symptom after the preprocessor crash fix:
+ `rt/lib/include/common/fp_lib.h:338:7: fatal: undeclared identifier
+ '__builtin_sadd_overflow'`. The crash reduced to a self-referential macro
+ argument-prescan case and is covered by
+ `test/pp/cases/65_rescan_arg_hideset_prescan.c`.
- [ ] Lift remaining coroutine file-scope assembly.
Current file-scope asm remains in:
diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c
@@ -383,7 +383,7 @@ void pcg_cmp(Parser* p, CmpOp op) {
if (pcg_emit_enabled(p)) cfree_cg_int_cmp(p->cg, pcg_int_cmp(op));
}
pcg_drop_type(p);
- pcg_retag_top(p, type_prim(p->pool, TY_BOOL));
+ pcg_retag_top(p, type_prim(p->pool, TY_INT));
}
void pcg_convert(Parser* p, const Type* dst) {
diff --git a/lang/c/parse/parse_expr.c b/lang/c/parse/parse_expr.c
@@ -389,6 +389,7 @@ CfreeCgSym emit_string_to_rodata(Parser* p, const u8* bytes, size_t n) {
* ============================================================ */
static i64 cexpr_unary(Parser* p, SrcLoc loc);
+static i64 cexpr_cond(Parser* p, SrcLoc loc);
static const Type* offsetof_designator(Parser* p, const Type* base, u32* off);
static i64 cexpr_mul(Parser* p, SrcLoc loc) {
@@ -484,6 +485,33 @@ static i64 cexpr_bor(Parser* p, SrcLoc loc) {
}
return v;
}
+static i64 cexpr_land(Parser* p, SrcLoc loc) {
+ i64 v = cexpr_bor(p, loc);
+ while (accept_punct(p, P_AND)) {
+ i64 r = cexpr_bor(p, loc);
+ v = (v != 0 && r != 0) ? 1 : 0;
+ }
+ return v;
+}
+static i64 cexpr_lor(Parser* p, SrcLoc loc) {
+ i64 v = cexpr_land(p, loc);
+ while (accept_punct(p, P_OR)) {
+ i64 r = cexpr_land(p, loc);
+ v = (v != 0 || r != 0) ? 1 : 0;
+ }
+ return v;
+}
+static i64 cexpr_cond(Parser* p, SrcLoc loc) {
+ i64 v = cexpr_lor(p, loc);
+ if (accept_punct(p, '?')) {
+ i64 then_v = cexpr_cond(p, loc);
+ i64 else_v;
+ expect_punct(p, ':', "':' in constant conditional expression");
+ else_v = cexpr_cond(p, loc);
+ v = v ? then_v : else_v;
+ }
+ return v;
+}
static i64 cexpr_unary(Parser* p, SrcLoc loc) {
if (accept_punct(p, '+')) return cexpr_unary(p, loc);
@@ -555,7 +583,7 @@ static i64 cexpr_unary(Parser* p, SrcLoc loc) {
}
}
{
- i64 v = cexpr_bor(p, loc);
+ i64 v = cexpr_cond(p, loc);
expect_punct(p, ')', "')' in constant expression");
return v;
}
@@ -595,7 +623,7 @@ static i64 cexpr_unary(Parser* p, SrcLoc loc) {
compiler_panic(p->c, loc, "expected constant expression");
}
-i64 eval_const_int(Parser* p, SrcLoc loc) { return cexpr_bor(p, loc); }
+i64 eval_const_int(Parser* p, SrcLoc loc) { return cexpr_cond(p, loc); }
/* ============================================================
* to_rvalue
diff --git a/lang/c/pp/pp_directive.c b/lang/c/pp/pp_directive.c
@@ -138,7 +138,7 @@ static void expand_for_if(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
if (nin == 0) return;
slice = arena_array(pp->arena, Tok, nin);
memcpy(slice, in, sizeof(Tok) * nin);
- expand_arg_to_eof(pp, slice, nin, out);
+ expand_arg_to_eof(pp, slice, NULL, nin, out);
/* Replace remaining identifiers with `0`. */
{
u32 i;
@@ -728,7 +728,7 @@ static void parse_include_path(Pp* pp, const Tok* line, u32 n, SrcLoc loc,
TokVec exp = {0};
Tok* slice = arena_array(pp->arena, Tok, n);
memcpy(slice, line, sizeof(Tok) * n);
- expand_arg_to_eof(pp, slice, n, &exp);
+ expand_arg_to_eof(pp, slice, NULL, n, &exp);
if (exp.n == 0) {
compiler_panic(pp->c, loc, "#include: empty after macro replacement");
@@ -838,7 +838,7 @@ static void do_line(Pp* pp, const Tok* line, u32 n, SrcLoc loc) {
if (n == 0) compiler_panic(pp->c, loc, "#line: missing arguments");
slice = arena_array(pp->arena, Tok, n);
memcpy(slice, line, sizeof(Tok) * n);
- expand_arg_to_eof(pp, slice, n, &exp);
+ expand_arg_to_eof(pp, slice, NULL, n, &exp);
if (exp.n == 0 || exp.data[0].kind != TOK_NUM) {
compiler_panic(pp->c, loc, "#line: expected line number");
diff --git a/lang/c/pp/pp_expand.c b/lang/c/pp/pp_expand.c
@@ -391,6 +391,7 @@ static void expand_object_macro(Pp* pp, const Macro* m, const Tok* invoke,
* are restored as a buffer source so subsequent reads still see them. */
int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) {
TokVec saved = {0};
+ HsVec saved_hs = {0};
int saw_ws = 0;
Tok t;
HidesetId hs;
@@ -400,11 +401,12 @@ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) {
if (t.kind == TOK_NEWLINE) {
saw_ws = 1;
tv_push(pp, &saved, t);
+ hsv_push(pp, &saved_hs, hs);
continue;
}
if (t.kind == TOK_EOF) {
/* No '(' — push back saved tokens, leave EOF for next read. */
- if (saved.n) push_buf(pp, saved.data, NULL, saved.n);
+ if (saved.n) push_buf(pp, saved.data, saved_hs.data, saved.n);
*ws_has_space_out = saw_ws;
return 0;
}
@@ -417,7 +419,8 @@ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) {
}
/* Save this non-`(` token too and push back. */
tv_push(pp, &saved, t);
- push_buf(pp, saved.data, NULL, saved.n);
+ hsv_push(pp, &saved_hs, hs);
+ push_buf(pp, saved.data, saved_hs.data, saved.n);
*ws_has_space_out = saw_ws;
return 0;
}
@@ -426,7 +429,7 @@ int peek_for_invoke_paren(Pp* pp, int* ws_has_space_out) {
/* Run macro expansion on a fixed token sequence to completion, yielding the
* fully-expanded token sequence. Used to pre-expand each function-macro
* argument before substitution (§6.10.3.1 ¶1). */
-void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out) {
+void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) {
TokSrc src;
Tok t;
@@ -434,7 +437,7 @@ void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out) {
src.kind = SRC_BUF;
src.scope_top = 1;
src.toks = in;
- src.hs = NULL;
+ src.hs = hs;
src.n = nin;
src_push(pp, src);
@@ -458,6 +461,7 @@ void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out) {
typedef struct ArgList {
/* Unexpanded arg tokens (raw as collected from invocation). */
Tok* raw;
+ HidesetId* raw_hs;
u32 raw_n;
u32* raw_start; /* size n_args + 1 (sentinel = raw_n) */
/* Pre-expanded tokens. */
@@ -472,6 +476,7 @@ typedef struct ArgList {
static Tok read_invocation_args(Pp* pp, const Macro* m, SrcLoc invoke_loc,
ArgList* out) {
TokVec raw = {0};
+ HsVec raw_hs = {0};
u32* starts;
u32 starts_cap = 0;
u32 n_args = 0;
@@ -552,6 +557,7 @@ static Tok read_invocation_args(Pp* pp, const Macro* m, SrcLoc invoke_loc,
if (m->is_variadic && n_args + 1 >= m->n_params) {
/* This comma is part of __VA_ARGS__. Push it. */
tv_push(pp, &raw, t);
+ hsv_push(pp, &raw_hs, hs);
first_token_of_arg = 0;
continue;
}
@@ -571,9 +577,8 @@ static Tok read_invocation_args(Pp* pp, const Macro* m, SrcLoc invoke_loc,
}
}
tv_push(pp, &raw, t);
+ hsv_push(pp, &raw_hs, hs);
first_token_of_arg = 0;
- (void)hs; /* hideset of raw arg tokens carried for blue-paint
- * propagation in the arg's pre-expansion */
}
done:
/* Validate arity. */
@@ -612,6 +617,7 @@ done:
}
}
out->raw = raw.data;
+ out->raw_hs = raw_hs.data;
out->raw_n = raw.n;
out->raw_start = starts;
out->n_args = n_args;
@@ -633,7 +639,8 @@ static void preexpand_args(Pp* pp, ArgList* a) {
* own it without aliasing. */
Tok* slice = arena_array(pp->arena, Tok, hi - lo);
memcpy(slice, &a->raw[lo], sizeof(Tok) * (hi - lo));
- expand_arg_to_eof(pp, slice, hi - lo, &exp);
+ expand_arg_to_eof(pp, slice, a->raw_hs ? &a->raw_hs[lo] : NULL,
+ hi - lo, &exp);
}
exp_start[i + 1] = exp.n;
}
diff --git a/lang/c/pp/pp_priv.h b/lang/c/pp/pp_priv.h
@@ -186,6 +186,12 @@ typedef struct TokVec {
u32 cap;
} TokVec;
+typedef struct HsVec {
+ HidesetId* data;
+ u32 n;
+ u32 cap;
+} HsVec;
+
static inline void tv_grow(Pp* pp, TokVec* v, u32 want) {
u32 nc;
if (v->cap >= want) return;
@@ -204,6 +210,24 @@ static inline void tv_push(Pp* pp, TokVec* v, Tok t) {
v->data[v->n++] = t;
}
+static inline void hsv_grow(Pp* pp, HsVec* v, u32 want) {
+ u32 nc;
+ if (v->cap >= want) return;
+ nc = v->cap ? v->cap * 2 : 8;
+ while (nc < want) nc *= 2;
+ {
+ HidesetId* nb = arena_array(pp->arena, HidesetId, nc);
+ if (v->n) memcpy(nb, v->data, sizeof(HidesetId) * v->n);
+ v->data = nb;
+ v->cap = nc;
+ }
+}
+
+static inline void hsv_push(Pp* pp, HsVec* v, HidesetId hs) {
+ hsv_grow(pp, v, v->n + 1);
+ v->data[v->n++] = hs;
+}
+
/* Growable char buffer (arena-backed). */
typedef struct CharBuf {
char* data;
@@ -251,7 +275,7 @@ int hs_contains(Pp* pp, HidesetId id, Sym s);
Macro* mt_get(Pp* pp, Sym name);
void mt_put(Pp* pp, Sym name, Macro* m);
void mt_del(Pp* pp, Sym name);
-void expand_arg_to_eof(Pp* pp, Tok* in, u32 nin, TokVec* out);
+void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out);
/* --- pp_directive.c → pp_expand.c --- */
i64 eval_if_expr(Pp* pp, const Tok* line, u32 n, SrcLoc loc);
diff --git a/src/api/cg.c b/src/api/cg.c
@@ -3560,6 +3560,8 @@ void cfree_cg_addr(CfreeCg *g) {
return;
v = api_pop(g);
pty = cg_type_ptr_to(g->c, api_sv_type(&v));
+ if (v.source_local != CFREE_CG_LOCAL_NONE)
+ api_local_const_address_taken(g, v.source_local);
dst = api_lvalue_addr(g, &v, pty);
api_release(g, &v);
api_push(g, api_make_sv(dst, pty));
diff --git a/test/parse/CORPUS.md b/test/parse/CORPUS.md
@@ -172,6 +172,7 @@ here for completeness once they're real cases.
| `6_5_44_relational_all` | ★ | `(3>1)+(1<3)+(2<=2)+(2>=2)+(1!=2)+(2==2)` — covers `>`, `<=`, `>=`, `!=` | 6 |
| `6_5_45_ptr_eq_null` | ★ | `(p==0)+(q!=0)+(p!=q)` — pointer equality with null and pointer-vs-pointer `!=` | 3 |
| `6_5_46_ptr_relcmp` | ★ | `(p<q)+(q>p)+(p<=p)` — relational comparison of pointers into same array | 3 |
+| `6_5_46b_cmp_to_bool_init` | ★ | `_Bool b = (1 != 0);` — comparison result is assignable through `_Bool` conversion | 42 |
| `6_5_47_ternary_arith_conv` | ★ | `int c=1; double r = c ? 1 : 2.5; return (int)(r+41.0);` — ternary arms get usual arithmetic conversion to double | 42 |
| `6_5_48_ternary_ptr_null` | ★ | `int *p = 1 ? &x : 0; return *p;` — one arm pointer, other null pointer constant | 42 |
| `6_5_49_compound_assign_sub` | ★ | `int x=44; x-=2; return x;` — `-=` operator | 42 |
@@ -213,6 +214,7 @@ copy (BYVAL). Each row exercises one cell of the (1-reg / 2-reg / copy)
| `6_6_07_enum_array_bound` | ★ | file scope: `typedef enum {A,B,C,N} E; static const int marks[N]={10,12,20};` — enum constant as array bound | 42 |
| `6_6_08_func_addr_static_init` | ★ | `static int helper(int x){return x;}` + `static const FN g = helper;` — function designator as static-init address constant (no `&`) | 42 |
| `6_6_09_func_addr_array_static` | ★ | `static const FN ops[2] = {add1, dbl};` — array of function-pointer static-init constants | 42 |
+| `6_6_10_logical_cond_const` | ★ | logical `&&` / `||` and `?:` in integer constant expressions | 42 |
## §6.7 Declarations
@@ -499,6 +501,7 @@ ordinary calls.
| `builtin_23_atomic_long_literal_convert` | ★ | store/RMW integer literals converted to unsigned long atomic object type | 42 |
| `builtin_24_atomic_lock_free` | ★ | target-aware lock-free folding through `if`, `&&`, and `||`; dead 16-byte atomic arms suppress codegen | 42 |
| `builtin_25_atomic_fetch_nand` | ★ | `__atomic_fetch_nand` lowers to atomic NAND RMW | 42 |
+| `builtin_26_sadd_overflow` | RED | `__builtin_sadd_overflow` stores result and returns overflow flag; currently undeclared | 42 |
| `builtin_99_syscall0` | (deferred) | `__cfree_syscall0` requires linking against the syscall stub; covered in `test/libc` | — |
## Variadic coverage
diff --git a/test/parse/cases/6_5_46b_cmp_to_bool_init.c b/test/parse/cases/6_5_46b_cmp_to_bool_init.c
@@ -0,0 +1,4 @@
+int test_main(void) {
+ _Bool b = (1 != 0);
+ return b ? 42 : 0;
+}
diff --git a/test/parse/cases/6_5_46b_cmp_to_bool_init.expected b/test/parse/cases/6_5_46b_cmp_to_bool_init.expected
@@ -0,0 +1 @@
+42
diff --git a/test/parse/cases/6_6_10_logical_cond_const.c b/test/parse/cases/6_6_10_logical_cond_const.c
@@ -0,0 +1,6 @@
+_Static_assert(0 || (1 && 1), "logical operators are integer constants");
+
+int test_main(void) {
+ int a[0 ? 1 : 7];
+ return (int)(sizeof a / sizeof a[0]) * (1 ? 6 : 0);
+}
diff --git a/test/parse/cases/6_6_10_logical_cond_const.expected b/test/parse/cases/6_6_10_logical_cond_const.expected
@@ -0,0 +1 @@
+42
diff --git a/test/parse/cases/builtin_26_sadd_overflow.c b/test/parse/cases/builtin_26_sadd_overflow.c
@@ -0,0 +1,7 @@
+int test_main(void) {
+ int r = 0;
+ _Bool ov = __builtin_sadd_overflow(40, 2, &r);
+ if (ov || r != 42) return 1;
+ ov = __builtin_sadd_overflow(2147483647, 1, &r);
+ return ov ? 42 : 2;
+}
diff --git a/test/parse/cases/builtin_26_sadd_overflow.expected b/test/parse/cases/builtin_26_sadd_overflow.expected
@@ -0,0 +1 @@
+42
diff --git a/test/pp/CORPUS.md b/test/pp/CORPUS.md
@@ -103,6 +103,7 @@ Both runners pin the values of the otherwise-environmental predefined macros:
| 62_rescan_paste_to_macro | 6.10.3.3 ¶3 | post-paste token is available for further replacement |
| 63_rescan_not_directive | 6.10.3.4 ¶3 | tokens produced by expansion are not a directive |
| 64_rescan_self_in_func | 6.10.3.4 ¶2 | function-like macro does not re-invoke itself in rescan |
+| 65_rescan_arg_hideset_prescan | 6.10.3.4 ¶2 | argument prescan keeps no-reexpand state from caller |
### 70–79 Variadic macros (§6.10.3 ¶12, §6.10.3.1 ¶2)
diff --git a/test/pp/cases/65_rescan_arg_hideset_prescan.c b/test/pp/cases/65_rescan_arg_hideset_prescan.c
@@ -0,0 +1,5 @@
+#define CAT1(a, b) a##b
+#define CAT(a, b) CAT1(a, b)
+#define SUF x
+#define T CAT(T, SUF)
+T
diff --git a/test/pp/cases/65_rescan_arg_hideset_prescan.expected b/test/pp/cases/65_rescan_arg_hideset_prescan.expected
@@ -0,0 +1 @@
+Tx