M2 item 7: dyn if tests truthiness, and or hands back its deciding operand

This commit is contained in:
Joseph Ferano 2026-09-20 10:08:44 +07:00
commit f61f83f796
10 changed files with 373 additions and 6 deletions

41
FIX.org
View File

@ -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.

View File

@ -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" ->

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View 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)

View File

@ -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

View File

@ -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