1use crate::error::MathError;
49use crate::node::Span;
50use crate::token::{Tok, TokKind, lex};
51use std::collections::BTreeMap;
52
53const EXPANSION_TOKEN_BUDGET: usize = 65_536;
55const EXPANSION_DEPTH_BUDGET: usize = 32;
57
58#[derive(Clone, Debug, Default, PartialEq, Eq)]
61pub struct MacroSet {
62 defs: BTreeMap<String, MacroDef>,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66struct MacroDef {
67 params: u8,
69 body: String,
71}
72
73impl MacroSet {
74 #[must_use]
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 #[must_use]
86 pub fn pack(id: &str) -> Option<Self> {
87 match id {
88 "fmd-math/pack/default" | "default" => {
89 let mut defs = BTreeMap::new();
96 defs.insert(
97 "minus".to_owned(),
98 MacroDef {
99 params: 0,
100 body: "-".to_owned(),
101 },
102 );
103 Some(Self { defs })
104 }
105 "fmd-math/pack/basic" | "basic" | "fmd-math/pack/empty" | "empty" => Some(Self::new()),
106 _ => None,
107 }
108 }
109
110 pub fn define(&mut self, name: &str, params: u8, body: &str) -> Result<(), MathError> {
120 let malformed = |what: String| MathError::Malformed { what, at: 0 };
121 if name.is_empty() || !name.bytes().all(|b| b.is_ascii_alphabetic()) {
122 return Err(malformed(format!(
123 "macro name {name:?} must be one or more ASCII letters"
124 )));
125 }
126 if params > 9 {
127 return Err(malformed(format!(
128 "macro \\{name} declares {params} parameters; TeX allows at most 9"
129 )));
130 }
131 validate_body(name, params, body)?;
132 self.defs.insert(
133 name.to_owned(),
134 MacroDef {
135 params,
136 body: body.to_owned(),
137 },
138 );
139 Ok(())
140 }
141
142 pub fn names(&self) -> impl Iterator<Item = &str> {
144 self.defs.keys().map(String::as_str)
145 }
146
147 #[must_use]
149 pub fn len(&self) -> usize {
150 self.defs.len()
151 }
152
153 #[must_use]
155 pub fn is_empty(&self) -> bool {
156 self.defs.is_empty()
157 }
158
159 #[must_use]
166 pub fn canonical_bytes(&self) -> Vec<u8> {
167 let mut out = b"fmd-math-macroset-v1\x1e".to_vec();
168 for (name, def) in &self.defs {
169 out.extend_from_slice(name.as_bytes());
170 out.push(0x1f);
171 out.push(b'0' + def.params);
172 out.push(0x1f);
173 out.extend_from_slice(def.body.as_bytes());
174 out.push(0x1e);
175 }
176 out
177 }
178}
179
180fn validate_body(name: &str, params: u8, body: &str) -> Result<(), MathError> {
183 let malformed = |what: String| MathError::Malformed { what, at: 0 };
184 let mut depth = 0_i32;
185 let toks = lex(body);
186 let mut i = 0;
187 while i < toks.len() {
188 match toks[i].kind {
189 TokKind::BeginGroup => depth += 1,
190 TokKind::EndGroup => {
191 depth -= 1;
192 if depth < 0 {
193 return Err(malformed(format!(
194 "macro \\{name} body has an unmatched '}}'"
195 )));
196 }
197 }
198 TokKind::Char('#') => {
199 let param = toks.get(i + 1).and_then(|t| match t.kind {
200 TokKind::Char(c) => c.to_digit(10),
201 _ => None,
202 });
203 match param {
204 Some(d) if (1..=u32::from(params)).contains(&d) => i += 1,
205 Some(d) => {
206 return Err(malformed(format!(
207 "macro \\{name} body uses #{d} but declares {params} parameter(s)"
208 )));
209 }
210 None => {
211 return Err(malformed(format!(
212 "macro \\{name} body has a '#' not followed by a parameter digit"
213 )));
214 }
215 }
216 }
217 _ => {}
218 }
219 i += 1;
220 }
221 if depth != 0 {
222 return Err(malformed(format!(
223 "macro \\{name} body has {depth} unclosed '{{'"
224 )));
225 }
226 Ok(())
227}
228
229struct Live<'a> {
231 params: u8,
232 body: Vec<Tok<'a>>,
233 default: Option<Vec<Tok<'a>>>,
236}
237
238fn optional_group(
242 toks: &[Tok<'_>],
243 open: usize,
244 src_len: usize,
245 name: &str,
246) -> Result<(usize, usize), MathError> {
247 let start = open + 1;
248 let mut groups = 0usize;
249 for (index, token) in toks.iter().enumerate().skip(start) {
250 match token.kind {
251 TokKind::BeginGroup => groups += 1,
252 TokKind::EndGroup if groups == 0 => {
253 return Err(MathError::Malformed {
254 what: format!("\\{name}: optional argument closes a surrounding group"),
255 at: token.span.start,
256 });
257 }
258 TokKind::EndGroup => groups -= 1,
259 TokKind::Char(']') if groups == 0 => return Ok((start, index)),
260 _ => {}
261 }
262 }
263 Err(MathError::Malformed {
264 what: format!("\\{name}: unclosed optional argument (expected ']')"),
265 at: src_len,
266 })
267}
268
269fn skip_spaces(toks: &[Tok<'_>], index: &mut usize) {
270 while toks
271 .get(*index)
272 .is_some_and(|token| matches!(token.kind, TokKind::Space))
273 {
274 *index += 1;
275 }
276}
277
278pub(crate) fn expand<'a>(
282 toks: Vec<Tok<'a>>,
283 set: &'a MacroSet,
284 src_len: usize,
285) -> Result<Vec<Tok<'a>>, MathError> {
286 let involved = !set.is_empty()
288 || toks
289 .iter()
290 .any(|t| matches!(t.kind, TokKind::ControlWord("newcommand" | "renewcommand")));
291 if !involved {
292 return Ok(toks);
293 }
294
295 let mut table: BTreeMap<&'a str, Live<'a>> = BTreeMap::new();
296 for (name, def) in &set.defs {
297 table.insert(
298 name.as_str(),
299 Live {
300 params: def.params,
301 body: lex(&def.body),
302 default: None,
303 },
304 );
305 }
306
307 let mut cx = Expansion {
308 table,
309 budget: EXPANSION_TOKEN_BUDGET,
310 src_len,
311 };
312 let mut out = Vec::with_capacity(toks.len());
313 let mut i = 0;
314 while i < toks.len() {
315 let tok = &toks[i];
316 match tok.kind {
317 TokKind::ControlWord(cw @ ("newcommand" | "renewcommand")) => {
318 i = cx.definition(&toks, i, cw == "renewcommand")?;
319 }
320 TokKind::ControlWord(name) if cx.table.contains_key(name) => {
321 let mut active = Vec::new();
322 i = cx.call(&toks, i, name, &mut active, 0, &mut out)?;
323 }
324 _ => {
325 out.push(tok.clone());
326 i += 1;
327 }
328 }
329 }
330 Ok(out)
331}
332
333struct Expansion<'a> {
334 table: BTreeMap<&'a str, Live<'a>>,
335 budget: usize,
336 src_len: usize,
337}
338
339impl<'a> Expansion<'a> {
340 fn definition(&mut self, toks: &[Tok<'a>], i: usize, renew: bool) -> Result<usize, MathError> {
342 let cw_span = toks[i].span;
343 let which = if renew {
344 "\\renewcommand"
345 } else {
346 "\\newcommand"
347 };
348 let mut j = i + 1;
349 skip_spaces(toks, &mut j);
350 let braced = matches!(toks.get(j).map(|t| &t.kind), Some(TokKind::BeginGroup));
352 if braced {
353 j += 1;
354 skip_spaces(toks, &mut j);
355 }
356 let Some(name_tok) = toks.get(j) else {
357 return Err(MathError::Malformed {
358 what: format!("{which} ends before its macro name"),
359 at: self.src_len,
360 });
361 };
362 let TokKind::ControlWord(name) = name_tok.kind else {
363 return Err(MathError::Malformed {
364 what: format!("{which} expects a \\name to define"),
365 at: name_tok.span.start,
366 });
367 };
368 j += 1;
369 if braced {
370 skip_spaces(toks, &mut j);
371 let Some(Tok {
372 kind: TokKind::EndGroup,
373 ..
374 }) = toks.get(j)
375 else {
376 return Err(MathError::Malformed {
377 what: format!("{which}{{\\{name}}} has an unclosed name group"),
378 at: toks.get(j).map_or(self.src_len, |t| t.span.start),
379 });
380 };
381 j += 1;
382 }
383 skip_spaces(toks, &mut j);
384 let mut params = 0_u8;
386 let mut default = None;
387 if matches!(toks.get(j).map(|t| &t.kind), Some(TokKind::Char('['))) {
388 let digit = toks.get(j + 1).and_then(|t| match t.kind {
389 TokKind::Char(c) => c.to_digit(10),
390 _ => None,
391 });
392 let close = matches!(toks.get(j + 2).map(|t| &t.kind), Some(TokKind::Char(']')));
393 match (digit, close) {
394 (Some(d @ 1..=9), true) => {
395 params = u8::try_from(d).unwrap_or(9);
396 j += 3;
397 }
398 _ => {
399 return Err(MathError::Malformed {
400 what: format!("{which}{{\\{name}}}: expected [1]..[9] parameter count"),
401 at: toks.get(j).map_or(self.src_len, |t| t.span.start),
402 });
403 }
404 }
405 skip_spaces(toks, &mut j);
406 if matches!(toks.get(j).map(|t| &t.kind), Some(TokKind::Char('['))) {
407 let (start, end) = optional_group(toks, j, self.src_len, name)?;
408 validate_body_tokens(name, 0, &toks[start..end], cw_span.start)?;
411 self.charge(end - start, cw_span)?;
412 default = Some(toks[start..end].to_vec());
413 j = end + 1;
414 skip_spaces(toks, &mut j);
415 }
416 }
417 let Some(Tok {
419 kind: TokKind::BeginGroup,
420 ..
421 }) = toks.get(j)
422 else {
423 return Err(MathError::Malformed {
424 what: format!("{which}{{\\{name}}}: expected a {{body}} group"),
425 at: toks.get(j).map_or(self.src_len, |t| t.span.start),
426 });
427 };
428 let body_start = j + 1;
429 let mut depth = 1_i32;
430 let mut k = body_start;
431 while k < toks.len() {
432 match toks[k].kind {
433 TokKind::BeginGroup => depth += 1,
434 TokKind::EndGroup => {
435 depth -= 1;
436 if depth == 0 {
437 break;
438 }
439 }
440 _ => {}
441 }
442 k += 1;
443 }
444 if depth != 0 {
445 return Err(MathError::Malformed {
446 what: format!("{which}{{\\{name}}}: unclosed body group"),
447 at: self.src_len,
448 });
449 }
450 let exists = self.table.contains_key(name);
451 if !renew && exists {
452 return Err(MathError::Malformed {
453 what: format!(
454 "\\newcommand: \\{name} is already defined (use \\renewcommand to replace it)"
455 ),
456 at: cw_span.start,
457 });
458 }
459 if renew && !exists {
460 return Err(MathError::Malformed {
461 what: format!("\\renewcommand: \\{name} is not defined (use \\newcommand)"),
462 at: cw_span.start,
463 });
464 }
465 validate_body_tokens(name, params, &toks[body_start..k], cw_span.start)?;
468 self.charge(k - body_start, cw_span)?;
469 let body = toks[body_start..k].to_vec();
470 self.table.insert(
471 name,
472 Live {
473 params,
474 body,
475 default,
476 },
477 );
478 Ok(k + 1)
479 }
480
481 fn call(
484 &mut self,
485 toks: &[Tok<'a>],
486 i: usize,
487 name: &'a str,
488 active: &mut Vec<String>,
489 depth: usize,
490 out: &mut Vec<Tok<'a>>,
491 ) -> Result<usize, MathError> {
492 let call_start = toks[i].span;
493 if depth >= EXPANSION_DEPTH_BUDGET {
494 return Err(MathError::Malformed {
495 what: format!(
496 "macro expansion nests deeper than {EXPANSION_DEPTH_BUDGET} (at \\{name})"
497 ),
498 at: call_start.start,
499 });
500 }
501 if active.iter().any(|a| a == name) {
502 return Err(MathError::Malformed {
503 what: format!(
504 "recursive macro: \\{name} expands itself (macros are non-recursive substitutions)"
505 ),
506 at: call_start.start,
507 });
508 }
509 self.charge(1, call_start)?;
510 let params = self.table.get(name).map(|live| live.params).unwrap_or(0);
511 let optional = self
512 .table
513 .get(name)
514 .is_some_and(|live| live.default.is_some());
515 let mut j = i + 1;
516 let mut arguments: Vec<Option<&[Tok<'a>]>> = Vec::new();
519 let mut end_span = call_start;
520 let mut use_default = false;
521 if optional {
522 skip_spaces(toks, &mut j);
523 if matches!(
524 toks.get(j).map(|token| &token.kind),
525 Some(TokKind::Char('['))
526 ) {
527 let (start, end) = optional_group(toks, j, self.src_len, name)?;
528 arguments.push(Some(&toks[start..end]));
529 end_span = toks[end].span;
530 j = end + 1;
531 } else {
532 arguments.push(None);
533 use_default = true;
534 }
535 }
536 for argn in arguments.len() + 1..=usize::from(params) {
537 skip_spaces(toks, &mut j);
538 let Some(first) = toks.get(j) else {
539 return Err(MathError::Malformed {
540 what: format!("\\{name} needs {params} argument(s); input ends before #{argn}"),
541 at: self.src_len,
542 });
543 };
544 if matches!(first.kind, TokKind::BeginGroup) {
545 let start = j + 1;
546 let mut depth_b = 1_i32;
547 let mut k = start;
548 while k < toks.len() {
549 match toks[k].kind {
550 TokKind::BeginGroup => depth_b += 1,
551 TokKind::EndGroup => {
552 depth_b -= 1;
553 if depth_b == 0 {
554 break;
555 }
556 }
557 _ => {}
558 }
559 k += 1;
560 }
561 if depth_b != 0 {
562 return Err(MathError::Malformed {
563 what: format!("\\{name}: unclosed argument group for #{argn}"),
564 at: self.src_len,
565 });
566 }
567 arguments.push(Some(&toks[start..k]));
568 end_span = toks[k].span;
569 j = k + 1;
570 } else if matches!(first.kind, TokKind::EndGroup) {
571 return Err(MathError::Malformed {
572 what: format!("\\{name}: missing argument #{argn} before closing group"),
573 at: first.span.start,
574 });
575 } else {
576 arguments.push(Some(core::slice::from_ref(first)));
577 end_span = first.span;
578 j += 1;
579 }
580 }
581 let call_span = call_start.union(end_span);
582 let mut default = Vec::new();
583 if use_default {
584 let count = self
585 .table
586 .get(name)
587 .and_then(|live| live.default.as_ref())
588 .map_or(0, Vec::len);
589 self.charge(count, call_span)?;
590 default = self
591 .table
592 .get(name)
593 .and_then(|live| live.default.clone())
594 .unwrap_or_default();
595 for token in &mut default {
596 token.span = call_span;
597 }
598 }
599 let args: Vec<&[Tok<'a>]> = arguments
600 .iter()
601 .map(|argument| argument.unwrap_or(default.as_slice()))
602 .collect();
603 self.splice(name, &args, call_span, active, depth, out)?;
604 Ok(j)
605 }
606
607 fn splice(
612 &mut self,
613 name: &'a str,
614 args: &[&[Tok<'a>]],
615 call_span: Span,
616 active: &mut Vec<String>,
617 depth: usize,
618 out: &mut Vec<Tok<'a>>,
619 ) -> Result<(), MathError> {
620 let count = self.table.get(name).map_or(0, |live| live.body.len());
621 self.charge(count, call_span)?;
624 let body = self
625 .table
626 .get(name)
627 .map(|live| live.body.clone())
628 .unwrap_or_default();
629 let mut replacement = Vec::new();
630 let mut j = 0;
631 while j < body.len() {
632 let token = &body[j];
633 if matches!(token.kind, TokKind::Char('#')) {
634 let index = body.get(j + 1).and_then(|next| match next.kind {
635 TokKind::Char(c @ '1'..='9') => Some(c as usize - '1' as usize),
636 _ => None,
637 });
638 let Some(argument) = index.and_then(|index| args.get(index)) else {
639 return Err(MathError::Malformed {
640 what: format!("macro \\{name} body has an invalid parameter reference"),
641 at: call_span.start,
642 });
643 };
644 self.charge(argument.len(), call_span)?;
645 replacement.extend_from_slice(argument);
646 j += 2;
647 } else {
648 self.charge(1, call_span)?;
649 let mut token = token.clone();
650 token.span = call_span;
651 replacement.push(token);
652 j += 1;
653 }
654 }
655
656 active.push(name.to_owned());
657 let result = (|| {
658 let mut cursor = 0;
659 while cursor < replacement.len() {
660 match replacement[cursor].kind {
661 TokKind::ControlWord(nested) if self.table.contains_key(nested) => {
662 cursor = self.call(&replacement, cursor, nested, active, depth + 1, out)?;
663 }
664 _ => {
665 out.push(replacement[cursor].clone());
666 cursor += 1;
667 }
668 }
669 }
670 Ok(())
671 })();
672 active.pop();
673 result
674 }
675
676 fn budget_error(span: Span) -> MathError {
677 MathError::Malformed {
678 what: format!(
679 "macro expansion requires more than {EXPANSION_TOKEN_BUDGET} token work units"
680 ),
681 at: span.start,
682 }
683 }
684
685 fn charge(&mut self, units: usize, span: Span) -> Result<(), MathError> {
689 self.budget = self
690 .budget
691 .checked_sub(units)
692 .ok_or_else(|| Self::budget_error(span))?;
693 Ok(())
694 }
695}
696
697fn validate_body_tokens(
700 name: &str,
701 params: u8,
702 body: &[Tok<'_>],
703 at: usize,
704) -> Result<(), MathError> {
705 let mut j = 0;
706 while j < body.len() {
707 if let TokKind::Char('#') = body[j].kind {
708 let d = body.get(j + 1).and_then(|n| match n.kind {
709 TokKind::Char(c) => c.to_digit(10),
710 _ => None,
711 });
712 match d {
713 Some(d) if (1..=u32::from(params)).contains(&d) => j += 1,
714 Some(d) => {
715 return Err(MathError::Malformed {
716 what: format!(
717 "macro \\{name} body uses #{d} but declares {params} parameter(s)"
718 ),
719 at,
720 });
721 }
722 None => {
723 return Err(MathError::Malformed {
724 what: format!("macro \\{name} body has a '#' not followed by a digit"),
725 at,
726 });
727 }
728 }
729 }
730 j += 1;
731 }
732 Ok(())
733}
734
735#[cfg(test)]
736mod tests {
737 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
738
739 use super::*;
740
741 fn expand_str<'a>(src: &'a str, set: &'a MacroSet) -> Result<String, MathError> {
742 let toks = expand(lex(src), set, src.len())?;
743 Ok(toks
744 .iter()
745 .map(|t| match &t.kind {
746 TokKind::ControlWord(w) => format!("\\{w} "),
747 TokKind::ControlSymbol(c) => format!("\\{c}"),
748 TokKind::BeginGroup => "{".into(),
749 TokKind::EndGroup => "}".into(),
750 TokKind::Sup => "^".into(),
751 TokKind::Sub => "_".into(),
752 TokKind::AlignTab => "&".into(),
753 TokKind::Tie => "~".into(),
754 TokKind::MathShift => "$".into(),
755 TokKind::Space => " ".into(),
756 TokKind::Char(c) => (*c).to_string(),
757 })
758 .collect())
759 }
760
761 #[test]
762 fn pack_macros_expand_with_call_site_spans() {
763 let set = MacroSet::pack("fmd-math/pack/default").unwrap();
764 let src = r"a\minus b";
765 let toks = expand(lex(src), &set, src.len()).unwrap();
766 let minus = toks
767 .iter()
768 .find(|t| matches!(t.kind, TokKind::Char('-')))
769 .expect("expanded minus");
770 assert_eq!((minus.span.start, minus.span.end), (1, 7));
771 }
772
773 #[test]
774 fn inline_definition_with_arguments() {
775 let set = MacroSet::new();
776 let out = expand_str(r"\newcommand{\half}[1]{\frac{#1}{2}}\half{x}", &set).unwrap();
777 assert_eq!(out, r"\frac {x}{2}");
778 }
779
780 #[test]
781 fn arguments_keep_their_own_spans_and_bodies_take_the_call() {
782 let set = MacroSet::new();
783 let src = r"\newcommand{\half}[1]{\frac{#1}{2}}\half{x}";
784 let toks = expand(lex(src), &set, src.len()).unwrap();
785 let call_start = src.find(r"\half{x}").unwrap();
786 let x = toks
787 .iter()
788 .find(|t| matches!(t.kind, TokKind::Char('x')))
789 .unwrap();
790 assert_eq!(&src[x.span.start..x.span.end], "x");
791 let frac = toks
792 .iter()
793 .find(|t| matches!(t.kind, TokKind::ControlWord("frac")))
794 .unwrap();
795 assert_eq!(frac.span.start, call_start);
796 assert_eq!(frac.span.end, src.len());
797 }
798
799 #[test]
800 fn macros_reference_other_macros() {
801 let mut set = MacroSet::new();
802 set.define("dd", 0, r"\mathrm{d}").unwrap();
803 set.define("dx", 0, r"\dd x").unwrap();
804 let out = expand_str(r"\dx", &set).unwrap();
805 assert_eq!(out, r"\mathrm {d}x");
806 }
807
808 #[test]
809 fn recursion_is_refused_with_the_macro_named() {
810 let mut set = MacroSet::new();
811 set.define("loop", 0, r"a\loop").unwrap();
812 let err = expand_str(r"\loop", &set).unwrap_err();
813 assert!(err.to_string().contains("recursive macro: \\loop"), "{err}");
814 let mut set = MacroSet::new();
815 set.define("ping", 0, r"\pong").unwrap();
816 set.define("pong", 0, r"\ping").unwrap();
817 let err = expand_str(r"\ping", &set).unwrap_err();
818 assert!(err.to_string().contains("recursive macro"), "{err}");
819 }
820
821 #[test]
822 fn expansion_bombs_hit_the_budget() {
823 let mut set = MacroSet::new();
824 set.define("a", 0, "xx").unwrap();
825 for (prev, name) in [
826 ("a", "b"),
827 ("b", "c"),
828 ("c", "d"),
829 ("d", "e"),
830 ("e", "f"),
831 ("f", "g"),
832 ("g", "h"),
833 ("h", "i"),
834 ("i", "j"),
835 ("j", "k"),
836 ("k", "l"),
837 ("l", "m"),
838 ("m", "n"),
839 ("n", "o"),
840 ("o", "p"),
841 ("p", "q"),
842 ("q", "r"),
843 ] {
844 let body = format!("\\{prev}\\{prev}");
845 set.define(name, 0, &body).unwrap();
846 }
847 let err = expand_str(r"\r", &set).unwrap_err();
848 let msg = err.to_string();
849 assert!(
850 msg.contains("more than") || msg.contains("nests deeper"),
851 "{msg}"
852 );
853 }
854
855 #[test]
856 fn shadowing_rules_are_latexs() {
857 let set = MacroSet::new();
858 let err = expand_str(r"\newcommand{\x}{a}\newcommand{\x}{b}", &set).unwrap_err();
859 assert!(err.to_string().contains("already defined"), "{err}");
860 let err = expand_str(r"\renewcommand{\y}{a}", &set).unwrap_err();
861 assert!(err.to_string().contains("not defined"), "{err}");
862 let out = expand_str(r"\newcommand{\x}{a}\renewcommand{\x}{b}\x", &set).unwrap();
863 assert_eq!(out, "b");
864 }
865
866 #[test]
867 fn definition_faults_are_precise() {
868 let set = MacroSet::new();
869 for (src, needle) in [
870 (r"\newcommand", "ends before its macro name"),
871 (r"\newcommand{x}{a}", "expects a \\name"),
872 (r"\newcommand{\x}[0]{a}", "expected [1]..[9]"),
873 (r"\newcommand{\x}[2]{#3}", "uses #3 but declares 2"),
874 (r"\newcommand{\x}", "expected a {body} group"),
875 (r"\newcommand{\x}{a", "unclosed body group"),
876 ] {
877 let err = expand_str(src, &set).unwrap_err();
878 assert!(err.to_string().contains(needle), "{src}: {err}");
879 }
880 }
881
882 #[test]
883 fn undelimited_single_token_arguments() {
884 let set = MacroSet::new();
885 let out = expand_str(r"\newcommand{\sq}[1]{#1^2}\sq x", &set).unwrap();
886 assert_eq!(out, "x^2");
887 }
888
889 #[test]
890 fn canonical_bytes_are_deterministic_and_content_sensitive() {
891 let mut a = MacroSet::new();
892 a.define("dd", 0, r"\mathrm{d}").unwrap();
893 a.define("half", 1, r"\frac{#1}{2}").unwrap();
894 let mut b = MacroSet::new();
895 b.define("half", 1, r"\frac{#1}{2}").unwrap();
896 b.define("dd", 0, r"\mathrm{d}").unwrap();
897 assert_eq!(a.canonical_bytes(), b.canonical_bytes());
898 let mut c = MacroSet::new();
899 c.define("dd", 0, r"\mathrm{D}").unwrap();
900 c.define("half", 1, r"\frac{#1}{2}").unwrap();
901 assert_ne!(a.canonical_bytes(), c.canonical_bytes());
902 }
903
904 #[test]
905 fn define_validation_is_precise() {
906 let mut set = MacroSet::new();
907 assert!(set.define("", 0, "x").is_err());
908 assert!(set.define("bad name", 0, "x").is_err());
909 assert!(set.define("x", 10, "y").is_err());
910 assert!(set.define("x", 1, "#2").is_err());
911 assert!(set.define("x", 0, "{unclosed").is_err());
912 assert!(set.define("x", 0, "}stray").is_err());
913 assert!(set.define("ok", 2, r"\frac{#1}{#2}").is_ok());
914 }
915
916 #[test]
917 fn packs_exist_by_content_id_and_name() {
918 for id in [
919 "fmd-math/pack/default",
920 "default",
921 "fmd-math/pack/basic",
922 "basic",
923 "fmd-math/pack/empty",
924 "empty",
925 ] {
926 assert!(MacroSet::pack(id).is_some(), "{id}");
927 }
928 assert!(MacroSet::pack("nonexistent").is_none());
929 assert_eq!(MacroSet::pack("default").unwrap().len(), 1);
930 assert!(MacroSet::pack("empty").unwrap().is_empty());
931 }
932
933 #[test]
934 fn nested_parameterized_macros_receive_substituted_arguments() {
935 let mut set = MacroSet::new();
936 set.define("ratio", 2, r"\frac{#1}{#2}").unwrap();
937 set.define("inverse", 2, r"\ratio{#2}{#1}").unwrap();
938 set.define("twice", 1, r"\inverse{2}{#1}+\inverse{2}{#1}")
939 .unwrap();
940 assert_eq!(
941 expand_str(r"\twice{x+y}", &set).unwrap(),
942 r"\frac {x+y}{2}+\frac {x+y}{2}",
943 );
944 }
945
946 #[test]
947 fn parameter_slots_are_not_nested_argument_boundaries() {
948 let mut set = MacroSet::new();
949 set.define("pair", 2, "#1+#2").unwrap();
950 set.define("apply", 2, "#1{#2}").unwrap();
951 assert_eq!(expand_str(r"\apply{\pair{x}}{y}", &set).unwrap(), "x+y");
952 set.define("identity", 1, "#1").unwrap();
953 assert_eq!(expand_str(r"\apply{\identity}{x}", &set).unwrap(), "x");
954 }
955
956 #[test]
957 fn nested_expansion_preserves_literal_spans_and_rebases_generated_tokens() {
958 let mut set = MacroSet::new();
959 set.define("ratio", 2, r"\frac{#1}{#2}").unwrap();
960 set.define("half", 1, r"\ratio{#1}{2}").unwrap();
961 let src = r"a+\half{中}";
962 let call_start = src.find(r"\half").unwrap();
963 let tokens = expand(lex(src), &set, src.len()).unwrap();
964 for token in &tokens {
965 assert!(token.span.start <= token.span.end && token.span.end <= src.len());
966 match token.kind {
967 TokKind::Char('中') => assert_eq!(&src[token.span.start..token.span.end], "中"),
968 TokKind::Char('2') | TokKind::ControlWord("frac") => {
969 assert_eq!((token.span.start, token.span.end), (call_start, src.len()));
970 }
971 _ => {}
972 }
973 }
974 assert!(
975 tokens
976 .iter()
977 .any(|token| matches!(token.kind, TokKind::Char('中')))
978 );
979 }
980
981 #[test]
982 fn empty_output_fanout_is_bounded_too() {
983 let mut set = MacroSet::new();
984 set.define("a", 0, "").unwrap();
985 for (previous, name) in [
986 ("a", "b"),
987 ("b", "c"),
988 ("c", "d"),
989 ("d", "e"),
990 ("e", "f"),
991 ("f", "g"),
992 ("g", "h"),
993 ("h", "i"),
994 ("i", "j"),
995 ("j", "k"),
996 ("k", "l"),
997 ("l", "m"),
998 ("m", "n"),
999 ("n", "o"),
1000 ("o", "p"),
1001 ("p", "q"),
1002 ("q", "r"),
1003 ] {
1004 set.define(name, 0, &format!("\\{previous}\\{previous}"))
1005 .unwrap();
1006 }
1007 let error = expand_str(r"\r", &set).unwrap_err();
1008 assert!(error.to_string().contains("token work units"), "{error}");
1009 }
1010
1011 #[test]
1012 fn discarded_intermediate_replacements_still_consume_budget() {
1013 let mut set = MacroSet::new();
1014 set.define("discard", 1, "").unwrap();
1015 set.define("large", 1, r"\discard{#1#1#1#1#1#1#1#1#1}")
1016 .unwrap();
1017 let source = format!("\\large{{{}}}", "x".repeat(8192));
1018 let error = expand_str(&source, &set).unwrap_err();
1019 assert!(error.to_string().contains("token work units"), "{error}");
1020 }
1021
1022 #[test]
1023 fn composed_recursive_arguments_remain_rejected() {
1024 let mut set = MacroSet::new();
1025 set.define("identity", 1, "#1").unwrap();
1026 let error = expand_str(r"\identity{\identity{x}}", &set).unwrap_err();
1027 assert!(error.to_string().contains("recursive macro"), "{error}");
1028 }
1029
1030 #[test]
1031 fn missing_argument_cannot_consume_a_closing_group() {
1032 let mut set = MacroSet::new();
1033 set.define("identity", 1, "#1").unwrap();
1034 let error = expand_str(r"{\identity}", &set).unwrap_err();
1035 assert!(error.to_string().contains("missing argument #1"), "{error}");
1036 }
1037
1038 #[test]
1039 fn optional_defaults_and_explicit_overrides_compose_with_required_arguments() {
1040 let set = MacroSet::new();
1041 let source = r"\newcommand{\power}[2][2]{#2^{#1}}\power{x}+\power[3]{y}";
1042 assert_eq!(expand_str(source, &set).unwrap(), "x^{2}+y^{3}");
1043 }
1044
1045 #[test]
1046 fn explicit_empty_optional_argument_does_not_select_the_default() {
1047 let set = MacroSet::new();
1048 let source = r"\newcommand{\join}[2][d]{#1#2}\join[]{x}+\join{y}";
1049 assert_eq!(expand_str(source, &set).unwrap(), "x+dy");
1050 let source = r"\newcommand{\empty}[1][]{#1}\empty+\empty[z]";
1051 assert_eq!(expand_str(source, &set).unwrap(), "+z");
1052 }
1053
1054 #[test]
1055 fn optional_brackets_respect_braced_and_escaped_closers() {
1056 let set = MacroSet::new();
1057 assert_eq!(
1058 expand_str(r"\newcommand{\pick}[1][{]}]{#1}\pick", &set).unwrap(),
1059 "{]}",
1060 );
1061 assert_eq!(
1062 expand_str(r"\newcommand{\pick}[1][x]{#1}\pick[{]}]", &set).unwrap(),
1063 "{]}",
1064 );
1065 assert_eq!(
1066 expand_str(r"\newcommand{\pick}[1][\]]{#1}\pick", &set).unwrap(),
1067 r"\]",
1068 );
1069 }
1070
1071 #[test]
1072 fn defaults_may_invoke_macros_and_nested_calls_may_override_them() {
1073 let set = MacroSet::new();
1074 let source = concat!(
1075 r"\newcommand{\denom}{2}",
1076 r"\newcommand{\ratio}[2][\denom]{\frac{#2}{#1}}",
1077 r"\newcommand{\third}[1]{\ratio[3]{#1}}",
1078 r"\ratio{x}+\third{y}",
1079 );
1080 assert_eq!(
1081 expand_str(source, &set).unwrap(),
1082 r"\frac {x}{2}+\frac {y}{3}"
1083 );
1084 }
1085
1086 #[test]
1087 fn renewcommand_replaces_and_can_remove_optional_defaults() {
1088 let set = MacroSet::new();
1089 let source = concat!(
1090 r"\newcommand{\pick}[1][a]{#1}\pick+",
1091 r"\renewcommand{\pick}[1][b]{#1}\pick+",
1092 r"\renewcommand{\pick}[1]{#1}\pick{c}",
1093 );
1094 assert_eq!(expand_str(source, &set).unwrap(), "a+b+c");
1095 }
1096
1097 #[test]
1098 fn default_tokens_take_call_spans_but_explicit_optional_tokens_keep_their_source() {
1099 let set = MacroSet::new();
1100 let prefix = r"\newcommand{\pick}[1][z]{#1}";
1101 let source = format!("{prefix}\\pick");
1102 let tokens = expand(lex(&source), &set, source.len()).unwrap();
1103 assert_eq!(tokens.len(), 1);
1104 assert_eq!(
1105 (tokens[0].span.start, tokens[0].span.end),
1106 (prefix.len(), source.len())
1107 );
1108 let source = format!("{prefix}\\pick[中]");
1109 let tokens = expand(lex(&source), &set, source.len()).unwrap();
1110 assert_eq!(tokens.len(), 1);
1111 assert_eq!(&source[tokens[0].span.start..tokens[0].span.end], "中");
1112 }
1113
1114 #[test]
1115 fn malformed_optional_arguments_fail_without_stealing_outer_delimiters() {
1116 let set = MacroSet::new();
1117 for (source, message) in [
1118 (r"\newcommand{\pick}[1][abc", "unclosed optional argument"),
1119 (r"\newcommand{\pick}[1][#1]{#1}", "uses #1 but declares 0"),
1120 (
1121 r"\newcommand{\pick}[1][x]{#1}\pick[a",
1122 "unclosed optional argument",
1123 ),
1124 (
1125 r"\newcommand{\pick}[1][x]{#1}{\pick[a}",
1126 "surrounding group",
1127 ),
1128 (
1129 r"\newcommand{\pick}[2][x]{#2}\pick[y]",
1130 "input ends before #2",
1131 ),
1132 ] {
1133 let error = expand_str(source, &set).unwrap_err();
1134 assert!(error.to_string().contains(message), "{source}: {error}");
1135 }
1136 }
1137
1138 #[test]
1139 fn optional_defaults_do_not_bypass_recursion_or_work_limits() {
1140 let set = MacroSet::new();
1141 let error =
1142 expand_str(r"\newcommand{\selfref}[1][\selfref]{#1}\selfref", &set).unwrap_err();
1143 assert!(error.to_string().contains("recursive macro"), "{error}");
1144 let source = format!(
1145 "\\newcommand{{\\large}}[1][{}]{{#1#1#1#1#1#1#1#1#1}}\\large",
1146 "x".repeat(8192),
1147 );
1148 let error = expand_str(&source, &set).unwrap_err();
1149 assert!(error.to_string().contains("token work units"), "{error}");
1150 }
1151
1152 #[test]
1153 fn empty_parameter_substitution_still_charges_body_traversal() {
1154 let mut set = MacroSet::new();
1155 set.define("erase", 1, &"#1".repeat(1024)).unwrap();
1156 let source = r"\erase{}".repeat(100);
1157 let error = expand_str(&source, &set).unwrap_err();
1158 assert!(error.to_string().contains("token work units"), "{error}");
1159 }
1160
1161 #[test]
1162 fn optional_macros_reach_the_real_math_parser_and_mathml_renderer() {
1163 let source = concat!(
1164 r"\newcommand{\ratio}[2][2]{\frac{#2}{#1}}",
1165 r"\newcommand{\third}[1]{\ratio[3]{#1}}",
1166 r"\ratio{x}+\third{y}",
1167 );
1168 let actual = crate::parse(source).unwrap();
1169 let expected = crate::parse(r"\frac{x}{2}+\frac{y}{3}").unwrap();
1170 for display in [false, true] {
1171 let xml = crate::to_mathml(&actual, display);
1172 assert_eq!(xml, crate::to_mathml(&expected, display));
1173 crate::mathml_well_formed(&xml).unwrap();
1174 }
1175 }
1176}