From 7ce6043c473b2c020bc2c8df3e862a5b59f47e21 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 21:58:12 +0700 Subject: [PATCH] A second and third sort, and why there is not a generic one sort-i32! was the only sort in the language. sort-f32! and sort-bytes! are the other two, and they are copies rather than an abstraction for a reason worth naming precisely: map, filter, reduce and a sort taking a comparator are not blocked on generics, they are blocked on *function values*. Types.Fn exists and 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. The f32 family carries one caveat the i32 family cannot have: a NaN makes the order undefined, because every comparison against one is false, so the insertion loop never moves it and never moves anything past it. sum-f32 accumulates in f64 for a stronger version of sum-i32's argument -- an f32 total does not wrap, it absorbs, and the answer comes out silently short. The test prints the difference rather than the total, because %g hides it. sort-bytes! is the one a caller of split actually wants, and its ordering is memcmp's: bytewise, unsigned, prefix first. Not alphabetical -- "Zebra" sorts before "apple" -- and the note says so, for the same reason the ASCII-case note refuses a locale. The slices move and the bytes never do, so it sorts fields borrowed out of a string literal, which an in-place byte sort could not. --- lib/prelude.ml | 138 ++++++++++++++++++++++++++++++++-- test/programs/algorithms.flan | 120 +++++++++++++++++++++++++++++ test/test_acceptance.ml | 24 ++++++ 3 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 test/programs/algorithms.flan 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