1use 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#[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
73pub 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
112pub 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
120pub 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 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
189pub 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}