M2 item 7: dyn if tests truthiness, and or hands back its deciding operand
This commit is contained in:
commit
f61f83f796
41
FIX.org
41
FIX.org
@ -436,7 +436,46 @@ rename. typed-flan branch freezes the static language pre-dyn.
|
||||
6. defclass = named dyn map + shape tag; CLOS class dispatch AND
|
||||
Clojure-style arbitrary dispatch functions. After 1.
|
||||
7. dyn if: truthiness (nil/false are false, all else true). Typed stays
|
||||
strict bool.
|
||||
strict bool. — LANDED, 264765a
|
||||
|
||||
Reaches when, cond, if's own condition, and's condition, or's
|
||||
condition, not and while for free or by hand, all through one funnel
|
||||
in check.ml (check_truthy). Two things fell out of it that nobody had
|
||||
decided going in, one fixed on review and one left as the author's
|
||||
call:
|
||||
|
||||
- or's answer used to stay a strict bool where and's already carried a
|
||||
non-bool dyn value through, Clojure-style — and's short-circuit
|
||||
sentinel sits in the else arm, so the real value's type wins there,
|
||||
but or's sat in the then arm, the one check_if types first, so it
|
||||
decided the whole expression's type and a later non-bool dyn answer
|
||||
hit the strict bool boundary and trapped. (or nil "x"), the
|
||||
canonical (or x default) idiom, crashed rather than answering "x".
|
||||
FIXED, ad0f1fb: or now binds its test to a temp and answers the
|
||||
temp, Clojure's own expansion, evaluating the test once and handing
|
||||
back whichever operand actually decided it.
|
||||
- A bare keyword condition used to be checked with want:Bool from the
|
||||
start and refused by the keyword arm's enum-or-refuse case: ":kw is
|
||||
an enum member where an enum is expected and a dyn keyword
|
||||
elsewhere, but bool is expected here", there being no enum in play.
|
||||
Checked with no expectation first, as every scrutinee now is, it
|
||||
resolves as the dyn keyword instead, and a dyn keyword is
|
||||
unconditionally truthy — a typed if with a bare keyword condition
|
||||
now compiles and always takes the then branch. The author's call:
|
||||
lispy truthiness wins here, the lost diagnostic is not brought back.
|
||||
Pinned in test_flan.ml so it does not regress by accident.
|
||||
|
||||
Also noted at check_truthy (check.ml) and not acted on: check_truthy's
|
||||
own retry-on-failure, needed to keep a refused literal's or None's
|
||||
message unchanged, re-runs the whole failing subtree rather than only
|
||||
the leaf that needs it, which is exponential in how deep a chain of
|
||||
nested not gets on a program that does not type-check. Moot for
|
||||
anything that compiles; visible only around twenty levels deep, and
|
||||
only the dev daemon's half-typed-form recompiles could ever feel it.
|
||||
A cheaper retry was tried and shelved — it would need to thread want
|
||||
exactly as far as the full retry already does, or it changes which
|
||||
literal further inside a compound condition gets the nicer message,
|
||||
not just the speed.
|
||||
8. Return slot stays mandatory (dyn or ()) — the parse ambiguity it closes
|
||||
is real; revisit only if it grates. SETTLED 2026-09-19, reconfirmed with
|
||||
the author: both spellings stay legal, () is not collapsing into dyn.
|
||||
|
||||
95
lib/check.ml
95
lib/check.ml
@ -2032,7 +2032,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
|
||||
top of every trip — but it stays outside [in_loop], because a [break]
|
||||
in a condition still means the enclosing loop and a [defer] there is
|
||||
still the outer block's. *)
|
||||
let c = check ctx ~want:Types.Bool c in
|
||||
let c = check_truthy ctx c in
|
||||
let body = in_loop ctx ?label (fun () ->
|
||||
scoped ctx (fun () -> map_lr (fun b -> check ctx b) body))
|
||||
in
|
||||
@ -3174,8 +3174,95 @@ and check_recur ctx ~tail loc args =
|
||||
mk loc Types.Never
|
||||
(Tast.Let (temps, sets @ [ mk loc Types.Never (Tast.Continue depth) ]))
|
||||
|
||||
(* Every boolean-position test in the language funnels through here: [if]'s
|
||||
own condition, [while]'s, and [not]'s argument. ([when] and [cond] are
|
||||
sugar built out of [Ast.If] in parse.ml, so they get this for free
|
||||
without a separate case. [and] and [or] are sugar too, and both their
|
||||
tests and their answers get this for free the same way — see
|
||||
[shortcircuit] in parse.ml for how [or] carries its deciding operand
|
||||
through, the same way [and] always has.)
|
||||
|
||||
A dyn scrutinee is tested for truthiness, Clojure's rule: nil and false
|
||||
are the only falsey values, and everything else — 0, "", an empty vec, an
|
||||
empty map, a keyword — is truthy. A typed scrutinee stays strictly bool,
|
||||
exactly as before.
|
||||
|
||||
The scrutinee is checked with no expectation first so its own type
|
||||
decides which rule applies. That is fine for the passing cases — dyn, or
|
||||
already bool — but a *refused* one has to be re-checked with the old
|
||||
[want:Bool] rather than reported from here, and that covers two shapes of
|
||||
"asked the wrong question first": a bare integer or float literal answers
|
||||
differently to "what type is this" than to "is this a bool" (check.ml's
|
||||
int_literal/float arms only give the nicer answer, "expected bool, found
|
||||
the integer literal 5", when asked the second way), and [None] does not
|
||||
even have an answer to the first question — "nothing here says what None
|
||||
is an Option of" — where the second gets straight to "expected bool,
|
||||
found None". Asking the first way first is what makes the dyn case work,
|
||||
so the refusal path — success or exception both — asks the second way
|
||||
again, after the fact, purely to get the sentence a typed if has always
|
||||
given.
|
||||
|
||||
[loc] comes from [c] itself, not from the caller's [if]/[while]/[not] —
|
||||
the built-in rt call and cast have to sit at the condition's own position
|
||||
or an --x86 disassembly and the dev inspector point at the wrong column
|
||||
when the two differ (an [if] whose test is not its first token).
|
||||
|
||||
The [exception Loc.Error _] arm swallows a rejection from arbitrary depth
|
||||
inside [c] and re-runs the whole of [check ctx c] a second time, which is
|
||||
sound only because every one of [ctx]'s save-restore sites — [barrier],
|
||||
and the [in_frames]/[in_defer]/[loops]/[scope] plumbing [check] itself
|
||||
uses — is not exception-safe: a failure mid-walk can leave one of those
|
||||
pushed without its pop. Today that is harmless, because the second call
|
||||
always either succeeds outright or raises again and this function's own
|
||||
caller then aborts the compile — nothing downstream ever reads [ctx]
|
||||
again on that path. It would stop being harmless the day some later
|
||||
want-sensitive elaboration on this path can *succeed* by yielding a
|
||||
concrete [Bool] on retry rather than failing a second time: then the
|
||||
first, swallowed pass's half-restored state and any name or slot it
|
||||
registered before raising would both still be live.
|
||||
|
||||
That same retry-on-failure is also, deliberately, not made cheaper by
|
||||
only retrying at the leaf that actually needs a nicer message (an int or
|
||||
float literal, or [None]) and re-raising everywhere else: the shorter
|
||||
path was tried and shelved, because "everywhere else" is not safe to
|
||||
generalise past [not]'s one level of nesting — a condition that is
|
||||
itself a compound form carrying its own literals arbitrarily deep (an
|
||||
[if] or [let] standing where a condition is expected) would need [want]
|
||||
threaded through exactly as far as this function's own second call
|
||||
already threads it, and stopping short changes which of *those*
|
||||
literals gets the nicer message, not just the speed. The cost that
|
||||
buys is real: nested [not] on a program that does not type-check re-runs
|
||||
this whole function once per level of nesting inside the level above it,
|
||||
which is exponential in how deep the nesting goes — moot for a program
|
||||
that compiles, since neither retry ever fires, and moot for ordinary
|
||||
nesting depths, but visible within a second or so around twenty levels
|
||||
of a [not] wrapped in a [not] wrapped in .... The dev daemon is the one
|
||||
caller that could feel this, recompiling a half-typed form on every
|
||||
edit; nobody has hit it in practice and it is not fixed here.
|
||||
|
||||
Keywords are a separate, deliberate loss rather than a bug: a bare
|
||||
[:kw] used to be checked here with [want:Types.Bool] from the start, so
|
||||
it hit the keyword arm's [Some other] case and refused by name — "is an
|
||||
enum member where an enum is expected and a dyn keyword elsewhere, but
|
||||
bool is expected here". Checking it here with no expectation first, as
|
||||
every other scrutinee now is, resolves it as the dyn keyword instead
|
||||
(there being no enum in play), and dyn keywords are unconditionally
|
||||
truthy — so [(if :kw a b)] now takes [a], where it used to refuse
|
||||
outright. The author's call: lispy truthiness wins wherever it can, so
|
||||
this refusal is given up on purpose and not special-cased back in;
|
||||
test_flan.ml pins the new answer down so it is not lost again by
|
||||
accident. *)
|
||||
and check_truthy ctx c =
|
||||
let loc = c.Ast.loc in
|
||||
match check ctx c with
|
||||
| c0 when c0.Tast.ty = Types.Dyn ->
|
||||
widen loc Types.Bool (rt loc (Types.Int Types.I32) "flan_dyn_truthy" [ c0 ])
|
||||
| c0 when Types.fits ~expected:Types.Bool ~actual:c0.Tast.ty -> c0
|
||||
| _ -> check ctx ~want:Types.Bool c
|
||||
| exception Loc.Error _ -> check ctx ~want:Types.Bool c
|
||||
|
||||
and check_if ctx ?(tail = false) ?want loc c t e =
|
||||
let c = check ctx ~want:Types.Bool c in
|
||||
let c = check_truthy ctx c in
|
||||
(* Both arms are the tail, and a one-armed [if] counts: [(when c (recur ...))]
|
||||
is how nearly every loop is written, and the branch is still the last
|
||||
thing the body does. *)
|
||||
@ -4213,7 +4300,9 @@ and named_call ctx ~want loc name args =
|
||||
end
|
||||
| "not" ->
|
||||
arity loc name 1 args;
|
||||
prim Tast.Not Types.Bool [ check ctx ~want:Types.Bool (List.hd args) ]
|
||||
(* Same truthiness as [if]: a dyn argument is negated on nil/false vs.
|
||||
everything else, not narrowed to a strict bool first. *)
|
||||
prim Tast.Not Types.Bool [ check_truthy ctx (List.hd args) ]
|
||||
(* Bitwise operators are integers-only, and the shift count has the same type
|
||||
as the value shifted — there is no implicit widening anywhere else either. *)
|
||||
| "bit-and" | "bit-or" | "bit-xor" ->
|
||||
|
||||
@ -3387,6 +3387,7 @@ declare i32 @flan_dyn_need_bool(i64)
|
||||
; builtin.
|
||||
declare i32 @flan_dyn_is_nil(i64)
|
||||
declare i64 @flan_dyn_need_not_nil(i64)
|
||||
declare i32 @flan_dyn_truthy(i64)
|
||||
declare void @flan_dyn_root_push(ptr)
|
||||
declare void @flan_dyn_root_push_desc(ptr, ptr)
|
||||
declare void @flan_dyn_root_pop(i64)
|
||||
|
||||
25
lib/parse.ml
25
lib/parse.ml
@ -881,6 +881,25 @@ and cond f (args : Form.t list) : Ast.expr =
|
||||
in
|
||||
if args = [] then Loc.fail f.loc "cond needs at least one clause" else go args
|
||||
|
||||
(* Every test here is an [if]'s condition, so a dyn operand is truthy-tested
|
||||
(check.ml's check_truthy) exactly the way a bare [if]'s is, for both
|
||||
[and] and [or]. The *answer* used to be asymmetric between them: [and]'s
|
||||
"false" sentinel sits in the else arm, so check_if picks the real
|
||||
branch's type first and boxes "false" to match it, which lets [and] hand
|
||||
back the actual last dyn value, Clojure-style. [or] used to put its
|
||||
"true" sentinel in the then arm instead — the one check_if types first —
|
||||
so that sentinel decided the whole expression's type, and a later
|
||||
non-bool dyn answer hit the strict bool boundary instead of surviving as
|
||||
itself: (or nil "x") traps rather than answering "x", exactly the
|
||||
canonical (or x default) idiom Clojure is reached for.
|
||||
|
||||
[or] now binds its test to a temp and asks the temp itself, the way
|
||||
Clojure's own or expands: [(or a b)] is [(let [t a] (if t t b))], not
|
||||
[(if a true b)]. The temp is what lets the answer be [a] itself without
|
||||
writing [a] a second time as the then arm — [(if a a b)] would evaluate
|
||||
it twice, once for the test and again for the answer — and it is the
|
||||
temp's own type check_if sees first, so [or] hands back the actual
|
||||
truthy value the same way [and] hands back its own. *)
|
||||
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
||||
let mk e = { Ast.e; loc = f.loc } in
|
||||
let rec go = function
|
||||
@ -888,7 +907,11 @@ and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
||||
| [ last ] -> expr last
|
||||
| x :: rest ->
|
||||
if is_and then mk (Ast.If (expr x, go rest, Some (mk (Ast.Var "false"))))
|
||||
else mk (Ast.If (expr x, mk (Ast.Var "true"), Some (go rest)))
|
||||
else
|
||||
let t = fresh_temp () in
|
||||
let tvar = mk (Ast.Var t) in
|
||||
let bind = { Ast.bname = t; bty = None; bval = expr x; bloc = f.loc } in
|
||||
mk (Ast.Let ([ bind ], [ mk (Ast.If (tvar, tvar, Some (go rest))) ]))
|
||||
in
|
||||
go args
|
||||
|
||||
|
||||
@ -1021,6 +1021,16 @@ flan_dyn flan_dyn_need_not_nil(flan_dyn v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Clojure's truthiness, not C's or Python's: nil and false are the only
|
||||
* falsey values, and everything else — 0, 0.0, "", an empty vec, an empty
|
||||
* map, any keyword — is truthy. Never traps; every tag answers. */
|
||||
uint8_t flan_dyn_truthy(flan_dyn v) {
|
||||
int32_t t = flan_dyn_tag(v);
|
||||
if (t == FLAN_DYN_TAG_NIL) return 0;
|
||||
if (t == FLAN_DYN_TAG_BOOL) return (uint8_t)(dyn_payload(v) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ── Arithmetic ────────────────────────────────────────────────────────
|
||||
*
|
||||
* Two ints answer an int; anything else numeric answers a float. The promotion
|
||||
|
||||
@ -145,6 +145,12 @@ uint8_t flan_dyn_need_bool(flan_dyn v);
|
||||
int32_t flan_dyn_is_nil(flan_dyn v);
|
||||
flan_dyn flan_dyn_need_not_nil(flan_dyn v);
|
||||
|
||||
/* Truthiness for a dyn used where a typed value would need a strict bool —
|
||||
* an [if]'s condition when the scrutinee's own type is dyn. Clojure's rule:
|
||||
* nil and false are falsey, every other value is truthy, including 0, 0.0,
|
||||
* "", an empty vec, an empty map, and any keyword. Never traps. */
|
||||
uint8_t flan_dyn_truthy(flan_dyn v);
|
||||
|
||||
/* ── The collector ─────────────────────────────────────────────────────
|
||||
*
|
||||
* Mark-sweep, precise, and never moving. [flan_gc_init] is idempotent, and the
|
||||
|
||||
@ -340,6 +340,20 @@ static void ops(void) {
|
||||
check(flan_dyn_need_bool(flan_dyn_from_bool(1)) == 1, "need-bool true");
|
||||
check(flan_dyn_need_bool(flan_dyn_from_bool(0)) == 0, "need-bool false");
|
||||
|
||||
/* Truthiness: only nil and false are falsey. Everything else — including
|
||||
the values C or Python would call falsey — is truthy. */
|
||||
check(flan_dyn_truthy(flan_dyn_nil()) == 0, "truthy nil");
|
||||
check(flan_dyn_truthy(flan_dyn_from_bool(0)) == 0, "truthy false");
|
||||
check(flan_dyn_truthy(flan_dyn_from_bool(1)) == 1, "truthy true");
|
||||
check(flan_dyn_truthy(flan_dyn_from_i64(0)) == 1, "truthy 0");
|
||||
check(flan_dyn_truthy(flan_dyn_from_i64(5)) == 1, "truthy nonzero int");
|
||||
check(flan_dyn_truthy(flan_dyn_from_f64(0.0)) == 1, "truthy 0.0");
|
||||
check(flan_dyn_truthy(text("")) == 1, "truthy empty string");
|
||||
check(flan_dyn_truthy(text("x")) == 1, "truthy nonempty string");
|
||||
check(flan_dyn_truthy(flan_dyn_kw((const uint8_t *)"k", 1)) == 1, "truthy keyword");
|
||||
check(flan_dyn_truthy(flan_dyn_vec_new()) == 1, "truthy empty vec");
|
||||
check(flan_dyn_truthy(flan_dyn_map_new()) == 1, "truthy empty map");
|
||||
|
||||
s = text("kept");
|
||||
w = v;
|
||||
flan_dyn_root_pop(6);
|
||||
|
||||
103
test/programs/dyn-if-truthy.flan
Normal file
103
test/programs/dyn-if-truthy.flan
Normal file
@ -0,0 +1,103 @@
|
||||
;;;; 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] used to put its sentinel in the then arm instead, so a non-bool
|
||||
;;;; dyn answer hit the strict bool boundary and traps -- (or nil "x"), the
|
||||
;;;; canonical Clojure (or x default) idiom, used to crash. [or] now binds
|
||||
;;;; its test to a temp and answers the temp itself, Clojure's own
|
||||
;;;; expansion, so its answer carries a non-bool dyn value through exactly
|
||||
;;;; the way [and]'s does; both are exercised 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"))
|
||||
|
||||
(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 answer is the last truthy operand itself (:kw here); or's is the
|
||||
;; first truthy one (0, then "x") rather than a bare true.
|
||||
(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)))
|
||||
;; 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")))
|
||||
|
||||
;; 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)
|
||||
@ -1248,6 +1248,30 @@ let () =
|
||||
string_eq_out;
|
||||
outputs ~x86:true "string equality, --x86" "programs/string-eq.flan"
|
||||
string_eq_out;
|
||||
(* dyn if: truthiness -- M2 queue item 7. A dyn scrutinee is tested for
|
||||
nil/false vs. everything else, Clojure's rule, on both backends; a
|
||||
typed scrutinee stays strictly bool, which is a checker test
|
||||
(test_flan.ml) and not a row here since a typed if given a non-bool
|
||||
scrutinee would not compile. [when], [cond], [and] and [or] ride
|
||||
along for free because their tests desugar to [if] in parse.ml;
|
||||
[not] and [while] get the same treatment by hand in check.ml's
|
||||
check_truthy. [and] and [or] both hand back the actual operand that
|
||||
decided them, Clojure-style -- [or] used to answer a bare bool
|
||||
instead, its short-circuit sentinel sitting in the arm check_if
|
||||
types first, and a non-bool dyn value reaching that position (the
|
||||
canonical (or x default) idiom) used to trap rather than survive as
|
||||
itself; [or] now binds its test to a temp and answers the temp,
|
||||
fixing that. *)
|
||||
let dyn_if_truthy_out =
|
||||
"falsey\nfalsey\ntruthy\ntruthy\ntruthy\ntruthy\ntruthy\ntruthy\ntruthy\n\
|
||||
truthy\ntruthy\ntruthy\nwhen 0 ran\nwhen empty-string ran\nb\nb\n:kw\n\
|
||||
false\n0\nfalse\nx\n5\ntrue\ntrue\nfalse\nfalse\nfalse\n3\n2\n1\n"
|
||||
in
|
||||
outputs "dyn if truthiness" "programs/dyn-if-truthy.flan" dyn_if_truthy_out;
|
||||
outputs ~opt:"-O0" "dyn if truthiness, -O0" "programs/dyn-if-truthy.flan"
|
||||
dyn_if_truthy_out;
|
||||
outputs ~x86:true "dyn if truthiness, --x86" "programs/dyn-if-truthy.flan"
|
||||
dyn_if_truthy_out;
|
||||
(* format-f64, the first number formatter a caller can steer. The three
|
||||
lines that would ship wrong are pinned deliberately: 0.999995 at five
|
||||
places, where the rounded fraction equals the scale and is the next
|
||||
|
||||
@ -354,8 +354,13 @@ let () =
|
||||
(match (parse1 "(and a b)").e with
|
||||
| If (_, _, Some { e = Var "false"; _ }) -> ()
|
||||
| _ -> check "and short-circuits" false);
|
||||
(* or binds its test to a temp and answers the temp itself -- Clojure's
|
||||
own expansion, and what lets or hand back the actual truthy operand
|
||||
rather than a bare true (M2 queue item 7's review pass). *)
|
||||
(match (parse1 "(or a b)").e with
|
||||
| If (_, { e = Var "true"; _ }, Some _) -> ()
|
||||
| Let ([ _ ],
|
||||
[ { e = If ({ e = Var t1; _ }, { e = Var t2; _ }, Some _); _ } ])
|
||||
when t1 = t2 -> ()
|
||||
| _ -> check "or short-circuits" false);
|
||||
|
||||
(* ── Forms that bind or alter control are never calls ──────────── *)
|
||||
@ -835,6 +840,59 @@ let () =
|
||||
rejects_check "if branches disagree"
|
||||
"(defn f [] i32 (if true 1 true))" ~needle:"expected i32";
|
||||
|
||||
(* M2 queue item 7: a dyn if's scrutinee is truthiness-tested (Clojure's
|
||||
rule -- nil and false are the only falsey values); a typed if keeps
|
||||
needing a strict bool, exactly as before. The runtime survey is
|
||||
programs/dyn-if-truthy.flan; these three pin the checker's own half. *)
|
||||
accepts "typed if still takes a bare bool" "(defn f [] i32 (if true 1 2))";
|
||||
rejects_check "typed if still refuses a non-bool scrutinee"
|
||||
"(defn f [] i32 (if 1 1 2))"
|
||||
~needle:"expected bool, found the integer literal 1";
|
||||
(* Not just refused -- refused with the exact sentence a typed if has
|
||||
always given here. check_truthy's dyn-or-bool check runs first, but a
|
||||
value that is neither is re-checked with the old want:Bool so the
|
||||
message a literal gets is the one it names itself with, not the type
|
||||
it silently defaulted to along the way. *)
|
||||
rejects_check "typed if still refuses None with its own message"
|
||||
"(defn f [] i32 (if None 1 2))" ~needle:"expected bool, found None";
|
||||
(* None is the other shape of "asked the wrong question first": checked
|
||||
with no expectation at all it has no answer -- "nothing here says what
|
||||
None is an Option of" -- rather than a wrong one, so check_truthy's
|
||||
first attempt *raises* here instead of returning some non-bool,
|
||||
non-dyn type. The re-check has to run on that path too. *)
|
||||
accepts "dyn if accepts a non-bool dyn scrutinee"
|
||||
"(defn f [x] dyn (if x 1 2))";
|
||||
(* not, while: not [if] under the hood, so check_truthy is reached at
|
||||
their own call sites (check.ml) rather than for free through
|
||||
desugaring -- both take a non-bool dyn condition too. *)
|
||||
accepts "dyn not accepts a non-bool dyn argument"
|
||||
"(defn f [x] dyn (not x))";
|
||||
accepts "dyn while accepts a non-bool dyn condition"
|
||||
"(defn f [x] () (let [y x] (while y (set y false))))";
|
||||
(* when, cond, and, or: sugar built out of [Ast.If] in parse.ml, so a
|
||||
non-bool dyn condition reaches them with no separate check.ml case --
|
||||
confirmed here rather than assumed. *)
|
||||
accepts "dyn when accepts a non-bool dyn condition"
|
||||
"(defn f [x] () (when x 0))";
|
||||
accepts "dyn cond accepts a non-bool dyn condition"
|
||||
"(defn f [x] dyn (cond x 1 :else 2))";
|
||||
accepts "dyn and accepts a non-bool dyn operand"
|
||||
"(defn f [x] i32 (let [y x] (if (and y true) 0 1)))";
|
||||
accepts "dyn or accepts a non-bool dyn operand"
|
||||
"(defn f [x] i32 (let [y x] (if (or y true) 0 1)))";
|
||||
(* A bare keyword condition used to be checked with want:Bool from the
|
||||
start, landing on the keyword arm's enum-or-refuse case and refusing
|
||||
by name -- ":kw is an enum member where an enum is expected ... but
|
||||
bool is expected here", since there was no enum in play. Checked with
|
||||
no expectation first, as every scrutinee now is, it resolves as the
|
||||
dyn keyword instead, and a dyn keyword is unconditionally truthy: a
|
||||
typed if with a bare keyword condition now compiles, and always takes
|
||||
the then branch. The author's call, recorded at check_truthy: lispy
|
||||
truthiness wins here, the lost diagnostic is not brought back, and
|
||||
this pins the new answer down so it does not regress by accident. *)
|
||||
accepts "a typed if with a bare keyword condition now compiles"
|
||||
"(defn f [] i32 (if :kw 1 2))";
|
||||
|
||||
(* ── Unknown types ─────────────────────────────────────────────── *)
|
||||
(* A lowercase name is a type variable (plan.org, Types), so a mistyped
|
||||
primitive would otherwise be reported as unimplemented generics and send
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user