A dyn scrutinee is no longer required to already be a bool: it is tested
for truthiness, Clojure's rule, not C's or Python's — nil and false are
the only falsey values, and everything else, including 0, "", an empty
vec, an empty map and a keyword, is truthy. A typed scrutinee is
unchanged and keeps needing a strict bool.
The runtime side is one new entry point, flan_dyn_truthy
(runtime/flan_dyn.c/.h), reading the tag directly rather than unboxing —
it never traps, unlike flan_dyn_need_bool. Both backends reach it the
same generic way flan_dyn_need_bool already did: check.ml emits an
ordinary Rt call plus the existing i32-to-bool Cast, so emit.ml only
needed the LLVM declare added and x86.ml needed nothing at all.
check.ml's check_truthy is the one funnel every boolean position in the
language goes through: if's own condition, while's, and not's argument.
when and cond reach it for free because they desugar to Ast.If in
parse.ml, and so does and's condition; or's condition does too, but its
answer position is a separate story — its short-circuit sentinel is the
then arm of its own if, which check_if types before anything else, so a
non-bool dyn value reaching that position still meets the strict bool
boundary. and's sentinel sits in the else arm instead, so the real
value's type wins and and hands back the actual last operand,
Clojure-style; or does not get that for the reason above, and reordering
it is a decision for another day, not this one. shortcircuit in parse.ml
carries the note.
check_truthy checks the scrutinee with no expectation first, so a dyn
value takes the truthy path and everything else takes the strict one. A
refusal on that second path is re-checked with the old want:Bool rather
than reported from the bare check, because a bare integer or float
literal, or a bare None, answers "what type is this" differently than
"is this a bool" — check.ml's own arms only give the nicer sentence
("expected bool, found the integer literal 5", "expected bool, found
None") when asked the second way, and that sentence is preserved exactly,
letter for letter, against what a typed if already said.
test/programs/dyn-if-truthy.flan surveys every falsey and truthy case —
nil, false, true, 0, a nonzero number, an empty and nonempty string, an
empty and nonempty vec, an empty and nonempty map, a keyword — through
if, when, cond, and, or, not and while, with real output pinned in
test_acceptance.ml across LLVM, -O0 and --x86. test_flan.ml covers the
checker side directly: a typed if still takes a bare bool and still
refuses a non-bool scrutinee and a bare None with their original
messages, a dyn if/not/while/when/cond/and/or all accept a non-bool dyn
condition. test/dyn_ops.c gets a matching set of direct calls to
flan_dyn_truthy, keeping the header's own contract with the C side.
98 lines
4.2 KiB
Plaintext
98 lines
4.2 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]'s sentinel is the then arm instead, so it is the one that decides
|
|
;;;; the whole expression's type when nothing else does, and [or]'s answer
|
|
;;;; stays a strict bool -- a pre-existing asymmetry (parse.ml, shortcircuit)
|
|
;;;; this item leaves alone; only [or]'s tests are exercised here, not a
|
|
;;;; non-bool value in its answer position. [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.
|
|
;; and's answer is the last truthy operand itself (:kw here), Clojure's
|
|
;; and. or's answer stays a plain bool -- "true" below is or's own
|
|
;; sentinel, not the operand that made it truthy; that asymmetry is
|
|
;; parse.ml's, not this item's to close.
|
|
(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)))
|
|
|
|
;; 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)
|