;;;; 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. Their *answers* ;;;; are covered here too: both [and] and [or] hand back the operand that ;;;; decided them, Clojure-style, rather than a bare bool. Each used to ;;;; answer a sentinel on its deciding path instead -- [and]'s "false" sat ;;;; in the else arm, so an all-truthy [and] did carry its last dyn operand ;;;; through but a falsey one answered [false] where Clojure answers the ;;;; falsey operand itself, and [or]'s "true" sat in the then arm, the one ;;;; the checker types first, so a non-bool dyn answer hit the strict bool ;;;; boundary and trapped: (or nil "x"), the canonical Clojure (or x ;;;; default) idiom, used to crash. Both now bind the test to a temp and ;;;; answer the temp on the deciding path, Clojure's own expansion, which ;;;; also evaluates each test exactly once -- pinned 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")) ;; Prints its tag, then answers its value unchanged. An operand written this ;; way leaves a mark when it is evaluated, which is how the short-circuit ;; rows below prove that a skipped operand really was skipped. (defn loud [tag x] dyn (println tag) x) (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 is the falsey one that stopped it, or the last operand if none ;; did; or's is the first truthy one, or the last operand if none was. (println (and (box 1) (box "") (box :kw))) (println (and (box 1) (box false) (box "unreached"))) ;; The falsey operand itself, not a bare false: nil comes back as nil. (println (and (box 1) (box nil) (box "unreached"))) ;; 0 and "" are truthy, so neither stops and -- the last operand answers. (println (and (box 0) (box ""))) (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"))) ;; One operand is that operand, whatever it is -- no test, no sentinel. (println (and (box nil))) (println (and (box ""))) (println (or (box nil))) (println (or (box 0))) ;; No operands at all: the identity of each, and the one place a bare ;; sentinel is still the answer -- (and) is true, (or) is false. (println (and)) (println (or)) ;; Short-circuit proof. [loud] prints its tag before answering, so an ;; operand that is never evaluated is an absent line, not a wrong value. ;; and stops at the first falsey operand and or at the first truthy one, ;; so the "unreached" tags below must never appear. (println (and (box nil) (loud "and-unreached" 1))) (println (and (loud "and-reached" 1) (box 2))) (println (or (box 7) (loud "or-unreached" 1))) (println (or (box nil) (loud "or-reached" 1))) ;; The test is evaluated once, not twice. Here the loud operand is the one ;; that decides the form, so it is both the test and the answer -- and it ;; still prints exactly one line, which is what the temp binding buys over ;; writing the operand into the arm a second time. (println (and (loud "and-decider" nil) (box 3))) (println (or (loud "or-decider" 9) (box 4))) ;; 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)