148 lines
6.8 KiB
Plaintext
148 lines
6.8 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. 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 is-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 (is-truthy nil))
|
|
(println (is-truthy false))
|
|
(println (is-truthy true))
|
|
(println (is-truthy 0))
|
|
(println (is-truthy 7))
|
|
(println (is-truthy ""))
|
|
(println (is-truthy "x"))
|
|
(println (is-truthy (vec-new dyn)))
|
|
(let [xs (vec-new dyn)]
|
|
(push xs 1)
|
|
(println (is-truthy xs)))
|
|
(let [m {}]
|
|
(println (is-truthy m)))
|
|
(println (is-truthy {:a 1}))
|
|
(println (is-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")))
|
|
;; A typed bool operand beside a dyn one: the two meet at dyn, the bool
|
|
;; boxed, so the dyn one comes back whichever side of the if it lands on.
|
|
(println (or false (box "s")))
|
|
(println (or (= 1 2) (box nil)))
|
|
(println (and (box nil) (= 1 1)))
|
|
(println (and true (box "y")))
|
|
(println (or true (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)
|