Finishing the 2D lane's unfinished work: the collision family was written and had no tests when the session ended. It is the best material a headless table gets, since every one of these is pure and needs no GL context. Two plausible tests in a row turned out to check nothing, and that is the part worth keeping. A struct round trip is symmetric and passes for any field order - the texture lane found that one. The second is subtler: no axis-aligned geometry can pin Vector2's fields, because exchanging x and y is a reflection that is applied on the way in and undone on the way out. Swapping the shim's own typedef leaves every collision case passing. Distances never even see it. What does pin Vector2 is the rotated camera, because a rotation is not axis-aligned and does not commute with the reflection. That case is load-bearing and the comment now says so, because the collision cases look like they cover the same ground and do not. What the new cases do pin is Rectangle, completely: swapping width and height turns three of the four predicates the wrong way. Verified by doing it. collision-lines answers (Option Vector2) rather than a bool and an out-parameter, because raylib leaves the out-parameter untouched when the segments do not meet and a caller who forgets reads whatever was there.
241 lines
9.1 KiB
OCaml
241 lines
9.1 KiB
OCaml
(** The milestone-2 prelude, written in Flan.
|
|
|
|
Printing is deliberately *not* a primitive (plan.org, Milestone-2
|
|
primitives): [write-stdout] is the one output primitive and everything
|
|
above it is Flan. That is what keeps a second backend cheap — a primitive
|
|
is the only thing implemented twice.
|
|
|
|
It lives here as a string rather than as a file because there is no package
|
|
loader yet; at milestone 3 it becomes an ordinary [core:] package and this
|
|
module goes away. The acceptance programs may call anything defined here.
|
|
|
|
No overloading: [print-f64] and [print-str] name the type, because
|
|
compile-time overloading before the checker is stable is how a small
|
|
language stops being one. A single [println] is milestone 5. *)
|
|
|
|
let source = {flan|
|
|
(defn print-bytes [b [u8]]
|
|
(write-stdout b))
|
|
|
|
(defn print-str [s string]
|
|
(write-stdout (bytes s)))
|
|
|
|
(defn print-f64 [x f64]
|
|
(write-stdout (f64->bytes x)))
|
|
|
|
(defn print-i64 [x i64]
|
|
(write-stdout (i64->bytes x)))
|
|
|
|
(defn newline []
|
|
(write-stdout (bytes "\n")))
|
|
|
|
;; A seeded PRNG in Flan rather than libc's, because a grid hash is only a
|
|
;; regression test if the sequence is byte-identical on native and wasm32
|
|
;; (plan.org, RNG is ours). PCG-XSH-RR 32: one u64 LCG step per draw, folded
|
|
;; down to 32 bits by an xorshift and rotated by the state's top five bits.
|
|
(defvar rand-state u64 6364136223846793005)
|
|
|
|
(defn rand-seed [seed u64]
|
|
(set rand-state (+ (* seed 6364136223846793005) 1442695040888963407)))
|
|
|
|
(defn rand-u32 [] u32
|
|
(let [s rand-state]
|
|
(set rand-state (+ (* s 6364136223846793005) 1442695040888963407))
|
|
;; The rotate is masked to 5 bits: a 32-bit shift by 32 is poison in LLVM,
|
|
;; and r = 0 is the case that would ask for it.
|
|
(let [x (u32 (>> (bit-xor (>> s 18) s) 27))
|
|
r (u32 (>> s 59))]
|
|
(bit-or (>> x r) (<< x (bit-and (- 32 r) 31))))))
|
|
|
|
;; In [0, 1). The divisor is 2^32 exactly, so the result never reaches 1.0.
|
|
(defn rand-f32 [] f32
|
|
(/ (f32 (rand-u32)) 4294967296.0))
|
|
|
|
;; Prints s and then a newline. Takes a string, not an Option or an any —
|
|
;; there is nothing to dispatch on yet.
|
|
(defn print-line [s string]
|
|
(print-str s)
|
|
(newline))
|
|
|
|
;; ── 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.
|
|
;;
|
|
;; 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.
|
|
|
|
(defn swap-i32! [s [i32] i i32 j i32]
|
|
(let [t (at s i)]
|
|
(set (at s i) (at s j))
|
|
(set (at s j) t)))
|
|
|
|
(defn reverse-i32! [s [i32]]
|
|
(let [i 0
|
|
j (- (len s) 1)]
|
|
(while (< i j)
|
|
(swap-i32! s i j)
|
|
(set i (+ i 1))
|
|
(set j (- j 1)))))
|
|
|
|
;; Insertion sort: in place, no recursion, no auxiliary array and no
|
|
;; comparison function — quicksort would want a stack and mergesort a buffer,
|
|
;; and neither exists. Ascending, and stable, though with no payload type to
|
|
;; carry that is not yet observable.
|
|
(defn sort-i32! [s [i32]]
|
|
(let [i 1]
|
|
(while (< i (len s))
|
|
(let [j i]
|
|
;; `and` short-circuits, which is load-bearing: at j = 0 the left test
|
|
;; fails and (at s -1) is never evaluated, so this does not trap.
|
|
(while (and (> j 0) (> (at s (- j 1)) (at s j)))
|
|
(swap-i32! s (- j 1) j)
|
|
(set j (- j 1))))
|
|
(set i (+ i 1)))))
|
|
|
|
;; The first index holding x. None rather than -1, because Option is what the
|
|
;; language has and a sentinel index is the bug this avoids.
|
|
(defn index-of-i32 [s [i32] x i32] (Option i32)
|
|
(dotimes [i (len s)]
|
|
(when (= (at s i) x)
|
|
(return (Some i))))
|
|
None)
|
|
|
|
;; None for an empty slice: there is no least i32 that is also an honest
|
|
;; answer, and returning one would be a value the caller cannot tell from a
|
|
;; real element.
|
|
(defn min-i32 [s [i32]] (Option i32)
|
|
(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-i32 [s [i32]] (Option i32)
|
|
(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 i64 and each element is widened explicitly — there is no
|
|
;; implicit widening anywhere in the language, and summing a screenful of i32
|
|
;; into an i32 is how a total silently wraps.
|
|
(defn sum-i32 [s [i32]] i64
|
|
(let [t (i64 0)]
|
|
(dotimes [i (len s)]
|
|
(set t (+ t (i64 (at s i)))))
|
|
t))
|
|
|
|
;; ── Bytes ─────────────────────────────────────────────────────────────
|
|
;;
|
|
;; Over [u8] and not over string, so (bytes s) is what a caller writes and one
|
|
;; copy of each serves strings and byte slices both — which is as close to a
|
|
;; generic as a language without them gets. Nothing here allocates: every
|
|
;; result is a bool, an index, or a number.
|
|
|
|
(defn bytes=? [a [u8] b [u8]] bool
|
|
(if (!= (len a) (len b))
|
|
false
|
|
(do
|
|
(dotimes [i (len a)]
|
|
(when (!= (at a i) (at b i))
|
|
(return false)))
|
|
true)))
|
|
|
|
;; The length test comes first and `and` short-circuits, so the slice is only
|
|
;; built once it is known to be in bounds — otherwise a prefix longer than the
|
|
;; string would trap rather than answer false.
|
|
(defn starts-with? [s [u8] p [u8]] bool
|
|
(and (<= (len p) (len s))
|
|
(bytes=? (slice s 0 (len p)) p)))
|
|
|
|
(defn ends-with? [s [u8] p [u8]] bool
|
|
(and (<= (len p) (len s))
|
|
(bytes=? (slice s (- (len s) (len p)) (len s)) p)))
|
|
|
|
(defn index-of-byte [s [u8] b u8] (Option i32)
|
|
(dotimes [i (len s)]
|
|
(when (= (at s i) b)
|
|
(return (Some i))))
|
|
None)
|
|
|
|
;; The whole slice is an integer, or it is None. bytes->i64 is strtoll, which
|
|
;; answers 0 for "" and for "abc" and stops at the first junk byte in "12x" —
|
|
;; three wrong answers a caller cannot tell from a real 12. This is also the
|
|
;; one that has to be Flan rather than the primitive: strtoll is locale- and
|
|
;; libc-dependent, and a parser in the language gives the same answer on
|
|
;; wasm32 as on native for the same reason rand-f32 does.
|
|
;; Overflow wraps, as all arithmetic here does; it is not reported.
|
|
(defn parse-i64 [s [u8]] (Option i64)
|
|
(let [i 0
|
|
n (i64 0)
|
|
neg false]
|
|
(when (= (len s) 0)
|
|
(return None))
|
|
(when (or (= (at s 0) \-) (= (at s 0) \+))
|
|
(set neg (= (at s 0) \-))
|
|
(set i 1))
|
|
(when (= i (len s))
|
|
(return None)) ; a lone sign is not a number
|
|
(while (< i (len s))
|
|
(let [b (at s i)]
|
|
(when (or (< b \0) (> b \9))
|
|
(return None))
|
|
(set n (+ (* n 10) (i64 (- b \0)))))
|
|
(set i (+ i 1)))
|
|
(if neg (Some (- 0 n)) (Some n))))
|
|
|
|
;; ── Numbers ───────────────────────────────────────────────────────────
|
|
;;
|
|
;; Only the two that encode a decision. clamp is (min hi (max lo x)) over two
|
|
;; builtins and abs is (max x (- 0 x)); a wrapper over those is a function
|
|
;; emitted into every program to save a caller nothing.
|
|
|
|
;; Zero for zero, and zero for NaN — neither is positive nor negative, so
|
|
;; neither comparison fires. A caller that needs to know which it got should
|
|
;; be testing for NaN, not reading a sign.
|
|
(defn sign-f32 [x f32] f32
|
|
(cond
|
|
(> x 0.0) 1.0
|
|
(< x 0.0) -1.0
|
|
:else 0.0))
|
|
|
|
;; Written as the weighted sum and not as a + t*(b - a): the second form does
|
|
;; not return b exactly at t = 1.0 once rounding is involved, and a position
|
|
;; that does not arrive is the bug an interpolation gets reported for.
|
|
(defn lerp [a f32 b f32 t f32] f32
|
|
(+ (* (- 1.0 t) a) (* t b)))
|
|
|
|
;; ── More of the RNG ───────────────────────────────────────────────────
|
|
;;
|
|
;; Both draw exactly one rand-u32, so the sequence a program consumes is the
|
|
;; same one; neither touches the generator.
|
|
|
|
;; [lo, hi). An empty or reversed range answers lo — a defined value rather
|
|
;; than a remainder by zero, which is immediate undefined behaviour and not a
|
|
;; wrong number. The span must fit in i32, since hi - lo is computed there.
|
|
;; One draw, and therefore modulo bias: the low (2^32 % span) values of the
|
|
;; range come up very slightly more often. Rejection sampling would remove it
|
|
;; and would consume an unpredictable number of draws, which is the one thing
|
|
;; this generator exists not to do.
|
|
(defn rand-i32-range [lo i32 hi i32] i32
|
|
(if (<= hi lo)
|
|
lo
|
|
(+ lo (i32 (% (rand-u32) (u32 (- hi lo)))))))
|
|
|
|
;; [lo, hi), because rand-f32 never reaches 1.0.
|
|
(defn rand-f32-range [lo f32 hi f32] f32
|
|
(+ lo (* (rand-f32) (- hi lo))))
|
|
|flan}
|
|
|
|
let file = "<prelude>"
|
|
|
|
let forms () = Reader.read_all ~file source
|