commit 74593e204e86eea39e9d16381ba506bf05448c80
parent 5dbea4132c4faacb1c5a1056b67a75ce698c36bf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 1 May 2026 15:14:04 -0700
cc: short-circuit && and || in constant-expression evaluator
The const-expr evaluator's `parse-const-lor` and `parse-const-land`
greedily evaluated both operands, so a constant expression like
`1 || (1/0)` aborted with "const-expr: divide by zero" instead of
yielding 1. C11 §6.5.13 ¶4 / §6.5.14 ¶4 require the rhs to remain
unevaluated when the result is determined by the lhs, including in
constant expressions used by `enum`, array sizes, and `case`.
Skip the rhs by scanning tokens at paren/brack depth 0 until a
boundary token (lower-or-equal-binding operator, `?`, `:`, `,`, `;`,
`}`, EOF, or matching close). For `||` we also stop on `||` (left
associative) but absorb `&&` since it binds tighter; for `&&` we
stop on either.
Regression: tests/cc/240-parse-const-shortcircuit.c.
Diffstat:
3 files changed, 74 insertions(+), 6 deletions(-)
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -4577,25 +4577,77 @@
(else c))))
(define (parse-const-lor ps)
+ ;; Short-circuit per C11 §6.5.14 ¶4: if `a` is non-zero the rhs is
+ ;; not evaluated, so `1 || (1/0)` must yield 1, not abort.
(let lp ((a (parse-const-land ps)))
(cond
((at-punct? ps 'lor)
(advance ps)
- (let ((b (parse-const-land ps)))
- (lp (cons (if (or (%const-bool? a) (%const-bool? b)) 1 0)
- %t-i32))))
+ (cond
+ ((%const-bool? a)
+ (%const-skip-lor-rhs ps)
+ (lp (cons 1 %t-i32)))
+ (else
+ (let ((b (parse-const-land ps)))
+ (lp (cons (if (%const-bool? b) 1 0) %t-i32))))))
(else a))))
(define (parse-const-land ps)
+ ;; Short-circuit per C11 §6.5.13 ¶4: if `a` is zero the rhs is not
+ ;; evaluated, so `0 && (1/0)` must yield 0, not abort.
(let lp ((a (parse-const-bor ps)))
(cond
((at-punct? ps 'land)
(advance ps)
- (let ((b (parse-const-bor ps)))
- (lp (cons (if (and (%const-bool? a) (%const-bool? b)) 1 0)
- %t-i32))))
+ (cond
+ ((not (%const-bool? a))
+ (%const-skip-land-rhs ps)
+ (lp (cons 0 %t-i32)))
+ (else
+ (let ((b (parse-const-bor ps)))
+ (lp (cons (if (%const-bool? b) 1 0) %t-i32))))))
(else a))))
+;; Skip the rhs of a short-circuited && / ||. The rhs grammar is
+;; the operand level of the operator: parse-const-bor for &&,
+;; parse-const-land for ||. We can't just call those parsers because
+;; the rhs may itself be invalid as a constant expression (e.g.
+;; `1/0`); instead, scan tokens at paren/brack depth 0 until we hit
+;; another operator at the same-or-lower binding level, comma,
+;; semicolon, colon, qmark, rbrace, rbrack, rparen, or EOF.
+(define (%const-skip-land-rhs ps)
+ ;; rhs of && is a parse-const-bor — stop on `&&`, `||`, `?`, `:`,
+ ;; `,`, `;`, `}`, and any closing/separator at depth 0.
+ (%const-skip-rhs-til ps
+ (lambda (v)
+ (or (eq? v 'land) (eq? v 'lor) (eq? v 'qmark) (eq? v 'colon)
+ (eq? v 'comma) (eq? v 'semi) (eq? v 'rbrace)))))
+(define (%const-skip-lor-rhs ps)
+ ;; rhs of || is a parse-const-land — stop on `||` (left-assoc),
+ ;; `?`, `:`, `,`, `;`, `}`. `&&` binds TIGHTER than `||`, so it is
+ ;; absorbed into the rhs and we do NOT stop on it.
+ (%const-skip-rhs-til ps
+ (lambda (v)
+ (or (eq? v 'lor) (eq? v 'qmark) (eq? v 'colon)
+ (eq? v 'comma) (eq? v 'semi) (eq? v 'rbrace)))))
+(define (%const-skip-rhs-til ps stop?)
+ (let lp ((d 0))
+ (let ((t (peek ps)))
+ (cond
+ ((eq? (tok-kind t) 'EOF) #t)
+ ((not (eq? (tok-kind t) 'PUNCT))
+ (advance ps) (lp d))
+ (else
+ (let ((v (tok-value t)))
+ (cond
+ ((or (eq? v 'lparen) (eq? v 'lbrack))
+ (advance ps) (lp (+ d 1)))
+ ((or (eq? v 'rparen) (eq? v 'rbrack))
+ (cond ((zero? d) #t)
+ (else (advance ps) (lp (- d 1)))))
+ ((and (zero? d) (stop? v)) #t)
+ (else (advance ps) (lp d)))))))))
+
(define (parse-const-bor ps)
(let lp ((a (parse-const-bxor ps)))
(cond
diff --git a/tests/cc/240-parse-const-shortcircuit.c b/tests/cc/240-parse-const-shortcircuit.c
@@ -0,0 +1,15 @@
+/* Constant-expression && / || must short-circuit (C11 §6.6 ¶3 and
+ * §6.5.13/14): the unevaluated subexpression need not be a valid
+ * constant expression. The parser's constant-expression evaluator
+ * was eagerly evaluating both sides, so `1 || (1/0)` aborted with
+ * "const-expr: divide by zero" instead of returning 1.
+ */
+
+enum { ANY_TRUE = 1 || (1/0) };
+enum { ANY_FALSE = 0 && (1/0) };
+
+int main(int argc, char **argv) {
+ if (ANY_TRUE != 1) return 1;
+ if (ANY_FALSE != 0) return 2;
+ return 0;
+}
diff --git a/tests/cc/240-parse-const-shortcircuit.expected-exit b/tests/cc/240-parse-const-shortcircuit.expected-exit
@@ -0,0 +1 @@
+0