Skip to main content

retex_arith/
lib.rs

1//! TeX-exact fixed-point arithmetic.
2//!
3//! TeX does all dimension arithmetic with integers. A dimension is a *scaled*
4//! value: an `i32` counting units of 2⁻¹⁶ pt (`sp`). Results must match
5//! TeX bit for bit, so every routine here follows the specification in
6//! *TeX: The Program* (§99–§108) and the e-TeX expression routines
7//! (`\numexpr`, `\dimexpr`) exactly, including the overflow rules.
8//!
9//! Overflow is reported as [`Overflow`]. TeX sets its global `arith_error`
10//! flag in that case and the caller decides what to do; callers that need
11//! TeX's exact fallback values get them from the documented error variants.
12
13#![forbid(unsafe_code)]
14
15use std::fmt;
16
17pub mod random;
18
19/// One point in scaled units (2¹⁶ sp).
20pub const UNITY: i32 = 0x1_0000;
21/// Two points in scaled units, used by decimal rounding (§102).
22pub const TWO: i32 = 0x2_0000;
23/// The largest legal dimension, `\maxdimen` = 16383.99999 pt (§421).
24pub const MAX_DIMEN: i32 = 0o7777777777;
25/// The largest legal integer, 2³¹ − 1 (TeX's `infinity`).
26pub const INFINITY: i32 = 0o17777777777;
27/// Badness of an infinitely bad box (§108).
28pub const INF_BAD: i32 = 10000;
29
30/// An arithmetic overflow, equivalent to TeX setting `arith_error`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Overflow;
33
34impl fmt::Display for Overflow {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.write_str("arithmetic overflow")
37    }
38}
39
40impl std::error::Error for Overflow {}
41
42/// A scaled dimension: an integer number of sp (2⁻¹⁶ pt).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
44pub struct Scaled(pub i32);
45
46impl Scaled {
47    /// Zero.
48    pub const ZERO: Scaled = Scaled(0);
49    /// `\maxdimen`.
50    pub const MAX: Scaled = Scaled(MAX_DIMEN);
51
52    /// Formats the value the way TeX's `print_scaled` does (§103), without
53    /// the `pt` unit. For example, 65536 sp becomes `"1.0"`.
54    pub fn to_tex_string(self) -> String {
55        let mut out = String::new();
56        print_scaled(&mut out, self.0);
57        out
58    }
59}
60
61impl fmt::Display for Scaled {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        let mut s = String::new();
64        print_scaled(&mut s, self.0);
65        f.write_str(&s)
66    }
67}
68
69/// `half(x)` (§100): x/2 rounded, with odd values rounded towards +∞.
70pub fn half(x: i32) -> i32 {
71    if x & 1 != 0 {
72        // Pascal `div` truncates towards zero, like Rust's `/`.
73        (x + 1) / 2
74    } else {
75        x / 2
76    }
77}
78
79/// `round_decimals` (§102): converts the decimal fraction `.d₀d₁…dₖ₋₁`
80/// to scaled units, rounded. `digits` holds the digit values (0–9). TeX
81/// only looks at the first 17 digits; callers must truncate to that.
82pub fn round_decimals(digits: &[u8]) -> i32 {
83    let mut a: i32 = 0;
84    for &d in digits.iter().rev() {
85        a = (a + i32::from(d) * TWO) / 10;
86    }
87    (a + 1) / 2
88}
89
90/// Appends TeX's decimal form of a scaled value to `out` (§103). It prints
91/// the shortest decimal that rounds back to the same scaled value, always
92/// with at least one digit after the point.
93pub fn print_scaled(out: &mut String, s: i32) {
94    if s == i32::MIN {
95        // TeX never produces this value, but Lua can store it (e.g.
96        // `pdf.setlinkmargin(2^31)`); LuaTeX's negation wraps around and it
97        // prints a doubled minus sign.
98        out.push_str("--32768.0");
99        return;
100    }
101    let mut s = i64::from(s);
102    if s < 0 {
103        out.push('-');
104        s = -s;
105    }
106    let unity = i64::from(UNITY);
107    out.push_str(&(s / unity).to_string());
108    out.push('.');
109    s = 10 * (s % unity) + 5;
110    let mut delta: i64 = 10;
111    loop {
112        if delta > unity {
113            // Round the last digit.
114            s += 0o100000 - 50000;
115        }
116        let digit = (s / unity) as u8; // always 0..=9
117        out.push(char::from(b'0' + digit));
118        s = 10 * (s % unity);
119        delta *= 10;
120        if s <= delta {
121            break;
122        }
123    }
124}
125
126/// `mult_and_add` (§105): computes `n*x + y`, failing if the absolute
127/// value would exceed `max_answer`.
128pub fn mult_and_add(mut n: i32, mut x: i32, y: i32, max_answer: i32) -> Result<i32, Overflow> {
129    if n < 0 {
130        x = x.wrapping_neg();
131        n = -n;
132    }
133    if n == 0 {
134        return Ok(y);
135    }
136    // TeX's test, done in i64 to avoid intermediate overflow in Rust.
137    let (n64, x64, y64, m) = (
138        i64::from(n),
139        i64::from(x),
140        i64::from(y),
141        i64::from(max_answer),
142    );
143    if x64 <= (m - y64) / n64 && -x64 <= (m + y64) / n64 {
144        Ok((n64 * x64 + y64) as i32)
145    } else {
146        Err(Overflow)
147    }
148}
149
150/// `nx_plus_y(n, x, y)` (§105) for dimensions: the result must be at most
151/// 2³⁰ − 1 in absolute value.
152pub fn nx_plus_y(n: i32, x: i32, y: i32) -> Result<i32, Overflow> {
153    mult_and_add(n, x, y, 0o7777777777)
154}
155
156/// `mult_integers(n, x)` (§105): the result must fit in 2³¹ − 1.
157pub fn mult_integers(n: i32, x: i32) -> Result<i32, Overflow> {
158    mult_and_add(n, x, 0, 0o17777777777)
159}
160
161/// Quotient and remainder from [`x_over_n`] and [`xn_over_d`].
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct DivResult {
164    /// The quotient.
165    pub quotient: i32,
166    /// The remainder, which has the sign TeX gives it.
167    pub remainder: i32,
168}
169
170/// `x_over_n` (§106): divides scaled `x` by integer `n`, truncating towards
171/// zero. Division by zero is an overflow; in that case TeX returns quotient 0
172/// and remainder `x`, available via [`x_over_n_unchecked`].
173pub fn x_over_n(x: i32, n: i32) -> Result<DivResult, Overflow> {
174    if n == 0 {
175        return Err(Overflow);
176    }
177    Ok(x_over_n_unchecked(x, n))
178}
179
180/// [`x_over_n`] without the error, returning TeX's fallback
181/// `{quotient: 0, remainder: x}` when `n == 0`.
182pub fn x_over_n_unchecked(mut x: i32, mut n: i32) -> DivResult {
183    if n == 0 {
184        return DivResult {
185            quotient: 0,
186            remainder: x,
187        };
188    }
189    let mut negative = false;
190    if n < 0 {
191        x = x.wrapping_neg();
192        n = n.wrapping_neg();
193        negative = true;
194    }
195    let (q, mut r) = if x >= 0 {
196        (x / n, x % n)
197    } else {
198        // -x cannot overflow for TeX-legal values; wrap to be total.
199        let nx = x.wrapping_neg();
200        (-(nx / n), -(nx % n))
201    };
202    if negative {
203        r = -r;
204    }
205    DivResult {
206        quotient: q,
207        remainder: r,
208    }
209}
210
211/// `xn_over_d` (§107): computes `x*n/d` for `0 ≤ n, d ≤ 2¹⁶`, truncating
212/// towards zero. TeX simulates 48-bit arithmetic with 15-bit pieces; this is
213/// a direct transcription of that algorithm so that overflow is detected in
214/// exactly the same cases.
215pub fn xn_over_d(x: i32, n: i32, d: i32) -> Result<DivResult, Overflow> {
216    const B: i64 = 0o100000; // 2^15
217    let positive = x >= 0;
218    let x = i64::from(x).abs();
219    let (n, d) = (i64::from(n), i64::from(d));
220    let t = (x % B) * n;
221    let mut u = (x / B) * n + (t / B);
222    let v = (u % d) * B + (t % B);
223    if u / d >= B {
224        return Err(Overflow);
225    }
226    u = B * (u / d) + (v / d);
227    let (q, r) = (u as i32, (v % d) as i32);
228    Ok(if positive {
229        DivResult {
230            quotient: q,
231            remainder: r,
232        }
233    } else {
234        DivResult {
235            quotient: -q,
236            remainder: -r,
237        }
238    })
239}
240
241/// `badness(t, s)` (§108): approximately 100(t/s)³, capped at
242/// [`INF_BAD`]. `t` is the excess or shortfall, `s` the available stretch or
243/// shrink.
244pub fn badness(t: i32, s: i32) -> i32 {
245    if t == 0 {
246        return 0;
247    }
248    if s <= 0 {
249        return INF_BAD;
250    }
251    let r: i32 = if t <= 7230584 {
252        (t * 297) / s
253    } else if s >= 1663497 {
254        t / (s / 297)
255    } else {
256        t
257    };
258    if r > 1290 {
259        INF_BAD
260    } else {
261        (r * r * r + 0o400000) / 0o1000000
262    }
263}
264
265/// e-TeX `add_or_sub`: computes `x ± y`, failing if the absolute value of
266/// the result would exceed `max_answer`.
267pub fn add_or_sub(x: i32, mut y: i32, max_answer: i32, negative: bool) -> Result<i32, Overflow> {
268    if negative {
269        y = y.wrapping_neg();
270    }
271    if x >= 0 {
272        if y <= max_answer - x {
273            Ok(x + y)
274        } else {
275            Err(Overflow)
276        }
277    } else if y >= -max_answer - x {
278        Ok(x + y)
279    } else {
280        Err(Overflow)
281    }
282}
283
284/// e-TeX `quotient(n, d)`: the rounded quotient ⌊n/d + ½⌋, with the sign
285/// applied symmetrically.
286pub fn quotient(mut n: i32, mut d: i32) -> Result<i32, Overflow> {
287    if d == 0 {
288        return Err(Overflow);
289    }
290    let mut negative = if d > 0 {
291        false
292    } else {
293        d = -d;
294        true
295    };
296    if n < 0 {
297        n = n.wrapping_neg();
298        negative = !negative;
299    }
300    let mut a = n / d;
301    n -= a * d;
302    d = n - d;
303    if d + n >= 0 {
304        a += 1;
305    }
306    Ok(if negative { -a } else { a })
307}
308
309/// e-TeX `fract(x, n, d, max_answer)`: ⌊xn/d + ½⌋ computed without
310/// overflow, failing if the result exceeds `max_answer`. This is a
311/// transcription of the e-TeX algorithm.
312pub fn fract(mut x: i32, mut n: i32, mut d: i32, max_answer: i32) -> Result<i32, Overflow> {
313    if d == 0 {
314        return Err(Overflow);
315    }
316    let mut a: i32 = 0;
317    let mut negative = if d > 0 {
318        false
319    } else {
320        d = -d;
321        true
322    };
323    if x < 0 {
324        x = x.wrapping_neg();
325        negative = !negative;
326    } else if x == 0 {
327        return Ok(0);
328    }
329    if n < 0 {
330        n = n.wrapping_neg();
331        negative = !negative;
332    }
333    let finish = |a: i32, negative: bool| if negative { -a } else { a };
334    let mut t = n / d;
335    if t > max_answer / x {
336        return Err(Overflow);
337    }
338    a = a.wrapping_add(t * x);
339    n -= t * d;
340    if n == 0 {
341        return Ok(finish(a, negative));
342    }
343    t = x / d;
344    if t > (max_answer - a) / n {
345        return Err(Overflow);
346    }
347    a += t * n;
348    x -= t * d;
349    if x == 0 {
350        return Ok(finish(a, negative));
351    }
352    if x < n {
353        std::mem::swap(&mut x, &mut n);
354    }
355    // Compute f = floor(xn/d + 1/2) with 0 < n <= x < d.
356    let mut f: i32 = 0;
357    let mut r: i32 = (d / 2) - d;
358    let h = -r;
359    loop {
360        if n & 1 != 0 {
361            r += x;
362            if r >= 0 {
363                r -= d;
364                f += 1;
365            }
366        }
367        n /= 2;
368        if n == 0 {
369            break;
370        }
371        if x < h {
372            x += x;
373        } else {
374            t = x - d;
375            x += t;
376            f += n;
377            if x < n {
378                if x == 0 {
379                    break;
380                }
381                std::mem::swap(&mut x, &mut n);
382            }
383        }
384    }
385    if f > max_answer - a {
386        return Err(Overflow);
387    }
388    a += f;
389    Ok(finish(a, negative))
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use proptest::prelude::*;
396
397    #[test]
398    fn print_scaled_examples() {
399        let cases = [
400            (0, "0.0"),
401            (UNITY, "1.0"),
402            (-UNITY, "-1.0"),
403            (UNITY / 2, "0.5"),
404            (1, "0.00002"),
405            (MAX_DIMEN, "16383.99998"),
406            (655360, "10.0"),
407            (26214, "0.4"),
408            (32768 + 1, "0.50002"),
409            // As LuaTeX prints it.
410            (i32::MIN, "--32768.0"),
411        ];
412        for (v, s) in cases {
413            assert_eq!(Scaled(v).to_tex_string(), s, "value {v}");
414        }
415    }
416
417    #[test]
418    fn round_decimals_examples() {
419        assert_eq!(round_decimals(&[5]), 32768);
420        assert_eq!(round_decimals(&[2, 5]), 16384);
421        assert_eq!(round_decimals(&[]), 0);
422        assert_eq!(round_decimals(&[9, 9, 9, 9, 9, 9]), UNITY);
423    }
424
425    #[test]
426    fn half_rounds_up_on_odd() {
427        assert_eq!(half(3), 2);
428        assert_eq!(half(-3), -1);
429        assert_eq!(half(4), 2);
430        assert_eq!(half(-4), -2);
431    }
432
433    #[test]
434    fn badness_examples() {
435        assert_eq!(badness(0, 0), 0);
436        assert_eq!(badness(1, 0), INF_BAD);
437        assert_eq!(badness(UNITY, UNITY), 100);
438        assert_eq!(badness(UNITY, 2 * UNITY), 12);
439        assert_eq!(badness(3 * UNITY, UNITY), 2698);
440        assert_eq!(badness(5 * UNITY, UNITY), INF_BAD);
441    }
442
443    #[test]
444    fn overflow_cases() {
445        assert_eq!(nx_plus_y(2, MAX_DIMEN, 0), Err(Overflow));
446        assert_eq!(x_over_n(5, 0), Err(Overflow));
447        assert_eq!(quotient(5, 0), Err(Overflow));
448        assert_eq!(fract(5, 1, 0, INFINITY), Err(Overflow));
449        assert_eq!(add_or_sub(INFINITY, 1, INFINITY, false), Err(Overflow));
450    }
451
452    proptest! {
453        /// print_scaled must produce a decimal that round_decimals maps back
454        /// to the same value (TeX's documented guarantee, §103).
455        #[test]
456        fn print_scaled_round_trips(v in -MAX_DIMEN..=MAX_DIMEN) {
457            let s = Scaled(v).to_tex_string();
458            let (neg, body) = match s.strip_prefix('-') {
459                Some(b) => (true, b),
460                None => (false, s.as_str()),
461            };
462            let (int, frac) = body.split_once('.').unwrap();
463            let digits: Vec<u8> = frac.bytes().map(|b| b - b'0').collect();
464            let back = int.parse::<i32>().unwrap() * UNITY + round_decimals(&digits);
465            prop_assert_eq!(if neg { -back } else { back }, v);
466        }
467
468        #[test]
469        fn x_over_n_truncates(x in -MAX_DIMEN..=MAX_DIMEN, n in -100_000i32..100_000) {
470            prop_assume!(n != 0);
471            let r = x_over_n(x, n).unwrap();
472            prop_assert_eq!(r.quotient, x / n);
473            // TeX's remainder carries the sign of x, like Rust's `%`.
474            prop_assert_eq!(r.remainder, x % n);
475        }
476
477        #[test]
478        fn xn_over_d_matches_wide(x in -MAX_DIMEN..=MAX_DIMEN, n in 0i32..=0x10000, d in 1i32..=0x10000) {
479            let wide = i64::from(x) * i64::from(n);
480            let q = wide / i64::from(d);
481            match xn_over_d(x, n, d) {
482                Ok(r) => {
483                    prop_assert_eq!(i64::from(r.quotient), q);
484                    prop_assert_eq!(i64::from(r.remainder), wide % i64::from(d));
485                }
486                Err(Overflow) => prop_assert!(q.abs() >= 1 << 30),
487            }
488        }
489
490        #[test]
491        fn fract_matches_wide(x in -INFINITY..=INFINITY, n in -INFINITY..=INFINITY, d in -INFINITY..=INFINITY) {
492            prop_assume!(d != 0);
493            let (ax, an, ad) = (i128::from(x).abs(), i128::from(n).abs(), i128::from(d).abs());
494            let q = (2 * ax * an + ad) / (2 * ad);
495            let neg = (x < 0) ^ (n < 0) ^ (d < 0);
496            match fract(x, n, d, INFINITY) {
497                Ok(v) => prop_assert_eq!(i128::from(v), if neg && x != 0 { -q } else { q }),
498                Err(Overflow) => prop_assert!(q > i128::from(INFINITY)),
499            }
500        }
501
502        #[test]
503        fn quotient_rounds_half_away(n in -INFINITY..=INFINITY, d in -INFINITY..=INFINITY) {
504            prop_assume!(d != 0);
505            let (an, ad) = (i64::from(n).abs(), i64::from(d).abs());
506            let q = (2 * an + ad) / (2 * ad);
507            let neg = (n < 0) ^ (d < 0);
508            prop_assert_eq!(i64::from(quotient(n, d).unwrap()), if neg { -q } else { q });
509        }
510    }
511}