Skip to main content

logicaffeine_compile/accel/
softfloat.rs

1//! IEEE-754 binary64 on integer lanes: the VM's `Float` arithmetic, exact to the bit, for devices whose
2//! silicon has no binary64 unit.
3//!
4//! Every operation unpacks to sign, exponent and a normalized 53-bit significand, computes the exact
5//! significand result with the multi-word core (wide products, restoring division, digit-by-digit square
6//! root), and rounds once to nearest-even in [`round_pack`], which also produces subnormals, overflow to
7//! infinity, and the signed zeros. Special operands (NaN, infinities, zeros) are resolved by selects beside
8//! the finite path. A NaN result follows the x86 rule the host runs by: the first NaN operand, quieted, or
9//! the default NaN `0xFFF8_0000_0000_0000` for an invalid operation.
10//!
11//! Values are 64-bit words of this builder's lanes (`int64::n64`); exponents are 64-bit two's complement.
12
13use super::int64::n64;
14use super::multiword::{Builder, Mw};
15
16/// The x86 default NaN an invalid operation produces.
17pub const DEFAULT_NAN: u64 = 0xFFF8_0000_0000_0000;
18
19/// A binary64 operand taken apart.
20pub struct Unpacked {
21    pub bits: Mw,
22    pub sign: u32,
23    pub is_nan: u32,
24    pub is_inf: u32,
25    pub is_zero: u32,
26    /// For a finite nonzero operand: the unbiased exponent of the leading significand bit.
27    pub exp: Mw,
28    /// For a finite nonzero operand: the significand normalized to `[2^52, 2^53)`.
29    pub sig: Mw,
30}
31
32fn c64(b: &mut Builder, v: u64) -> Mw {
33    let n = n64(b);
34    b.mw_const(v as u128, n)
35}
36
37/// Take `x` apart. Subnormals are normalized here, so every finite nonzero operand leaves with its leading
38/// significand bit at 52.
39pub fn unpack(b: &mut Builder, x: &Mw) -> Unpacked {
40    let n = x.len();
41    let sign = b.mw_top_bit(x);
42    let e_field = {
43        let s = b.mw_shr(x, 52);
44        let m = c64(b, 0x7FF);
45        b.mw_and(&s, &m)
46    };
47    let frac = {
48        let m = c64(b, (1u64 << 52) - 1);
49        b.mw_and(x, &m)
50    };
51    let zero = b.mw_zero(n);
52    let e_zero = b.mw_eq(&e_field, &zero);
53    let e_max_c = c64(b, 0x7FF);
54    let e_max = b.mw_eq(&e_field, &e_max_c);
55    let f_zero = b.mw_is_zero(&frac);
56    let f_nonzero = b.bit_not(f_zero);
57    let is_nan = b.and(e_max, f_nonzero);
58    let is_inf = b.and(e_max, f_zero);
59    let is_zero = b.and(e_zero, f_zero);
60    let hidden = c64(b, 1u64 << 52);
61    let with_hidden = b.mw_or(&frac, &hidden);
62    let raw_sig = b.mw_sel(e_zero, &frac, &with_hidden);
63    let one = c64(b, 1);
64    let raw_exp_biased = b.mw_sel(e_zero, &one, &e_field);
65    let bias = c64(b, 1023);
66    let raw_exp = b.mw_minus(&raw_exp_biased, &bias);
67    let nc = b.shape().words_for(16);
68    let lz = b.mw_clz(&raw_sig, nc);
69    let lz64 = b.mw_resize(&lz, n);
70    let eleven = c64(b, 11);
71    let norm_shift = b.mw_minus(&lz64, &eleven);
72    let norm_shift = b.mw_sel(is_zero, &zero, &norm_shift);
73    let sig = b.mw_shl_var(&raw_sig, &norm_shift, 7);
74    let exp = b.mw_minus(&raw_exp, &norm_shift);
75    Unpacked {
76        bits: x.clone(),
77        sign,
78        is_nan,
79        is_inf,
80        is_zero,
81        exp,
82        sig,
83    }
84}
85
86/// `x` with its quiet bit set.
87fn quiet(b: &mut Builder, x: &Mw) -> Mw {
88    let q = c64(b, 1u64 << 51);
89    b.mw_or(x, &q)
90}
91
92/// The NaN a two-operand operation returns when an operand is NaN: the first NaN, quieted.
93fn propagate(b: &mut Builder, x: &Unpacked, y: &Unpacked) -> Mw {
94    let qx = quiet(b, &x.bits);
95    let qy = quiet(b, &y.bits);
96    b.mw_sel(x.is_nan, &qx, &qy)
97}
98
99/// `±∞`.
100fn infinity(b: &mut Builder, sign: u32) -> Mw {
101    let pos = c64(b, 0x7FF0_0000_0000_0000);
102    let neg = c64(b, 0xFFF0_0000_0000_0000);
103    b.mw_sel(sign, &neg, &pos)
104}
105
106/// `±0`.
107fn signed_zero(b: &mut Builder, sign: u32) -> Mw {
108    let pos = c64(b, 0);
109    let neg = c64(b, 1u64 << 63);
110    b.mw_sel(sign, &neg, &pos)
111}
112
113/// Round `sig × 2^(exp − 62)` to binary64, nearest-even, and pack it with `sign`. `sig` has its leading bit
114/// at 62 (a nonzero significand), with any lost low bits folded into bit 0 as a sticky bit. The one rounding
115/// every operation performs: normal results, gradual underflow to subnormals, and overflow to infinity.
116pub fn round_pack(b: &mut Builder, sign: u32, exp: &Mw, sig: &Mw) -> Mw {
117    let n = sig.len();
118    let bias = c64(b, 1023);
119    let be = b.mw_plus(exp, &bias);
120    let zero = b.mw_zero(n);
121    let one = c64(b, 1);
122    let be_pos = {
123        let le_zero = b.mw_lts(&be, &one);
124        b.bit_not(le_zero)
125    };
126    let under = b.mw_minus(&one, &be);
127    let cap = c64(b, 64);
128    let under_capped = {
129        let big = b.mw_lts(&cap, &under);
130        b.mw_sel(big, &cap, &under)
131    };
132    let shift = b.mw_sel(be_pos, &zero, &under_capped);
133    let (shifted, lost) = b.mw_shr_var_sticky(sig, &shift, 7);
134    let lost64 = b.mw_from_word(lost, n);
135    let jammed = b.mw_or(&shifted, &lost64);
136    let be_minus_1 = b.mw_minus(&be, &one);
137    let exp_field = b.mw_sel(be_pos, &be_minus_1, &zero);
138    let m = b.mw_shr(&jammed, 10);
139    let rmask = c64(b, 0x3FF);
140    let r = b.mw_and(&jammed, &rmask);
141    let half = c64(b, 0x200);
142    let above = b.mw_ltu(&half, &r);
143    let at_half = b.mw_eq(&r, &half);
144    let lsb = b.mw_bit(&m, 0);
145    let tie_up = b.and(at_half, lsb);
146    let inc = b.or(above, tie_up);
147    let (m_rounded, _) = b.mw_inc(&m, inc);
148    let placed_exp = b.mw_shl(&exp_field, 52);
149    let magnitude = b.mw_plus(&placed_exp, &m_rounded);
150    let sign_bit = {
151        let s = b.mw_from_word(sign, n);
152        b.mw_shl(&s, 63)
153    };
154    let packed = b.mw_or(&magnitude, &sign_bit);
155    let limit = c64(b, 0x7FF);
156    let overflow = {
157        let lt = b.mw_lts(&be, &limit);
158        b.bit_not(lt)
159    };
160    let inf = infinity(b, sign);
161    b.mw_sel(overflow, &inf, &packed)
162}
163
164/// Shift a nonzero `sig` so its leading bit sits at 62, returning it with the amount `exp` rises by (`p − 62`
165/// for a leading bit at `p`); the sticky bit rides into bit 0 when bits fall off the bottom.
166fn normalize_to_62(b: &mut Builder, sig: &Mw) -> (Mw, Mw) {
167    let n = sig.len();
168    let nc = b.shape().words_for(16);
169    let lz = b.mw_clz(sig, nc);
170    let lz64 = b.mw_resize(&lz, n);
171    let one = c64(b, 1);
172    let top_set = b.mw_top_bit(sig);
173    let (down, lost) = b.mw_shr_sticky(sig, 1);
174    let lost64 = b.mw_from_word(lost, n);
175    let down = b.mw_or(&down, &lost64);
176    let left = b.mw_minus(&lz64, &one);
177    let up = b.mw_shl_var(sig, &left, 7);
178    let normalized = b.mw_sel(top_set, &down, &up);
179    let rise = b.mw_minus(&one, &lz64);
180    (normalized, rise)
181}
182
183/// `a + b` (`negate_b` makes it `a − b`).
184pub fn add(b: &mut Builder, x: &Mw, y: &Mw, negate_b: bool) -> Mw {
185    let n = x.len();
186    let ux = unpack(b, x);
187    let uy = unpack(b, y);
188    let sy = if negate_b {
189        b.bit_not(uy.sign)
190    } else {
191        uy.sign
192    };
193    let zero = b.mw_zero(n);
194    // Order by magnitude: `big` has the larger (exponent, significand).
195    let ex_gt = b.mw_lts(&uy.exp, &ux.exp);
196    let ex_eq = b.mw_eq(&ux.exp, &uy.exp);
197    let sig_ge = {
198        let lt = b.mw_ltu(&ux.sig, &uy.sig);
199        b.bit_not(lt)
200    };
201    let tie_ge = b.and(ex_eq, sig_ge);
202    let x_big = b.or(ex_gt, tie_ge);
203    let big_exp = b.mw_sel(x_big, &ux.exp, &uy.exp);
204    let small_exp = b.mw_sel(x_big, &uy.exp, &ux.exp);
205    let big_sig = b.mw_sel(x_big, &ux.sig, &uy.sig);
206    let small_sig = b.mw_sel(x_big, &uy.sig, &ux.sig);
207    let big_sign = b.sel(x_big, ux.sign, sy);
208    let small_sign = b.sel(x_big, sy, ux.sign);
209    let d = b.mw_minus(&big_exp, &small_exp);
210    let cap = c64(b, 64);
211    let d_capped = {
212        let big = b.mw_lts(&cap, &d);
213        b.mw_sel(big, &cap, &d)
214    };
215    let big9 = b.mw_shl(&big_sig, 9);
216    let small9 = b.mw_shl(&small_sig, 9);
217    let (aligned, lost) = b.mw_shr_var_sticky(&small9, &d_capped, 7);
218    let lost64 = b.mw_from_word(lost, n);
219    let aligned = b.mw_or(&aligned, &lost64);
220    let same_sign = {
221        let differ = b.xor(big_sign, small_sign);
222        b.bit_not(differ)
223    };
224    let sum = b.mw_plus(&big9, &aligned);
225    let diff = b.mw_minus(&big9, &aligned);
226    let raw = b.mw_sel(same_sign, &sum, &diff);
227    let raw_zero = b.mw_is_zero(&raw);
228    let (norm, rise) = normalize_to_62(b, &raw);
229    // `raw`'s leading bit at 61 stands for 2^big_exp; `rise` counts from 62, so one less.
230    let one = c64(b, 1);
231    let e1 = b.mw_plus(&big_exp, &rise);
232    let e = b.mw_plus(&e1, &one);
233    let finite = round_pack(b, big_sign, &e, &norm);
234    // An exact zero from opposite signs is +0; from two zeros, −0 only when both are −0.
235    let pos_zero = c64(b, 0);
236    // One operand zero: the other one exactly.
237    let y_signed = {
238        let flip = if negate_b {
239            c64(b, 1u64 << 63)
240        } else {
241            b.mw_zero(n)
242        };
243        b.mw_xor(y, &flip)
244    };
245    let both_zero = b.and(ux.is_zero, uy.is_zero);
246    let zeros_sign = b.and(ux.sign, sy);
247    let zz = signed_zero(b, zeros_sign);
248    let mut out = b.mw_sel(raw_zero, &pos_zero, &finite);
249    out = b.mw_sel(uy.is_zero, x, &out);
250    out = b.mw_sel(ux.is_zero, &y_signed, &out);
251    out = b.mw_sel(both_zero, &zz, &out);
252    // Infinities: ∞ − ∞ is invalid; otherwise the infinite operand.
253    let both_inf = b.and(ux.is_inf, uy.is_inf);
254    let opposite = b.xor(ux.sign, sy);
255    let invalid = b.and(both_inf, opposite);
256    let inf_x = infinity(b, ux.sign);
257    let inf_y = infinity(b, sy);
258    out = b.mw_sel(uy.is_inf, &inf_y, &out);
259    out = b.mw_sel(ux.is_inf, &inf_x, &out);
260    let dnan = c64(b, DEFAULT_NAN);
261    out = b.mw_sel(invalid, &dnan, &out);
262    let any_nan = b.or(ux.is_nan, uy.is_nan);
263    let nan = propagate(b, &ux, &uy);
264    let _ = zero;
265    b.mw_sel(any_nan, &nan, &out)
266}
267
268/// `a × b`.
269pub fn mul(b: &mut Builder, x: &Mw, y: &Mw) -> Mw {
270    let n = x.len();
271    let ux = unpack(b, x);
272    let uy = unpack(b, y);
273    let sign = b.xor(ux.sign, uy.sign);
274    let p = b.mw_mul(&ux.sig, &uy.sig);
275    let top = b.mw_bit(&p, 105);
276    let (s43, l43) = b.mw_shr_sticky(&p, 43);
277    let (s42, l42) = b.mw_shr_sticky(&p, 42);
278    let s43 = s43.slice(0, n);
279    let s42 = s42.slice(0, n);
280    let lost = b.sel(top, l43, l42);
281    let chosen = b.mw_sel(top, &s43, &s42);
282    let lost64 = b.mw_from_word(lost, n);
283    let sig = b.mw_or(&chosen, &lost64);
284    let e = b.mw_plus(&ux.exp, &uy.exp);
285    let top64 = b.mw_from_word(top, n);
286    let e = b.mw_plus(&e, &top64);
287    let finite = round_pack(b, sign, &e, &sig);
288    let zero = signed_zero(b, sign);
289    let inf = infinity(b, sign);
290    let any_zero = b.or(ux.is_zero, uy.is_zero);
291    let any_inf = b.or(ux.is_inf, uy.is_inf);
292    let invalid = b.and(any_zero, any_inf);
293    let mut out = b.mw_sel(any_zero, &zero, &finite);
294    out = b.mw_sel(any_inf, &inf, &out);
295    let dnan = c64(b, DEFAULT_NAN);
296    out = b.mw_sel(invalid, &dnan, &out);
297    let any_nan = b.or(ux.is_nan, uy.is_nan);
298    let nan = propagate(b, &ux, &uy);
299    b.mw_sel(any_nan, &nan, &out)
300}
301
302/// `a ÷ b`. A zero divisor yields the IEEE result (∞, or NaN for `0 ÷ 0`); the VM's failure there is the
303/// cell's divergence, reported beside this value.
304pub fn div(b: &mut Builder, x: &Mw, y: &Mw) -> Mw {
305    let n = x.len();
306    let ux = unpack(b, x);
307    let uy = unpack(b, y);
308    let sign = b.xor(ux.sign, uy.sign);
309    // The numerator `sig_x << 62` over the divisor, as 128-bit values; a zero divisor divides by one.
310    let wide = 2 * n;
311    let num = b.mw_resize(&ux.sig, wide);
312    let num = b.mw_shl(&num, 62);
313    let one_w = b.mw_const(1, wide);
314    let den = b.mw_resize(&uy.sig, wide);
315    let den_zero = b.mw_is_zero(&den);
316    let den = b.mw_sel(den_zero, &one_w, &den);
317    let (q, r) = b.mw_divrem(&num, &den);
318    let r_nonzero = {
319        let z = b.mw_is_zero(&r);
320        b.bit_not(z)
321    };
322    let q = q.slice(0, n);
323    let sticky = b.mw_from_word(r_nonzero, n);
324    let q = b.mw_or(&q, &sticky);
325    let (norm, rise) = normalize_to_62(b, &q);
326    let e = b.mw_minus(&ux.exp, &uy.exp);
327    let e = b.mw_plus(&e, &rise);
328    let finite = round_pack(b, sign, &e, &norm);
329    let zero = signed_zero(b, sign);
330    let inf = infinity(b, sign);
331    let both_zero = b.and(ux.is_zero, uy.is_zero);
332    let both_inf = b.and(ux.is_inf, uy.is_inf);
333    let invalid = b.or(both_zero, both_inf);
334    let mut out = b.mw_sel(uy.is_inf, &zero, &finite);
335    out = b.mw_sel(ux.is_zero, &zero, &out);
336    out = b.mw_sel(uy.is_zero, &inf, &out);
337    out = b.mw_sel(ux.is_inf, &inf, &out);
338    let dnan = c64(b, DEFAULT_NAN);
339    out = b.mw_sel(invalid, &dnan, &out);
340    let any_nan = b.or(ux.is_nan, uy.is_nan);
341    let nan = propagate(b, &ux, &uy);
342    b.mw_sel(any_nan, &nan, &out)
343}
344
345/// `√a`.
346pub fn sqrt(b: &mut Builder, x: &Mw) -> Mw {
347    let n = x.len();
348    let ux = unpack(b, x);
349    // Make the exponent even: an odd exponent moves one bit into the significand.
350    let odd = b.mw_bit(&ux.exp, 0);
351    let one = c64(b, 1);
352    let odd64 = b.mw_from_word(odd, n);
353    let e_even = b.mw_minus(&ux.exp, &odd64);
354    let sig2 = b.mw_shl(&ux.sig, 1);
355    let m = b.mw_sel(odd, &sig2, &ux.sig);
356    // √(m · 2^72) has its leading bit at 62 for m in [2^52, 2^54).
357    let wide = 2 * n;
358    let rad = b.mw_resize(&m, wide);
359    let rad = b.mw_shl(&rad, 72);
360    let rad_state = b.mw_state(&rad);
361    let zero_w = b.mw_zero(wide);
362    let rem = b.mw_state(&zero_w);
363    let root = b.mw_state(&zero_w);
364    b.repeat(64, |s| {
365        let rem4 = s.mw_shl(&rem, 2);
366        let top2 = s.mw_shr(&rad_state, 126);
367        let rem_in = s.mw_or(&rem4, &top2);
368        let rad2 = s.mw_shl(&rad_state, 2);
369        let root4 = s.mw_shl(&root, 2);
370        let one_w = s.mw_const(1, wide);
371        let trial = s.mw_or(&root4, &one_w);
372        let z = s.k(0);
373        let (d, borrow) = s.mw_sub(&rem_in, &trial, z);
374        let fits = s.bit_not(borrow);
375        let rem2 = s.mw_sel(fits, &d, &rem_in);
376        let root2 = s.mw_shl(&root, 1);
377        let fits_w = s.mw_from_word(fits, wide);
378        let root3 = s.mw_or(&root2, &fits_w);
379        s.mw_assign(&rem, &rem2);
380        s.mw_assign(&root, &root3);
381        s.mw_assign(&rad_state, &rad2);
382    });
383    let r_nonzero = {
384        let z = b.mw_is_zero(&rem);
385        b.bit_not(z)
386    };
387    let sig = root.slice(0, n);
388    let sticky = b.mw_from_word(r_nonzero, n);
389    let sig = b.mw_or(&sig, &sticky);
390    let e = b.mw_sar(&e_even, 1);
391    let positive = b.k(0);
392    let finite = round_pack(b, positive, &e, &sig);
393    let neg_nonzero = {
394        let nz = b.bit_not(ux.is_zero);
395        b.and(ux.sign, nz)
396    };
397    let pos_inf = c64(b, f64::INFINITY.to_bits());
398    let mut out = b.mw_sel(ux.is_inf, &pos_inf, &finite);
399    out = b.mw_sel(ux.is_zero, x, &out);
400    let dnan = c64(b, DEFAULT_NAN);
401    out = b.mw_sel(neg_nonzero, &dnan, &out);
402    let qx = quiet(b, x);
403    let _ = one;
404    b.mw_sel(ux.is_nan, &qx, &out)
405}
406
407/// The IEEE comparisons as 0/1: `(a < b, a ≤ b, a == b)`. NaN compares false; `−0 == +0`.
408pub fn compare(b: &mut Builder, x: &Mw, y: &Mw) -> (u32, u32, u32) {
409    let ux = unpack(b, x);
410    let uy = unpack(b, y);
411    let any_nan = b.or(ux.is_nan, uy.is_nan);
412    let ordered = b.bit_not(any_nan);
413    let both_zero = b.and(ux.is_zero, uy.is_zero);
414    let same_bits = b.mw_eq(x, y);
415    let eq_raw = b.or(same_bits, both_zero);
416    let eq = b.and(ordered, eq_raw);
417    let differ = b.xor(ux.sign, uy.sign);
418    let mag_x = {
419        let m = c64(b, u64::MAX >> 1);
420        b.mw_and(x, &m)
421    };
422    let mag_y = {
423        let m = c64(b, u64::MAX >> 1);
424        b.mw_and(y, &m)
425    };
426    let x_lt_mag = b.mw_ltu(&mag_x, &mag_y);
427    let y_lt_mag = b.mw_ltu(&mag_y, &mag_x);
428    let same_sign_lt = b.sel(ux.sign, y_lt_mag, x_lt_mag);
429    let lt_raw = b.sel(differ, ux.sign, same_sign_lt);
430    let not_zeros = b.bit_not(both_zero);
431    let lt1 = b.and(lt_raw, not_zeros);
432    let lt = b.and(lt1, ordered);
433    let le = b.or(lt, eq);
434    (lt, le, eq)
435}
436
437/// `i64 as f64`, nearest-even.
438pub fn from_int(b: &mut Builder, a: &Mw) -> Mw {
439    let n = a.len();
440    let sign = b.mw_top_bit(a);
441    let neg = b.mw_neg(a);
442    let mag = b.mw_sel(sign, &neg, a);
443    let is_zero = b.mw_is_zero(a);
444    let (norm, rise) = normalize_to_62(b, &mag);
445    let e62 = c64(b, 62);
446    let e = b.mw_plus(&e62, &rise);
447    let finite = round_pack(b, sign, &e, &norm);
448    let z = b.mw_zero(n);
449    b.mw_sel(is_zero, &z, &finite)
450}
451
452/// `f.floor() as i64`: NaN is 0, and the conversion saturates at the `i64` bounds.
453pub fn floor_to_int(b: &mut Builder, x: &Mw) -> Mw {
454    let n = x.len();
455    let ux = unpack(b, x);
456    let zero = b.mw_zero(n);
457    let minus_one = c64(b, u64::MAX);
458    let i_max = c64(b, i64::MAX as u64);
459    let i_min = c64(b, i64::MIN as u64);
460    // |x| < 1: 0 for x ≥ 0 (and −0), −1 for a negative x.
461    let e_neg = b.mw_top_bit(&ux.exp);
462    let small = b.mw_sel(ux.sign, &minus_one, &zero);
463    // 0 ≤ e ≤ 62: the integer part of sig · 2^(e − 52), and whether a fraction was dropped.
464    let c52 = c64(b, 52);
465    let down_amt = b.mw_minus(&c52, &ux.exp);
466    let up_amt = b.mw_minus(&ux.exp, &c52);
467    let e_le_52 = {
468        let gt = b.mw_lts(&c52, &ux.exp);
469        b.bit_not(gt)
470    };
471    let (down, frac) = b.mw_shr_var_sticky(&ux.sig, &down_amt, 6);
472    let up = b.mw_shl_var(&ux.sig, &up_amt, 4);
473    let int_part = b.mw_sel(e_le_52, &down, &up);
474    let had_frac = b.and(e_le_52, frac);
475    let neg_int = b.mw_neg(&int_part);
476    let (neg_floor, _) = {
477        let z = b.k(0);
478        let dec = b.mw_from_word(had_frac, n);
479        b.mw_sub(&neg_int, &dec, z)
480    };
481    let mid = b.mw_sel(ux.sign, &neg_floor, &int_part);
482    // e ≥ 63: beyond i64 (−2^63 itself saturates to MIN too).
483    let c63 = c64(b, 63);
484    let huge = {
485        let lt = b.mw_lts(&ux.exp, &c63);
486        b.bit_not(lt)
487    };
488    let sat = b.mw_sel(ux.sign, &i_min, &i_max);
489    let mut out = b.mw_sel(e_neg, &small, &mid);
490    out = b.mw_sel(huge, &sat, &out);
491    out = b.mw_sel(ux.is_inf, &sat, &out);
492    out = b.mw_sel(ux.is_zero, &zero, &out);
493    b.mw_sel(ux.is_nan, &zero, &out)
494}
495
496/// `|x|`.
497pub fn abs(b: &mut Builder, x: &Mw) -> Mw {
498    let m = c64(b, u64::MAX >> 1);
499    b.mw_and(x, &m)
500}
501
502/// `f64::max(x, y)` for operands that are not NaN: `y` when `x < y`, else `x`.
503fn max_ordered(b: &mut Builder, x: &Mw, y: &Mw) -> Mw {
504    let (lt, _, _) = compare(b, x, y);
505    b.mw_sel(lt, y, x)
506}
507
508/// The VM's `is approximately` on two integers: both converted to `f64`, equal, or apart by at most
509/// `max(1e-9 · max(|a|, |b|), 1e-12)`.
510pub fn approx_eq_ints(b: &mut Builder, x: &Mw, y: &Mw) -> u32 {
511    let fx = from_int(b, x);
512    let fy = from_int(b, y);
513    let (_, _, eq) = compare(b, &fx, &fy);
514    let d = add(b, &fx, &fy, true);
515    let diff = abs(b, &d);
516    let ax = abs(b, &fx);
517    let ay = abs(b, &fy);
518    let m = max_ordered(b, &ax, &ay);
519    let tiny = c64(b, 1e-9f64.to_bits());
520    let scaled = mul(b, &tiny, &m);
521    let floor = c64(b, 1e-12f64.to_bits());
522    let bound = max_ordered(b, &scaled, &floor);
523    let (_, within, _) = compare(b, &diff, &bound);
524    b.or(eq, within)
525}