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::parse::Command;
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    array_refs: HashMap<token::CsName, (usize, usize)>,
70}
71
72/// Get the `\newInt` execution command.
73pub fn get_newint<S: HasComponent<Component>>() -> command::BuiltIn<S> {
74    command::BuiltIn::new_execution(newint_primitive_fn)
75}
76
77fn newint_primitive_fn<S: HasComponent<Component>>(
78    _: token::Token,
79    input: &mut vm::ExecutionInput<S>,
80) -> command::Result<()> {
81    let Command::ControlSequence(name) = Command::parse(input)?;
82    let component = input.state_mut().component_mut();
83    let index = component.singletons.len();
84    component.singletons.push(Default::default());
85    input.commands_map_mut().insert_variable_command(
86        name,
87        variable::Command::new_array(
88            singleton_ref_fn,
89            singleton_mut_ref_fn,
90            variable::IndexResolver::Static(variable::Index(index)),
91        ),
92        groupingmap::Scope::Local,
93    );
94    Ok(())
95}
96
97fn singleton_ref_fn<S: HasComponent<Component>>(state: &S, index: variable::Index) -> &i32 {
98    &state.component().singletons[index.0]
99}
100
101fn singleton_mut_ref_fn<S: HasComponent<Component>>(
102    state: &mut S,
103    index: variable::Index,
104) -> &mut i32 {
105    &mut state.component_mut().singletons[index.0]
106}
107
108/// Return a getter provider for the `\newInt` command.
109///
110/// The initial commands for a VM must include this command in order for
111///     the allocation component to be serializable.
112pub fn get_newint_getter_provider<S: HasComponent<Component>>() -> command::BuiltIn<S> {
113    variable::Command::new_getter_provider(singleton_ref_fn, singleton_mut_ref_fn).into()
114}
115
116/// Get the `\newIntArray` execution command.
117pub fn get_newintarray<S: HasComponent<Component>>() -> command::BuiltIn<S> {
118    command::BuiltIn::new_execution(newintarray_primitive_fn)
119}
120
121fn newintarray_primitive_fn<S: HasComponent<Component>>(
122    _: token::Token,
123    input: &mut vm::ExecutionInput<S>,
124) -> command::Result<()> {
125    let Command::ControlSequence(name) = Command::parse(input)?;
126    let len = usize::parse(input)?;
127    let component = input.state_mut().component_mut();
128    let start = component.arrays.len();
129    component.arrays.resize(start + len, Default::default());
130    component.array_refs.insert(name, (start, len));
131    input.commands_map_mut().insert_variable_command(
132        name,
133        variable::Command::new_array(
134            array_element_ref_fn,
135            array_element_mut_ref_fn,
136            variable::IndexResolver::Dynamic(resolve),
137        ),
138        groupingmap::Scope::Local,
139    );
140    // TODO: Return the arraydef version
141    Ok(())
142}
143
144fn resolve<S: HasComponent<Component>>(
145    token: token::Token,
146    input: &mut vm::ExpandedStream<S>,
147) -> command::Result<variable::Index> {
148    let name = match token.value() {
149        token::Value::ControlSequence(name) => name,
150        _ => todo!(),
151    };
152    let (array_index, array_len) = *input.state().component().array_refs.get(&name).unwrap();
153    let inner_index = usize::parse(input)?;
154    if inner_index >= array_len {
155        return Err(error::SimpleTokenError::new(input.vm(),
156            token,
157            format![
158                "Array out of bounds: cannot access index {inner_index} of array with length {array_len}"
159            ],
160        )
161        .into());
162    }
163    Ok(variable::Index(array_index + inner_index))
164}
165
166fn array_element_ref_fn<S: HasComponent<Component>>(state: &S, index: variable::Index) -> &i32 {
167    &state.component().arrays[index.0]
168}
169
170fn array_element_mut_ref_fn<S: HasComponent<Component>>(
171    state: &mut S,
172    index: variable::Index,
173) -> &mut i32 {
174    &mut state.component_mut().arrays[index.0]
175}
176
177/// Return a getter provider for the `\newIntArray` command.
178///
179/// The initial commands for a VM must include this command in order to support
180///     the allocation component to be serializable.
181pub fn get_newintarray_getter_provider<S: HasComponent<Component>>() -> command::BuiltIn<S> {
182    variable::Command::new_array(
183        array_element_ref_fn,
184        array_element_mut_ref_fn,
185        variable::IndexResolver::Dynamic(resolve),
186    )
187    .into()
188}
189
190#[cfg(test)]
191mod test {
192    use super::*;
193    use crate::the::get_the;
194    use crate::{script, testing::*};
195    use texlang::vm::implement_has_component;
196
197    #[derive(Default)]
198    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199    struct State {
200        alloc: Component,
201        script: script::Component,
202    }
203
204    impl TexlangState for State {}
205
206    implement_has_component![State, (Component, alloc), (script::Component, script),];
207
208    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
209        HashMap::from([
210            ("newInt", get_newint()),
211            ("newInt_getter_provider_\u{0}", get_newint_getter_provider()),
212            ("newIntArray", get_newintarray()),
213            (
214                "newIntArray_getter_provider_\u{0}",
215                get_newintarray_getter_provider(),
216            ),
217            ("the", get_the()),
218        ])
219    }
220
221    test_suite![
222        expansion_equality_tests(
223            (newint_base_case, r"\newInt\a \a=3 \the\a", "3"),
224            (
225                newintarray_base_case_0,
226                r"\newIntArray \a 3 \a 0 = 2 \the\a 0",
227                "2"
228            ),
229            (
230                newintarray_base_case_1,
231                r"\newIntArray \a 3 \a 1 = 2 \the\a 1",
232                "2"
233            ),
234            (
235                newintarray_base_case_2,
236                r"\newIntArray \a 3 \a 2 = 2 \the\a 2",
237                "2"
238            ),
239        ),
240        serde_tests(
241            (serde_singleton, r"\newInt\a \a=-1 ", r"\the\a"),
242            (serde_array, r"\newIntArray\a 20 \a 3=-1 ", r"\the\a 3"),
243        ),
244        failure_tests(
245            (newintarray_out_of_bounds, r"\newIntArray \a 3 \a 3 = 2"),
246            (newintarray_negative_index, r"\newIntArray \a 3 \a -3 = 2"),
247            (newintarray_negative_length, r"\newIntArray \a -3"),
248        ),
249    ];
250}