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 common::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 common::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::SmallInt(_)
295                        | variable::Variable::CatCode(_)
296                        | variable::Variable::TokenList(_)
297                        | variable::Variable::MathCode(_)
298                        | variable::Variable::Font(_) => {
299                            unreachable!("only arithmetic commands are considered");
300                        }
301                    }
302                }
303                Some(cmd) => input.error(
304                    parse::Error::new("a variable", Some(token), "")
305                        .with_got_override("got a non-variable command")
306                        .with_annotation_override(format!["control sequence referencing {cmd}"]),
307                ),
308            }
309        }
310        _ => input.error(
311            parse::Error::new("a variable", Some(token), "")
312                .with_got_override("got a character token"),
313        ),
314    }
315}
316
317#[derive(Debug)]
318struct ArithmeticVariableEndOfInput;
319
320impl error::EndOfInputError for ArithmeticVariableEndOfInput {
321    fn doing(&self) -> String {
322        "parsing an arithmetic variable".into()
323    }
324}
325
326/// When parsed, this type consumes an optional `by` keyword from the input stream.
327struct OptionalBy;
328
329impl Parsable for OptionalBy {
330    fn parse_impl<S: TexlangState>(input: &mut vm::ExpandedStream<S>) -> txl::Result<Self> {
331        texlang::parse::parse_keyword(input, "by")?;
332        Ok(OptionalBy)
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use std::collections::HashMap;
339
340    use super::*;
341    use crate::codes;
342    use crate::prefix;
343    use crate::registers;
344    use crate::the;
345    use texlang::types::CatCode;
346    use texlang::vm::implement_has_component;
347    use texlang_testing::*;
348
349    #[derive(Default)]
350    struct State {
351        catcode: codes::Component<CatCode>,
352        prefix: prefix::Component,
353        registers: registers::Component<i32, 256>,
354        registers_dimen: registers::Component<common::Scaled, 256>,
355        registers_skip: registers::Component<common::Glue, 256>,
356        testing: TestingComponent,
357    }
358
359    impl TexlangState for State {
360        fn variable_assignment_scope_hook(
361            state: &mut Self,
362        ) -> texcraft_stdext::collections::groupingmap::Scope {
363            prefix::variable_assignment_scope_hook(state)
364        }
365        fn recoverable_error_hook(
366            &self,
367            recoverable_error: error::TracedTexError,
368        ) -> Result<(), Box<dyn error::TexError>> {
369            texlang_testing::TestingComponent::recoverable_error_hook(self, recoverable_error)
370        }
371    }
372    impl the::TheCompatible for State {}
373
374    implement_has_component![State{
375        catcode: codes::Component<CatCode>,
376        prefix: prefix::Component,
377        registers: registers::Component<i32, 256>,
378        registers_dimen: registers::Component<common::Scaled, 256>,
379        registers_skip: registers::Component<common::Glue, 256>,
380        testing: TestingComponent,
381    }];
382
383    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
384        HashMap::from([
385            ("advance", get_advance()),
386            ("advanceChecked", get_advance_checked()),
387            ("multiply", get_multiply()),
388            ("multiplyWrapped", get_multiply_wrapped()),
389            ("divide", get_divide()),
390            //
391            ("catcode", codes::get_catcode()),
392            ("count", registers::get_count()),
393            ("dimen", registers::get_dimen()),
394            ("skip", registers::get_skip()),
395            ("global", prefix::get_global()),
396            ("the", the::get_the()),
397        ])
398    }
399
400    macro_rules! arithmetic_tests {
401        ( $register: expr, $( ($name: ident, $op: expr, $lhs: expr, $rhs: expr, $expected: expr) ),* $(,)? ) => {
402            test_suite![
403                expansion_equality_tests(
404                    $(
405                        (
406                            $name,
407                            format![r"{} 1 {} {} {} 1 {} \the{} 1", $register, $lhs, $op, $register, $rhs, $register],
408                            $expected
409                        ),
410                    )*
411                ),
412            ];
413        };
414    }
415
416    arithmetic_tests![
417        r"\count",
418        (advance_base_case, r"\advance", "1", "2", "3"),
419        (advance_base_case_with_by, r"\advance", "1", "by 2", "3"),
420        (advance_negative_summand, r"\advance", "10", "-2", "8"),
421        (
422            advance_overflow_case,
423            r"\advance",
424            "2147483647",
425            "1",
426            "-2147483648"
427        ),
428        (multiply_base_case, r"\multiply", "5", "4", "20"),
429        (multiply_base_case_with_by, r"\multiply", "5", "by 4", "20"),
430        (multiply_pos_neg, r"\multiply", "-5", "4", "-20"),
431        (multiply_neg_pos, r"\multiply", "5", "-4", "-20"),
432        (multiply_neg_neg, r"\multiply", "-5", "-4", "20"),
433        (
434            multiply_wrapping_overflow,
435            r"\multiplyWrapped",
436            "100000",
437            "100000",
438            "1410065408"
439        ),
440        (
441            multiply_wrapping_base_case,
442            r"\multiplyWrapped",
443            "5",
444            "4",
445            "20"
446        ),
447        (divide_base_case, r"\divide", "9", "4", "2"),
448        (divide_with_by, r"\divide", "9", "by 4", "2"),
449        (divide_pos_neg, r"\divide", "-9", "4", "-2"),
450        (divide_neg_pos, r"\divide", "9", "-4", "-2"),
451        (divide_neg_neg, r"\divide", "-9", "-4", "2"),
452        (divide_exact, r"\divide", "100", "10", "10"),
453        (advance_checked_base_case, r"\advanceChecked", "1", "2", "3"),
454        (
455            advance_checked_negative_summand,
456            r"\advanceChecked",
457            "10",
458            "-2",
459            "8"
460        )
461    ];
462
463    arithmetic_tests![
464        r"\dimen",
465        (advance_dimen_1, r"\advance", "1pt", "2pt", "3.0pt"),
466        (advance_dimen_2, r"\advance", "0.025pt", "0.5pt", "0.525pt"),
467        (mul_dimen_1, r"\multiply", "10pt", "2", "20.0pt"),
468        (div_dimen_1, r"\divide", "10pt", "2", "5.0pt"),
469    ];
470
471    arithmetic_tests![
472        r"\skip",
473        (
474            advance_glue_1,
475            r"\advance",
476            "1pt plus 2pt minus 3pt",
477            "60pt plus 50pt minus 40pt",
478            "61.0pt plus 52.0pt minus 43.0pt"
479        ),
480        // This next test IS correct. The skip is expanded before the addition
481        // as part of scanning the optional plus/minus keywords...
482        (advance_glue_2, r"\advance", "0.025pt", "0.5pt", "0.025pt"),
483        (
484            advance_glue_3,
485            r"\advance",
486            "1pt plus 2fill minus 3fil",
487            "60pt plus 50pt minus 40filll",
488            "61.0pt plus 2.0fill minus 40.0filll"
489        ),
490        (
491            mul_glue_1,
492            r"\multiply",
493            "1pt plus 2pt minus 1.25pt",
494            "2",
495            "2.0pt plus 4.0pt minus 2.5pt"
496        ),
497        (
498            div_glue_1,
499            r"\divide",
500            "10pt plus 20pt minus 3pt",
501            "2",
502            "5.0pt plus 10.0pt minus 1.5pt"
503        ),
504    ];
505    test_suite![
506        expansion_equality_tests(
507            (
508                advance_x_by_x,
509                r"\count 1 200 \advance \count 1 by \count 1 a\the\count 1",
510                r"a400"
511            ),
512            (
513                global_advance,
514                r"\count 1 5{\global\advance\count 1 8}\the\count 1",
515                "13"
516            ),
517            (
518                local_advance,
519                r"\count 1 5{\advance\count 1 8}\the\count 1",
520                "5"
521            ),
522        ),
523        recoverable_failure_tests(
524            (
525                advance_incorrect_keyword_1,
526                r"\count 1 1\advance\count 1 fy 2 \the \count 1",
527                "fy 2 1",
528            ),
529            (
530                advance_incorrect_keyword_2,
531                r"\count 1 1\advance\count 1 be 2 \the \count 1",
532                "be 2 1",
533            ),
534            (
535                advance_catcode_not_supported,
536                r"\advance\catcode 100 by 2",
537                "100 by 2",
538            ),
539            (
540                advance_checked_overflow,
541                r"\count 1 2147483647 \advanceChecked\count 1 by 1",
542                "",
543            ),
544            (
545                multiply_overflow,
546                r"\count 1 100000 \multiply\count 1 by 100000 \the \count 1",
547                "100000"
548            ),
549            (
550                divide_by_zero,
551                r"\count 1 20 \divide\count 1 by 0 \the\count 1",
552                "20"
553            ),
554        ),
555    ];
556}