From 5d65dcf1c8c3191a229072532eef4d436abd0630 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 09:04:25 +0700 Subject: [PATCH] sin and cos in the prelude, with the caveat sqrt does not have The gestures testbed declared sinf and cosf at the top of its own file, which is a copy in every file that wants an angle. The reason sqrt is a declare does not transplant: IEEE-754 makes sqrt correctly rounded and requires nothing of the kind for sinf, so these two are the one place in the prelude where native and wasm32 may disagree bit for bit. That is written down beside them, along with what the fix would be if a program ever needs trig that agrees across targets. Float abs stays unwrapped for the reason integer abs is -- it is (max x (- 0.0 x)) over two builtins. The integer caveat does not carry over and the note says so: -0.0 answers +0.0 and a NaN answers a NaN, both checked. --- examples/core-input-gestures-testbed.flan | 6 ---- lib/prelude.ml | 37 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan index 577184b..996a96a 100644 --- a/examples/core-input-gestures-testbed.flan +++ b/examples/core-input-gestures-testbed.flan @@ -47,12 +47,6 @@ (import rl "vendor:raylib") (import d "digits.flan") -;; libm, as the prelude declares sqrtf. Not declare-c: these take a float and -;; answer a float in C's own convention with no struct anywhere, which is what -;; plain `declare` is for. -(declare sin-f32 [x f32] f32 "sinf") -(declare cos-f32 [x f32] f32 "cosf") - (defconst screen-width 800) (defconst screen-height 450) diff --git a/lib/prelude.ml b/lib/prelude.ml index c668e2a..fe39163 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -197,6 +197,15 @@ let source = {flan| ;; the negation wraps. That is what every two's-complement abs does, a ;; function here would do it too, and the only fix is not to hand it that ;; value — so it is written down rather than wrapped. +;; +;; The float abs is the same one-liner, (max x (- 0.0 x)), and it is not +;; wrapped for the same reason — but the caveat above does not carry over, so +;; it is not inherited by silence. f32 negation is exact at every value, there +;; is no least representable float that negates to itself, and the two edge +;; inputs both come out right: -0.0 answers +0.0 (the max picks the subtracted +;; side, since neither zero is greater than the other), and a NaN answers a +;; NaN (every comparison fails, so the same max picks the subtracted side, +;; which is still a NaN). There is nothing left for a function to fix. ;; 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 @@ -303,6 +312,34 @@ let source = {flan| ;; builtin in check.ml and emit.ml is one instruction with no symbol at all. (declare sqrt-f32 [x f32] f32 "sqrtf") +;; sin and cos go out to libm too, and the argument is *not* the one above — +;; it is weaker, and which way it is weaker is the thing to know before +;; calling them. IEEE-754 requires sqrt to be correctly rounded, which is why +;; sqrtf's answer is the same bit pattern wherever it runs. It requires +;; nothing of the kind for sinf and cosf: each implementation is free to be a +;; fraction of an ulp off in its own direction, and glibc, musl and wasi-libc +;; do differ. So these two are the one place in this file where native and +;; wasm32 may not agree bit for bit, and a program whose output is hashed +;; across targets — the sand grid of plan.org's "RNG is ours", which is why +;; rand-u32 above is written in Flan and not called out of libc — must not +;; route that hash through a sine. +;; +;; They are here anyway, because the alternative on offer today is worse: a +;; caller that wants an angle writes the same two `declare` lines at the top +;; of its own file (examples/core-input-gestures-testbed.flan did, before +;; this), which is the identical libm call with the identical caveat and +;; nobody's name on it. One copy with the caveat written down beats a copy per +;; file with none. +;; +;; The fix, if a program ever does need trig that agrees across targets, is a +;; body rather than a declare: Cody-Waite reduction onto [-pi/4, pi/4] and a +;; minimax polynomial, which is reachable from the four operations and +;; floor-f32 and would therefore be exactly as reproducible as rand-u32. That +;; is a numerics job with its own accuracy budget, and it waits for a program +;; that needs it. +(declare sin-f32 [x f32] f32 "sinf") +(declare cos-f32 [x f32] f32 "cosf") + ;; ── Byte classes ────────────────────────────────────────────────────── ;; ;; ASCII only, and deliberately: a byte is a byte here, there is no code point