texlang_stdlib/
registers.rs

1//! Register variables (`\count`, `\countdef`)
2
3use texcraft_stdext::collections::groupingmap;
4use texlang::parse::{Command, OptionalEquals};
5use texlang::token::trace;
6use texlang::traits::*;
7use texlang::*;
8
9pub const COUNT_DOC: &str = "Get or set an integer register";
10pub const COUNTDEF_DOC: &str = "Bind an integer register to a control sequence";
11
12pub struct Component<T, const N: usize>(
13    // We currently box the values because putting them directly on the stack causes the
14    // message pack decoder to stack overflow. It's a pity that we have to pay a runtime
15    // cost due to this, and it would be nice to fix the issue another way.
16    Box<[T; N]>,
17);
18
19#[cfg(feature = "serde")]
20impl<T: serde::Serialize, const N: usize> serde::Serialize for Component<T, N> {
21    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
22    where
23        S: serde::Serializer,
24    {
25        let v: Vec<&T> = self.0.iter().collect();
26        v.serialize(serializer)
27    }
28}
29
30#[cfg(feature = "serde")]
31impl<'de, T: std::fmt::Debug + serde::Deserialize<'de>, const N: usize> serde::Deserialize<'de>
32    for Component<T, N>
33{
34    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35    where
36        D: serde::Deserializer<'de>,
37    {
38        let v = Vec::<T>::deserialize(deserializer)?;
39        let a: Box<[T; N]> = v.try_into().unwrap();
40        Ok(Component(a))
41    }
42}
43
44impl<T: Default + Copy, const N: usize> Default for Component<T, N> {
45    fn default() -> Self {
46        Self(Box::new([Default::default(); N]))
47    }
48}
49
50/// Get the `\count` command.
51pub fn get_count<S: HasComponent<Component<i32, N>>, const N: usize>() -> command::BuiltIn<S> {
52    variable::Command::new_array(ref_fn, mut_fn, variable::IndexResolver::Dynamic(count_fn)).into()
53}
54
55fn count_fn<T, S: HasComponent<Component<T, N>>, const N: usize>(
56    count_token: token::Token,
57    input: &mut vm::ExpandedStream<S>,
58) -> command::Result<variable::Index> {
59    let index = usize::parse(input)?;
60    if index >= N {
61        return Err(IndexTooLargeError {
62            trace: input.vm().trace(count_token),
63            index,
64            num: N,
65        }
66        .into());
67    }
68    Ok(index.into())
69}
70
71/// Get the `\countdef` command.
72pub fn get_countdef<S: HasComponent<Component<i32, N>>, const N: usize>() -> command::BuiltIn<S> {
73    command::BuiltIn::new_execution(countdef_fn)
74}
75
76fn countdef_fn<T: variable::SupportedType, S: HasComponent<Component<T, N>>, const N: usize>(
77    countdef_token: token::Token,
78    input: &mut vm::ExecutionInput<S>,
79) -> command::Result<()> {
80    let (target, _, index) = <(Command, OptionalEquals, usize)>::parse(input)?;
81    let Command::ControlSequence(cs_name) = target;
82    if index >= N {
83        return Err(IndexTooLargeError {
84            trace: input.vm().trace(countdef_token),
85            index,
86            num: N,
87        }
88        .into());
89    }
90    // TODO: I suspect \countdef should honor \global, but haven't checked pdfTeX.
91    input.commands_map_mut().insert_variable_command(
92        cs_name,
93        variable::Command::new_array(
94            ref_fn,
95            mut_fn,
96            variable::IndexResolver::Static(index.into()),
97        ),
98        groupingmap::Scope::Local,
99    );
100    Ok(())
101}
102
103fn ref_fn<T, S: HasComponent<Component<T, N>>, const N: usize>(
104    state: &S,
105    index: variable::Index,
106) -> &T {
107    state.component().0.get(index.0).unwrap()
108}
109
110fn mut_fn<T, S: HasComponent<Component<T, N>>, const N: usize>(
111    state: &mut S,
112    index: variable::Index,
113) -> &mut T {
114    state.component_mut().0.get_mut(index.0).unwrap()
115}
116
117#[derive(Debug)]
118struct IndexTooLargeError {
119    trace: trace::SourceCodeTrace,
120    index: usize,
121    num: usize,
122}
123
124impl error::TexError for IndexTooLargeError {
125    fn kind(&self) -> error::Kind {
126        error::Kind::Token(&self.trace)
127    }
128
129    fn title(&self) -> String {
130        format![
131            "Index {} is too large; there are only {} integer registers",
132            self.index, self.num,
133        ]
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::collections::HashMap;
140
141    use super::*;
142    use crate::script;
143    use crate::testing::*;
144    use crate::the;
145    use texlang::vm::implement_has_component;
146
147    #[derive(Default)]
148    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149    struct State {
150        registers: Component<i32, 256>,
151        script: script::Component,
152    }
153
154    impl TexlangState for State {}
155
156    implement_has_component![
157        State,
158        (Component<i32, 256>, registers),
159        (script::Component, script),
160    ];
161
162    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
163        HashMap::from([
164            ("the", the::get_the()),
165            ("count", get_count()),
166            ("countdef", get_countdef()),
167        ])
168    }
169
170    test_suite![
171        expansion_equality_tests(
172            (write_and_read_register, r"\count 23 4 \the\count 23", r"4"),
173            (
174                write_and_read_register_eq,
175                r"\count 23 = 4 \the\count 23",
176                r"4"
177            ),
178            (countdef_base_case, r"\countdef\A 23\A 4 \the\A", r"4"),
179            (countdef_base_case_eq, r"\countdef\A = 23\A 4 \the\A", r"4"),
180            (
181                countdef_with_count,
182                r"\countdef\A 23\A 4\count 1 0 \the\A",
183                r"4"
184            ),
185            (
186                countdef_with_same_count,
187                r"\countdef\A 23\A 4\count 23 5 \the\A",
188                r"5"
189            ),
190        ),
191        serde_tests(
192            (serde_basic, r"\count 100 200 ", r"\the \count 100"),
193            (serde_countdef, r"\countdef \A 100 \A = 200 ", r"\the \A"),
194        ),
195        failure_tests(
196            (write_register_index_too_big, r"\count 260 = 4"),
197            (write_register_negative_index, r"\count -1 = 4"),
198            (countdef_register_index_too_big, r"\countdef\A 260 \A= 4"),
199        ),
200    ];
201}