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