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. Division by zero is an overflow (Lua code can
215/// set a space factor of zero, for instance).
216pub fn xn_over_d(x: i32, n: i32, d: i32) -> Result<DivResult, Overflow> {
217    if d == 0 {
218        return Err(Overflow);
219    }
220    const B: i64 = 0o100000; // 2^15
221    let positive = x >= 0;
222    let x = i64::from(x).abs();
223    let (n, d) = (i64::from(n), i64::from(d));
224    let t = (x % B) * n;
225    let mut u = (x / B) * n + (t / B);
226    let v = (u % d) * B + (t % B);
227    if u / d >= B {
228        return Err(Overflow);
229    }
230    u = B * (u / d) + (v / d);
231    let (q, r) = (u as i32, (v % d) as i32);
232    Ok(if positive {
233        DivResult {
234            quotient: q,
235            remainder: r,
236        }
237    } else {
238        DivResult {
239            quotient: -q,
240            remainder: -r,
241        }
242    })
243}
244
245/// `badness(t, s)` (§108): approximately 100(t/s)³, capped at
246/// [`INF_BAD`]. `t` is the excess or shortfall, `s` the available stretch or
247/// shrink.
248pub fn badness(t: i32, s: i32) -> i32 {
249    if t == 0 {
250        return 0;
251    }
252    if s <= 0 {
253        return INF_BAD;
254    }
255    let r: i32 = if t <= 7230584 {
256        (t * 297) / s
257    } else if s >= 1663497 {
258        t / (s / 297)
259    } else {
260        t
261    };
262    if r > 1290 {
263        INF_BAD
264    } else {
265        (r * r * r + 0o400000) / 0o1000000
266    }
267}
268
269/// e-TeX `add_or_sub`: computes `x ± y`, failing if the absolute value of
270/// the result would exceed `max_answer`.
271pub fn add_or_sub(x: i32, mut y: i32, max_answer: i32, negative: bool) -> Result<i32, Overflow> {
272    if negative {
273        y = y.wrapping_neg();
274    }
275    if x >= 0 {
276        if y <= max_answer - x {
277            Ok(x + y)
278        } else {
279            Err(Overflow)
280        }
281    } else if y >= -max_answer - x {
282        Ok(x + y)
283    } else {
284        Err(Overflow)
285    }
286}
287
288/// e-TeX `quotient(n, d)`: the rounded quotient ⌊n/d + ½⌋, with the sign
289/// applied symmetrically.
290pub fn quotient(mut n: i32, mut d: i32) -> Result<i32, Overflow> {
291    if d == 0 {
292        return Err(Overflow);
293    }
294    let mut negative = if d > 0 {
295        false
296    } else {
297        d = -d;
298        true
299    };
300    if n < 0 {
301        n = n.wrapping_neg();
302        negative = !negative;
303    }
304    let mut a = n / d;
305    n -= a * d;
306    d = n - d;
307    if d + n >= 0 {
308        a += 1;
309    }
310    Ok(if negative { -a } else { a })
311}
312
313/// e-TeX `fract(x, n, d, max_answer)`: ⌊xn/d + ½⌋ computed without
314/// overflow, failing if the result exceeds `max_answer`. This is a
315/// transcription of the e-TeX algorithm.
316pub fn fract(mut x: i32, mut n: i32, mut d: i32, max_answer: i32) -> Result<i32, Overflow> {
317    if d == 0 {
318        return Err(Overflow);
319    }
320    let mut a: i32 = 0;
321    let mut negative = if d > 0 {
322        false
323    } else {
324        d = -d;
325        true
326    };
327    if x < 0 {
328        x = x.wrapping_neg();
329        negative = !negative;
330    } else if x == 0 {
331        return Ok(0);
332    }
333    if n < 0 {
334        n = n.wrapping_neg();
335        negative = !negative;
336    }
337    let finish = |a: i32, negative: bool| if negative { -a } else { a };
338    let mut t = n / d;
339    if t > max_answer / x {
340        return Err(Overflow);
341    }
342    a = a.wrapping_add(t * x);
343    n -= t * d;
344    if n == 0 {
345        return Ok(finish(a, negative));
346    }
347    t = x / d;
348    if t > (max_answer - a) / n {
349        return Err(Overflow);
350    }
351    a += t * n;
352    x -= t * d;
353    if x == 0 {
354        return Ok(finish(a, negative));
355    }
356    if x < n {
357        std::mem::swap(&mut x, &mut n);
358    }
359    // Compute f = floor(xn/d + 1/2) with 0 < n <= x < d.
360    let mut f: i32 = 0;
361    let mut r: i32 = (d / 2) - d;
362    let h = -r;
363    loop {
364        if n & 1 != 0 {
365            r += x;
366            if r >= 0 {
367                r -= d;
368                f += 1;
369            }
370        }
371        n /= 2;
372        if n == 0 {
373            break;
374        }
375        if x < h {
376            x += x;
377        } else {
378            t = x - d;
379            x += t;
380            f += n;
381            if x < n {
382                if x == 0 {
383                    break;
384                }
385                std::mem::swap(&mut x, &mut n);
386            }
387        }
388    }
389    if f > max_answer - a {
390        return Err(Overflow);
391    }
392    a += f;
393    Ok(finish(a, negative))
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use proptest::prelude::*;
400
401    #[test]
402    fn print_scaled_examples() {
403        let cases = [
404            (0, "0.0"),
405            (UNITY, "1.0"),
406            (-UNITY, "-1.0"),
407            (UNITY / 2, "0.5"),
408            (1, "0.00002"),
409            (MAX_DIMEN, "16383.99998"),
410            (655360, "10.0"),
411            (26214, "0.4"),
412            (32768 + 1, "0.50002"),
413            // As LuaTeX prints it.
414            (i32::MIN, "--32768.0"),
415        ];
416        for (v, s) in cases {
417            assert_eq!(Scaled(v).to_tex_string(), s, "value {v}");
418        }
419    }
420
421    #[test]
422    fn xn_over_d_by_zero_overflows() {
423        assert!(xn_over_d(UNITY, 1000, 0).is_err());
424        assert!(xn_over_d(-UNITY, 0, 0).is_err());
425    }
426
427    #[test]
428    fn round_decimals_examples() {
429        assert_eq!(round_decimals(&[5]), 32768);
430        assert_eq!(round_decimals(&[2, 5]), 16384);
431        assert_eq!(round_decimals(&[]), 0);
432        assert_eq!(round_decimals(&[9, 9, 9, 9, 9, 9]), UNITY);
433    }
434
435    #[test]
436    fn half_rounds_up_on_odd() {
437        assert_eq!(half(3), 2);
438        assert_eq!(half(-3), -1);
439        assert_eq!(half(4), 2);
440        assert_eq!(half(-4), -2);
441    }
442
443    #[test]
444    fn badness_examples() {
445        assert_eq!(badness(0, 0), 0);
446        assert_eq!(badness(1, 0), INF_BAD);
447        assert_eq!(badness(UNITY, UNITY), 100);
448        assert_eq!(badness(UNITY, 2 * UNITY), 12);
449        assert_eq!(badness(3 * UNITY, UNITY), 2698);
450        assert_eq!(badness(5 * UNITY, UNITY), INF_BAD);
451    }
452
453    #[test]
454    fn overflow_cases() {
455        assert_eq!(nx_plus_y(2, MAX_DIMEN, 0), Err(Overflow));
456        assert_eq!(x_over_n(5, 0), Err(Overflow));
457        assert_eq!(quotient(5, 0), Err(Overflow));
458        assert_eq!(fract(5, 1, 0, INFINITY), Err(Overflow));
459        assert_eq!(add_or_sub(INFINITY, 1, INFINITY, false), Err(Overflow));
460    }
461
462    proptest! {
463        /// print_scaled must produce a decimal that round_decimals maps back
464        /// to the same value (TeX's documented guarantee, §103).
465        #[test]
466        fn print_scaled_round_trips(v in -MAX_DIMEN..=MAX_DIMEN) {
467            let s = Scaled(v).to_tex_string();
468            let (neg, body) = match s.strip_prefix('-') {
469                Some(b) => (true, b),
470                None => (false, s.as_str()),
471            };
472            let (int, frac) = body.split_once('.').unwrap();
473            let digits: Vec<u8> = frac.bytes().map(|b| b - b'0').collect();
474            let back = int.parse::<i32>().unwrap() * UNITY + round_decimals(&digits);
475            prop_assert_eq!(if neg { -back } else { back }, v);
476        }
477
478        #[test]
479        fn x_over_n_truncates(x in -MAX_DIMEN..=MAX_DIMEN, n in -100_000i32..100_000) {
480            prop_assume!(n != 0);
481            let r = x_over_n(x, n).unwrap();
482            prop_assert_eq!(r.quotient, x / n);
483            // TeX's remainder carries the sign of x, like Rust's `%`.
484            prop_assert_eq!(r.remainder, x % n);
485        }
486
487        #[test]
488        fn xn_over_d_matches_wide(x in -MAX_DIMEN..=MAX_DIMEN, n in 0i32..=0x10000, d in 1i32..=0x10000) {
489            let wide = i64::from(x) * i64::from(n);
490            let q = wide / i64::from(d);
491            match xn_over_d(x, n, d) {
492                Ok(r) => {
493                    prop_assert_eq!(i64::from(r.quotient), q);
494                    prop_assert_eq!(i64::from(r.remainder), wide % i64::from(d));
495                }
496                Err(Overflow) => prop_assert!(q.abs() >= 1 << 30),
497            }
498        }
499
500        #[test]
501        fn fract_matches_wide(x in -INFINITY..=INFINITY, n in -INFINITY..=INFINITY, d in -INFINITY..=INFINITY) {
502            prop_assume!(d != 0);
503            let (ax, an, ad) = (i128::from(x).abs(), i128::from(n).abs(), i128::from(d).abs());
504            let q = (2 * ax * an + ad) / (2 * ad);
505            let neg = (x < 0) ^ (n < 0) ^ (d < 0);
506            match fract(x, n, d, INFINITY) {
507                Ok(v) => prop_assert_eq!(i128::from(v), if neg && x != 0 { -q } else { q }),
508                Err(Overflow) => prop_assert!(q > i128::from(INFINITY)),
509            }
510        }
511
512        #[test]
513        fn quotient_rounds_half_away(n in -INFINITY..=INFINITY, d in -INFINITY..=INFINITY) {
514            prop_assume!(d != 0);
515            let (an, ad) = (i64::from(n).abs(), i64::from(d).abs());
516            let q = (2 * an + ad) / (2 * ad);
517            let neg = (n < 0) ^ (d < 0);
518            prop_assert_eq!(i64::from(quotient(n, d).unwrap()), if neg { -q } else { q });
519        }
520    }
521}