texlang_stdlib/
def.rs

1//! User-defined macros (`\def` and friends)
2
3use crate::prefix;
4use texcraft_stdext::algorithms::substringsearch::Matcher;
5use texcraft_stdext::collections::groupingmap;
6use texcraft_stdext::collections::nevec::Nevec;
7use texcraft_stdext::nevec;
8use texlang::parse::Command;
9use texlang::traits::*;
10use texlang::*;
11
12pub const DEF_DOC: &str = "Define a custom macro";
13
14/// Get the `\def` command.
15pub fn get_def<S: HasComponent<prefix::Component>>() -> command::BuiltIn<S> {
16    command::BuiltIn::new_execution(def_primitive_fn).with_tag(def_tag())
17}
18
19/// Get the `\gdef` command.
20pub fn get_gdef<S: HasComponent<prefix::Component>>() -> command::BuiltIn<S> {
21    command::BuiltIn::new_execution(gdef_primitive_fn).with_tag(def_tag())
22}
23
24static DEF_TAG: command::StaticTag = command::StaticTag::new();
25
26pub fn def_tag() -> command::Tag {
27    DEF_TAG.get()
28}
29
30fn def_primitive_fn<S: HasComponent<prefix::Component>>(
31    def_token: token::Token,
32    input: &mut vm::ExecutionInput<S>,
33) -> Result<(), Box<error::Error>> {
34    parse_and_set_macro(def_token, input, false)
35}
36
37fn gdef_primitive_fn<S: HasComponent<prefix::Component>>(
38    def_token: token::Token,
39    input: &mut vm::ExecutionInput<S>,
40) -> Result<(), Box<error::Error>> {
41    parse_and_set_macro(def_token, input, true)
42}
43
44fn parse_and_set_macro<S: HasComponent<prefix::Component>>(
45    _: token::Token,
46    input: &mut vm::ExecutionInput<S>,
47    set_globally_override: bool,
48) -> Result<(), Box<error::Error>> {
49    let mut scope = input.state_mut().component_mut().read_and_reset_global();
50    if set_globally_override {
51        scope = groupingmap::Scope::Global;
52    }
53    let Command::ControlSequence(name) = Command::parse(input)?;
54    let (prefix, raw_parameters, replacement_end_token) =
55        parse_prefix_and_parameters(input.unexpanded())?;
56    let parameters: Vec<texmacro::Parameter> = raw_parameters
57        .into_iter()
58        .map(|a| match a {
59            RawParameter::Undelimited => texmacro::Parameter::Undelimited,
60            RawParameter::Delimited(vec) => texmacro::Parameter::Delimited(Matcher::new(vec)),
61        })
62        .collect();
63    let mut replacement =
64        parse_replacement_text(input.unexpanded(), replacement_end_token, parameters.len())?;
65    for r in replacement.iter_mut() {
66        if let texmacro::Replacement::Tokens(tokens) = r {
67            tokens.reverse();
68        }
69    }
70    let user_defined_macro = texmacro::Macro::new(prefix, parameters, replacement);
71    input
72        .commands_map_mut()
73        .insert_macro(name, user_defined_macro, scope);
74    Ok(())
75}
76
77enum RawParameter {
78    Undelimited,
79    Delimited(Nevec<token::Token>),
80}
81
82impl RawParameter {
83    fn push(&mut self, t: token::Token) {
84        match self {
85            RawParameter::Undelimited => {
86                *self = RawParameter::Delimited(nevec![t]);
87            }
88            RawParameter::Delimited(vec) => {
89                vec.push(t);
90            }
91        }
92    }
93}
94
95fn char_to_parameter_index(c: char) -> Option<usize> {
96    match c {
97        '1' => Some(0),
98        '2' => Some(1),
99        '3' => Some(2),
100        '4' => Some(3),
101        '5' => Some(4),
102        '6' => Some(5),
103        '7' => Some(6),
104        '8' => Some(7),
105        '9' => Some(8),
106        _ => None,
107    }
108}
109
110fn parse_prefix_and_parameters<S: TexlangState>(
111    input: &mut vm::UnexpandedStream<S>,
112) -> command::Result<(Vec<token::Token>, Vec<RawParameter>, Option<token::Token>)> {
113    let mut prefix = Vec::new();
114    let mut parameters = Vec::new();
115    let mut replacement_end_token = None;
116
117    while let Some(token) = input.next()? {
118        match token.value() {
119            token::Value::BeginGroup(_) => {
120                return Ok((prefix, parameters, replacement_end_token));
121            }
122            token::Value::EndGroup(_) => {
123                return Err(error::SimpleTokenError::new(
124                    input.vm(),
125                    token,
126                    "unexpected end group token while parsing the parameter of a macro definition",
127                )
128                .into());
129            }
130            token::Value::Parameter(_) => {
131                let parameter_token = match input.next()? {
132                    None => {
133                        return Err(error::SimpleEndOfInputError::new(input.vm(),
134                "unexpected end of input while reading the token after a parameter token").into());
135                        // TODO .add_note("a parameter token must be followed by a single digit number, another parameter token, or a closing brace {.")
136                    }
137                    Some(token) => token,
138                };
139                match parameter_token.value() {
140                    token::Value::BeginGroup(_) => {
141                        // In this case we end the group according to the special #{ rule
142                        replacement_end_token = Some(parameter_token);
143                        match parameters.last_mut() {
144                            None => {
145                                prefix.push(parameter_token);
146                            }
147                            Some(spec) => {
148                                spec.push(parameter_token);
149                            }
150                        }
151                        return Ok((prefix, parameters, replacement_end_token));
152                    }
153                    token::Value::ControlSequence(..) => {
154                        return Err(error::SimpleTokenError::new(
155                            input.vm(),
156                            parameter_token,
157                            "unexpected control sequence after a parameter token",
158                        )
159                        .into());
160                        // TODO "a parameter token must be followed by a single digit number, another parameter token, or a closing brace {.")
161                    }
162                    _ => {
163                        let c = parameter_token.char().unwrap();
164                        let parameter_index = match char_to_parameter_index(c) {
165                            None => {
166                                return Err(error::SimpleTokenError::new(
167                                    input.vm(),
168                                    parameter_token,
169                                    "unexpected character after a parameter token",
170                                )
171                                .into());
172                                // TODO .add_note("a parameter token must be followed by a single digit number, another parameter token, or a closing brace {.")
173                            }
174                            Some(n) => n,
175                        };
176                        if parameter_index != parameters.len() {
177                            return Err(error::SimpleTokenError::new(
178                                input.vm(),
179                                parameter_token,
180                                format!["unexpected parameter number {}", parameter_index + 1],
181                            )
182                            .into());
183                            // TODO format!["this macro has {} parameter(s) so far, so parameter number #{} was expected.",
184                            //TODO parameters.len(), parameters.len()+1
185                        }
186                        parameters.push(RawParameter::Undelimited);
187                    }
188                }
189            }
190            _ => match parameters.last_mut() {
191                None => {
192                    prefix.push(token);
193                }
194                Some(parameter) => {
195                    parameter.push(token);
196                }
197            },
198        }
199    }
200    Err(error::SimpleEndOfInputError::new(
201        input.vm(),
202        "unexpected end of input while reading the parameter text of a macro",
203    )
204    .into())
205    // TODO .add_note("the parameter text of a macro must end with a closing brace { or another token with catcode 1 (begin group)")
206}
207
208fn parse_replacement_text<S: TexlangState>(
209    input: &mut vm::UnexpandedStream<S>,
210    opt_final_token: Option<token::Token>,
211    num_parameters: usize,
212) -> command::Result<Vec<texmacro::Replacement>> {
213    // TODO: could we use a pool of vectors to avoid some of the allocations here?
214    let mut result = vec![];
215    let mut scope_depth = 0;
216    let push = |result: &mut Vec<texmacro::Replacement>, token| match result.last_mut() {
217        Some(texmacro::Replacement::Tokens(tokens)) => {
218            tokens.push(token);
219        }
220        _ => {
221            result.push(texmacro::Replacement::Tokens(vec![token]));
222        }
223    };
224
225    while let Some(token) = input.next()? {
226        match token.value() {
227            token::Value::BeginGroup(_) => {
228                scope_depth += 1;
229            }
230            token::Value::EndGroup(_) => {
231                if scope_depth == 0 {
232                    if let Some(final_token) = opt_final_token {
233                        push(&mut result, final_token);
234                    }
235                    return Ok(result);
236                }
237                scope_depth -= 1;
238            }
239            token::Value::Parameter(_) => {
240                let parameter_token = match input.next()? {
241                    None => {
242                        return Err(error::SimpleEndOfInputError::new(
243                            input.vm(),
244                            "unexpected end of input while reading a parameter number",
245                        )
246                        .into())
247                    }
248                    Some(token) => token,
249                };
250                let c = match parameter_token.value() {
251                    token::Value::ControlSequence(..) => {
252                        return Err(error::SimpleTokenError::new(
253                            input.vm(),
254                            parameter_token,
255                            "unexpected character while reading a parameter number",
256                        )
257                        .into());
258                        // TODO .add_note("expected a number between 1 and 9 inclusive")
259                    }
260                    token::Value::Parameter(_) => {
261                        push(&mut result, parameter_token);
262                        continue;
263                    }
264                    _ => parameter_token.char().unwrap(),
265                };
266
267                let parameter_index = match char_to_parameter_index(c) {
268                    None => {
269                        return Err(error::SimpleTokenError::new(
270                            input.vm(),
271                            parameter_token,
272                            "unexpected character while reading a parameter number",
273                        )
274                        .into());
275                        // TODO .add_note("expected a number between 1 and 9 inclusive")
276                    }
277                    Some(n) => n,
278                };
279                if parameter_index >= num_parameters {
280                    return Err(error::SimpleTokenError::new(
281                        input.vm(),
282                        parameter_token,
283                        "unexpected character while reading a parameter number",
284                    )
285                    .into());
286
287                    /* TODO
288                            var msg string
289                            switch numParams {
290                            case 0:
291                                msg = "no parameter token because this macro has 0 parameters"
292                            case 1:
293                                msg = "the number 1 because this macro has only 1 parameter"
294                            default:
295                                msg = fmt.Sprintf(
296                                    "a number between 1 and %[1]d inclusive because this macro has only %[1]d parameters",
297                                    numParams)
298                            }
299                            return nil, errors.NewUnexpectedTokenError(t, msg, "the number "+t.Value(), parsingArgumentTemplate)
300                    */
301                }
302                result.push(texmacro::Replacement::Parameter(parameter_index));
303                continue;
304            }
305            _ => {}
306        }
307
308        push(&mut result, token);
309    }
310
311    Err(error::SimpleEndOfInputError::new(
312        input.vm(),
313        "unexpected end of input while reading a parameter number",
314    )
315    .into())
316}
317
318#[cfg(test)]
319mod test {
320    use std::collections::HashMap;
321
322    use super::*;
323    use crate::testing::*;
324
325    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
326        HashMap::from([
327            ("def", get_def()),
328            ("gdef", get_gdef()),
329            ("global", prefix::get_global()),
330            ("assertGlobalIsFalse", prefix::get_assert_global_is_false()),
331        ])
332    }
333
334    test_suite![
335        expansion_equality_tests(
336            (def_parsed_successfully, "\\def\\A{abc}", ""),
337            (output_is_correct, "\\def\\A{abc}\\A", "abc"),
338            (output_twice, "\\def\\A{abc}\\A\\A", "abcabc"),
339            (parse_one_parameter, "\\def\\A#1{a-#1-b}", ""),
340            (one_undelimited_parameter, "\\def\\A#1{a-#1-b}\\A1", "a-1-b"),
341            (
342                one_undelimited_parameter_multiple_times,
343                "\\def\\A#1{#1 #1 #1}\\A1",
344                "1 1 1"
345            ),
346            (
347                one_undelimited_parameter_multiple_tokens,
348                "\\def\\A#1{a-#1-b}\\A{123}",
349                "a-123-b"
350            ),
351            (
352                two_undelimited_parameters,
353                "\\def\\A#1#2{#2-#1}\\A56",
354                "6-5"
355            ),
356            (
357                two_undelimited_parameters_multiple_token_inputs,
358                "\\def\\A#1#2{#2-#1}\\A{abc}{xyz}",
359                "xyz-abc"
360            ),
361            (
362                consume_prefix_correctly,
363                "\\def\\A fgh{567}\\A fghi",
364                "567i"
365            ),
366            (
367                one_undelimited_parameter_with_prefix,
368                "\\def\\A abc#1{y#1z}\\A abcdefg",
369                "ydzefg"
370            ),
371            (
372                one_undelimited_parameter_with_prefix_multiple_tokens,
373                "\\def\\A abc#1{y#1z}\\A abcdefg",
374                "ydzefg"
375            ),
376            (
377                one_delimited_parameter,
378                "\\def\\A #1xxx{y#1z}\\A abcxxx",
379                "yabcz"
380            ),
381            (
382                one_delimited_parameter_empty,
383                "\\def\\A #1xxx{y#1z}\\A xxx",
384                "yz"
385            ),
386            (
387                one_delimited_parameter_with_scope,
388                "\\def\\A #1xxx{#1}\\A abc{123xxx}xxx",
389                "abc{123xxx}"
390            ),
391            (
392                one_delimited_parameter_with_prefix,
393                "\\def\\A a#1c{x#1y}\\A abcdef",
394                "xbydef"
395            ),
396            (
397                two_delimited_parameters_with_prefix,
398                r"\def\A a#1c#2e{x#2y#1z}\A abcdef",
399                "xdybzf"
400            ),
401            (
402                one_delimited_parameter_grouped_value,
403                r"\def\A #1c{x#1y}\A {Hello}c",
404                "xHelloy"
405            ),
406            (
407                parameter_brace_special_case,
408                r"\def\A #{Mint says }\A{hello}",
409                "Mint says {hello}"
410            ),
411            (
412                grouping,
413                r"\def\A{Hello}\A{\def\A{World}\A}\A",
414                r"HelloWorldHello"
415            ),
416            (
417                grouping_global,
418                r"\def\A{Hello}\A{\global\def\A{World}\A}\A",
419                r"HelloWorldWorld"
420            ),
421            (
422                gdef,
423                r"\def\A{Hello}\A{\gdef\A{World}\A}\A",
424                r"HelloWorldWorld"
425            ),
426            (
427                gdef_global,
428                r"\def\A{Hello}\A{\global\gdef\A{World}\A}\A",
429                r"HelloWorldWorld"
430            ),
431            (
432                def_takes_global,
433                r"\global\def\A{Hello}\assertGlobalIsFalse",
434                r""
435            ),
436            (
437                gdef_takes_global,
438                r"\global\gdef\A{Hello}\assertGlobalIsFalse",
439                r""
440            ),
441            (
442                texbook_exercise_20_1,
443                r"\def\mustnt{I must not talk in class.}%
444          \def\five{\mustnt\mustnt\mustnt\mustnt\mustnt}%
445          \def\twenty{\five\five\five\five}%
446          \def\punishment{\twenty\twenty\twenty\twenty\twenty}%
447          \punishment",
448                "I must not talk in class.".repeat(100)
449            ),
450            (
451                texbook_exercise_20_2,
452                r"\def\a{\b}%
453          \def\b{A\def\a{B\def\a{C\def\a{\b}}}}%
454          \def\puzzle{\a\a\a\a\a}%
455          \puzzle",
456                "ABCAB"
457            ),
458            (
459                texbook_exercise_20_3_part_1,
460                "\\def\\row#1{(#1_1,\\ldots,#1_n)}\\row{\\bf x}",
461                "(\\bf x_1,\\ldots,\\bf x_n)"
462            ),
463            (
464                texbook_exercise_20_3_part_2,
465                "\\def\\row#1{(#1_1,\\ldots,#1_n)}\\row{{\\bf x}}",
466                "({\\bf x}_1,\\ldots,{\\bf x}_n)"
467            ),
468            (
469                texbook_exercise_20_4_part_1,
470                r#"\def\mustnt#1#2{I must not #1 in #2.}%
471           \def\five#1#2{\mustnt{#1}{#2}\mustnt{#1}{#2}\mustnt{#1}{#2}\mustnt{#1}{#2}\mustnt{#1}{#2}}%
472           \def\twenty#1#2{\five{#1}{#2}\five{#1}{#2}\five{#1}{#2}\five{#1}{#2}}%
473           \def\punishment#1#2{\twenty{#1}{#2}\twenty{#1}{#2}\twenty{#1}{#2}\twenty{#1}{#2}\twenty{#1}{#2}}%
474           \punishment{run}{the halls}"#,
475                "I must not run in the halls.".repeat(100)
476            ),
477            (
478                texbook_exercise_20_4_part_2,
479                r#"\def\mustnt{I must not \doit\ in \thatplace.}%
480           \def\five{\mustnt\mustnt\mustnt\mustnt\mustnt}%
481           \def\twenty{\five\five\five\five}%
482           \def\punishment#1#2{\def\doit{#1}\def\thatplace{#2}\twenty\twenty\twenty\twenty\twenty}%
483           \punishment{run}{the halls}"#,
484                r"I must not run\ in the halls.".repeat(100)
485            ),
486            (
487                texbook_exercise_20_5,
488                r"\def\a#1{\def\b##1{##1#1}}\a!\b{Hello}",
489                "Hello!"
490            ),
491            (
492                texbook_exercise_20_5_temp,
493                r"\def\b#1{#1!}\b{Hello}",
494                "Hello!"
495            ),
496            (
497                texbook_exercise_20_5_example_below,
498                "\\def\\a#1#{\\hbox to #1}\\a3pt{x}",
499                "\\hbox to 3pt{x}"
500            ),
501            (
502                texbook_exercise_20_6,
503                r"\def\b#1{And #1, World!}\def\a#{\b}\a{Hello}",
504                "And Hello, World!"
505            ),
506        ),
507        serde_tests((
508            serde_basic,
509            r"\def\helloWorld{Hello World} ",
510            r"\helloWorld"
511        ),),
512        failure_tests(
513            (end_of_input_scanning_target, "\\def"),
514            (end_of_input_scanning_argument_text, "\\def\\A"),
515            (end_of_input_scanning_replacement, "\\def\\A{"),
516            (end_of_input_scanning_nested_replacement, "\\def\\A{{}"),
517            (end_of_input_reading_parameter_number, "\\def\\A#"),
518            (end_of_input_scanning_argument, "\\def\\A#1{} \\A"),
519            (
520                end_of_input_reading_value_for_parameter,
521                "\\def\\A#1{} \\A{this {is parameter 1 but it never ends}"
522            ),
523            (end_of_input_reading_prefix, "\\def\\A abc{} \\A ab"),
524            (
525                end_of_input_reading_delimiter,
526                "\\def\\A #1abc{} \\A {first parameter}ab"
527            ),
528            (unexpected_token_target, "\\def a"),
529            (unexpected_token_argument, "\\def\\A }"),
530            (unexpected_token_parameter_number, "\\def\\A #a}"),
531            (unexpected_parameter_number_in_argument, "\\def\\A #2{}"),
532            (unexpected_parameter_token_in_replacement, "\\def\\A #1{#a}"),
533            (unexpected_parameter_number_in_replacement, "\\def\\A {#2}"),
534            (
535                unexpected_parameter_number_in_replacement_2,
536                "\\def\\A #1{#2}"
537            ),
538            (unexpected_token_in_prefix, "\\def\\A abc{d} \\A abd"),
539        ),
540    ];
541
542    /* TODO: renable using \catcode
543    fn setup_texbook_exercise_20_7<S: TexState<S>>(s: &mut S) {
544        initial_commands(s);
545        s.cat_code_map_mut().insert(
546            '[' as u32,
547            catcode::RawCatCode::Regular(catcode::CatCode::BeginGroup),
548        );
549        s.cat_code_map_mut().insert(
550            ']' as u32,
551            catcode::RawCatCode::Regular(catcode::CatCode::EndGroup),
552        );
553        s.cat_code_map_mut().insert(
554            '!' as u32,
555            catcode::RawCatCode::Regular(catcode::CatCode::texmacro::Parameter),
556        );
557    }
558
559    expansion_test![
560        texbook_exercise_20_7,
561        "\\def\\!!1#2![{!#]#!!2}\\! x{[y]][z}",
562        "{#]![y][z}",
563        setup_texbook_exercise_20_7
564    ];
565    */
566}