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