commit a91451aff30d4b7ac581ac642f0c69641e3127f8
parent e883106d20eda5ab8aa4c0f52c3594c9043b633c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 1 May 2026 19:44:25 -0700
cc/parse: support brace elision in nested aggregate initializers (C99 §6.7.8 ¶22)
When an inner aggregate inside an outer initializer has no enclosing
brace, draw items from the parent list deep enough to fill the inner
aggregate. Previously only the first sub-aggregate consumed elements;
subsequent ones were left zero (e.g. `struct S arr[2] = {1,2,3,4}`
zeroed `arr[1]`).
Adds /mode variants of the four list parsers (global+local × array+
struct) parameterized by `brace?`. In no-brace mode each list:
- terminates on field/element exhaustion, on `}` (parent's closer),
or on `.`/`[` designators (which target the enclosing aggregate);
- consumes commas only between its own items, leaving the trailing
comma for the parent loop;
- dispatches into a nested no-brace recursion when its element is
itself an aggregate without `{`.
Tests:
241-init-brace-elision-struct-array block-scope struct array
242-init-brace-elision-array-struct block-scope int[2][2]
243-init-brace-elision-global file-scope struct array
Diffstat:
7 files changed, 179 insertions(+), 37 deletions(-)
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -5590,15 +5590,30 @@
;; initializers actually consumed (used by parse-init-global to resolve
;; an inferred top-level length). C99 forbids inferred length in
;; nested array elements, so recursive callers ignore `count`.
+;;
+;; `brace?` controls termination: when #t (the normal case), the loop
+;; consumes elements until `}` is seen. When #f (brace-elision recursion
+;; from C99 §6.7.8 ¶22), the loop consumes exactly `decl` elements from
+;; the parent stream and returns without expecting `}`. In no-brace
+;; mode, a leading `.` or `[` designator targets the enclosing aggregate
+;; — the recursion terminates immediately, padding the unfilled tail.
(define (%parse-init-array-list ps ty)
- ;; Element-list array initializer; assumes `{` already consumed.
+ (%parse-init-array-list/mode ps ty #t))
+
+(define (%parse-init-array-list/mode ps ty brace?)
+ ;; Element-list array initializer; assumes `{` already consumed when
+ ;; brace? is #t.
(let* ((elem (%init-array-elem-type ty))
(esize (ctype-size elem))
(decl (%init-array-decl-len ty)))
(let lp ((acc '()) (count 0))
(cond
- ((at-punct? ps 'rbrace)
- (advance ps)
+ ((cond (brace? (at-punct? ps 'rbrace))
+ (else (or (>= count (cond ((< decl 0) 0) (else decl)))
+ (at-punct? ps 'rbrace)
+ (at-punct? ps 'dot)
+ (at-punct? ps 'lbrack))))
+ (cond (brace? (advance ps)))
;; Pad to declared length if longer than count.
(let* ((final (cond ((< decl 0) count) (else decl)))
(pad (- final count)))
@@ -5637,9 +5652,32 @@
(cond ((at-punct? ps 'comma) (advance ps)))
(expect-punct ps 'rbrace)
(list p)))))
+ ;; Brace elision: aggregate element with no `{`.
+ ;; Draw items from the SAME parent stream deep
+ ;; enough to fill `elem`. C99 §6.7.8 ¶22.
+ ((eq? (ctype-kind elem) 'arr)
+ (let-values (((p _c)
+ (%parse-init-array-list/mode ps elem #f)))
+ p))
+ ((or (eq? (ctype-kind elem) 'struct)
+ (eq? (ctype-kind elem) 'union))
+ (%parse-init-struct-list/mode ps elem #f))
(else
(list (%const-init-piece ps elem))))))
- (cond ((at-punct? ps 'comma) (advance ps)))
+ ;; Inter-item comma: consume except for the comma
+ ;; following our LAST item in no-brace mode — that
+ ;; one belongs to the enclosing parent.
+ (cond
+ (brace?
+ (cond ((at-punct? ps 'comma) (advance ps))))
+ (else
+ ;; no-brace: consume comma only if more items
+ ;; remain in our quota.
+ (cond
+ ((and (< (+ count 1)
+ (cond ((< decl 0) 0) (else decl)))
+ (at-punct? ps 'comma))
+ (advance ps)))))
p)))))
(lp (%init-main-prepend-reversed piece acc) (+ count 1))))))))
@@ -5703,16 +5741,24 @@
(ins (cdr ys) (cons (car ys) head)))))))))))
(define (%parse-init-struct-list ps ty)
- ;; Struct/union initializer; assumes `{` already consumed. Supports
- ;; positional and `.field = expr` forms — including out-of-order
- ;; designators ({.y=5, .x=7}). Each entry records its absolute
- ;; offset; %merge-init-entries sorts and pads at the closing brace.
+ (%parse-init-struct-list/mode ps ty #t))
+
+(define (%parse-init-struct-list/mode ps ty brace?)
+ ;; Struct/union initializer; assumes `{` already consumed when brace?.
+ ;; In no-brace mode (brace elision, C99 §6.7.8 ¶22), terminate when
+ ;; positional fields are exhausted, when a `}` is seen (belongs to
+ ;; the enclosing aggregate), or when a designator (`.`) appears (it
+ ;; targets the enclosing aggregate). Doesn't consume the trailing
+ ;; comma after the last field — that belongs to the parent list.
(let* ((fields (%init-struct-fields ty))
(size (ctype-size ty)))
(let lp ((entries '()) (rest fields))
(cond
- ((at-punct? ps 'rbrace)
- (advance ps)
+ ((cond (brace? (at-punct? ps 'rbrace))
+ (else (or (null? rest)
+ (at-punct? ps 'rbrace)
+ (at-punct? ps 'dot))))
+ (cond (brace? (advance ps)))
(%merge-init-entries (reverse entries) size))
(else
(let* ((designated? (at-punct? ps 'dot))
@@ -5752,19 +5798,36 @@
(cond ((at-punct? ps 'comma) (advance ps)))
(expect-punct ps 'rbrace)
(list p)))))
+ ;; Brace elision for aggregate field with no `{`.
+ ((eq? (ctype-kind fty) 'arr)
+ (let-values (((p _c)
+ (%parse-init-array-list/mode ps fty #f)))
+ p))
+ ((or (eq? (ctype-kind fty) 'struct)
+ (eq? (ctype-kind fty) 'union))
+ (%parse-init-struct-list/mode ps fty #f))
(else
- (list (%const-init-piece ps fty))))))
- (cond ((at-punct? ps 'comma) (advance ps)))
- (lp (cons (cons foff piece-list) entries)
- (cond
- (designated?
- ;; designated init: drop fields up to and including target
- (let drop ((xs fields))
- (cond
- ((null? xs) '())
- ((equal? (car (car xs)) fname) (cdr xs))
- (else (drop (cdr xs))))))
- (else (cdr rest))))))))))
+ (list (%const-init-piece ps fty)))))
+ (rest1
+ (cond
+ (designated?
+ ;; designated init: drop fields up to and including target
+ (let drop ((xs fields))
+ (cond
+ ((null? xs) '())
+ ((equal? (car (car xs)) fname) (cdr xs))
+ (else (drop (cdr xs))))))
+ (else (cdr rest)))))
+ ;; Inter-item comma: consume except for the comma after our
+ ;; LAST field in no-brace mode (belongs to enclosing list).
+ (cond
+ (brace?
+ (cond ((at-punct? ps 'comma) (advance ps))))
+ (else
+ (cond ((and (not (null? rest1))
+ (at-punct? ps 'comma))
+ (advance ps)))))
+ (lp (cons (cons foff piece-list) entries) rest1)))))))
;; ----- Local aggregate initializers ------------------------------------
;; Emits per-element store sequences via cg ops into the slot of `sm`
@@ -5822,13 +5885,20 @@
(cg-push (ps-cg ps) (%opnd 'frame ty base-off #t)))
(define (%parse-init-local-array-list ps sm base-off ty)
+ (%parse-init-local-array-list/mode ps sm base-off ty #t))
+
+(define (%parse-init-local-array-list/mode ps sm base-off ty brace?)
(let* ((elem (%init-array-elem-type ty))
(esize (ctype-size elem))
(decl (%init-array-decl-len ty)))
(let lp ((i 0))
(cond
- ((at-punct? ps 'rbrace)
- (advance ps)
+ ((cond (brace? (at-punct? ps 'rbrace))
+ (else (or (>= i (cond ((< decl 0) 0) (else decl)))
+ (at-punct? ps 'rbrace)
+ (at-punct? ps 'dot)
+ (at-punct? ps 'lbrack))))
+ (cond (brace? (advance ps)))
;; Inferred-length auto path is pre-existing broken (slot
;; allocated off size=-1, sm-type unfixed). See note in
;; parse-init-local-aggregate STR branch.
@@ -5877,12 +5947,29 @@
(cg-assign (ps-cg ps)) (cg-pop (ps-cg ps))
(cond ((at-punct? ps 'comma) (advance ps)))
(expect-punct ps 'rbrace))))
+ ;; Brace elision: aggregate element with no `{`. Draw items
+ ;; from the SAME parent stream deep enough to fill `elem`.
+ ;; C99 §6.7.8 ¶22.
+ ((eq? (ctype-kind elem) 'arr)
+ (%parse-init-local-array-list/mode ps sm eoff elem #f))
+ ((or (eq? (ctype-kind elem) 'struct)
+ (eq? (ctype-kind elem) 'union))
+ (%parse-init-local-struct-list/mode ps sm eoff elem #f))
(else
(%push-frame-elem-lval ps eoff elem)
(parse-expr-bp ps 4) (rval! ps)
(cg-cast (ps-cg ps) elem)
(cg-assign (ps-cg ps)) (cg-pop (ps-cg ps))))
- (cond ((at-punct? ps 'comma) (advance ps)))
+ ;; Inter-item comma: in no-brace mode, don't eat the comma
+ ;; that follows our LAST item (it belongs to the parent).
+ (cond
+ (brace?
+ (cond ((at-punct? ps 'comma) (advance ps))))
+ (else
+ (cond ((and (< (+ i 1)
+ (cond ((< decl 0) 0) (else decl)))
+ (at-punct? ps 'comma))
+ (advance ps)))))
(lp (+ i 1))))))))
(define (%bv-in-list? bv xs)
@@ -5928,17 +6015,27 @@
(zb (+ j 1)))))))
(define (%parse-init-local-struct-list ps sm base-off ty)
+ (%parse-init-local-struct-list/mode ps sm base-off ty #t))
+
+(define (%parse-init-local-struct-list/mode ps sm base-off ty brace?)
;; Track each initialized field by name in `seen`; at the closing brace
;; zero every field NOT in `seen`. Tracking by name (rather than
;; positional "remaining" fields) handles a designator jumping
;; backwards correctly — e.g. `{.y = 5}` must still zero `x`.
;; C requires every unmentioned member of an aggregate with at least
;; one designator/initializer to be zeroed (C11 §6.7.9 ¶21).
+ ;;
+ ;; In no-brace mode (brace elision, C99 §6.7.8 ¶22): terminate when
+ ;; positional fields exhausted, on `}` (parent's), or on `.` designator
+ ;; (targets parent). Don't consume trailing comma after our last field.
(let ((fields (%init-struct-fields ty)))
(let lp ((rest fields) (seen '()))
(cond
- ((at-punct? ps 'rbrace)
- (advance ps)
+ ((cond (brace? (at-punct? ps 'rbrace))
+ (else (or (null? rest)
+ (at-punct? ps 'rbrace)
+ (at-punct? ps 'dot))))
+ (cond (brace? (advance ps)))
(for-each
(lambda (f)
(cond ((not (%anon-touched? f seen))
@@ -5980,21 +6077,36 @@
(cg-assign (ps-cg ps)) (cg-pop (ps-cg ps))
(cond ((at-punct? ps 'comma) (advance ps)))
(expect-punct ps 'rbrace))))
+ ;; Brace elision for aggregate field with no `{`.
+ ((eq? (ctype-kind fty) 'arr)
+ (%parse-init-local-array-list/mode ps sm eoff fty #f))
+ ((or (eq? (ctype-kind fty) 'struct)
+ (eq? (ctype-kind fty) 'union))
+ (%parse-init-local-struct-list/mode ps sm eoff fty #f))
(else
(%push-frame-elem-lval ps eoff fty)
(parse-expr-bp ps 4) (rval! ps)
(cg-cast (ps-cg ps) fty)
(cg-assign (ps-cg ps)) (cg-pop (ps-cg ps))))
- (cond ((at-punct? ps 'comma) (advance ps)))
- (lp (cond
- (designated?
- (let drop ((xs fields))
- (cond
- ((null? xs) '())
- ((equal? (car (car xs)) fname) (cdr xs))
- (else (drop (cdr xs))))))
- (else (cdr rest)))
- (cons fname seen))))))))
+ (let ((rest1
+ (cond
+ (designated?
+ (let drop ((xs fields))
+ (cond
+ ((null? xs) '())
+ ((equal? (car (car xs)) fname) (cdr xs))
+ (else (drop (cdr xs))))))
+ (else (cdr rest)))))
+ ;; Inter-item comma: in no-brace mode, don't eat the comma
+ ;; that follows our LAST field (belongs to enclosing list).
+ (cond
+ (brace?
+ (cond ((at-punct? ps 'comma) (advance ps))))
+ (else
+ (cond ((and (not (null? rest1))
+ (at-punct? ps 'comma))
+ (advance ps)))))
+ (lp rest1 (cons fname seen)))))))))
;; parse-fn-body: bind the fn-sym for recursive lookup, then parse the
diff --git a/tests/cc/241-init-brace-elision-struct-array.c b/tests/cc/241-init-brace-elision-struct-array.c
@@ -0,0 +1,9 @@
+// tests/cc/241-init-brace-elision-struct-array.c — brace elision for
+// nested aggregate initializers. C99 §6.7.8 ¶22: when an inner
+// aggregate has no enclosing brace, items are drawn from the parent
+// list deep enough to fill it.
+struct S { int a; int b; };
+int main(void) {
+ struct S arr[2] = { 1, 2, 3, 4 }; /* no inner braces */
+ return arr[0].a + arr[0].b + arr[1].a + arr[1].b; /* expect 10 */
+}
diff --git a/tests/cc/241-init-brace-elision-struct-array.expected-exit b/tests/cc/241-init-brace-elision-struct-array.expected-exit
@@ -0,0 +1 @@
+10
diff --git a/tests/cc/242-init-brace-elision-array-struct.c b/tests/cc/242-init-brace-elision-array-struct.c
@@ -0,0 +1,11 @@
+// tests/cc/242-init-brace-elision-array-struct.c — brace elision in
+// nested arrays of arrays. C99 §6.7.8 ¶22.
+//
+// int m[2][2] = {1,2,3,4} should fill m[0][0]=1, m[0][1]=2,
+// m[1][0]=3, m[1][1]=4. Sum proves all four cells received the right
+// value (1+2+3+4 = 10).
+int main(void) {
+ int m[2][2] = { 1, 2, 3, 4 };
+ int *p = (int *)m;
+ return *p + *(p + 1) + *(p + 2) + *(p + 3);
+}
diff --git a/tests/cc/242-init-brace-elision-array-struct.expected-exit b/tests/cc/242-init-brace-elision-array-struct.expected-exit
@@ -0,0 +1 @@
+10
diff --git a/tests/cc/243-init-brace-elision-global.c b/tests/cc/243-init-brace-elision-global.c
@@ -0,0 +1,7 @@
+// tests/cc/243-init-brace-elision-global.c — brace elision in
+// file-scope (global) initializer. C99 §6.7.8 ¶22.
+struct S { int a; int b; };
+struct S arr[2] = { 1, 2, 3, 4 }; /* no inner braces */
+int main(void) {
+ return arr[0].a + arr[0].b + arr[1].a + arr[1].b; /* expect 10 */
+}
diff --git a/tests/cc/243-init-brace-elision-global.expected-exit b/tests/cc/243-init-brace-elision-global.expected-exit
@@ -0,0 +1 @@
+10