moon_script/
lib.rs

1//! [![crates.io](https://img.shields.io/crates/v/moon_script.svg)](https://crates.io/crates/moon_script)
2//! [![docs.rs](https://img.shields.io/docsrs/moon_script)](https://docs.rs/moon_script/latest/moon_script/)
3//! [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/JorgeRicoVivas/moon_script/rust.yml)](https://github.com/JorgeRicoVivas/moon_script/actions)
4//! [![GitHub last commit](https://img.shields.io/github/last-commit/JorgeRicoVivas/moon_script)](https://github.com/JorgeRicoVivas/moon_script)
5//! [![GitHub License](https://img.shields.io/github/license/JorgeRicoVivas/moon_script)](https://github.com/JorgeRicoVivas/moon_script?tab=CC0-1.0-1-ov-file)
6//!
7//! MoonScript is a very basic scripting language for simple scripting with some syntax based on
8//! Rust's, the idea of MoonScript it's for those writing MoonScript to find themselves scripts in
9//! the simplest manner possible while still boosting performance.
10//!
11//! If you want a tour on MoonScript, feel free to check the
12//! [web book](https://jorgericovivas.github.io/moon_script_book/) out!
13//!
14//! ## Features
15//! - std (Default): MoonScript will target the Standard library, implementing the Error trait on
16//! error types and using Sync with std::sync mechanisms where possible.
17//! - colorization (Default): Parsing errors will get colorized when printing them in the terminal.
18//! - medium_functions: Functions added to an Engine can be up to 16 parameters, instead of 8.
19//! - big_functions: Functions added to an Engine can be up to 24 parameters, instead of 8.
20//! - massive_functions: Functions added to an Engine can be up to 40 parameters, instead of 8.
21
22#![cfg_attr(not(feature = "std"), no_std)]
23
24extern crate alloc;
25extern crate core;
26extern crate pest;
27
28pub use engine::context::ContextBuilder;
29pub use engine::context::InputVariable;
30pub use engine::Constant;
31pub use engine::Engine;
32
33pub use execution::ast::ASTExecutor;
34pub use execution::ast::AST;
35pub use execution::RuntimeError;
36
37pub use execution::optimized_ast::OptimizedAST;
38pub use execution::optimized_ast::OptimizedASTExecutor;
39
40pub use function::ToAbstractFunction;
41
42pub use parsing::error::ASTBuildingError;
43pub use parsing::error::ParsingError;
44pub use parsing::FunctionDefinition;
45pub use parsing::MoonValueKind;
46
47pub use value::MoonValue;
48
49
50#[cfg(feature = "std")]
51type HashSet<T> = std::collections::HashSet<T>;
52#[cfg(feature = "std")]
53type HashMap<K, V> = std::collections::HashMap<K, V>;
54#[cfg(feature = "std")]
55type LazyLock<T> = std::sync::LazyLock<T>;
56
57
58#[cfg(not(feature = "std"))]
59type HashMap<K, V> = alloc::collections::BTreeMap<K, V>;
60#[cfg(not(feature = "std"))]
61type LazyLock<T> = lazy_lock::LazyLock<T>;
62#[cfg(not(feature = "std"))]
63type HashSet<T> = alloc::collections::btree_set::BTreeSet<T>;
64
65
66pub mod engine;
67pub(crate) mod external_utils;
68pub mod execution;
69mod reduced_value_impl;
70pub mod parsing;
71pub mod function;
72pub mod value;
73
74#[cfg(not(feature = "std"))]
75pub(crate) mod lazy_lock;
76
77
78#[cfg(test)]
79mod test {
80    use crate::engine::context::ContextBuilder;
81    use crate::engine::Engine;
82    use crate::{FunctionDefinition, InputVariable};
83    use log::Level;
84
85    #[cfg(feature = "std")]
86    #[test]
87    fn test_optimizations() {
88        let mut engine = Engine::new();
89        engine.add_constant("ONE_AS_CONSTANT", 1);
90        engine.add_function(FunctionDefinition::new("constant_fn_get_two", || { 2 }).inline());
91        let context_with_a_constant_input_variable = ContextBuilder::new()
92            .with_variable(InputVariable::new("four").value(4));
93
94        let unoptimized_script_source = r###"
95            let three = ONE_AS_CONSTANT + constant_fn_get_two();
96            if three == 3{
97                print("First line!");
98            } else if three!=3 {
99                print("How?");
100            } else {
101                print("This won't ever happen 1");
102            }
103            if three!=3 {
104                print("This wont ever happen either 1");
105            } else {
106                print("Second line!");
107            }
108            if four == 4 {
109                print("Third line!");
110            } else if four!=4 {
111                print("How?");
112            } else {
113                print("This won't ever happen 2");
114            }
115            if four!=4 {
116                print("This wont ever happen either 2");
117            } else {
118                print("Fourth line!");
119            }
120            while three == 3 {
121                print("Eternal loop!");
122            }
123        "###;
124
125        let optimized_script_source = r###"
126            print("First line!");
127            print("Second line!");
128            print("Third line!");
129            print("Fourth line!");
130            while true {
131                print("Eternal loop!");
132            }
133        "###;
134
135        let ast_from_optimized = Engine::new()
136            .parse(optimized_script_source, context_with_a_constant_input_variable.clone()).unwrap();
137        let ast_from_unoptimized = engine
138            .parse(unoptimized_script_source, context_with_a_constant_input_variable.clone()).unwrap();
139
140        assert_eq!(ast_from_optimized, ast_from_unoptimized);
141    }
142
143    #[test]
144    fn test_array() {
145        simple_logger::init_with_level(Level::Trace);
146        let engine = Engine::default();
147
148        let ast = engine.parse("let a = [[4 2 5] [3 9 1] [6 8 7]]; a[1][2]", Default::default())
149            .unwrap();
150        let moon_result: i32 = ast.executor().execute().unwrap().try_into().unwrap();
151
152        let rust_executed = (|| {
153            let a = [[4, 2, 5], [3, 9, 1], [6, 8, 7]];
154            a[1][2]
155        })();
156        assert_eq!(rust_executed, moon_result);
157    }
158
159    #[test]
160    fn test_precedence() {
161        simple_logger::init_with_level(Level::Trace);
162        let engine = Engine::default();
163        let expected = 2 * 3 + 5 > 4 && true;
164        let moon_result: bool = engine.parse("2 * 3 + 5 > 4 && true", Default::default())
165            .unwrap().executor().execute().unwrap().try_into().unwrap();
166        assert_eq!(expected, moon_result);
167
168        let expected = true && 4 < 5 + 3 * 2;
169        let moon_result: bool = engine.parse("true && 4 < 5 + 3 * 2", Default::default())
170            .unwrap().executor().execute().unwrap().try_into().unwrap();
171        assert_eq!(expected, moon_result);
172    }
173
174    #[test]
175    fn test_binary_comparator_and_unary() {
176        simple_logger::init_with_level(Level::Trace);
177        let mut engine = Engine::default();
178        engine.add_function(FunctionDefinition::new("is_flag", |()| false)
179            .associated_type_name("agent").known_return_type_name("bool"));
180        engine.add_function(crate::parsing::FunctionDefinition::new("get_bool", |()| false)
181            .associated_type_name("agent").known_return_type_name("bool"));
182
183        let mut context = ContextBuilder::default();
184        context.push_variable(crate::engine::context::InputVariable::new("agent")
185            .associated_type("agent")
186            .lazy_value(|| 46397));
187
188        let res : bool = engine.parse(r#"(!agent.is_flag() && agent.get_bool())"#, context)
189            .unwrap().executor().execute().expect("TODO: panic message").try_into().unwrap();
190        assert_eq!(false, res);
191    }
192
193    #[cfg_attr(not(feature = "std"), test)]
194    fn test_custom_unnamed_type() {
195        let _ = simple_logger::init_with_level(log::Level::Trace);
196
197        let mut engine = Engine::default();
198        let mut context = ContextBuilder::default();
199        context.push_variable(crate::engine::context::InputVariable::new("agent")
200            .lazy_value(|| 46397)
201            .associated_type("agent"));
202        context.push_variable(crate::engine::context::InputVariable::new("effect")
203            .lazy_value(|| 377397)
204            .associated_type("effect"));
205
206        context.push_variable(crate::engine::context::InputVariable::new("forced_true")
207            .associated_type("boolean")
208            .lazy_value(|| true));
209
210        engine.add_function(crate::parsing::FunctionDefinition::new("alt", |()| 0)
211            .associated_type_name("agent").known_return_type_name("int"));
212        engine.add_function(crate::parsing::FunctionDefinition::new("is_flag", |()| false)
213            .associated_type_name("agent").known_return_type_name("bool"));
214
215
216        engine.add_function(crate::parsing::FunctionDefinition::new("set_scale",
217                                                                    |(), _scale: f32| {}, )
218            .associated_type_name("effect"));
219        engine.add_function(crate::parsing::FunctionDefinition::new("lived_time",
220                                                                    |()| { 3 }, )
221            .associated_type_name("effect"));
222        engine.add_function(crate::parsing::FunctionDefinition::new("set_pos",
223                                                                    |(), _x: f32, _y: f32, _z: f32| {},
224        ).associated_type_name("effect"));
225        engine.add_function(crate::parsing::FunctionDefinition::new("set_color",
226                                                                    |(), x: f32, y: f32, z: f32| {
227                                                                        x + y + z
228                                                                    },
229        ).associated_type_name("effect"));
230
231        engine.add_function(crate::parsing::FunctionDefinition::new("kill", |()| {
232            #[cfg(feature = "std")]
233            println!("Removing effect");
234        })
235            .associated_type_name("effect").known_return_type_name("effect"));
236        engine.add_function(crate::parsing::FunctionDefinition::new("effect", |()| 1)
237            .associated_type_name("effect").known_return_type_name("effect"));
238
239        let ast = engine.parse(r#"
240        agent.alt%2==1
241
242
243        "#, context).map_err(|error| panic!("{error}"));
244        ast.unwrap().executor().execute().unwrap();
245    }
246}
247
248#[cfg(test)]
249mod book_tests {
250    use crate::{ContextBuilder, Engine, FunctionDefinition, InputVariable, MoonValue};
251    use alloc::format;
252    use alloc::string::{String, ToString};
253
254
255    #[cfg(feature = "std")]
256    #[test]
257    fn developers_guide___engine() {
258        let engine = Engine::new();
259        let context = ContextBuilder::new();
260
261        // Create an AST out of a script that prints to the standard output
262        let ast = engine.parse(r###"println("Hello world")"###, context).unwrap();
263
264        /// Execute the AST
265        ast.execute();
266    }
267
268    #[test]
269    fn developers_guide___engine___add_constants() {
270        let mut engine = Engine::new();
271
272        // Create a constant named ONE
273        engine.add_constant("ONE", 1);
274
275        // Creates and executes a script that returns the constant
276        let ast_result = engine.parse(r###"return ONE;"###, ContextBuilder::new()).unwrap()
277            .execute().unwrap();
278
279        assert_eq!(MoonValue::Integer(1), ast_result);
280
281        // The value returned by an AST execution is a MoonValue, luckily, MoonValue implements
282        // TryFrom for basic rust primitives and String, so we can get it with try_into()
283        // as an i32
284        let ast_result_as_i32: i32 = ast_result.try_into().unwrap();
285        assert_eq!(1, ast_result_as_i32);
286    }
287
288    #[test]
289    fn developers_guide___engine___add_functions() {
290        let mut engine = Engine::new();
291
292        // Creates a function that adds two numbers, this function is a function that can be
293        // called at compile time, so we also call 'inline' to enable this optimization.
294        let function_sum_two = FunctionDefinition::new("sum_two", |n: u8, m: u8| n + m)
295            .inline();
296
297        // The function is added to the engine
298        engine.add_function(function_sum_two);
299
300        // Creates and executes a script that sums 1 and 2 and returns its result
301        let ast_result: i32 = engine.parse(r###"sum_two(1,2);"###, ContextBuilder::new())
302            .unwrap().execute().unwrap().try_into().unwrap();
303        assert_eq!(3, ast_result);
304    }
305
306    #[test]
307    fn developers_guide___engine___add_functions__Result() {
308        let mut engine = Engine::new();
309
310        // Creates a function that adds two numbers, this function is a function that can be
311        // called at compile time, so we also call 'inline' to enable this optimization.
312        let function_sum_two = FunctionDefinition::new("sum_two", |n: u8, m: u8| n.checked_add(m).ok_or(format!("Error, numbers too large ({n}, {m})")))
313            .inline();
314
315        // The function is added to the engine
316        engine.add_function(function_sum_two);
317
318        // Creates and executes a script that sums 1 and 2 and returns its result, not failing
319        let ast_result: i32 = engine.parse(r###"sum_two(1,2);"###, ContextBuilder::new())
320            .unwrap().execute().unwrap().try_into().unwrap();
321        assert_eq!(3, ast_result);
322
323        // Creates and executes a script that sums 100 and 200, forcing the compilation to fail
324        let error = engine.parse(r###"sum_two(100,200);"###, ContextBuilder::new())
325            .err().unwrap().couldnt_build_ast_error().unwrap().as_display_struct(false);
326        let compilation_error = format!("{}", error);
327        #[cfg(feature = "std")]
328        println!("{}", compilation_error);
329
330        assert_eq!(compilation_error.replace(" ", "").replace("\n", ""), (r###"
331            Error: Could not compile.
332            Cause:
333              - Position: On line 1 and column 1
334              - At: sum_two(100,200)
335              - Error: The constant function sum_two was tried to be inlined, but it returned this error:
336                       Could not execute a function due to: Error, numbers too large (100, 200).
337                       "###).replace(" ", "").replace("\n", ""));
338    }
339
340    #[derive(Clone)]
341    struct MyType {
342        name: String,
343        age: u16,
344    }
345
346    impl From<MyType> for MoonValue {
347        fn from(value: MyType) -> Self {
348            MoonValue::from([
349                MoonValue::from(value.name),
350                MoonValue::from(value.age)
351            ])
352        }
353    }
354
355    impl TryFrom<MoonValue> for MyType {
356        type Error = ();
357
358        fn try_from(value: MoonValue) -> Result<Self, Self::Error> {
359            match value {
360                MoonValue::Array(mut moon_values) => Ok(
361                    Self {
362                        name: String::try_from(moon_values.remove(0)).map_err(|_| ())?,
363                        age: u16::try_from(moon_values.remove(0)).map_err(|_| ())?,
364                    }
365                ),
366                _ => Err(())
367            }
368        }
369    }
370
371    #[test]
372    fn developers_guide___engine___custom_type() {
373        let mut engine = Engine::new();
374
375        // Create a value for the type
376        let my_type_example = MyType { name: "Jorge".to_string(), age: 23 };
377
378        // Create a constant with said type
379        engine.add_constant("BASE_HUMAN", my_type_example.clone());
380
381        // Create a getter for the field age
382        engine.add_function(FunctionDefinition::new("age", |value: MyType| {
383            value.age
384        })
385            // The function is associated to the type 'MyType', this means the function age
386            // will work as a property, so instead of calling 'age(BASE_HUMAN)',
387            // we write 'BASE_HUMAN.age', for more information about properties, check the
388            // properties secion of the user's guide
389            .associated_type_of::<MyType>());
390
391        // Create and execute a script that uses the custom constant and function, where
392        // it gets the age of the human
393        let age: u16 = engine.parse("BASE_HUMAN.age", Default::default())
394            .unwrap().execute().unwrap().try_into().unwrap();
395
396        assert_eq!(my_type_example.age, age);
397    }
398
399    #[test]
400    fn developers_guide___context___input_variables() {
401        let engine = Engine::new();
402
403        // Create a context with an inlined variable named user_name whose value is 'Jorge'
404        let context = ContextBuilder::new()
405            .with_variable(InputVariable::new("user_name").value("Jorge"));
406
407        // Creates and executes a script that returns the value of said variable
408        let user_name: String = engine.parse("return user_name;", context)
409            .unwrap().execute().unwrap().try_into().unwrap();
410
411        assert_eq!("Jorge", user_name);
412    }
413
414    #[test]
415    fn developers_guide___context___ast_input_variables() {
416        let engine = Engine::new();
417
418        // Create a context with a late variable named user_name whose type is that of a String
419        let context = ContextBuilder::new()
420            .with_variable(InputVariable::new("user_name").associated_type_of::<String>());
421
422        // Compiles a script that returns the value of said variable and creates an executor to
423        // execute said script, to give the value of this variable to the executor, the method
424        // push_variable is called with the name of the late variable and it's value
425        let ast = engine.parse("return user_name;", context)
426            .unwrap();
427        let ast_executor = ast.executor()
428            .push_variable("user_name", "Jorge");
429
430        // Executes the AST to return the value of said variable
431        let user_name: String = ast_executor
432            .execute().unwrap().try_into().unwrap();
433
434        assert_eq!("Jorge", user_name);
435    }
436
437    #[test]
438    fn developers_guide___context___line_error() {
439        let engine = Engine::new();
440        // The script we are going to make to fail on compilation has an error in line 5,
441        // where it calls a function that does not exist
442        let script = r###"
443let a = 5;
444let b = 10;
445let sum = a + b;
446calling_an_non_existing_function(sum)
447        "###;
448
449        // The default context builder didn't specify a starting position for the script.
450        let context_builder = ContextBuilder::new();
451
452        // The following line just parses the error as a string and searches for the line
453        // specifying the position, don't worry; you will likely never do this.
454        let error = engine.parse(script, context_builder).err().unwrap();
455
456        #[cfg(feature = "std")]
457        // Shows the error to the user, so he can look up what was wrong.
458        println!("{error}");
459
460        let simple_error = format!("{error}").lines().skip(2).next().unwrap().to_string();
461
462        // The error is successfully located at line 5
463        assert_eq!("  - Position: On line 5 and column 1", simple_error);
464
465        // Now we will run the same test again, this time however, we will specify the script
466        // has an offset of starting in line 100 and column 100 of a file.
467        let context_builder = ContextBuilder::new()
468            .with_start_parsing_position_offset(100, 100);
469        let error = engine.parse(script, context_builder).err().unwrap();
470
471        let simple_error = format!("{error}").lines().skip(2).next().unwrap().to_string();
472
473        // Since the error happens on line 5 and column 1, this error happens on line 105 and
474        // column 1 of the file.
475        //
476        // The reason for the column to be 1 and not 101 it's because it interprets each new line
477        // starts at column 1, so it would have shown 101 if it was on line 1, but not in any
478        // following, if the column is fixed, you can call
479        // [ContextBuilder::parsing_column_fixed(true)], that way it would have shown column 101
480        assert_eq!("  - Position: On line 105 and column 1", simple_error);
481    }
482}
483