boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

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:
Mcc/cc.scm | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Atests/cc/126-offsetof-const.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/cc/126-offsetof-const.expected-exit | 1+
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