flan/test/programs/dyn-if-truthy.flan
Joseph Ferano bddc8fc5dd and hands back its deciding operand too, and both locs get sharper
(and a b) desugared to (if a b false), so it answered the last operand
only when every operand was truthy; a falsey one came back as a bare
false, where Clojure answers the falsey operand itself. It now uses the
same expansion or got in ad0f1fb -- (let [t a] (if t b t)) against or's
(let [t a] (if t t b)) -- so the operand that decided the form is the
answer, and the test is still evaluated exactly once.

The temp binding and its if now carry the operand's own loc rather than
the whole form's, which the or fix had lost: (or (vec-new i32) v) blamed
the enclosing form at 3:13 and now points at 3:18, the operand, and and's
second operand gained the same precision.

The parse pins in test_flan.ml now tie the bound name to the temp the if
tests and the bound value to the first operand, so a desugaring that
dropped the temp and wrote the operand into the arm twice no longer
passes; and has its own pin. dyn-if-truthy.flan grows the falsey-nil and
falsey-false answers, 0 and "" as truthy operands, one- and zero-operand
forms, and a printing operand that proves both the short circuit and the
single evaluation.

One behaviour that used to compile changed: with both arms of the
desugared if now holding real values, (and dyn-value typed-bool) unifies
on the typed arm and a non-bool dyn decider traps at the strict bool
boundary -- (and (box nil) some-bool) printed false and now traps, the
mirror of what (or false (box "s")) already did on dev-loop. Recorded in
FIX.org as the author's call on how check_if should join a bool arm and a
dyn arm.
2026-09-20 10:36:05 +07:00

141 lines
6.5 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")))
;; 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)