diff --git a/lib/prelude.ml b/lib/prelude.ml index 5e84334..48e6b51 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -90,15 +90,27 @@ let source = {flan| ;; ── Slice algorithms, all in place ──────────────────────────────────── ;; -;; Over [i32] and nothing else. There are no generics, so one of these per -;; element type is one *copy* per element type, emitted into every program; -;; i32 is the type indices, ids and tile values already have, and f32 copies -;; wait until a program actually wants them. +;; One family per element type, because there are no generics: each of these +;; is a *copy* per element type, and the set below is i32 (what indices, ids +;; and tile values are), f32 (what positions, velocities and weights are) and +;; [u8] (what a field coming out of `split` is). ;; ;; A slice is ptr+len and non-owning, so these mutate the storage they were ;; handed: sorting (slice grid 4 9) sorts those five elements of grid and -;; leaves the rest alone. That is the whole reason the shape is in-place — -;; there is no allocator to return a new sequence from. +;; leaves the rest alone. That was originally forced — there was no allocator +;; to return a new sequence from — and it stays the right shape now that there +;; is one, because sorting a thing you already own should not allocate. The +;; allocating tier is further down, and a caller sorts a Vec by sorting +;; (as-slice v). +;; +;; **map, filter, reduce and a sort taking a comparator are not here, and they +;; are not blocked on generics.** They are blocked on *function values*: each +;; of them takes a callable as an argument, Types.Fn exists but check.ml +;; refuses it with "a function type is not implemented yet — milestone 5", and +;; there is nothing else in the language to pass. Generics on top of that is +;; what would make them one copy instead of one per element type; without +;; either, the honest form is the concrete fold, which is what sum-i32 and +;; sum-f32 below already are — (reduce + 0) with the + written in. (defn swap-i32! [s [i32] i i32 j i32] (let [t (at s i)] @@ -164,6 +176,74 @@ let source = {flan| (set t (+ t (i64 (at s i))))) t)) +;; ── The same family over f32 ────────────────────────────────────────── +;; +;; sort-i32! was the only sort in the language, which is what NEXT.md's second +;; tier means by "a sort that is not integers-only". This is the second, and it +;; is a copy and not an abstraction — see the note above on why. +;; +;; One caveat that has no counterpart in the i32 family, because it cannot +;; arise there: **a NaN in the input makes the order undefined.** Every +;; comparison against a NaN is false, so the insertion loop never moves one and +;; never moves anything past one; what comes out is sorted within each run +;; between NaNs and not sorted across them. That is what C's qsort with a naive +;; comparator does too. The fix is not to have NaNs in the array — which is +;; also the only fix, since there is no ordering of the reals that a NaN sits +;; anywhere in. + +(defn swap-f32! [s [f32] i i32 j i32] + (let [t (at s i)] + (set (at s i) (at s j)) + (set (at s j) t))) + +(defn reverse-f32! [s [f32]] + (let [i 0 + j (- (len s) 1)] + (while (< i j) + (swap-f32! s i j) + (set i (+ i 1)) + (set j (- j 1))))) + +(defn sort-f32! [s [f32]] + (let [i 1] + (while (< i (len s)) + (let [j i] + (while (and (> j 0) (> (at s (- j 1)) (at s j))) + (swap-f32! s (- j 1) j) + (set j (- j 1)))) + (set i (+ i 1))))) + +;; None for an empty slice, exactly as min-i32 does. A NaN in the input is not +;; special-cased and propagates the same way it does through the builtins: the +;; comparison fails, so the running value simply does not change. +(defn min-f32 [s [f32]] (Option f32) + (if (= (len s) 0) + None + (let [m (at s 0)] + (dotimes [i (len s)] + (set m (min m (at s i)))) + (Some m)))) + +(defn max-f32 [s [f32]] (Option f32) + (if (= (len s) 0) + None + (let [m (at s 0)] + (dotimes [i (len s)] + (set m (max m (at s i)))) + (Some m)))) + +;; Accumulates in f64 and widens each element explicitly, which is sum-i32's +;; argument in its floating form and a stronger one: summing a screenful of f32 +;; in f32 does not wrap, it *absorbs* — once the running total is large enough, +;; adding a small element rounds to no change at all, and the answer is silently +;; short rather than obviously wrong. An f64 accumulator has 29 more bits of +;; mantissa and pushes that failure out of reach of any array a game holds. +(defn sum-f32 [s [f32]] f64 + (let [t 0.0] + (dotimes [i (len s)] + (set t (+ t (f64 (at s i))))) + t)) + ;; ── Bytes ───────────────────────────────────────────────────────────── ;; ;; Over [u8] and not over string, so (bytes s) is what a caller writes and one @@ -790,6 +870,52 @@ let source = {flan| (return false))) true))) +;; ── Ordering byte slices, and sorting them ──────────────────────────── +;; +;; The third element type the slice family covers, and the one a caller of +;; `split` actually has: a [[u8]] of fields, wanting to come out in order. +;; +;; The order is bytewise-lexicographic — memcmp's, and the one every sane +;; sorted format uses. It is explicitly *not* alphabetical and not a collation: +;; "Zebra" sorts before "apple" because 'Z' is 90 and 'a' is 97, and a +;; non-ASCII byte sorts by its UTF-8 encoding, which for code points happens to +;; agree with code-point order and for anything a human would call alphabetical +;; does not. A locale-aware comparison is not a byte operation at all, for the +;; same reasons the ASCII-case note above gives. +;; +;; The comparison is over u8 and therefore unsigned, which is the bug a version +;; written over a signed byte type has: 0x80 would compare *below* 0x00 and +;; every multi-byte character would sort before every ASCII one. +;; +;; A prefix sorts before what extends it — "ab" before "abc" — which falls out +;; of running to the shorter length and then comparing lengths, and is the case +;; a loop written to (len a) alone reads off the end for. +(defn bytes j 0) (bytes