floor, ceil and round over f32, which is what a position and a tile coordinate are here. The only rounding mode available is the cast's truncation toward zero, so each of these is that cast plus the correction the mode does not make, and the content is which inputs make the cast itself undefined. NaN fails every comparison, so it needs its own (not (= x x)) and nothing else finds it; the infinities fall out of the magnitude test; and above 2^23 an f32 has no fractional bits left, which makes returning the input there the exact answer and also the guard that keeps the cast inside i32. round is half away from zero, written as floor of the magnitude and mirrored. The obvious (floor-f32 (+ x 0.5)) is wrong twice: half-up rather than half-away, so -2.5 comes out -2, and at the largest f32 below 0.5 the addition alone rounds to 1.0 and answers 1 for a number under a half. Both are in the table, which is why every case there is a negative or a half. sqrt is the decision in this commit and it goes out to libm, which is a change to the release link and so is said out loud. Every other number in the prelude is reachable from the four operations and a cast; a square root is not. Newton's method needs a starting guess, the good guess comes from reinterpreting the exponent bits, and the language has only value-preserving casts - no bit-cast between f32 and u32. Without one the iteration needs a scaling loop to normalise and still produces a result that is merely close, which is the one thing a standard library must not hand back. IEEE-754 makes sqrt correctly rounded, so libm's answer is the same bit pattern on native and on wasm32; for this function the byte-identical argument points at C rather than away from it. The cost is -lm on every link, and its placement matters. It goes after the objects, not in the leading flags, because --as-needed drops a library named before the object that wants it. Worse, at -O2 LLVM folds most sqrtf calls into the hardware instruction and the symbol never has to resolve - so this looked linked before the flag existed and failed only at -O0, which is exactly why the table runs both. Untested against --target=wasm32: wasi-libc ships libm.a as a stub because the symbols live in libc, so it should be inert there, but nothing here exercises it. The better fix is not in this lane. llvm.sqrt.f32 as a builtin in check.ml and emit.ml is one instruction, no symbol and no flag, and it belongs to whoever owns the compiler.
61 lines
2.7 KiB
Plaintext
61 lines
2.7 KiB
Plaintext
;;;; The prelude's rounding, and sqrt.
|
|
;;;;
|
|
;;;; Every input is one a plausible wrong implementation gets wrong. The
|
|
;;;; negatives are the whole point: a floor written as a bare cast truncates
|
|
;;;; toward zero and answers -2 for -2.5, and a round written as
|
|
;;;; (floor-f32 (+ x 0.5)) answers -2 for -2.5 as well, where C's round says
|
|
;;;; -3. The exact halves appear on both signs for that reason. The values
|
|
;;;; that are already integers check that the correction does *not* fire —
|
|
;;;; a floor that always subtracts one turns 3.0 into 2.0 — and 16777216.0 is
|
|
;;;; past 2^24, where an f32 has no fractional bits and the guard, not the
|
|
;;;; cast, has to produce the answer.
|
|
|
|
(defn show [x f32]
|
|
(print-f64 (f64 x))
|
|
(print-str " "))
|
|
|
|
(defn main [] i32
|
|
;; floor: down on both signs, and unmoved on the integers.
|
|
(show (floor-f32 2.7)) (show (floor-f32 2.0)) (show (floor-f32 2.3))
|
|
(show (floor-f32 -2.7)) (show (floor-f32 -2.0)) (show (floor-f32 -2.3))
|
|
(show (floor-f32 0.5)) (show (floor-f32 -0.5))
|
|
(newline)
|
|
|
|
;; ceil: up on both signs. -2.7 must give -2, which is where a ceil written
|
|
;; as "floor plus one" goes wrong.
|
|
(show (ceil-f32 2.7)) (show (ceil-f32 2.0)) (show (ceil-f32 2.3))
|
|
(show (ceil-f32 -2.7)) (show (ceil-f32 -2.0)) (show (ceil-f32 -2.3))
|
|
(show (ceil-f32 0.5)) (show (ceil-f32 -0.5))
|
|
(newline)
|
|
|
|
;; Zero keeps its sign through floor, which is what the (= x 0.0) guard in
|
|
;; it is for and the only place that guard is observable: the cast it skips
|
|
;; would turn -0.0 into +0.0, and %g prints the difference. Drop the guard
|
|
;; and the third column here reads 0 instead of -0.
|
|
(show (floor-f32 0.0)) (show (ceil-f32 0.0)) (show (floor-f32 -0.0))
|
|
(newline)
|
|
|
|
;; round: half away from zero on both signs, so -2.5 is -3 and not -2.
|
|
(show (round-f32 2.4)) (show (round-f32 2.5)) (show (round-f32 2.6))
|
|
(show (round-f32 -2.4)) (show (round-f32 -2.5)) (show (round-f32 -2.6))
|
|
(show (round-f32 0.5)) (show (round-f32 -0.5))
|
|
(newline)
|
|
|
|
;; Past 2^24 there is no fraction left; the answer is the input, and the
|
|
;; cast that would produce it is out of i32's range on the way there.
|
|
(show (floor-f32 16777216.0)) (show (ceil-f32 16777216.0))
|
|
(show (round-f32 16777216.0)) (show (floor-f32 -16777216.0))
|
|
(newline)
|
|
|
|
;; sqrt, including the two values a wrong-sense iteration still passes
|
|
;; (0 and 1) and one that is not a perfect square.
|
|
(show (sqrt-f32 0.0)) (show (sqrt-f32 1.0)) (show (sqrt-f32 4.0))
|
|
(show (sqrt-f32 2.0)) (show (sqrt-f32 0.25)) (show (sqrt-f32 1e6))
|
|
(newline)
|
|
|
|
;; A squared distance through sqrt, which is what a game actually calls it
|
|
;; for: 3-4-5 exactly, so a last-bit error would show.
|
|
(show (sqrt-f32 (+ (* 3.0 3.0) (* 4.0 4.0))))
|
|
(newline)
|
|
0)
|