1use texlang::traits::*;
4use texlang::*;
5
6pub fn get_noexpand<S>() -> command::BuiltIn<S> {
8 command::BuiltIn::new_expansion(noexpand_fn).with_tag(NO_EXPAND_TAG.get())
9}
10
11static NO_EXPAND_TAG: command::StaticTag = command::StaticTag::new();
12
13fn noexpand_fn<S>(
14 _: token::Token,
15 _: &mut vm::ExpansionInput<S>,
16) -> command::Result<Vec<token::Token>> {
17 panic!(
18 "The \\noexpand expansion function is never invoked directly. \
19 Instead, the primitive operates through the \\noexpand hook, \
20 which is a method on the `TexlangState` trait. \
21 Ensure your Texcraft VM is configured to use this hook."
22 )
23}
24
25#[inline]
26pub fn noexpand_hook<S: TexlangState>(
27 token: token::Token,
28 input: &mut vm::ExpansionInput<S>,
29 tag: Option<command::Tag>,
30) -> command::Result<Option<token::Token>> {
31 if tag != Some(NO_EXPAND_TAG.get()) {
34 return Ok(None);
35 }
36 noexpand_hook_finish(token, input)
39}
40
41fn noexpand_hook_finish<S: TexlangState>(
42 token: token::Token,
43 input: &mut vm::ExpansionInput<S>,
44) -> command::Result<Option<token::Token>> {
45 match input.unexpanded().next()? {
46 None => Err(error::SimpleTokenError::new(
47 input.vm(),
48 token,
49 "unexpected end of input while expanding a `\\noexpand` command",
50 )
51 .into()),
52 Some(token) => Ok(Some(token)),
54 }
55}
56
57pub fn get_expandafter_simple<S: TexlangState>() -> command::BuiltIn<S> {
63 command::BuiltIn::new_expansion(expandafter_simple_fn)
64}
65
66pub fn get_expandafter_optimized<S: TexlangState>() -> command::BuiltIn<S> {
110 command::BuiltIn::new_expansion(expandafter_optimized_fn)
111}
112
113fn expandafter_simple_fn<S: TexlangState>(
114 expandafter_token: token::Token,
115 input: &mut vm::ExpansionInput<S>,
116) -> command::Result<Vec<token::Token>> {
117 let next = match input.unexpanded().next()? {
118 None => {
119 return Err(expandafter_missing_first_token_error(
120 input.vm(),
121 expandafter_token,
122 ));
123 }
124 Some(next) => next,
125 };
126 if input.unexpanded().peek()?.is_none() {
127 return Err(expandafter_missing_second_token_error(
128 input.vm(),
129 expandafter_token,
130 next,
131 ));
132 }
133 input.expanded().expand_once()?;
134 input.expansions_mut().push(next);
135 Ok(vec![])
136}
137
138fn expandafter_optimized_fn<S: TexlangState>(
139 expandafter_token: token::Token,
140 input: &mut vm::ExpansionInput<S>,
141) -> command::Result<Vec<token::Token>> {
142 let mut buffer: Vec<token::Token> = input.checkout_token_buffer();
143 let unexpanded_input = input.unexpanded();
144 loop {
145 match unexpanded_input.next()? {
146 None => {
147 return Err(expandafter_missing_first_token_error(
148 input.vm(),
149 expandafter_token,
150 ))
151 }
152 Some(next) => buffer.push(next),
153 };
154 let token = match unexpanded_input.peek()? {
155 None => {
156 return Err(expandafter_missing_second_token_error(
157 input.vm(),
158 expandafter_token,
159 *buffer.last().unwrap(),
160 ))
161 }
162 Some(token) => *token,
163 };
164 if token.value() != expandafter_token.value() {
165 break;
166 }
167 _ = unexpanded_input.next()?;
169 }
170 input.expanded().expand_once()?;
171
172 while let Some(&root) = buffer.first() {
173 if root.value() != expandafter_token.value() {
174 input.expansions_mut().extend(buffer.iter().rev());
175 break;
176 }
177 let mut last_expandafter_index = 0;
178 while let Some(next) = buffer.get(last_expandafter_index + 2) {
179 if next.value() != root.value() {
180 break;
181 }
182 last_expandafter_index += 2;
183 }
184 match buffer
187 .len()
188 .checked_sub(last_expandafter_index + 1)
189 .unwrap()
190 {
191 0 => {
193 let next = match input.unexpanded().next()? {
194 None => return Err(expandafter_missing_first_token_error(input.vm(), root)),
195 Some(next) => next,
196 };
197 buffer.push(next);
198 }
199 1 => {}
201 _ => {
203 input
204 .expansions_mut()
205 .extend(buffer[last_expandafter_index + 2..].iter().rev());
206 buffer.truncate(last_expandafter_index + 2);
207 }
208 }
209 if input.unexpanded().peek()?.is_none() {
212 return Err(expandafter_missing_second_token_error(
213 input.vm(),
214 root,
215 *buffer.last().unwrap(),
216 ));
217 }
218 input.expanded().expand_once()?;
219 remove_even_indices(&mut buffer);
220 }
221 input.return_token_buffer(buffer);
222 Ok(vec![])
223}
224
225fn remove_even_indices(v: &mut Vec<token::Token>) {
226 let mut src = 1;
227 let mut dest = 0;
228 while let Some(token) = v.get(src) {
229 v[dest] = *token;
230 dest += 1;
231 src += 2;
232 }
233 v.truncate(dest);
234}
235
236fn expandafter_missing_first_token_error<S>(
237 vm: &vm::VM<S>,
238 expandafter_token: token::Token,
239) -> Box<error::Error> {
240 error::SimpleTokenError::new(
241 vm,
242 expandafter_token,
243 "unexpected end of input while expanding an `\\expandafter` command",
244 )
245 .into()
246 }
250
251fn expandafter_missing_second_token_error<S>(
252 vm: &vm::VM<S>,
253 expandafter_token: token::Token,
254 first_token: token::Token,
255) -> Box<error::Error> {
256 _ = first_token;
257 error::SimpleTokenError::new(
258 vm,
259 expandafter_token,
260 "unexpected end of input while expanding an `\\expandafter` command",
261 )
262 .into()
263 }
266
267pub fn get_relax<S>() -> command::BuiltIn<S> {
269 command::BuiltIn::new_execution(|_, _| Ok(()))
270}
271
272#[cfg(test)]
273mod test {
274 use super::*;
275 use crate::prefix;
276 use crate::script;
277 use crate::testing::*;
278 use std::collections::HashMap;
279
280 #[derive(Default)]
281 pub struct State {
282 prefix: prefix::Component,
283 script: script::Component,
284 integer: i32,
285 }
286
287 impl State {
288 pub fn get_integer() -> command::BuiltIn<State> {
289 variable::Command::new_singleton(
290 |state: &State, _: variable::Index| -> &i32 { &state.integer },
291 |state: &mut State, _: variable::Index| -> &mut i32 { &mut state.integer },
292 )
293 .into()
294 }
295 }
296
297 impl TexlangState for State {
298 fn expansion_override_hook(
299 token: token::Token,
300 input: &mut vm::ExpansionInput<Self>,
301 tag: Option<command::Tag>,
302 ) -> command::Result<Option<token::Token>> {
303 noexpand_hook(token, input, tag)
304 }
305 }
306
307 implement_has_component![
308 State,
309 (script::Component, script),
310 (prefix::Component, prefix),
311 ];
312
313 fn initial_commands(optimized: bool) -> HashMap<&'static str, command::BuiltIn<State>> {
314 HashMap::from([
315 ("def", crate::def::get_def()),
316 ("noexpand", get_noexpand()),
317 ("integer", State::get_integer()),
318 (
319 "xa",
320 match optimized {
321 true => get_expandafter_optimized(),
322 false => get_expandafter_simple(),
323 },
324 ),
325 ])
326 }
327
328 test_suite![
329 options(TestOption::InitialCommandsDyn(Box::new(|| {
330 initial_commands(true)
331 })),),
332 expansion_equality_tests(
333 (simple_case, r"\def\a{Hello}\noexpand\a", r"\a"),
334 (
335 expandafter_and_noexpand_1,
336 r"\def\a#1\b{Hello '#1'}\def\b{World} \a\b",
337 " Hello ''"
338 ),
339 (
340 expandafter_and_noexpand_2,
341 r"\def\a#1\b{Hello '#1'}\def\b{World} \a\b\b",
342 " Hello ''World"
343 ),
344 (
345 expandafter_and_noexpand_3,
346 r"\def\a#1\b{Hello '#1'}\def\b{World} \xa\a\b\b",
347 " Hello 'World'"
348 ),
349 (
350 expandafter_and_noexpand_4,
351 r"\def\a#1\b{Hello '#1'}\def\b{World} \xa\a\noexpand\b\b",
352 " Hello ''World"
353 ),
354 (
355 only_expands_once,
356 r"\def\A{\B}\def\B{Hello}\xa\noexpand\A",
357 r"\B",
358 ),
359 (
360 peek_consumes_noexpand_1,
361 r"\def\A{\B}\def\B{Hello}\integer = 1 \noexpand\A",
362 r"\A",
363 ),
364 (
365 peek_consumes_noexpand_2,
366 r"\def\A{\B}\def\B{Hello}\integer = 1\noexpand\A",
367 r"Hello",
368 ),
369 ),
371 failure_tests((end_of_input, r"\noexpand"),),
372 ];
373
374 static PREFIX: &str = r"\def\mk#1#2{\def#1##1\notes##2\end{##1\notes##2#2\end}}\mk\a a\mk\b b\mk\c c\mk\d d\def\notes#1\end{#1}";
375 static POSTFIX: &str = r"\notes\end";
376
377 #[macro_export]
378 macro_rules! expandafter_test {
379 ( $( ( $name: ident, $lhs: expr, $rhs: expr ) ),* $(,)? ) => {
380 mod expandafter_simple {
381 use super::*;
382 test_suite![
383 options(TestOption::InitialCommandsDyn(Box::new(|| { initial_commands(false) }))),
384 expansion_equality_tests(
385 $(
386 ( $name, format!("{}{}{}", PREFIX, $lhs, POSTFIX), $rhs ),
387 )*
388 ),
389 ];
390 }
391 mod expandafter_optimized {
392 use super::*;
393 test_suite![
394 options(TestOption::InitialCommandsDyn(Box::new(|| { initial_commands(true) }))),
395 expansion_equality_tests(
396 $(
397 ( $name, format!("{}{}{}", PREFIX, $lhs, POSTFIX), $rhs ),
398 )*
399 ),
400 ];
401 }
402 };
403 }
404
405 expandafter_test![
406 (texbook_p374_3, r"\xa\a\b", r"ba"),
407 (texbook_p374_4, r"\xa\xa\xa\a\xa\b\c", "cba"),
408 (
409 texbook_p374_5,
410 r"\xa\xa\xa\xa\xa\xa\xa\a\xa\xa\xa\b\xa\c\d",
411 "dcba"
412 ),
413 (permutation_abcd, r"\a\b\c\d", "abcd"),
436 (permutation_abdc, r"\a\b\xa\c\d", "abdc"),
437 (permutation_acbd, r"\a\xa\b\c\d", "acbd"),
438 (permutation_acdb, r"\a\xa\xa\xa\b\c\d", "acdb"),
439 (permutation_adbc, r"\a\xa\b\xa\c\d", "adbc"),
440 (permutation_adcb, r"\a\xa\xa\xa\b\xa\c\d", "adcb"),
441 (permutation_bacd, r"\xa\a\b\c\d", "bacd"),
442 (permutation_badc, r"\xa\a\b\xa\c\d", "badc"),
443 (permutation_bcad, r"\xa\xa\xa\a\b\c\d", "bcad"),
444 (permutation_bcda, r"\xa\xa\xa\xa\xa\xa\xa\a\b\c\d", "bcda"),
445 (permutation_bdac, r"\xa\xa\xa\a\b\xa\c\d", "bdac"),
446 (
447 permutation_bdca,
448 r"\xa\xa\xa\xa\xa\xa\xa\a\b\xa\c\d",
449 "bdca"
450 ),
451 (permutation_cabd, r"\xa\a\xa\b\c\d", "cabd"),
452 (permutation_cadb, r"\xa\a\xa\xa\xa\b\c\d", "cadb"),
453 (permutation_cbad, r"\xa\xa\xa\a\xa\b\c\d", "cbad"),
454 (
455 permutation_cbda,
456 r"\xa\xa\xa\xa\xa\xa\xa\a\xa\xa\xa\b\c\d",
457 "cdba"
458 ),
459 (permutation_cdab, r"\xa\xa\xa\a\xa\xa\xa\b\c\d", "cdab"),
460 (
461 permutation_cdba,
462 r"\xa\xa\xa\xa\xa\xa\xa\a\xa\xa\xa\b\c\d",
463 "cdba"
464 ),
465 (permutation_dabc, r"\xa\a\xa\b\xa\c\d", "dabc"),
466 (permutation_dacb, r"\xa\a\xa\xa\xa\b\xa\c\d", "dacb"),
467 (permutation_dbac, r"\xa\xa\xa\a\xa\b\xa\c\d", "dbac"),
468 (
469 permutation_dbca,
470 r"\xa\xa\xa\xa\xa\xa\xa\a\xa\b\xa\c\d",
471 "dbca"
472 ),
473 (permutation_dcab, r"\xa\xa\xa\a\xa\xa\xa\b\xa\c\d", "dcab"),
474 (
475 permutation_dcba,
476 r"\xa\xa\xa\xa\xa\xa\xa\a\xa\xa\xa\b\xa\c\d",
477 "dcba"
478 ),
479 (
480 expandafter_last_after_first_pass,
481 r"\xa\xa\xa\a\xa\xa\b\c\d",
482 "bdac"
483 ),
484 ];
485
486 fn run_expandafter_failure_test(input: &str, optimized: bool) {
487 let options = vec![crate::testing::TestOption::InitialCommandsDyn(Box::new(
488 || initial_commands(optimized),
489 ))];
490 crate::testing::run_failure_test(&input, &options);
491 }
492
493 #[macro_export]
494 macro_rules! expandafter_failure_test {
495 ($( ( $name: ident, $input: expr), )+) => {
496 $(
497 mod $name {
498 #[test]
499 fn simple() {
500 super::run_expandafter_failure_test($input, false);
501 }
502 #[test]
503 fn optimized() {
504 super::run_expandafter_failure_test($input, true);
505 }
506 }
507 )+
508 };
509 }
510
511 expandafter_failure_test![
512 (expandafter_missing_1st_token, r"\xa"),
513 (expandafter_missing_2nd_token, r"\xa\a"),
514 (expandafter_missing_1st_token_nested, r"\xa\xa\xa\a\xa\xa\b"),
515 (
516 expandafter_missing_2nd_token_nested,
517 r"\def\A{}\xa\xa\xa\A\A"
518 ),
519 ];
520}