The dyn if truthiness review turned up that or's answer position, unlike and's, still traps on a non-bool dyn value: or's short-circuit sentinel sat in the then arm of its own if, the one check_if types first, so that sentinel decided the whole expression's type and a later non-bool dyn answer hit the strict bool boundary and unboxed itself into a trap rather than surviving as itself. (or nil "x") — the canonical Clojure (or x default) idiom — crashed instead of answering "x", identical on all three backends. or now binds its test to a temp and answers the temp itself, exactly the way Clojure's own or macro expands: (or a b) becomes (let [t a] (if t t b)), not (if a true b). The temp evaluates a once and lets the answer be a without writing it a second time as the then arm; it is the temp's own type check_if sees first, so or hands back the actual truthy operand the same way and always has. Verified real output, unchanged, on LLVM, -O0 and --x86, and the survey program now exercises the case its own header used to exclude for being unsafe: a non-bool value stopping or and being handed back as-is. check_truthy also gets three corrections a closer look found. Its own [loc] used to come from the enclosing if/while/not rather than from the condition itself, so the rt call and cast it builds carried the wrong column in an --x86 disassembly or the dev inspector whenever the condition was not the form's first token; it now takes loc from the scrutinee's own AST node, confirmed against a real --x86 dump. A comment now names the precondition its exception-swallowing retry rests on: none of check.ml's save-restore sites (barrier, in_frames, in_defer, loops, scope) are exception-safe, which is harmless only because the retry always either succeeds cleanly or re-raises and aborts the compile before ctx is read again — and would stop being harmless the day some want-sensitive elaboration on this path could succeed differently on retry. And a bare keyword condition, which used to be checked with want:Bool from the start and refused by the keyword arm's enum-or-refuse case, now resolves as the dyn keyword instead and is unconditionally truthy — a deliberate loss of that diagnostic, the author's call, pinned in test_flan.ml so it does not regress by accident. The two typed-refusal messages captured before this pass (a float literal condition, an i32 while condition) are unchanged, checked again against the same baseline. test_flan.ml's parser test for or's shape is updated to match the new let-bound desugaring.
104 lines
4.6 KiB
Plaintext
104 lines
4.6 KiB
Plaintext
;;;; M2 queue item 7: dyn if tests a scrutinee's truthiness rather than
|
|
;;;; requiring a strict bool, when the scrutinee's own type is dyn. Clojure's
|
|
;;;; rule, not C's or Python's: nil and false are the only falsey values, and
|
|
;;;; everything else -- 0, "", an empty vec, an empty map, a keyword -- is
|
|
;;;; truthy.
|
|
;;;;
|
|
;;;; The rule reaches every form built out of [if] under the hood -- [when],
|
|
;;;; [cond], [and] and [or] all desugar to it in parse.ml -- so their *tests*
|
|
;;;; need no separate case in check.ml and get exercised below through their
|
|
;;;; own syntax rather than by inspecting the desugaring. [and]'s *answer*
|
|
;;;; carries a non-bool dyn value through too, Clojure-style, because its
|
|
;;;; short-circuit sentinel is the else arm and the real value's type wins.
|
|
;;;; [or] used to put its sentinel in the then arm instead, so a non-bool
|
|
;;;; dyn answer hit the strict bool boundary and traps -- (or nil "x"), the
|
|
;;;; canonical Clojure (or x default) idiom, used to crash. [or] now binds
|
|
;;;; its test to a temp and answers the temp itself, Clojure's own
|
|
;;;; expansion, so its answer carries a non-bool dyn value through exactly
|
|
;;;; the way [and]'s does; both are exercised below, including the case
|
|
;;;; that used to be excluded here for being unsafe. [not] and [while] are
|
|
;;;; not [if] in disguise, so check_truthy is called at their own sites by
|
|
;;;; hand, and get their own coverage too.
|
|
;;;;
|
|
;;;; A typed if keeps needing a strict bool -- that refusal, and its message,
|
|
;;;; is a checker test in test_flan.ml, not a row here, since a program that
|
|
;;;; gave a typed if a non-bool scrutinee would not compile.
|
|
|
|
;; An unannotated parameter is always dyn, so calling this on a literal is
|
|
;; what boxes it -- the same way an argument to a dyn-typed parameter always
|
|
;; does. Used below wherever a literal has to reach a boolean position as a
|
|
;; genuine dyn value rather than as the typed value it would otherwise default
|
|
;; to (a bare 0 is an i32 until something wants it as dyn).
|
|
(defn box [x] dyn x)
|
|
|
|
(defn truthy? [x] dyn (if x "truthy" "falsey"))
|
|
|
|
(defn main [] i32
|
|
;; nil and false: the only two falsey dyn values. Everything else Clojure
|
|
;; calls truthy that C or Python would not: 0, "", an empty vec, an empty
|
|
;; map, a keyword.
|
|
(println (truthy? nil))
|
|
(println (truthy? false))
|
|
(println (truthy? true))
|
|
(println (truthy? 0))
|
|
(println (truthy? 7))
|
|
(println (truthy? ""))
|
|
(println (truthy? "x"))
|
|
(println (truthy? (vec-new dyn)))
|
|
(let [xs (vec-new dyn)]
|
|
(push xs 1)
|
|
(println (truthy? xs)))
|
|
(let [m {}]
|
|
(println (truthy? m)))
|
|
(println (truthy? {:a 1}))
|
|
(println (truthy? :kw))
|
|
|
|
;; when: sugar for a one-armed if, so nil/false skip the body and every
|
|
;; other dyn value -- 0 and "" included -- runs it.
|
|
(when (box nil) (println "when nil ran"))
|
|
(when (box false) (println "when false ran"))
|
|
(when (box 0) (println "when 0 ran"))
|
|
(when (box "") (println "when empty-string ran"))
|
|
|
|
;; cond: each test is an if in a chain, so the same rule applies clause by
|
|
;; clause -- a boxed 0 falls through to its body just like a boxed "x".
|
|
(println (cond (box nil) "a" (box 0) "b" :else "c"))
|
|
(println (cond (box false) "a" (box "x") "b" :else "c"))
|
|
|
|
;; and/or: also if in disguise, so each test along the chain is
|
|
;; truthy-tested the same way if's own is -- 0 and "" do not stop and,
|
|
;; only nil and false do; 0 does stop or, the way any truthy value does.
|
|
;; Both hand back the actual operand that decided them, Clojure-style --
|
|
;; and's answer is the last truthy operand itself (:kw here); or's is the
|
|
;; first truthy one (0, then "x") rather than a bare true.
|
|
(println (and (box 1) (box "") (box :kw)))
|
|
(println (and (box 1) (box false) (box "unreached")))
|
|
(println (or (box 0) (box false)))
|
|
(println (or (box nil) (box false)))
|
|
;; The case excluded before the fix: a non-bool value stopping or and
|
|
;; being handed back as-is -- the canonical (or x default) idiom, which
|
|
;; used to trap trying to unbox "x" as a strict bool.
|
|
(println (or (box nil) (box "x")))
|
|
(println (or (box 5) (box "unreached")))
|
|
|
|
;; not: truthiness, negated -- true only for nil and false.
|
|
(println (not (box nil)))
|
|
(println (not (box false)))
|
|
(println (not (box 0)))
|
|
(println (not (box "")))
|
|
(println (not (box true)))
|
|
|
|
;; while: the loop condition is a truthiness test the same way if's is. A
|
|
;; dyn vec ending in nil stops the loop; the 0 and "" along the way, if any
|
|
;; were there, would not.
|
|
(let [n (vec-new dyn)]
|
|
(push n 3)
|
|
(push n 2)
|
|
(push n 1)
|
|
(push n nil)
|
|
(let [i 0]
|
|
(while (at n i)
|
|
(println (at n i))
|
|
(set i (+ i 1)))))
|
|
0)
|