flan/test/programs/generics.flan
Joseph Ferano d6fc15474b The count is length, so len is a name a program can have
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.

Shadowing and builtin/ had already taken most of the sting out: a (defn len
...) was legal and won in its own file, and builtin/len reached past it. What
was left is that len was still a builtin — the defn earned a warning, and a
wrapper had to say builtin/ at every inner call. Now there is nothing under
the short name: len is an ordinary identifier in every position, which is
what (let [len (length xs)] ...) wants.

length takes over as shadowing's worked example rather than the feature
losing one. shadow-builtin.flan, builtin-qualified.flan, pkgs/shadowed and the
builtin/ rows in test_flan move to it and go on testing shadowing.

A call to a len nothing defines is answered where an unknown function is,
after every table and after the shadowing guard, so a program with its own len
never reaches it. The sentence is said rather than guessed at — len and length
are three edits apart and the did-you-mean's net is one — and the call is
written back out through spell_arg, as-slice's spelling lifted out of it and
now shared, so what is printed compiles.

sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
2026-09-21 11:58:56 +07:00

254 lines
10 KiB
Plaintext

;;;; Generics by monomorphisation, end to end.
;;;;
;;;; A [$t] binds a type variable in a defn signature and every call site
;;;; instantiates the body at the types it passes. The body is checked once
;;;; *abstractly*, with nothing substituted, so an operator the variable is
;;;; not declared to support is refused at the definition and not at whichever
;;;; call site happened to reach a type that worked — see generic-reject.flan
;;;; and generic-runaway.flan for that half.
;;;;
;;;; What this program is asserting, in order: one variable at several types,
;;;; a variable bound inside a slice, a generic calling a generic at its own
;;;; variable so that instantiation has to be transitive, the four where
;;;; predicates, two variables at once, println deferred to the instantiation,
;;;; the collapsed prelude family the whole feature was for, and the family
;;;; over (Option $t) — or-else and some? — which is the one that declares no
;;;; predicate at all, so a $t that owns storage instantiates it too.
;; One variable, several types, and (ident 3) and (ident 7) share one copy.
;; The identity needs its parameter once, so it needs nothing declared: a type
;; variable is move-only by default and one move is what this is.
(defn ident [x $t] $t x)
;; The variable is bound *inside* a type constructor, which is a structural
;; walk rather than a name match.
(defn first-or [s [$t] d $t] $t
(if (= (length s) 0) d (at s 0)))
;; A generic calling a generic at its own variable: the copy of [swap] is
;; generated when [rotate] is instantiated and not before.
(defn rotate [s [$t]] ()
(dotimes [i (- (length s) 1)]
(swap s i (+ i 1))))
;; numeric? admits + - * / %.
(defn twice [x $t] $t
{:where (numeric? $t)}
(+ x x))
;; equal? admits = and !=; ordered? admits < <= > >= min max, and entails
;; equal?.
(defn count-of [s [$t] x $t] i32
{:where (equal? $t)}
(let [n 0]
(dotimes [i (length s)]
(when (= (at s i) x)
(set n (+ n 1))))
n))
(defn clamp-to [x $t lo $t hi $t] $t
{:where (ordered? $t)}
(min (max x lo) hi))
;; An integer *literal* where the type variable is wanted, which is what the
;; sign family needs: one pos? over every numeric type rather than one per
;; width. The literal is admitted because {:where (numeric? $t)} is declared,
;; and the bound is what makes it sound rather than optimistic — every type
;; numeric? admits is an integer or a float, and an untyped integer constant
;; is usable at all of them, so there is no instantiation at which this 0 has
;; no meaning. Without the clause it is refused at the definition; see the
;; rejects in test_flan.ml.
;;
;; The literal is never emitted from here. The abstract pass builds a
;; placeholder and throws it away with the rest of the body; each copy
;; re-checks (> x 0) with $t substituted, and *that* is where the literal is
;; built at the concrete width and range-checked.
;;
;; The -t? suffix is because the prelude now carries pos?/neg?/zero? itself.
;; These are the same three bodies written in an ordinary program, which is
;; what says the machinery belongs to the language and not to the prelude.
(defn pos-t? [x $t] bool {:where (numeric? $t)} (> x 0))
(defn neg-t? [x $t] bool {:where (numeric? $t)} (< x 0))
(defn zero-t? [x $t] bool {:where (numeric? $t)} (= x 0))
;; The same literal in arithmetic rather than comparison, and answering $t
;; rather than bool, so the placeholder has to survive being the operand of a
;; Prim and being returned.
(defn next-after [x $t] $t {:where (numeric? $t)} (+ x 1))
;; The range check is the instantiation's and not the definition's: 300 is
;; fine at i32 and would be a refusal at u8, and u8 is where it is refused.
;; This one is only ever asked for at i32.
(defn plus-300 [x $t] $t {:where (numeric? $t)} (+ x 300))
;; Two variables, and the second is determined by its own argument.
(defn fst [a $t b $u] $t
(do b a))
;; println over a type variable is the one form the abstract pass defers to
;; the instantiation, because its legality is only decidable after
;; substituting. The structural printer is selected per copy.
(defn show [x $t] ()
(println x))
;; A cast to a type variable. [(t x)] is not a name [is_cast] knows — [t] is
;; not a machine type — so it is its own arm, and [numeric?] is what admits
;; it, because a cast produces a number. Inside the copy the target is
;; concrete and the emitter sees an ordinary cast.
(defn widen [x i32 d $t] $t
{:where (numeric? $t)}
(do d (t x)))
;; The builtins that take a *type name* as an argument, over a variable. Each
;; reaches the one list of what names a type; (map-new t i32) is the other.
(defn one-of [x $t] (Vec $t)
(let [v (vec-new t)]
(push v x)
v))
;; An empty (Option (Vec u8)), which main needs to reach or-else's None branch
;; at a type that owns storage. It is a function and not a bare None at the
;; call site because a bare None there is refused — "nothing here says what
;; None is an Option of" — and a return type is one of the two places the
;; checker names as somewhere to say it. It is also the shape every real caller
;; is in: what arrives at or-else came out of something, the way edn/read's
;; answer does.
(defn none-vec [] (Option (Vec u8))
None)
;; (zeroed) takes its type from the position it is written in, so a variable
;; in that position is answered by the instantiation like any other type.
(defn zero-of [x $t] $t
(do x (zeroed)))
;; The map operations over a key that is a type variable. The hash and the
;; equality are concrete symbols chosen from the concrete key type, so there
;; is nothing to emit here — these are deferred to the instantiation, the way
;; println is, and {:where (hashable? $t)} is what allows it: the refusal for
;; a key type that cannot be hashed lands at the call site, against a
;; requirement written down in this signature. Without the clause the type
;; (Map $t i32) is refused where it is written; see generic-map-reject.flan
;; for the call-site half.
(defn bump [k $t n i32] i32
{:where (hashable? $t)}
(let [m (map-new t i32)]
(reserve m 8)
(put m k n)
(put m k (+ n (match (get m k) (Some v) v _ 0)))
(let [c (clone m)
answer (+ (match (get c k) (Some v) v _ -1)
(if (has-key? c k) 1 0))]
(free c)
(free m)
answer)))
(defn main [] ()
(println (ident 3))
(println (ident 4.5))
(println (ident true))
(println (ident 7))
(let [ns [5 3 9 1]
fs [2.5 0.5 1.5]]
(println (first-or (slice ns 0 4) -1))
(println (first-or (slice ns 0 0) -1))
(rotate (slice ns 0 4))
(println (at ns 3))
(println (twice 21))
(println (twice 1.5))
(println (count-of (slice ns 0 4) 9))
(println (clamp-to 12 0 10))
(println (clamp-to 0.5 1.0 9.0))
(println (fst 8 true))
;; The literal-at-a-type-variable family, at six numeric types from three
;; written bodies. i32, i64, u8, u16, f32 and f64 all reach the same 0 and
;; the same 1.
(println (pos-t? 3))
(println (neg-t? (i8 -3)))
(println (zero-t? (u8 0)))
(println (zero-t? 0.0))
(println (pos-t? (u16 1)))
(println (neg-t? (f32 -0.5)))
(println (next-after 3))
(println (next-after (i64 10)))
(println (next-after 2.5))
(println (next-after (u8 254)))
;; A byte the reader *can* spell, printed rather than inspected. The
;; break loop and the inspector show this one as `97 (\a)' — a u8 is the
;; one type that reads two ways, and which way depends on who is looking.
;; Printing is the program talking, so it stays the number. 254 above
;; cannot tell the two apart, because 255 has no spelling either way.
(println (u8 97))
(println (plus-300 1))
;; And the prelude's own three, which are these bodies under their real
;; names. The -0.0 is the one worth asserting: IEEE says -0.0 = 0.0 and
;; zero? does not second-guess it.
(println (pos? (i64 3)))
(println (zero? -0.0))
(println (neg? (u8 3)))
(show 3)
(show 4.5)
(show "text")
;; The collapsed prelude family, at both element types.
(sort (slice ns 0 4))
(println (at ns 0))
(sort-by (slice fs 0 3) (fn [a b] (> a b)))
(println (at fs 0))
(reverse (slice ns 0 4))
(println (at ns 0))
(map-in-place (slice ns 0 4) (fn [x] (* x 2)))
(println (reduce (slice ns 0 4) 0 (fn [a b] (+ a b))))
(match (min-of (slice ns 0 4)) (Some m) (println m) _ (println -1))
(match (max-of (slice fs 0 3)) (Some m) (println m) _ (println -1.0))
(match (index-of (slice ns 0 4) 18) (Some i) (println i) _ (println -1))
;; or-else and some?, which are the same family over (Option $t) and take
;; no predicate: they move the payload out or read the tag, and neither is
;; an operation the variable has to be declared to support.
;;
;; Both branches at two scalar types, because a default that is returned
;; and a default that is discarded are two different lowerings and only one
;; of them is exercised by a call that happens to be Some.
(println (or-else (index-of (slice ns 0 4) 18) -1)) ; the Some branch
(println (or-else (index-of (slice ns 0 4) 77) -1)) ; the None branch
(println (or-else (max-of (slice fs 0 3)) 0.0))
(println (or-else (max-of (slice fs 0 0)) 0.0))
(println (some? (index-of (slice ns 0 4) 18)))
(println (some? (index-of (slice ns 0 4) 77)))
(println (some? (parse-i64 (bytes-view "12"))))
;; And at a $t that owns storage, which is the case the scalars above say
;; nothing about. What comes back is a *header* onto one of the two
;; buffers, so both are still the caller's to free — hence two frees and
;; not one, and the lengths are what say which header each answer holds.
(let [full (vec-new u8)
empty (vec-new u8)]
(push full 65)
(push full 66)
(println (length (or-else (Some full) empty))) ; 2, full's header
(println (length (or-else (none-vec) empty))) ; 0, empty's
(free full)
(free empty))
(println (widen 3 0.0))
(println (widen 3 (i64 0)))
(println (zero-of 9))
;; One written body, two key types, two emitted copies.
(println (bump 7 10))
(println (bump "key" 3))
(let [a (arena-new 4096)
keep (filter (slice ns 0 4) (fn [x] (> x 5)))
one (one-of 4.5)]
(println (length (slice keep)))
(println (at (slice one) 0))
(free one)
(free keep)
(free-all a))))