commit 200e8f4f18f3894e47e56db1a1582f5b4ea24850
parent dd522a0494571c5ac9875945e924d23e8d2fb593
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 28 Apr 2026 15:49:42 -0700
cc: offsetof support in const exprs
Diffstat:
3 files changed, 186 insertions(+), 5 deletions(-)
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -4585,6 +4585,10 @@
(define (parse-const-cast ps)
;; (typename) operand — distinguished from ( expr ) by paren-is-group?.
+ ;; Pointer casts are accepted only as a type re-tag — the integer
+ ;; offset rides through unchanged. This is what the offsetof idiom
+ ;; `(T *)0` and the outer `(size_t) <ptr-const>` need; we do not
+ ;; admit general pointer arithmetic in const-expr.
(cond
((at-punct? ps 'lparen)
(cond
@@ -4594,11 +4598,16 @@
((_n ty) (parse-declarator ps bty)))
(expect-punct ps 'rparen)
(cond
- ((not (%ctype-int? ty))
- (die (tok-loc (peek ps)) "const-expr: cast must be integer"
- (ctype-kind ty))))
- (let ((v (parse-const-cast ps)))
- (cons (%const-trunc (car v) ty) ty))))
+ ((%ctype-int? ty)
+ (let ((v (parse-const-cast ps)))
+ (cons (%const-trunc (car v) ty) ty)))
+ ((eq? (ctype-kind ty) 'ptr)
+ (let ((v (parse-const-cast ps)))
+ (cons (car v) ty)))
+ (else
+ (die (tok-loc (peek ps))
+ "const-expr: cast must be integer or pointer"
+ (ctype-kind ty))))))
(else (parse-const-unary ps))))
(else (parse-const-unary ps))))
@@ -4640,6 +4649,16 @@
(advance ps)
(let ((vp (parse-const-cast ps)))
(cons (if (%const-bool? vp) 0 1) %t-i32)))
+ (($ tok? (kind PUNCT) (value amp))
+ ;; Address-of in const-expr context. Restricted to the offsetof
+ ;; idiom: a null-pointer-typed base reached via (T *)0 (with
+ ;; optional grouping/deref) followed by ->/. field selectors.
+ ;; The integer value is the running byte offset; '&' wraps the
+ ;; designator's type in a pointer for any outer integer cast to
+ ;; consume.
+ (advance ps)
+ (let* ((dp (%const-parse-addrof-postfix ps)))
+ (cons (car dp) (%mk-ptr (cdr dp)))))
(($ tok? (kind KW) (value sizeof))
(advance ps)
(cond
@@ -4700,6 +4719,91 @@
(else (die (tok-loc t) "const-expr: bad operand"
(tok-value t))))))
+;; ====================================================================
+;; offsetof support inside const-expr.
+;;
+;; Recognises `&((T *)0)->FIELD`, `&(*(T *)0).FIELD`, and chains thereof
+;; — the only address-of idioms that show up in static initializers
+;; (tcc.c options_W[] / options_f[] / options_m[] tables, and any
+;; offsetof macro expansion of the same shape). Each helper threads a
+;; (offset . ctype) pair: integer byte offset of the running designator
+;; from the null base, plus the lvalue's ctype. Field lookup reuses
+;; %cg-find-field, so anonymous union/struct members work the same way
+;; as in regular field access.
+;; ====================================================================
+
+(define (%const-parse-addrof-postfix ps)
+ ;; postfix: primary ( -> FIELD | . FIELD )*
+ (let lp ((p (%const-parse-addrof-primary ps)))
+ (pmatch (peek ps)
+ (($ tok? (kind PUNCT) (value arrow))
+ (advance ps) (lp (%const-addrof-arrow ps p)))
+ (($ tok? (kind PUNCT) (value dot))
+ (advance ps) (lp (%const-addrof-dot ps p)))
+ (else p))))
+
+(define (%const-parse-addrof-primary ps)
+ ;; primary: ( T )expr ; pointer cast — the offsetof base
+ ;; | ( postfix ) ; grouping
+ ;; | * primary ; deref
+ (cond
+ ((at-punct? ps 'lparen)
+ (cond
+ ((%const-paren-is-cast? ps)
+ (let ((cv (parse-const-cast ps)))
+ (cond
+ ((not (eq? (ctype-kind (cdr cv)) 'ptr))
+ (die #f "const-expr: addr-of: head must be a pointer cast"
+ (ctype-kind (cdr cv)))))
+ cv))
+ (else
+ (advance ps)
+ (let ((r (%const-parse-addrof-postfix ps)))
+ (expect-punct ps 'rparen) r))))
+ ((at-punct? ps 'star)
+ (advance ps)
+ (let ((h (%const-parse-addrof-primary ps)))
+ (cond
+ ((not (eq? (ctype-kind (cdr h)) 'ptr))
+ (die #f "const-expr: addr-of: '*' on non-pointer"
+ (ctype-kind (cdr h)))))
+ (cons (car h) (ctype-ext (cdr h)))))
+ (else
+ (die (tok-loc (peek ps)) "const-expr: addr-of: unexpected token"
+ (tok-value (peek ps))))))
+
+(define (%const-addrof-arrow ps p)
+ (let* ((off (car p)) (ty (cdr p)))
+ (cond ((not (eq? (ctype-kind ty) 'ptr))
+ (die (tok-loc (peek ps)) "const-expr: -> on non-pointer"
+ (ctype-kind ty))))
+ (let* ((sty (ctype-ext ty)) (sk (ctype-kind sty)))
+ (cond ((not (or (eq? sk 'struct) (eq? sk 'union)))
+ (die (tok-loc (peek ps))
+ "const-expr: -> target not aggregate" sk)))
+ (%const-addrof-field ps sty off))))
+
+(define (%const-addrof-dot ps p)
+ (let* ((off (car p)) (ty (cdr p)) (k (ctype-kind ty)))
+ (cond ((not (or (eq? k 'struct) (eq? k 'union)))
+ (die (tok-loc (peek ps))
+ "const-expr: . on non-aggregate" k)))
+ (%const-addrof-field ps ty off)))
+
+(define (%const-addrof-field ps sty base-off)
+ (let ((nt (peek ps)))
+ (cond ((not (eq? (tok-kind nt) 'IDENT))
+ (die (tok-loc nt)
+ "const-expr: field selector needs an identifier"
+ (tok-value nt))))
+ (advance ps)
+ (let* ((fields (car (cddr (ctype-ext sty))))
+ (f (%cg-find-field fields (tok-value nt))))
+ (cond ((not f) (die (tok-loc nt)
+ "const-expr: no such field"
+ (tok-value nt))))
+ (cons (+ base-off (car (cddr f))) (cadr f)))))
+
;; sizeof EXPR / sizeof(EXPR) in const-expr context. Delegates to the
;; regular expression parser under a cg snapshot/rewind — same contract
;; as parse-unary's sizeof: the operand is parsed to learn its type but
diff --git a/tests/cc/126-offsetof-const.c b/tests/cc/126-offsetof-const.c
@@ -0,0 +1,76 @@
+/* The offsetof idiom in static initializers, as used by tcc.c's
+ * options_W[] / options_f[] / options_m[] tables (line 18026 of
+ * tcc.flat.c):
+ *
+ * ((size_t) &((T *)0)->FIELD)
+ *
+ * Outer cast to integer type, address-of a member access through a
+ * null pointer of the parent struct type. The expression is evaluated
+ * at translation time and equals offsetof(T, FIELD). Required only at
+ * static-initializer / const-expr granularity; runtime address-of is
+ * already supported.
+ *
+ * Also exercises the same form through anonymous union members, since
+ * struct Sym in tcc.c relies on that combination.
+ */
+
+struct Inner {
+ int a;
+ int b;
+ int c;
+};
+
+struct Outer {
+ int first;
+ long second;
+ int third;
+ union {
+ int anon_x;
+ long anon_y;
+ };
+ int trailing;
+};
+
+struct Flag { unsigned long off; int mask; const char *name; };
+
+/* File-scope static array — the precise tcc.c shape. */
+static const struct Flag options[] = {
+ { ((unsigned long) &((struct Outer *)0)->first), 0, "first" },
+ { ((unsigned long) &((struct Outer *)0)->second), 1, "second" },
+ { ((unsigned long) &((struct Outer *)0)->third), 2, "third" },
+ { ((unsigned long) &((struct Outer *)0)->anon_x), 3, "anon_x" },
+ { ((unsigned long) &((struct Outer *)0)->anon_y), 4, "anon_y" },
+ { ((unsigned long) &((struct Outer *)0)->trailing), 5, "trailing" },
+ { 0, 0, 0 }
+};
+
+/* Plain scalar inits to confirm the cast on its own works. */
+static unsigned long off_a = (unsigned long) &((struct Inner *)0)->a;
+static unsigned long off_b = (unsigned long) &((struct Inner *)0)->b;
+static unsigned long off_c = (unsigned long) &((struct Inner *)0)->c;
+
+/* Designator using `.` rather than `->`, taken via dereference. */
+static unsigned long off_dot_b = (unsigned long) &(*(struct Inner *)0).b;
+
+int main(int argc, char **argv) {
+ struct Outer o;
+ char *base = (char *) &o;
+
+ if (off_a != 0) return 1;
+ if (off_b != sizeof(int)) return 2;
+ if (off_c != sizeof(int) * 2) return 3;
+ if (off_dot_b != sizeof(int)) return 4;
+
+ if (options[0].off != (unsigned long)((char *)&o.first - base)) return 10;
+ if (options[1].off != (unsigned long)((char *)&o.second - base)) return 11;
+ if (options[2].off != (unsigned long)((char *)&o.third - base)) return 12;
+ if (options[3].off != (unsigned long)((char *)&o.anon_x - base)) return 13;
+ if (options[4].off != (unsigned long)((char *)&o.anon_y - base)) return 14;
+ if (options[5].off != (unsigned long)((char *)&o.trailing - base)) return 15;
+
+ if (options[0].mask != 0) return 20;
+ if (options[5].mask != 5) return 21;
+ if (options[6].off != 0 || options[6].mask != 0 || options[6].name != 0) return 22;
+
+ return 0;
+}
diff --git a/tests/cc/126-offsetof-const.expected-exit b/tests/cc/126-offsetof-const.expected-exit
@@ -0,0 +1 @@
+0