From b0bc40ca05c2f7bd7de22cce9dcf3ddd0c826b17 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 21:48:37 +0700 Subject: [PATCH] atan2 and pow go out to libm, and clamp is a macro rather than four functions The two declares inherit the sin/cos caveat in full and not the sqrt one: IEEE-754 requires nothing of atan2f or powf either, so they are the third and fourth places in the prelude where native and wasm32 may differ in the last bit. Every case in math2.flan is therefore a value that is exact in binary -- a quadrant boundary, a power of two, a perfect square -- rather than one that would pin a particular libm and then fail on wasi. clamp is the interesting one. The prelude already argued against wrapping (min hi (max lo x)) in a function, and that argument gets stronger rather than weaker: min and max are builtins at every numeric type and there are no generics, so a clamp *function* is one copy per type. A macro is type-agnostic for free and emits nothing at all. The test calls the same three words at i32, i64, u8 and f32 to show it, and counts evaluations to show that each argument appears once -- the shape that names x twice reads identically and calls it twice. lo above hi answers hi and is not checked. A macro has no error facility, so the only diagnostic available would be a run-time one, in the construct whose whole point is that it costs nothing at run time. --- lib/prelude.ml | 53 +++++++++++++++++++++++++-- test/programs/math2.flan | 78 ++++++++++++++++++++++++++++++++++++++++ test/test_acceptance.ml | 21 +++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 test/programs/math2.flan diff --git a/lib/prelude.ml b/lib/prelude.ml index b88f4cd..059d186 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -225,9 +225,9 @@ let source = {flan| ;; ── Numbers ─────────────────────────────────────────────────────────── ;; -;; Only the ones 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. The one honest caveat +;; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over +;; that is a function emitted into every program to save a caller nothing. The +;; one honest caveat ;; on that abs: at the least representable integer it answers itself, because ;; 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 @@ -242,6 +242,28 @@ let source = {flan| ;; 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. +;; clamp is a macro and not a function, and the reason is the objection above +;; turned around rather than dropped. min and max are builtins that work at +;; every numeric type; a clamp *function* cannot, because there are no +;; generics, so it would be one copy per type — a clamp-i32, a clamp-f32, a +;; clamp-i64 — each emitted into every program to save a caller eleven +;; characters. A macro is type-agnostic for free and emits nothing at all: what +;; the program contains after expansion is the (min hi (max lo x)) the caller +;; would have written. +;; +;; Each of x, lo and hi appears exactly once in the expansion, so nothing here +;; is evaluated twice and an argument with a side effect behaves as it reads. +;; +;; lo above hi is not checked, and the answer there is hi — the outer min wins. +;; That is the same rule Odin's clamp follows and there is nowhere better to +;; put a complaint: a macro has no error facility (see `unless` at the foot of +;; this file), so a diagnostic would have to be a run-time one, in the one +;; construct whose whole point is that it costs nothing at run time. +(defmacro clamp [args] + (if (!= (len args) 3) + `(clamp-takes-a-value-a-low-and-a-high) + `(min ~(at args 2) (max ~(at args 1) ~(at args 0))))) + ;; 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. @@ -375,6 +397,31 @@ let source = {flan| (declare sin-f32 [x f32] f32 "sinf") (declare cos-f32 [x f32] f32 "cosf") +;; atan2 and pow inherit the paragraph above in full, and not the sqrt one. +;; IEEE-754 requires nothing of atan2f or powf either, so these are the third +;; and fourth places in this file where native and wasm32 may disagree in the +;; last bit, and the sand-grid rule stands unchanged: a hash compared across +;; targets must not be routed through any of the four. +;; +;; They are here for the reason the trig pair is. Without them a program that +;; wants a heading or a falloff curve writes the identical two declare lines at +;; the top of its own file, which is the same libm call with the same caveat +;; and nobody's name on it. +;; +;; atan2's y comes first, as it does in C, and the order is the answer rather +;; than a convention: knowing the quadrant of (y, x) is the whole of what it +;; has over (atan (/ y x)), and it is recovered from the two signs. It is +;; defined at x = 0, where the division is not. +;; +;; One caveat of pow-f32's own, because it is the one that gets reported as a +;; bug: it is not exact at integer exponents. powf goes through a logarithm, +;; so (pow-f32 10.0 2.0) is 100.0 or the float next to it depending on the +;; libm, and an index computed by casting that to i32 is off by one on the +;; wrong side. A small integer power is a multiplication, and should be +;; written as one. +(declare atan2-f32 [y f32 x f32] f32 "atan2f") +(declare pow-f32 [x f32 y f32] f32 "powf") + ;; ── Byte classes ────────────────────────────────────────────────────── ;; ;; ASCII only, and deliberately: a byte is a byte here, there is no code point diff --git a/test/programs/math2.flan b/test/programs/math2.flan new file mode 100644 index 0000000..145cbb0 --- /dev/null +++ b/test/programs/math2.flan @@ -0,0 +1,78 @@ +;;;; atan2, pow, and clamp — the three gaps NEXT.md's second-tier list names +;;;; under "maths gaps". +;;;; +;;;; Two of them are declares over libm and the third is a macro, and that +;;;; split is the whole content of this file. atan2f and powf are not correctly +;;;; rounded under IEEE-754, exactly as sinf and cosf are not, so every case +;;;; below is a value whose answer is exact in binary — a quadrant boundary, a +;;;; power of two, a perfect square — rather than one that would pin a +;;;; particular libm's last bit and then fail on wasi-libc. +;;;; +;;;; clamp is a macro because a function could not be: min and max are builtins +;;;; at every numeric type and there are no generics, so a clamp function is +;;;; one copy per type. The proof that the macro is not one copy per type is +;;;; that the same three-word call below is made at i32, i64, u8 and f32. + +(defn show [x f32] + (print x) + (print " ")) + +;;; clamp must evaluate each of its three arguments exactly once. A macro that +;;; repeated one — say (if (< x lo) lo (if (> x hi) hi x)), which names x twice +;;; — would read identically and call this twice. +(defvar calls i32 0) + +(defn tick [x i32] i32 + (set calls (+ calls 1)) + x) + +(defn main [] i32 + ;; atan2 across all four quadrants and on both axes, which is the whole + ;; reason for it over a division: the quadrant is recovered from two signs, + ;; and (/ y x) has thrown it away before atan sees it. The two on the x = 0 + ;; axis are where the division does not exist at all. + (show (atan2-f32 1.0 1.0)) ; pi/4 + (show (atan2-f32 1.0 -1.0)) ; 3pi/4 + (show (atan2-f32 -1.0 -1.0)) ; -3pi/4 + (show (atan2-f32 -1.0 1.0)) ; -pi/4 + (show (atan2-f32 0.0 1.0)) ; 0 + (show (atan2-f32 1.0 0.0)) ; pi/2 + (show (atan2-f32 -1.0 0.0)) ; -pi/2 + (println "") + + ;; pow. A power of two, a half-power that is a perfect square, a negative + ;; exponent, and the two edge exponents every implementation special-cases. + (show (pow-f32 2.0 10.0)) ; 1024 + (show (pow-f32 9.0 0.5)) ; 3 + (show (pow-f32 2.0 -2.0)) ; 0.25 + (show (pow-f32 5.0 0.0)) ; 1 + (show (pow-f32 5.0 1.0)) ; 5 + (println "") + + ;; clamp: below the range, above it, and inside it untouched. + (print (clamp 0 1 3)) (print " ") ; 1 + (print (clamp 5 1 3)) (print " ") ; 3 + (print (clamp 2 1 3)) (print " ") ; 2 + ;; Both ends are inclusive, which is the off-by-one a hand-written clamp + ;; gets wrong with a < where it wanted <=. + (print (clamp 1 1 3)) (print " ") ; 1 + (print (clamp 3 1 3)) ; 3 + (println "") + + ;; The same three words at three more types, none of which a function could + ;; have served without a copy of its own. + (print (clamp (i64 900) (i64 0) (i64 255))) (print " ") ; 255 + (print (clamp (u8 200) (u8 0) (u8 255))) (print " ") ; 200 + (show (clamp 2.5 0.0 1.0)) ; 1 + (println "") + + ;; lo above hi is not an error and answers hi: the outer min wins. Written + ;; down here rather than left for someone to discover. + (print (clamp 7 5 1)) + (println "") + + ;; Three arguments, three evaluations. + (print (clamp (tick 5) (tick 1) (tick 3))) (print " ") + (print calls) + (println "") + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 37db25f..098c3df 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -205,6 +205,27 @@ let () = in outputs "rounding and sqrt" "programs/math.flan" math_out; outputs ~opt:"-O0" "rounding and sqrt, -O0" "programs/math.flan" math_out; + (* atan2, pow and clamp. Every float here is exact in binary — quadrant + boundaries, powers of two, a perfect square — because atan2f and powf + are no more correctly rounded than sinf is, and a case pinning one + libm's last bit would pass native and fail wasi. The -O0 run is the one + that proves the two symbols resolve: at -O2 LLVM constant-folds a powf + of two literals and nothing is left to link, which is the same trap the + sqrt note describes. clamp is a macro, so the interesting lines are the + four types it is called at (a function would be four copies) and the + call counter, which is 3 and would be 4 or 5 for a macro that named an + argument twice. *) + let math2_out = + "0.785398 2.35619 -2.35619 -0.785398 0 1.5708 -1.5708 \n\ + 1024 3 0.25 1 5 \n\ + 1 3 2 1 3\n\ + 255 200 1 \n\ + 1\n\ + 3 3\n" + in + outputs "atan2, pow and clamp" "programs/math2.flan" math2_out; + outputs ~opt:"-O0" "atan2, pow and clamp, -O0" "programs/math2.flan" + math2_out; (* index-of-bytes, trim, the byte classes and parse-f64. The search cases are the ones that separate a correct loop from a lucky one: a match only at the end, "aab" in "aaab" (where the first byte matches twice