texlang_stdlib/
math.rs

1//! Operations on variables (add, multiply, divide)
2
3use texcraft_stdext::collections::groupingmap;
4use texlang::prelude as txl;
5use texlang::traits::*;
6use texlang::variable::SupportedType;
7use texlang::variable::TypedVariable;
8use texlang::*;
9
10/// Get the `\advance` command.
11pub fn get_advance<S: TexlangState>() -> command::BuiltIn<S> {
12    get_command::<S, AdvanceOp>()
13}
14
15/// Get the `\advanceChecked` command.
16pub fn get_advance_checked<S: TexlangState>() -> command::BuiltIn<S> {
17    get_command::<S, AdvanceCheckedOp>()
18}
19
20/// Get the `\multiply` command.
21pub fn get_multiply<S: TexlangState>() -> command::BuiltIn<S> {
22    get_command::<S, MultiplyOp>()
23}
24
25/// Get the `\multiplyWrapped` command.
26pub fn get_multiply_wrapped<S: TexlangState>() -> command::BuiltIn<S> {
27    get_command::<S, MultiplyWrappedOp>()
28}
29
30/// Get the `\divide` command.
31pub fn get_divide<S: TexlangState>() -> command::BuiltIn<S> {
32    get_command::<S, DivideOp>()
33}
34
35fn get_command<S: TexlangState, O: Op>() -> command::BuiltIn<S> {
36    command::BuiltIn::new_execution(math_primitive_fn::<S, O>)
37        .with_tag(variable_op_tag())
38        .with_doc(O::DOC)
39}
40
41static VARIABLE_OP_TAG: command::StaticTag = command::StaticTag::new();
42
43pub fn variable_op_tag() -> command::Tag {
44    VARIABLE_OP_TAG.get()
45}
46
47trait Number: Sized + Default + SupportedType + Copy + std::fmt::Display {
48    fn wrapping_add(lhs: Self, rhs: Self) -> Self;
49    fn checked_add(lhs: Self, rhs: Self) -> Option<Self>;
50    fn checked_mul(lhs: Self, rhs: i32) -> Option<Self>;
51    fn wrapping_mul(lhs: Self, rhs: i32) -> Self;
52    fn checked_div(lhs: Self, rhs: i32) -> Option<Self>;
53}
54
55impl Number for i32 {
56    fn wrapping_add(lhs: Self, rhs: Self) -> Self {
57        lhs.wrapping_add(rhs)
58    }
59    fn checked_add(lhs: Self, rhs: Self) -> Option<Self> {
60        lhs.checked_add(rhs)
61    }
62    fn checked_mul(lhs: Self, rhs: i32) -> Option<Self> {
63        lhs.checked_mul(rhs)
64    }
65    fn wrapping_mul(lhs: Self, rhs: i32) -> Self {
66        lhs.wrapping_mul(rhs)
67    }
68    fn checked_div(lhs: Self, rhs: i32) -> Option<Self> {
69        lhs.checked_div(rhs)
70    }
71}
72
73impl Number for core::Scaled {
74    fn wrapping_add(lhs: Self, rhs: Self) -> Self {
75        lhs.wrapping_add(rhs)
76    }
77    fn checked_add(lhs: Self, rhs: Self) -> Option<Self> {
78        lhs.checked_add(rhs)
79    }
80    fn checked_mul(lhs: Self, rhs: i32) -> Option<Self> {
81        lhs.checked_mul(rhs)
82    }
83    fn wrapping_mul(lhs: Self, rhs: i32) -> Self {
84        lhs.wrapping_mul(rhs)
85    }
86    fn checked_div(lhs: Self, rhs: i32) -> Option<Self> {
87        lhs.checked_div(rhs)
88    }
89}
90
91impl Number for core::Glue {
92    fn wrapping_add(lhs: Self, rhs: Self) -> Self {
93        lhs.wrapping_add(rhs)
94    }
95    fn checked_add(lhs: Self, rhs: Self) -> Option<Self> {
96        lhs.checked_add(rhs)
97    }
98    fn checked_mul(lhs: Self, rhs: i32) -> Option<Self> {
99        lhs.checked_mul(rhs)
100    }
101    fn wrapping_mul(lhs: Self, rhs: i32) -> Self {
102        lhs.wrapping_mul(rhs)
103    }
104    fn checked_div(lhs: Self, rhs: i32) -> Option<Self> {
105        lhs.checked_div(rhs)
106    }
107}
108
109trait Op {
110    const DOC: &'static str;
111    const RHS_SAME: bool;
112    type Error: error::TexError;
113    fn apply<N: Number>(lhs: N, rhs_i: i32, rhs_n: N) -> Result<N, Self::Error>;
114
115    fn apply_to_variable<S: TexlangState, N: Number>(
116        variable: TypedVariable<S, N>,
117        input: &mut vm::ExecutionInput<S>,
118        scope: groupingmap::Scope,
119    ) -> txl::Result<()> {
120        let lhs = *variable.get(input.state());
121        let (rhs_i, rhs_n) = if Self::RHS_SAME {
122            (0_i32, N::parse(input)?)
123        } else {
124            (i32::parse(input)?, Default::default())
125        };
126        let result = match Self::apply(lhs, rhs_i, rhs_n) {
127            Ok(result) => result,
128            Err(err) => {
129                return input.error(err);
130            }
131        };
132        variable.set(input, scope, result);
133        Ok(())
134    }
135}
136
137struct AdvanceOp;
138
139impl Op for AdvanceOp {
140    const DOC: &'static str = "Add a quantity to a variable";
141    const RHS_SAME: bool = true;
142    type Error = OverflowError;
143    fn apply<N: Number>(lhs: N, _: i32, rhs_n: N) -> Result<N, Self::Error> {
144        // TeX silently overflows in \advance
145        Ok(N::wrapping_add(lhs, rhs_n))
146    }
147}
148
149struct AdvanceCheckedOp;
150
151impl Op for AdvanceCheckedOp {
152    const DOC: &'static str = "Add a number to a variable (error on overflow)";
153    const RHS_SAME: bool = true;
154    type Error = OverflowError;
155    fn apply<N: Number>(lhs: N, _: i32, rhs_n: N) -> Result<N, Self::Error> {
156        match N::checked_add(lhs, rhs_n) {
157            Some(result) => Ok(result),
158            None => Err(OverflowError {
159                op_name: "addition",
160                lhs: format!["{lhs}"],
161                rhs: format!["{rhs_n}"],
162                wrapped_result: format!["{}", N::wrapping_add(lhs, rhs_n)],
163            }),
164        }
165    }
166}
167
168struct MultiplyOp;
169
170impl Op for MultiplyOp {
171    const DOC: &'static str = "Multiply a variable by an integer";
172    const RHS_SAME: bool = false;
173    type Error = OverflowError;
174    fn apply<N: Number>(lhs: N, rhs_i: i32, _: N) -> Result<N, Self::Error> {
175        match N::checked_mul(lhs, rhs_i) {
176            Some(result) => Ok(result),
177            None => Err(OverflowError {
178                op_name: "multiplication",
179                lhs: format!["{lhs}"],
180                rhs: format!["{rhs_i}"],
181                wrapped_result: format!["{}", N::wrapping_mul(lhs, rhs_i)],
182            }),
183        }
184    }
185}
186
187struct MultiplyWrappedOp;
188
189impl Op for MultiplyWrappedOp {
190    const DOC: &'static str = "Multiply a variable by an integer (wrap on overflow)";
191    const RHS_SAME: bool = false;
192    type Error = OverflowError;
193    fn apply<N: Number>(lhs: N, rhs_i: i32, _: N) -> Result<N, Self::Error> {
194        Ok(N::wrapping_mul(lhs, rhs_i))
195    }
196}
197
198struct DivideOp;
199
200impl Op for DivideOp {
201    const DOC: &'static str = "Divide a variable by an integer";
202    const RHS_SAME: bool = false;
203    type Error = DivisionByZeroError;
204    fn apply<N: Number>(lhs: N, rhs_i: i32, _: N) -> Result<N, Self::Error> {
205        match N::checked_div(lhs, rhs_i) {
206            Some(result) => Ok(result),
207            None => Err(DivisionByZeroError {
208                numerator: format!["{lhs}"],
209            }),
210        }
211    }
212}
213
214#[derive(Debug)]
215struct OverflowError {
216    op_name: &'static str,
217    lhs: String,
218    rhs: String,
219    wrapped_result: String,
220}
221
222impl error::TexError for OverflowError {
223    fn kind(&self) -> error::Kind {
224        error::Kind::FailedPrecondition
225    }
226
227    fn title(&self) -> String {
228        format!["overflow in checked {}", self.op_name]
229    }
230
231    fn notes(&self) -> Vec<error::display::Note> {
232        vec![
233            format!["left hand side evaluated to {}", self.lhs].into(),
234            format!["right hand side evaluated to {}", self.rhs].into(),
235            format!["wrapped result would be {}", self.wrapped_result].into(),
236        ]
237    }
238}
239
240#[derive(Debug)]
241struct DivisionByZeroError {
242    numerator: String,
243}
244
245impl error::TexError for DivisionByZeroError {
246    fn kind(&self) -> error::Kind {
247        error::Kind::FailedPrecondition
248    }
249
250    fn title(&self) -> String {
251        "division by zero".into()
252    }
253
254    fn notes(&self) -> Vec<error::display::Note> {
255        vec![format!["numerator evaluated to {}", self.numerator].into()]
256    }
257}
258
259/// TeX.2021.446-1240
260fn math_primitive_fn<S: TexlangState, O: Op>(
261    _: token::Token,
262    input: &mut vm::ExecutionInput<S>,
263) -> txl::Result<()> {
264    let scope = TexlangState::variable_assignment_scope_hook(input.state_mut());
265    let token = input.next_or_err(ArithmeticVariableEndOfInput {})?;
266    match token.value() {
267        token::Value::CommandRef(command_ref) => {
268            match input.commands_map().get_command(&command_ref) {
269                None => input.error(
270                    parse::Error::new("a variable", Some(token), "")
271                        .with_got_override("got an undefined control sequence")
272                        .with_annotation_override("undefined control sequence"),
273                ),
274                Some(command::Command::Variable(cmd)) => {
275                    if !cmd.is_arithmetic() {
276                        input.error(
277                            parse::Error::new("an arithmetic variable", Some(token), "")
278                                .with_got_override("got a non-arithmetic variable"),
279                        )?;
280                        return Ok(());
281                    }
282                    let variable = cmd.clone().resolve(token, input.as_mut())?;
283                    OptionalBy::parse(input)?;
284                    match variable {
285                        variable::Variable::Int(variable) => {
286                            O::apply_to_variable(variable, input, scope)
287                        }
288                        variable::Variable::Dimen(variable) => {
289                            O::apply_to_variable(variable, input, scope)
290                        }
291                        variable::Variable::Glue(variable) => {
292                            O::apply_to_variable(variable, input, scope)
293                        }
294                        variable::Variable::CatCode(_)
295                        | variable::Variable::TokenList(_)
296                        | variable::Variable::MathCode(_)
297                        | variable::Variable::Font(_) => {
298                            unreachable!("only arithmetic commands are considered");
299                        }
300                    }
301                }
302                Some(cmd) => input.error(
303                    parse::Error::new("a variable", Some(token), "")
304                        .with_got_override("got a non-variable command")
305                        .with_annotation_override(format!["control sequence referencing {cmd}"]),
306                ),
307            }
308        }
309        _ => input.error(
310            parse::Error::new("a variable", Some(token), "")
311                .with_got_override("got a character token"),
312        ),
313    }
314}
315
316#[derive(Debug)]
317struct ArithmeticVariableEndOfInput;
318
319impl error::EndOfInputError for ArithmeticVariableEndOfInput {
320    fn doing(&self) -> String {
321        "parsing an arithmetic variable".into()
322    }
323}
324
325/// When parsed, this type consumes an optional `by` keyword from the input stream.
326struct OptionalBy;
327
328impl Parsable for OptionalBy {
329    fn parse_impl<S: TexlangState>(input: &mut vm::ExpandedStream<S>) -> txl::Result<Self> {
330        texlang::parse::parse_keyword(input, "by")?;
331        Ok(OptionalBy)
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use std::collections::HashMap;
338
339    use super::*;
340    use crate::codes;
341    use crate::prefix;
342    use crate::registers;
343    use crate::the;
344    use texlang::types::CatCode;
345    use texlang::vm::implement_has_component;
346    use texlang_testing::*;
347
348    #[derive(Default)]
349    struct State {
350        catcode: codes::Component<CatCode>,
351        prefix: prefix::Component,
352        registers: registers::Component<i32, 256>,
353        registers_dimen: registers::Component<core::Scaled, 256>,
354        registers_skip: registers::Component<core::Glue, 256>,
355        testing: TestingComponent,
356    }
357
358    impl TexlangState for State {
359        fn variable_assignment_scope_hook(
360            state: &mut Self,
361        ) -> texcraft_stdext::collections::groupingmap::Scope {
362            prefix::variable_assignment_scope_hook(state)
363        }
364        fn recoverable_error_hook(
365            &self,
366            recoverable_error: error::TracedTexError,
367        ) -> Result<(), Box<dyn error::TexError>> {
368            texlang_testing::TestingComponent::recoverable_error_hook(self, recoverable_error)
369        }
370    }
371    impl the::TheCompatible for State {}
372
373    implement_has_component![State{
374        catcode: codes::Component<CatCode>,
375        prefix: prefix::Component,
376        registers: registers::Component<i32, 256>,
377        registers_dimen: registers::Component<core::Scaled, 256>,
378        registers_skip: registers::Component<core::Glue, 256>,
379        testing: TestingComponent,
380    }];
381
382    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
383        HashMap::from([
384            ("advance", get_advance()),
385            ("advanceChecked", get_advance_checked()),
386            ("multiply", get_multiply()),
387            ("multiplyWrapped", get_multiply_wrapped()),
388            ("divide", get_divide()),
389            //
390            ("catcode", codes::get_catcode()),
391            ("count", registers::get_count()),
392            ("dimen", registers::get_dimen()),
393            ("skip", registers::get_skip()),
394            ("global", prefix::get_global()),
395            ("the", the::get_the()),
396        ])
397    }
398
399    macro_rules! arithmetic_tests {
400        ( $register: expr, $( ($name: ident, $op: expr, $lhs: expr, $rhs: expr, $expected: expr) ),* $(,)? ) => {
401            test_suite![
402                expansion_equality_tests(
403                    $(
404                        (
405                            $name,
406                            format![r"{} 1 {} {} {} 1 {} \the{} 1", $register, $lhs, $op, $register, $rhs, $register],
407                            $expected
408                        ),
409                    )*
410                ),
411            ];
412        };
413    }
414
415    arithmetic_tests![
416        r"\count",
417        (advance_base_case, r"\advance", "1", "2", "3"),
418        (advance_base_case_with_by, r"\advance", "1", "by 2", "3"),
419        (advance_negative_summand, r"\advance", "10", "-2", "8"),
420        (
421            advance_overflow_case,
422            r"\advance",
423            "2147483647",
424            "1",
425            "-2147483648"
426        ),
427        (multiply_base_case, r"\multiply", "5", "4", "20"),
428        (multiply_base_case_with_by, r"\multiply", "5", "by 4", "20"),
429        (multiply_pos_neg, r"\multiply", "-5", "4", "-20"),
430        (multiply_neg_pos, r"\multiply", "5", "-4", "-20"),
431        (multiply_neg_neg, r"\multiply", "-5", "-4", "20"),
432        (
433            multiply_wrapping_overflow,
434            r"\multiplyWrapped",
435            "100000",
436            "100000",
437            "1410065408"
438        ),
439        (
440            multiply_wrapping_base_case,
441            r"\multiplyWrapped",
442            "5",
443            "4",
444            "20"
445        ),
446        (divide_base_case, r"\divide", "9", "4", "2"),
447        (divide_with_by, r"\divide", "9", "by 4", "2"),
448        (divide_pos_neg, r"\divide", "-9", "4", "-2"),
449        (divide_neg_pos, r"\divide", "9", "-4", "-2"),
450        (divide_neg_neg, r"\divide", "-9", "-4", "2"),
451        (divide_exact, r"\divide", "100", "10", "10"),
452        (advance_checked_base_case, r"\advanceChecked", "1", "2", "3"),
453        (
454            advance_checked_negative_summand,
455            r"\advanceChecked",
456            "10",
457            "-2",
458            "8"
459        )
460    ];
461
462    arithmetic_tests![
463        r"\dimen",
464        (advance_dimen_1, r"\advance", "1pt", "2pt", "3.0pt"),
465        (advance_dimen_2, r"\advance", "0.025pt", "0.5pt", "0.525pt"),
466        (mul_dimen_1, r"\multiply", "10pt", "2", "20.0pt"),
467        (div_dimen_1, r"\divide", "10pt", "2", "5.0pt"),
468    ];
469
470    arithmetic_tests![
471        r"\skip",
472        (
473            advance_glue_1,
474            r"\advance",
475            "1pt plus 2pt minus 3pt",
476            "60pt plus 50pt minus 40pt",
477            "61.0pt plus 52.0pt minus 43.0pt"
478        ),
479        // This next test IS correct. The skip is expanded before the addition
480        // as part of scanning the optional plus/minus keywords...
481        (advance_glue_2, r"\advance", "0.025pt", "0.5pt", "0.025pt"),
482        (
483            advance_glue_3,
484            r"\advance",
485            "1pt plus 2fill minus 3fil",
486            "60pt plus 50pt minus 40filll",
487            "61.0pt plus 2.0fill minus 40.0filll"
488        ),
489        (
490            mul_glue_1,
491            r"\multiply",
492            "1pt plus 2pt minus 1.25pt",
493            "2",
494            "2.0pt plus 4.0pt minus 2.5pt"
495        ),
496        (
497            div_glue_1,
498            r"\divide",
499            "10pt plus 20pt minus 3pt",
500            "2",
501            "5.0pt plus 10.0pt minus 1.5pt"
502        ),
503    ];
504    test_suite![
505        expansion_equality_tests(
506            (
507                advance_x_by_x,
508                r"\count 1 200 \advance \count 1 by \count 1 a\the\count 1",
509                r"a400"
510            ),
511            (
512                global_advance,
513                r"\count 1 5{\global\advance\count 1 8}\the\count 1",
514                "13"
515            ),
516            (
517                local_advance,
518                r"\count 1 5{\advance\count 1 8}\the\count 1",
519                "5"
520            ),
521        ),
522        recoverable_failure_tests(
523            (
524                advance_incorrect_keyword_1,
525                r"\count 1 1\advance\count 1 fy 2 \the \count 1",
526                "fy 2 1",
527            ),
528            (
529                advance_incorrect_keyword_2,
530                r"\count 1 1\advance\count 1 be 2 \the \count 1",
531                "be 2 1",
532            ),
533            (
534                advance_catcode_not_supported,
535                r"\advance\catcode 100 by 2",
536                "100 by 2",
537            ),
538            (
539                advance_checked_overflow,
540                r"\count 1 2147483647 \advanceChecked\count 1 by 1",
541                "",
542            ),
543            (
544                multiply_overflow,
545                r"\count 1 100000 \multiply\count 1 by 100000 \the \count 1",
546                "100000"
547            ),
548            (
549                divide_by_zero,
550                r"\count 1 20 \divide\count 1 by 0 \the\count 1",
551                "20"
552            ),
553        ),
554    ];
555}