texlang_stdlib/
lib.rs

1//! # The Texlang standard library
2//!
3//! This module contains implementations of TeX primitives for Texlang.
4
5extern crate texcraft_stdext;
6extern crate texlang;
7
8use std::collections::HashMap;
9
10use texlang::command;
11use texlang::prelude as txl;
12use texlang::token;
13use texlang::traits::*;
14use texlang::types;
15use texlang::types::CatCode;
16use texlang::vm;
17use texlang::vm::implement_has_component;
18use texlang_common::HasFileSystem;
19use texlang_common::HasLogging;
20use texlang_common::HasTerminalIn;
21
22pub mod alias;
23pub mod alloc;
24pub mod chardef;
25pub mod codes;
26pub mod conditional;
27pub mod def;
28pub mod endlinechar;
29pub mod errormode;
30pub mod expansion;
31pub mod input;
32pub mod job;
33pub mod math;
34pub mod mathchardef;
35pub mod prefix;
36pub mod registers;
37pub mod repl;
38pub mod script;
39pub mod sleep;
40pub mod texcraft;
41pub mod the;
42pub mod time;
43pub mod tracingmacros;
44
45/// A state struct that is compatible with every primitive in the Texlang standard library.
46#[derive(Default)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct StdLibState {
49    pub alloc: alloc::Component,
50    pub codes_cat_code: codes::Component<CatCode>,
51    pub codes_math_code: codes::Component<types::MathCode>,
52    pub conditional: conditional::Component,
53    pub end_line_char: endlinechar::Component,
54    pub error_mode: errormode::Component,
55    pub input: input::Component<16>,
56    pub job: job::Component,
57    pub prefix: prefix::Component,
58    pub registers_i32: registers::Component<i32, 32768>,
59    pub registers_scaled: registers::Component<core::Scaled, 32768>,
60    pub registers_glue: registers::Component<core::Glue, 32768>,
61    pub registers_token_list: registers::Component<Vec<token::Token>, 256>,
62    pub repl: repl::Component,
63    pub script: script::Component,
64    #[cfg(test)]
65    pub testing: texlang_testing::TestingComponent,
66    pub time: time::Component,
67    pub tracing_macros: tracingmacros::Component,
68}
69
70impl TexlangState for StdLibState {
71    #[inline]
72    fn cat_code(&self, c: char) -> texlang::types::CatCode {
73        codes::cat_code(self, c)
74    }
75
76    #[inline]
77    fn end_line_char(&self) -> Option<char> {
78        endlinechar::end_line_char(self)
79    }
80
81    #[inline]
82    fn post_macro_expansion_hook(
83        token: texlang::token::Token,
84        input: &vm::ExpansionInput<Self>,
85        tex_macro: &texlang::texmacro::Macro,
86        arguments: &[&[texlang::token::Token]],
87        reversed_expansion: &[texlang::token::Token],
88    ) {
89        tracingmacros::hook(token, input, tex_macro, arguments, reversed_expansion)
90    }
91
92    #[inline]
93    fn expansion_override_hook(
94        token: texlang::token::Token,
95        input: &mut vm::ExpansionInput<Self>,
96        tag: Option<texlang::command::Tag>,
97    ) -> txl::Result<Option<texlang::token::Token>> {
98        expansion::noexpand_hook(token, input, tag)
99    }
100
101    #[inline]
102    fn variable_assignment_scope_hook(
103        state: &mut Self,
104    ) -> texcraft_stdext::collections::groupingmap::Scope {
105        prefix::variable_assignment_scope_hook(state)
106    }
107
108    fn recoverable_error_hook(
109        &self,
110        recoverable_error: texlang::error::TracedTexError,
111    ) -> Result<(), Box<dyn texlang::error::TexError>> {
112        errormode::recoverable_error_hook(self, recoverable_error)
113    }
114}
115
116impl the::TheCompatible for StdLibState {}
117
118implement_has_component![StdLibState{
119    alloc: alloc::Component,
120    codes_cat_code: codes::Component<CatCode>,
121    codes_math_code: codes::Component<types::MathCode>,
122    conditional: conditional::Component,
123    end_line_char: endlinechar::Component,
124    error_mode: errormode::Component,
125    input: input::Component<16>,
126    job: job::Component,
127    prefix: prefix::Component,
128    registers_i32: registers::Component<i32, 32768>,
129    registers_scaled: registers::Component<core::Scaled, 32768>,
130    registers_glue: registers::Component<core::Glue, 32768>,
131    registers_token_list: registers::Component<Vec<token::Token>, 256>,
132    repl: repl::Component,
133    script: script::Component,
134    time: time::Component,
135    tracing_macros: tracingmacros::Component,
136}];
137
138impl texlang_common::HasLogging for StdLibState {}
139impl texlang_common::HasFileSystem for StdLibState {}
140impl texlang_common::HasTerminalIn for StdLibState {
141    fn terminal_in(&self) -> std::rc::Rc<std::cell::RefCell<dyn texlang_common::TerminalIn>> {
142        self.error_mode.terminal_in()
143    }
144}
145
146pub fn built_in_commands<S>() -> HashMap<&'static str, command::BuiltIn<S>>
147where
148    S: TexlangState
149        + HasFileSystem
150        + HasTerminalIn
151        + HasLogging
152        + HasComponent<alloc::Component>
153        + HasComponent<codes::Component<CatCode>>
154        + HasComponent<codes::Component<types::MathCode>>
155        + HasComponent<conditional::Component>
156        + the::TheCompatible
157        + HasComponent<endlinechar::Component>
158        + HasComponent<errormode::Component>
159        + HasComponent<input::Component<16>>
160        + HasComponent<job::Component>
161        + HasComponent<prefix::Component>
162        + HasComponent<registers::Component<i32, 32768>>
163        + HasComponent<registers::Component<core::Scaled, 32768>>
164        + HasComponent<registers::Component<core::Glue, 32768>>
165        + HasComponent<registers::Component<Vec<token::Token>, 256>>
166        + HasComponent<repl::Component>
167        + HasComponent<script::Component>
168        + HasComponent<time::Component>
169        + HasComponent<tracingmacros::Component>,
170{
171    HashMap::from([
172        ("advance", math::get_advance()),
173        //
174        ("batchmode", errormode::get_batchmode()),
175        //
176        ("catcode", codes::get_catcode()),
177        ("closein", input::get_closein()),
178        ("chardef", chardef::get_chardef()),
179        ("count", registers::get_count()),
180        ("countdef", registers::get_countdef()),
181        //
182        ("day", time::get_day()),
183        ("def", def::get_def()),
184        ("dimen", registers::get_dimen()),
185        ("divide", math::get_divide()),
186        ("dumpFormat", job::get_dumpformat()),
187        ("dumpValidate", job::get_dumpvalidate()),
188        //
189        ("else", conditional::get_else()),
190        ("endinput", input::get_endinput()),
191        ("endlinechar", endlinechar::get_endlinechar()),
192        ("errorstopmode", errormode::get_errorstopmode()),
193        ("expandafter", expansion::get_expandafter_optimized()),
194        //
195        ("fi", conditional::get_fi()),
196        //
197        ("gdef", def::get_gdef()),
198        ("global", prefix::get_global()),
199        ("globaldefs", prefix::get_globaldefs()),
200        //
201        ("ifcase", conditional::get_ifcase()),
202        ("ifeof", input::get_ifeof()),
203        ("iffalse", conditional::get_iffalse()),
204        ("ifnum", conditional::get_ifnum()),
205        ("ifodd", conditional::get_ifodd()),
206        ("iftrue", conditional::get_iftrue()),
207        ("input", input::get_input()),
208        //
209        ("jobname", job::get_jobname()),
210        //
211        ("let", alias::get_let()),
212        ("long", prefix::get_long()),
213        //
214        ("mathchardef", mathchardef::get_mathchardef()),
215        ("mathcode", codes::get_mathcode()),
216        ("month", time::get_month()),
217        ("multiply", math::get_multiply()),
218        //
219        ("newInt", alloc::get_newint()),
220        (
221            "newInt_getter_provider_\u{0}",
222            alloc::get_newint_getter_provider(),
223        ),
224        ("newIntArray", alloc::get_newintarray()),
225        (
226            "newIntArray_getter_provider_\u{0}",
227            alloc::get_newintarray_getter_provider(),
228        ),
229        ("noexpand", expansion::get_noexpand()),
230        ("nonstopmode", errormode::get_nonstopmode()),
231        //
232        ("or", conditional::get_or()),
233        ("openin", input::get_openin()),
234        ("outer", prefix::get_outer()),
235        //
236        ("read", input::get_read()),
237        ("relax", expansion::get_relax()),
238        //
239        ("scrollmode", errormode::get_scrollmode()),
240        ("skip", registers::get_skip()),
241        ("sleep", sleep::get_sleep()),
242        //
243        ("the", the::get_the()),
244        ("time", time::get_time()),
245        ("toks", registers::get_toks()),
246        ("toksdef", registers::get_toksdef()),
247        ("tracingmacros", tracingmacros::get_tracingmacros()),
248        //
249        ("year", time::get_year()),
250    ])
251}
252
253impl HasDefaultBuiltInCommands for StdLibState {
254    fn default_built_in_commands() -> HashMap<&'static str, command::BuiltIn<Self>> {
255        built_in_commands()
256    }
257}
258
259/// A TeX snippet that exercises some error case in the standard library.
260pub struct ErrorCase {
261    pub description: &'static str,
262    pub source_code: &'static str,
263}
264
265impl ErrorCase {
266    /// Returns a vector of TeX snippets that exercise all error paths in Texlang.
267    ///
268    /// TODO: instead of duplicating these here, we should have a way of dumping all
269    /// error cases in the regular unit tests into a file.
270    pub fn all_error_cases() -> Vec<ErrorCase> {
271        let mut cases = vec![];
272        for (description, source_code) in vec![
273            (r"\toks starts with a letter token", r"\toks 0 = a"),
274            (
275                r"\toks starts with a non-variable command",
276                r"\toks 0 = \def",
277            ),
278            (
279                r"\toks starts is a variable command of the wrong type",
280                r"\toks 0 = \count 0",
281            ),
282            (
283                r"end of input while scanning token list",
284                r"\toks 0 = {  no closing brace",
285            ),
286            (r"assign number from \toks", r"\count 0 = \toks 0"),
287            (r"end of input right after \toks", r"\toks 0"),
288            (r"\count is out of bounds (negative)", r"\count -200"),
289            (r"\count is out of bounds (positive)", r"\count 2000000"),
290            ("file does not exist", r"\input doesNotExist"),
291            ("end of input after \\global", r"\global"),
292            ("can't be prefixed by \\global", r"\global \sleep"),
293            ("can't be prefixed by \\global (character)", r"\global a"),
294            ("can't be prefixed by \\long", r"\long \let \a = \def"),
295            ("can't be prefixed by \\outer", r"\outer \let \a = \def"),
296            ("bad rhs in assignment", r"\year = X"),
297            ("invalid variable (undefined)", r"\advance \undefined by 4"),
298            (
299                "invalid variable (not a variable command)",
300                r"\advance \def by 4",
301            ),
302            ("invalid variable (character token)", r"\advance a by 4"),
303            ("invalid variable (eof)", r"\advance"),
304            ("invalid relation", r"\ifnum 3 z 4"),
305            ("malformed by keyword", r"\advance \year bg"),
306            ("undefined control sequence", r"\elephant"),
307            ("invalid character", "\u{7F}"),
308            ("empty control sequence", r"\"),
309            ("invalid end of group", r"}"),
310            ("invalid start of number", r"\count X"),
311            ("invalid start of number (eof)", r"\count"),
312            ("invalid start of number (not a variable)", r"\count \def"),
313            (
314                "case negative number to positive (from constant)",
315                r"\count -1",
316            ),
317            (
318                "cast negative number to positive (from variable)",
319                r"\count 0 = 1 \count - \count 0",
320            ),
321            (
322                "read positive number from negative variable value",
323                r"\count 0 = -1 \count \count 0",
324            ),
325            ("invalid character", r"\count `\def"),
326            ("invalid character (eof)", r"\count `"),
327            ("invalid octal digit", r"\count '9"),
328            ("invalid octal digit (eof)", r"\count '"),
329            ("invalid hexadecimal digit", "\\count \"Z"),
330            ("invalid hexadecimal digit (eof)", "\\count \""),
331            (
332                "decimal number too big (radix)",
333                r"\count 1000000000000000000000",
334            ),
335            (
336                "decimal number too big (sum)",
337                r"\count 18446744073709551617",
338            ),
339            ("octal number too big", r"\count '7777777777777777777777"),
340            (
341                "hexadecimal number too big",
342                "\\count \"AAAAAAAAAAAAAAAAAAAAAA",
343            ),
344            ("number with letter catcode", r"\catcode `1 = 11 \count 1"),
345            (
346                "non-arithmetic argument to math command",
347                r"\advance \catcode 1 by 3",
348            ),
349            /*
350            ("", r""),
351            ("", r""),
352            ("", r""),
353            ("", r""),
354            ("", r""),
355            ("", r""),
356            ("", r""),
357            ("", r""),
358            ("", r""), */
359            ("category code out of bounds", r"\catcode 0 = 17"),
360            ("invalid command target", r"\let a = \year"),
361            ("invalid command target (eof)", r"\let"),
362        ] {
363            cases.push(ErrorCase {
364                description,
365                source_code,
366            })
367        }
368        cases
369    }
370}
371
372#[cfg(test)]
373impl HasComponent<texlang_testing::TestingComponent> for StdLibState {
374    fn component(&self) -> &texlang_testing::TestingComponent {
375        &self.testing
376    }
377
378    fn component_mut(&mut self) -> &mut texlang_testing::TestingComponent {
379        &mut self.testing
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use texlang_testing::*;
387
388    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<StdLibState>> {
389        StdLibState::default_built_in_commands()
390    }
391
392    type State = StdLibState;
393
394    test_suite![
395        expansion_equality_tests(
396            (
397                relation_before_spaces,
398                r"\countdef\A1 \A=2 \def\cmp#1{\ifnum#1 <3}\cmp{\A}Pass\else Fail \fi",
399                r"Pass"
400            ),
401            (
402                overwrite_else,
403                r"\def\else{}\ifodd 2 \else should be skipped \fi",
404                r""
405            ),
406            (
407                math_and_active_char,
408                r"-\catcode`\A=13 \countdef A5 \countdef ~6 ~=7 A=8 \advance~byA \the~",
409                r"- 15",
410            ),
411            /*
412                        s.cat_code_map_mut().insert(
413                '[' as u32,
414                catcode::RawCatCode::Regular(catcode::CatCode::BeginGroup),
415            );
416            s.cat_code_map_mut().insert(
417                ']' as u32,
418                catcode::RawCatCode::Regular(catcode::CatCode::EndGroup),
419            );
420            s.cat_code_map_mut().insert(
421                '!' as u32,
422                catcode::RawCatCode::Regular(catcode::CatCode::texmacro::Parameter),
423                 */
424            (
425                texbook_exercise_20_7,
426                r"\catcode`\[=1 \catcode`\]=2 \catcode`\!=6 \def\!!1#2![{!#]#!!2}\! x{[y]][z}",
427                r"\catcode`\[=1 \catcode`\]=2 \catcode`\!=6 {#]![y][z}",
428            ),
429            (
430                variable_assignment_space_before_equal,
431                r"\def\assign#1{#1   =    20\relax}\assign\year\the\year",
432                "20",
433            ),
434        ),
435        serde_tests((serde_sanity, r"\def\HW{Hello World} ", r"\HW"),),
436    ];
437
438    #[test]
439    fn all_error_cases() {
440        let options = vec![TestOption::BuiltInCommands(
441            StdLibState::default_built_in_commands,
442        )];
443        for case in ErrorCase::all_error_cases() {
444            println!("CASE {}", case.description);
445            run_fatal_error_test::<StdLibState>(case.source_code, &options, false)
446        }
447    }
448}