commit e2487bfe317a6eb027d4c5f7f2d5e223adf6f447
parent 36ddcbc0a20ca1364fcc516dfef3a9d0b79871f7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 1 May 2026 15:20:34 -0700
merge: const-expr short-circuit && and ||
Port worktree-agent-afc5236c517f9208c onto HEAD. The branch was based
on the pre-%const-binl evaluator, so the lor/land short-circuit was
spliced in alongside the new generic combiner: parse-const-lor and
parse-const-land are kept hand-rolled (with %const-skip-{lor,land}-rhs
helpers) while the rest of the binary levels stay on %const-binl.
Diffstat:
3 files changed, 83 insertions(+), 6 deletions(-)
diff --git a/cc/cc.scm b/cc/cc.scm
@@ -4526,15 +4526,76 @@
(let-values (((av bv _rt) (%const-arith-conv (%const-promote a) (%const-promote b))))
(cons (if (fn av bv) 1 0) %t-i32)))
+;; Short-circuit per C11 §6.5.13/14 ¶4: rhs is not evaluated when the
+;; lhs determines the result. Required so `1 || (1/0)` and
+;; `0 && (1/0)` yield 1/0 rather than aborting on divide-by-zero.
(define (parse-const-lor ps)
- (%const-binl ps parse-const-land
- (list (cons 'lor (lambda (a b)
- (cons (if (or (%const-bool? a) (%const-bool? b)) 1 0) %t-i32))))))
+ (let lp ((a (parse-const-land ps)))
+ (cond
+ ((at-punct? ps 'lor)
+ (advance ps)
+ (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)
- (%const-binl ps parse-const-bor
- (list (cons 'land (lambda (a b)
- (cons (if (and (%const-bool? a) (%const-bool? b)) 1 0) %t-i32))))))
+ (let lp ((a (parse-const-bor ps)))
+ (cond
+ ((at-punct? ps 'land)
+ (advance ps)
+ (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)
(%const-binl ps parse-const-bxor (list (cons 'bar (lambda (a b) (%const-arith-op bit-or a b))))))
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