flan/test/programs/dyn-if-truthy.flan

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 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")))
;; 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)