boot2

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

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:
Mcc/cc.scm | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Atests/cc/240-parse-const-shortcircuit.c | 15+++++++++++++++
Atests/cc/240-parse-const-shortcircuit.expected-exit | 1+
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