boot2

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

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