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
|
6. defclass = named dyn map + shape tag; CLOS class dispatch AND
|
||||||
Clojure-style arbitrary dispatch functions. After 1.
|
Clojure-style arbitrary dispatch functions. After 1.
|
||||||
7. dyn if: truthiness (nil/false are false, all else true). Typed stays
|
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
|
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
|
is real; revisit only if it grates. SETTLED 2026-09-19, reconfirmed with
|
||||||
the author: both spellings stay legal, () is not collapsing into dyn.
|
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]
|
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
|
in a condition still means the enclosing loop and a [defer] there is
|
||||||
still the outer block's. *)
|
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 () ->
|
let body = in_loop ctx ?label (fun () ->
|
||||||
scoped ctx (fun () -> map_lr (fun b -> check ctx b) body))
|
scoped ctx (fun () -> map_lr (fun b -> check ctx b) body))
|
||||||
in
|
in
|
||||||
@ -3174,8 +3174,95 @@ and check_recur ctx ~tail loc args =
|
|||||||
mk loc Types.Never
|
mk loc Types.Never
|
||||||
(Tast.Let (temps, sets @ [ mk loc Types.Never (Tast.Continue depth) ]))
|
(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 =
|
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 ...))]
|
(* 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
|
is how nearly every loop is written, and the branch is still the last
|
||||||
thing the body does. *)
|
thing the body does. *)
|
||||||
@ -4213,7 +4300,9 @@ and named_call ctx ~want loc name args =
|
|||||||
end
|
end
|
||||||
| "not" ->
|
| "not" ->
|
||||||
arity loc name 1 args;
|
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
|
(* 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. *)
|
as the value shifted — there is no implicit widening anywhere else either. *)
|
||||||
| "bit-and" | "bit-or" | "bit-xor" ->
|
| "bit-and" | "bit-or" | "bit-xor" ->
|
||||||
|
|||||||
@ -3387,6 +3387,7 @@ declare i32 @flan_dyn_need_bool(i64)
|
|||||||
; builtin.
|
; builtin.
|
||||||
declare i32 @flan_dyn_is_nil(i64)
|
declare i32 @flan_dyn_is_nil(i64)
|
||||||
declare i64 @flan_dyn_need_not_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(ptr)
|
||||||
declare void @flan_dyn_root_push_desc(ptr, ptr)
|
declare void @flan_dyn_root_push_desc(ptr, ptr)
|
||||||
declare void @flan_dyn_root_pop(i64)
|
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
|
in
|
||||||
if args = [] then Loc.fail f.loc "cond needs at least one clause" else go args
|
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 =
|
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
||||||
let mk e = { Ast.e; loc = f.loc } in
|
let mk e = { Ast.e; loc = f.loc } in
|
||||||
let rec go = function
|
let rec go = function
|
||||||
@ -888,7 +907,11 @@ and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
|||||||
| [ last ] -> expr last
|
| [ last ] -> expr last
|
||||||
| x :: rest ->
|
| x :: rest ->
|
||||||
if is_and then mk (Ast.If (expr x, go rest, Some (mk (Ast.Var "false"))))
|
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
|
in
|
||||||
go args
|
go args
|
||||||
|
|
||||||
|
|||||||
@ -1021,6 +1021,16 @@ flan_dyn flan_dyn_need_not_nil(flan_dyn v) {
|
|||||||
return 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 ────────────────────────────────────────────────────────
|
/* ── Arithmetic ────────────────────────────────────────────────────────
|
||||||
*
|
*
|
||||||
* Two ints answer an int; anything else numeric answers a float. The promotion
|
* 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);
|
int32_t flan_dyn_is_nil(flan_dyn v);
|
||||||
flan_dyn flan_dyn_need_not_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 ─────────────────────────────────────────────────────
|
/* ── The collector ─────────────────────────────────────────────────────
|
||||||
*
|
*
|
||||||
* Mark-sweep, precise, and never moving. [flan_gc_init] is idempotent, and the
|
* 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(1)) == 1, "need-bool true");
|
||||||
check(flan_dyn_need_bool(flan_dyn_from_bool(0)) == 0, "need-bool false");
|
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");
|
s = text("kept");
|
||||||
w = v;
|
w = v;
|
||||||
flan_dyn_root_pop(6);
|
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;
|
string_eq_out;
|
||||||
outputs ~x86:true "string equality, --x86" "programs/string-eq.flan"
|
outputs ~x86:true "string equality, --x86" "programs/string-eq.flan"
|
||||||
string_eq_out;
|
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
|
(* format-f64, the first number formatter a caller can steer. The three
|
||||||
lines that would ship wrong are pinned deliberately: 0.999995 at five
|
lines that would ship wrong are pinned deliberately: 0.999995 at five
|
||||||
places, where the rounded fraction equals the scale and is the next
|
places, where the rounded fraction equals the scale and is the next
|
||||||
|
|||||||
@ -354,8 +354,13 @@ let () =
|
|||||||
(match (parse1 "(and a b)").e with
|
(match (parse1 "(and a b)").e with
|
||||||
| If (_, _, Some { e = Var "false"; _ }) -> ()
|
| If (_, _, Some { e = Var "false"; _ }) -> ()
|
||||||
| _ -> check "and short-circuits" 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
|
(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);
|
| _ -> check "or short-circuits" false);
|
||||||
|
|
||||||
(* ── Forms that bind or alter control are never calls ──────────── *)
|
(* ── Forms that bind or alter control are never calls ──────────── *)
|
||||||
@ -835,6 +840,59 @@ let () =
|
|||||||
rejects_check "if branches disagree"
|
rejects_check "if branches disagree"
|
||||||
"(defn f [] i32 (if true 1 true))" ~needle:"expected i32";
|
"(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 ─────────────────────────────────────────────── *)
|
(* ── Unknown types ─────────────────────────────────────────────── *)
|
||||||
(* A lowercase name is a type variable (plan.org, Types), so a mistyped
|
(* A lowercase name is a type variable (plan.org, Types), so a mistyped
|
||||||
primitive would otherwise be reported as unimplemented generics and send
|
primitive would otherwise be reported as unimplemented generics and send
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user