texlang_stdlib/
alloc.rs

1//! Dynamic allocation of variables and arrays
2//!
3//! This module contains implementations of brand new Texcraft commands
4//! `\newInt` and `\newIntArray` which perform dynamic memory allocation.
5
6use std::collections::HashMap;
7use texcraft_stdext::collections::groupingmap;
8use texlang::prelude as txl;
9use texlang::traits::*;
10use texlang::*;
11
12pub const NEWINT_DOC: &str = r"Allocate a new integer
13
14Usage: `\newInt <control sequence>`
15
16The `\newInt` command allocates a new integer
17that is referenced using the provided control sequence.
18Simple example:
19
20```
21\newInt \myvariable
22\myvariable = 4
23\advance \myvariable by 5
24\asserteq{\the \myvariable}{9}
25```
26
27You can think of `\newInt` as being a replacement for
28Plain TeX's `\newcount` macro (TeXBook p346).
29The benefit of `\newInt` is that different callers of the command
30do not share the underlying memory; the allocated memory is unique
31to the caller.
32Under the hood `\newInt` works by allocating new memory on the TeX engine's heap.
33";
34
35pub const NEWINTARRAY_DOC: &str = r"Allocate a new integer array
36
37Usage: `\newIntArray <control sequence> <array length>`
38
39The `\newIntArray` command allocates a new array of integers that
40is referenced using the provided control sequence.
41This new control sequence works pretty much like `\count`, but you can create
42    as many arrays as you like and don't need to worry about other
43    TeX code reusing the memory.
44Unlike `\count`, the size of the array is not fixed by
45the engine.
46The only constraint on the size is that you have enough RAM
47    on the machine to store it.
48Simple example:
49
50```
51\newIntArray \myarray 3
52\myarray 0 = 4
53\asserteq{\the \myarray 0}{4}
54\myarray 1 = 5
55\asserteq{\the \myarray 1}{5}
56\myarray 2 = 6
57\asserteq{\the \myarray 2}{6}
58```
59
60The new control sequence can *not* be aliased using \let.
61";
62
63/// Component required for the alloc commands.
64#[derive(Default)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct Component {
67    singletons: Vec<i32>,
68    arrays: Vec<i32>,
69    #[cfg_attr(feature = "serde", serde(with = "texcraft_stdext::serde_tools::iter"))]
70    array_refs: HashMap<token::CommandRef, (usize, usize)>,
71}
72
73/// Get the `\newInt` execution command.
74pub fn get_newint<S: HasComponent<Component>>() -> command::BuiltIn<S> {
75    command::BuiltIn::new_execution(newint_primitive_fn)
76}
77
78fn newint_primitive_fn<S: HasComponent<Component>>(
79    _: token::Token,
80    input: &mut vm::ExecutionInput<S>,
81) -> txl::Result<()> {
82    let command_ref = Option::<token::CommandRef>::parse(input)?;
83    let Some(command_ref) = command_ref else {
84        return Ok(());
85    };
86    let component = input.state_mut().component_mut();
87    let index = component.singletons.len();
88    component.singletons.push(Default::default());
89    input.commands_map_mut().insert_variable_command(
90        command_ref,
91        variable::Command::new_array(
92            singleton_ref_fn,
93            singleton_mut_ref_fn,
94            variable::IndexResolver::Static(variable::Index(index)),
95        ),
96        groupingmap::Scope::Local,
97    );
98    Ok(())
99}
100
101fn singleton_ref_fn<S: HasComponent<Component>>(state: &S, index: variable::Index) -> &i32 {
102    &state.component().singletons[index.0]
103}
104
105fn singleton_mut_ref_fn<S: HasComponent<Component>>(
106    state: &mut S,
107    index: variable::Index,
108) -> &mut i32 {
109    &mut state.component_mut().singletons[index.0]
110}
111
112/// Return a getter provider for the `\newInt` command.
113///
114/// The built-in commands for a VM must include this command in order for
115///     the allocation component to be serializable.
116pub fn get_newint_getter_provider<S: HasComponent<Component>>() -> command::BuiltIn<S> {
117    variable::Command::new_getter_provider(singleton_ref_fn, singleton_mut_ref_fn).into()
118}
119
120/// Get the `\newIntArray` execution command.
121pub fn get_newintarray<S: HasComponent<Component>>() -> command::BuiltIn<S> {
122    command::BuiltIn::new_execution(newintarray_primitive_fn)
123}
124
125fn newintarray_primitive_fn<S: HasComponent<Component>>(
126    _: token::Token,
127    input: &mut vm::ExecutionInput<S>,
128) -> txl::Result<()> {
129    let command_ref = Option::<token::CommandRef>::parse(input)?;
130    let len = parse::Uint::<{ parse::Uint::MAX }>::parse(input)?.0;
131    let Some(command_ref) = command_ref else {
132        return Ok(());
133    };
134    let component = input.state_mut().component_mut();
135    let start = component.arrays.len();
136    component.arrays.resize(start + len, Default::default());
137    component.array_refs.insert(command_ref, (start, len));
138    input.commands_map_mut().insert_variable_command(
139        command_ref,
140        variable::Command::new_array(
141            array_element_ref_fn,
142            array_element_mut_ref_fn,
143            variable::IndexResolver::Dynamic(resolve),
144        ),
145        groupingmap::Scope::Local,
146    );
147    // TODO: Return the arraydef version
148    Ok(())
149}
150
151fn resolve<S: HasComponent<Component>>(
152    token: token::Token,
153    input: &mut vm::ExpandedStream<S>,
154) -> txl::Result<variable::Index> {
155    let command_ref = match token.value() {
156        token::Value::CommandRef(command_ref) => command_ref,
157        _ => unreachable!(),
158    };
159    let (array_index, array_len) = *input
160        .state()
161        .component()
162        .array_refs
163        .get(&command_ref)
164        .unwrap();
165    let inner_index = parse::Uint::<{ parse::Uint::MAX }>::parse(input)?.0;
166    if inner_index >= array_len {
167        return Err(input.fatal_error(error::SimpleTokenError::new(
168            token,
169            format![
170                "Array out of bounds: cannot access index {inner_index} of array with length {array_len}"
171            ],
172        )
173        ));
174    }
175    Ok(variable::Index(array_index + inner_index))
176}
177
178fn array_element_ref_fn<S: HasComponent<Component>>(state: &S, index: variable::Index) -> &i32 {
179    &state.component().arrays[index.0]
180}
181
182fn array_element_mut_ref_fn<S: HasComponent<Component>>(
183    state: &mut S,
184    index: variable::Index,
185) -> &mut i32 {
186    &mut state.component_mut().arrays[index.0]
187}
188
189/// Return a getter provider for the `\newIntArray` command.
190///
191/// The built-in commands for a VM must include this command in order to support
192///     the allocation component to be serializable.
193pub fn get_newintarray_getter_provider<S: HasComponent<Component>>() -> command::BuiltIn<S> {
194    variable::Command::new_array(
195        array_element_ref_fn,
196        array_element_mut_ref_fn,
197        variable::IndexResolver::Dynamic(resolve),
198    )
199    .into()
200}
201
202#[cfg(test)]
203mod test {
204    use super::*;
205    use crate::the;
206    use texlang::vm::implement_has_component;
207    use texlang_testing::*;
208
209    #[derive(Default)]
210    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211    struct State {
212        alloc: Component,
213        testing: TestingComponent,
214    }
215
216    impl TexlangState for State {}
217
218    implement_has_component![State {
219        alloc: Component,
220        testing: TestingComponent,
221    }];
222    impl the::TheCompatible for State {}
223
224    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
225        HashMap::from([
226            ("newInt", get_newint()),
227            ("newInt_getter_provider_\u{0}", get_newint_getter_provider()),
228            ("newIntArray", get_newintarray()),
229            (
230                "newIntArray_getter_provider_\u{0}",
231                get_newintarray_getter_provider(),
232            ),
233            ("the", the::get_the()),
234        ])
235    }
236
237    test_suite![
238        expansion_equality_tests(
239            (newint_base_case, r"\newInt\a \a=3 \the\a", "3"),
240            (
241                newintarray_base_case_0,
242                r"\newIntArray \a 3 \a 0 = 2 \the\a 0",
243                "2"
244            ),
245            (newint_active_char, r"\newInt~~=3 \the~", "3"),
246            (
247                newintarray_base_case_1,
248                r"\newIntArray \a 3 \a 1 = 2 \the\a 1",
249                "2"
250            ),
251            (
252                newintarray_base_case_2,
253                r"\newIntArray \a 3 \a 2 = 5 \the\a 2",
254                "5"
255            ),
256            (newintarray_active_char, r"\newIntArray~3~2=5 \the~2", "5"),
257        ),
258        serde_tests(
259            (serde_singleton, r"\newInt\a \a=-1 ", r"\the\a"),
260            (serde_array, r"\newIntArray\a 20 \a 3=-1 ", r"\the\a 3"),
261        ),
262        fatal_error_tests(
263            (newintarray_out_of_bounds, r"\newIntArray \a 3 \a 3 = 2"),
264            (newintarray_negative_index, r"\newIntArray \a 3 \a -3 = 2"),
265            (newintarray_negative_length, r"\newIntArray \a -3"),
266        ),
267    ];
268}