From 5ad2d24251ab178eb6ac58a934abf805a22860d5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 14:50:12 +0800 Subject: [PATCH 01/24] Put comments into example scripts. --- README.md | 50 +++++++++++++++++++------------------ scripts/array.rhai | 7 ++++-- scripts/assignment.rhai | 2 +- scripts/comments.rhai | 6 ++--- scripts/for1.rhai | 30 +++++++++++----------- scripts/function_decl1.rhai | 6 +++-- scripts/function_decl2.rhai | 11 ++++++-- scripts/function_decl3.rhai | 4 ++- scripts/if1.rhai | 19 ++++++++++---- scripts/loop.rhai | 4 +++ scripts/op1.rhai | 2 +- scripts/op2.rhai | 3 ++- scripts/op3.rhai | 3 ++- scripts/primes.rhai | 5 ++-- scripts/speed_test.rhai | 11 ++++++-- scripts/string.rhai | 22 +++++++++++----- scripts/while.rhai | 2 ++ 17 files changed, 119 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f93b6eba..5a406fc6 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ Optional features | `no_index` | Disable arrays and indexing features if not needed. | | `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | -| `only_i32` | Set the system integer type to `i32` and disable all other integer types. | -| `only_i64` | Set the system integer type to `i64` and disable all other integer types. | +| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | +| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. @@ -93,27 +93,27 @@ Example Scripts There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: -| Language feature scripts | Description | -| ------------------------ | ------------------------------------------------------------- | -| `array.rhai` | arrays in Rhai | -| `assignment.rhai` | variable declarations | -| `comments.rhai` | just comments | -| `for1.rhai` | for loops | -| `function_decl1.rhai` | a function without parameters | -| `function_decl2.rhai` | a function with two parameters | -| `function_decl3.rhai` | a function with many parameters | -| `if1.rhai` | if example | -| `loop.rhai` | endless loop in Rhai, this example emulates a do..while cycle | -| `op1.rhai` | just a simple addition | -| `op2.rhai` | simple addition and multiplication | -| `op3.rhai` | change evaluation order with parenthesis | -| `string.rhai` | string operations | -| `while.rhai` | while loop | +| Language feature scripts | Description | +| ---------------------------------------------------- | ------------------------------------------------------------- | +| [`array.rhai`](scripts/array.rhai) | arrays in Rhai | +| [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | +| [`comments.rhai`](scripts/comments.rhai) | just comments | +| [`for1.rhai`](scripts/for1.rhai) | for loops | +| [`function_decl1.rhai`](scripts/function_decl1.rhai) | a function without parameters | +| [`function_decl2.rhai`](scripts/function_decl2.rhai) | a function with two parameters | +| [`function_decl3.rhai`](scripts/function_decl3.rhai) | a function with many parameters | +| [`if1.rhai`](scripts/if1.rhai) | if example | +| [`loop.rhai`](scripts/loop.rhai) | endless loop in Rhai, this example emulates a do..while cycle | +| [`op1.rhai`](scripts/op1.rhai) | just a simple addition | +| [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | +| [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | +| [`string.rhai`](scripts/string.rhai) | string operations | +| [`while.rhai`](scripts/while.rhai) | while loop | -| Example scripts | Description | -| ----------------- | ---------------------------------------------------------------------------------- | -| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | -| `primes.rhai` | use Sieve of Eratosthenes to find all primes smaller than a limit | +| Example scripts | Description | +| -------------------------------------------- | ---------------------------------------------------------------------------------- | +| [`speed_test.rhai`](scripts/speed_test.rhai) | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | +| [`primes.rhai`](scripts/primes.rhai) | use Sieve of Eratosthenes to find all primes smaller than a limit | To run the scripts, either make a tiny program or use of the `rhai_runner` example: @@ -740,7 +740,9 @@ The following standard functions (defined in the standard library but excluded i Strings and Chars ----------------- -String and char literals follow C-style formatting, with support for Unicode ('`\u`') and hex ('`\x`') escape sequences. +String and char literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. + +Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicode code points and '`\U`' maps the full, 32-bit extended Unicode code points. Although internally Rhai strings are stored as UTF-8 just like in Rust (they _are_ Rust `String`s), in the Rhai language they can be considered a stream of Unicode characters, and can be directly indexed (unlike Rust). @@ -1051,7 +1053,7 @@ for x in array { if x == 42 { break; } } -// The 'range' function allows iterating from first..last +// The 'range' function allows iterating from first to last-1 for x in range(0, 50) { print(x); if x == 42 { break; } diff --git a/scripts/array.rhai b/scripts/array.rhai index 87343a6a..1d0768bd 100644 --- a/scripts/array.rhai +++ b/scripts/array.rhai @@ -1,4 +1,7 @@ let x = [1, 2, 3]; -print(x[1]); + +print(x[1]); // prints 2 + x[1] = 5; -print(x[1]); + +print(x[1]); // prints 5 diff --git a/scripts/assignment.rhai b/scripts/assignment.rhai index 8e8d6047..9554a7e5 100644 --- a/scripts/assignment.rhai +++ b/scripts/assignment.rhai @@ -1,2 +1,2 @@ let x = 78; -print(x) +print(x); diff --git a/scripts/comments.rhai b/scripts/comments.rhai index 50f15278..645105bb 100644 --- a/scripts/comments.rhai +++ b/scripts/comments.rhai @@ -3,8 +3,8 @@ let /* I am a spy in a variable declaration! */ x = 5; /* I am a simple - multiline comment */ + multi-line comment */ -/* look /* at /* that, /* multiline */ comments */ can be */ nested */ +/* look /* at /* that, /* multi-line */ comments */ can be */ nested */ -/* sorrounded by */ x // comments +/* surrounded by */ x // comments diff --git a/scripts/for1.rhai b/scripts/for1.rhai index ea3675b7..8eafedde 100644 --- a/scripts/for1.rhai +++ b/scripts/for1.rhai @@ -1,15 +1,17 @@ -let arr = [1,2,3,4] -for a in arr { - for b in [10,20] { - print(a) - print(b) - } - if a == 3 { - break; - } -} -//print(a) +// This script runs for-loops -for i in range(0,5) { - print(i) -} \ No newline at end of file +let arr = [1,2,3,4]; + +for a in arr { + for b in [10, 20] { + print(a + "," + b); + } + + if a == 3 { break; } +} +//print(a); // <- if you uncomment this line, the script will fail to run + // because 'a' is not defined here + +for i in range(0, 5) { // runs through a range from 1 to 5 exclusive + print(i); +} diff --git a/scripts/function_decl1.rhai b/scripts/function_decl1.rhai index c23357b8..4e97e47c 100644 --- a/scripts/function_decl1.rhai +++ b/scripts/function_decl1.rhai @@ -1,5 +1,7 @@ +// This script defines a function and calls it + fn bob() { - 3 + return 3; } -print(bob()) +print(bob()); // should print 3 diff --git a/scripts/function_decl2.rhai b/scripts/function_decl2.rhai index b341688b..201dcea4 100644 --- a/scripts/function_decl2.rhai +++ b/scripts/function_decl2.rhai @@ -1,5 +1,12 @@ +// This script defines a function with two parameters + +let a = 3; + fn addme(a, b) { - a+b + a = 42; // notice that 'a' is passed by value + a + b; // notice that the last value is returned even if terminated by a semicolon } -print(addme(3, 4)) +print(addme(a, 4)); // should print 46 + +print(a); // should print 3 - 'a' is never changed diff --git a/scripts/function_decl3.rhai b/scripts/function_decl3.rhai index b23846f4..339c4b61 100644 --- a/scripts/function_decl3.rhai +++ b/scripts/function_decl3.rhai @@ -1,5 +1,7 @@ +// This script defines a function with many parameters and calls it + fn f(a, b, c, d, e, f) { a - b * c - d * e - f } -print(f(100, 5, 2, 9, 6, 32)) +print(f(100, 5, 2, 9, 6, 32)); // should print 4 diff --git a/scripts/if1.rhai b/scripts/if1.rhai index b45cbde2..6f414a78 100644 --- a/scripts/if1.rhai +++ b/scripts/if1.rhai @@ -1,5 +1,14 @@ -let a = true; -if (a) { - let x = 56; - print(x); -} +let a = 42; +let b = 123; +let x = 999; + +if a > b { + print("a > b"); +} else if a < b { + print("a < b"); + + let x = 0; // this 'x' shadows the global 'x' + print(x); // should print 0 +} else { + print("a == b"); +} \ No newline at end of file diff --git a/scripts/loop.rhai b/scripts/loop.rhai index 91b7a5d8..d173b86f 100644 --- a/scripts/loop.rhai +++ b/scripts/loop.rhai @@ -1,8 +1,12 @@ +// This script runs an infinite loop, ending it with a break statement + let x = 10; // simulate do..while using loop loop { print(x); + x = x - 1; + if x <= 0 { break; } } diff --git a/scripts/op1.rhai b/scripts/op1.rhai index 3f25f040..5351f999 100644 --- a/scripts/op1.rhai +++ b/scripts/op1.rhai @@ -1 +1 @@ -print(34 + 12) +print(34 + 12); // should be 46 diff --git a/scripts/op2.rhai b/scripts/op2.rhai index c7767bc8..fedbd0aa 100644 --- a/scripts/op2.rhai +++ b/scripts/op2.rhai @@ -1 +1,2 @@ -print(12 + 34 * 5) +let x = 12 + 34 * 5; +print(x); // should be 182 diff --git a/scripts/op3.rhai b/scripts/op3.rhai index 7e5eb4e2..7811dbac 100644 --- a/scripts/op3.rhai +++ b/scripts/op3.rhai @@ -1 +1,2 @@ -print(0 + (12 + 34) * 5) +let x = (12 + 34) * 5; +print(x); // should be 230 diff --git a/scripts/primes.rhai b/scripts/primes.rhai index 4f76291f..d09418e8 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -1,6 +1,6 @@ -// This is a script to calculate prime numbers. +// This script uses the Sieve of Eratosthenes to calculate prime numbers. -const MAX_NUMBER_TO_CHECK = 10000; // 1229 primes +const MAX_NUMBER_TO_CHECK = 10_000; // 1229 primes <= 10000 let prime_mask = []; prime_mask.pad(MAX_NUMBER_TO_CHECK, true); @@ -24,4 +24,3 @@ for p in range(2, MAX_NUMBER_TO_CHECK) { } print("Total " + total_primes_found + " primes."); - diff --git a/scripts/speed_test.rhai b/scripts/speed_test.rhai index 505b6f52..1aa67276 100644 --- a/scripts/speed_test.rhai +++ b/scripts/speed_test.rhai @@ -1,5 +1,12 @@ -let x = 1000000; +// This script runs 1 million iterations +// to test the speed of the scripting engine. + +let x = 1_000_000; + +print("Ready... Go!"); + while x > 0 { x = x - 1; } -print(x); + +print("Finished."); diff --git a/scripts/string.rhai b/scripts/string.rhai index ecf418ec..ab30a29a 100644 --- a/scripts/string.rhai +++ b/scripts/string.rhai @@ -1,7 +1,17 @@ +// This script tests string operations + print("hello"); -print("this\nis \\ nice"); -print("40 hex is \x40"); -print("fun with unicode: \u2764 and \U0001F603"); -print("foo" + " " + "bar"); -print("foo" < "bar"); -print("foo" >= "bar"); \ No newline at end of file +print("this\nis \\ nice"); // escape sequences +print("40 hex is \x40"); // hex escape sequence +print("unicode fun: \u2764"); // Unicode escape sequence +print("more fun: \U0001F603"); // Unicode escape sequence +print("foo" + " " + "bar"); // string building using strings +print("foo" < "bar"); // string comparison +print("foo" >= "bar"); // string comparison +print("the answer is " + 42); // string building using non-string types + +let s = "hello, world!"; // string variable +print("length=" + s.len()); // should be 13 + +s[s.len()-1] = '?'; // change the string +print(s); // should print 'hello, world?' diff --git a/scripts/while.rhai b/scripts/while.rhai index d0f788f3..3f58f6b6 100644 --- a/scripts/while.rhai +++ b/scripts/while.rhai @@ -1,3 +1,5 @@ +// This script runs a while loop + let x = 10; while x > 0 { From 705fbd0c1b2ddcfa35ba0fd62bf39b04448da146 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 23:51:32 +0800 Subject: [PATCH 02/24] Improve error messages to lists. --- Cargo.toml | 1 + README.md | 36 +-- src/engine.rs | 15 +- src/error.rs | 5 + src/parser.rs | 829 ++++++++++++++++++++++++-------------------------- 5 files changed, 433 insertions(+), 453 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 308685bf..0b776f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ include = [ "scripts/*.rhai", "Cargo.toml" ] +keywords = [ "scripting" ] [dependencies] num-traits = "*" diff --git a/README.md b/README.md index 5a406fc6..09a5d6d4 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ let ast = engine.compile("40 + 2")?; for _ in 0..42 { let result: i64 = engine.eval_ast(&ast)?; - println!("Answer: {}", result); // prints 42 + println!("Answer #{}: {}", i, result); // prints 42 } ``` @@ -193,14 +193,17 @@ use rhai::Engine; let mut engine = Engine::new(); // Define a function in a script and load it into the Engine. -engine.consume(true, // pass true to 'retain_functions' otherwise these functions - r" // will be cleared at the end of consume() - fn hello(x, y) { // a function with two parameters: String and i64 - x.len() + y // returning i64 +// Pass true to 'retain_functions' otherwise these functions will be cleared at the end of consume() +engine.consume(true, + r" + // a function with two parameters: String and i64 + fn hello(x, y) { + x.len() + y } - fn hello(x) { // functions can be overloaded: this one takes only one parameter - x * 2 // returning i64 + // functions can be overloaded: this one takes only one parameter + fn hello(x) { + x * 2 } ")?; @@ -495,22 +498,21 @@ let result = engine.eval::("let x = new_ts(); x.foo()")?; println!("result: {}", result); // prints 1 ``` -`type_of` works fine with custom types and returns the name of the type: +`type_of` works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type +with a special "pretty-print" name, `type_of` will return that name instead. ```rust let x = new_ts(); print(x.type_of()); // prints "foo::bar::TestStruct" + // prints "Hello" if TestStruct is registered with + // engine.register_type_with_name::("Hello")?; ``` -If `register_type_with_name` is used to register the custom type with a special "pretty-print" name, `type_of` will return that name instead. - Getters and setters ------------------- Similarly, custom types can expose members by registering a `get` and/or `set` function. -For example: - ```rust #[derive(Clone)] struct TestStruct { @@ -565,8 +567,8 @@ fn main() -> Result<(), EvalAltResult> // Then push some initialized variables into the state // NOTE: Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. // Better stick to them or it gets hard working with the script. - scope.push("y".into(), 42_i64); - scope.push("z".into(), 999_i64); + scope.push("y", 42_i64); + scope.push("z", 999_i64); // First invocation engine.eval_with_scope::<()>(&mut scope, r" @@ -684,7 +686,7 @@ Numeric operators generally follow C styles. | `%` | Modulo (remainder) | | | `~` | Power | | | `&` | Binary _And_ bit-mask | Yes | -| `|` | Binary _Or_ bit-mask | Yes | +| `\|` | Binary _Or_ bit-mask | Yes | | `^` | Binary _Xor_ bit-mask | Yes | | `<<` | Left bit-shift | Yes | | `>>` | Right bit-shift | Yes | @@ -948,9 +950,9 @@ Boolean operators | -------- | ------------------------------- | | `!` | Boolean _Not_ | | `&&` | Boolean _And_ (short-circuits) | -| `||` | Boolean _Or_ (short-circuits) | +| `\|\|` | Boolean _Or_ (short-circuits) | | `&` | Boolean _And_ (full evaluation) | -| `|` | Boolean _Or_ (full evaluation) | +| `\|` | Boolean _Or_ (full evaluation) | Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. diff --git a/src/engine.rs b/src/engine.rs index bee73cff..4e75b9f5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1026,30 +1026,29 @@ impl Engine<'_> { // Block scope Stmt::Block(block, _) => { let prev_len = scope.len(); - let mut last_result: Result = Ok(().into_dynamic()); + let mut result: Result = Ok(().into_dynamic()); - for block_stmt in block.iter() { - last_result = self.eval_stmt(scope, block_stmt); + for stmt in block.iter() { + result = self.eval_stmt(scope, stmt); - if let Err(x) = last_result { - last_result = Err(x); + if result.is_err() { break; } } scope.rewind(prev_len); - last_result + result } // If-else statement - Stmt::IfElse(guard, body, else_body) => self + Stmt::IfElse(guard, if_body, else_body) => self .eval_expr(scope, guard)? .downcast::() .map_err(|_| EvalAltResult::ErrorIfGuard(guard.position())) .and_then(|guard_val| { if *guard_val { - self.eval_stmt(scope, body) + self.eval_stmt(scope, if_body) } else if let Some(stmt) = else_body { self.eval_stmt(scope, stmt.as_ref()) } else { diff --git a/src/error.rs b/src/error.rs index a582bcb2..6c1d3857 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,6 +58,8 @@ pub enum ParseErrorType { /// An open `[` is missing the corresponding closing `]`. #[cfg(not(feature = "no_index"))] MissingRightBracket(String), + /// A list of expressions is missing the separating ','. + MissingComma(String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. @@ -116,6 +118,7 @@ impl ParseError { ParseErrorType::MissingRightBrace(_) => "Expecting '}'", #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(_) => "Expecting ']'", + ParseErrorType::MissingComma(_) => "Expecting ','", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", @@ -165,6 +168,8 @@ impl fmt::Display for ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::MissingComma(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { write!(f, "{}", self.desc())? } diff --git a/src/parser.rs b/src/parser.rs index 366329c3..102e51b9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -42,9 +42,11 @@ pub struct Position { impl Position { /// Create a new `Position`. pub fn new(line: usize, position: usize) -> Self { - if line == 0 || (line == usize::MAX && position == usize::MAX) { - panic!("invalid position: ({}, {})", line, position); - } + assert!(line != 0, "line cannot be zero"); + assert!( + line != usize::MAX || position != usize::MAX, + "invalid position" + ); Self { line, @@ -295,9 +297,10 @@ impl Expr { | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => { - e.position() - } + Expr::Assignment(expr, _, _) + | Expr::Dot(expr, _, _) + | Expr::And(expr, _) + | Expr::Or(expr, _) => expr.position(), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => *pos, @@ -306,7 +309,7 @@ impl Expr { Expr::Array(_, pos) => *pos, #[cfg(not(feature = "no_index"))] - Expr::Index(e, _, _) => e.position(), + Expr::Index(expr, _, _) => expr.position(), } } @@ -423,15 +426,15 @@ impl Token { pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; - match *self { - IntegerConstant(ref i) => i.to_string().into(), + match self { + IntegerConstant(i) => i.to_string().into(), #[cfg(not(feature = "no_float"))] - FloatConstant(ref f) => f.to_string().into(), - Identifier(ref s) => s.into(), - CharConstant(ref c) => c.to_string().into(), - LexError(ref err) => err.to_string().into(), + FloatConstant(f) => f.to_string().into(), + Identifier(s) => s.into(), + CharConstant(c) => c.to_string().into(), + LexError(err) => err.to_string().into(), - ref token => (match token { + token => (match token { StringConst(_) => "string", LeftBrace => "{", RightBrace => "}", @@ -505,7 +508,7 @@ impl Token { pub fn is_next_unary(&self) -> bool { use self::Token::*; - match *self { + match self { LexError(_) | LeftBrace | // (+expr) - is unary // RightBrace | {expr} - expr not unary & is closing @@ -562,28 +565,63 @@ impl Token { } } - #[allow(dead_code)] - pub fn is_binary_op(&self) -> bool { - use self::Token::*; + pub fn precedence(&self) -> u8 { + match self { + Self::Equals + | Self::PlusAssign + | Self::MinusAssign + | Self::MultiplyAssign + | Self::DivideAssign + | Self::LeftShiftAssign + | Self::RightShiftAssign + | Self::AndAssign + | Self::OrAssign + | Self::XOrAssign + | Self::ModuloAssign + | Self::PowerOfAssign => 10, - match *self { - RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan - | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo - | Pipe | Or | Ampersand | And | PowerOf => true, + Self::Or | Self::XOr | Self::Pipe => 50, - #[cfg(not(feature = "no_index"))] - RightBrace | RightBracket => true, + Self::And | Self::Ampersand => 60, - _ => false, + Self::LessThan + | Self::LessThanEqualsTo + | Self::GreaterThan + | Self::GreaterThanEqualsTo + | Self::EqualsTo + | Self::NotEqualsTo => 70, + + Self::Plus | Self::Minus => 80, + + Self::Divide | Self::Multiply | Self::PowerOf => 90, + + Self::LeftShift | Self::RightShift => 100, + + Self::Modulo => 110, + + Self::Period => 120, + + _ => 0, } } - #[allow(dead_code)] - pub fn is_unary_op(&self) -> bool { - use self::Token::*; + pub fn is_bind_right(&self) -> bool { + match self { + Self::Equals + | Self::PlusAssign + | Self::MinusAssign + | Self::MultiplyAssign + | Self::DivideAssign + | Self::LeftShiftAssign + | Self::RightShiftAssign + | Self::AndAssign + | Self::OrAssign + | Self::XOrAssign + | Self::ModuloAssign + | Self::PowerOfAssign => true, + + Self::Period => true, - match *self { - UnaryPlus | UnaryMinus | Equals | Bang | Return | Throw => true, _ => false, } } @@ -637,89 +675,40 @@ impl<'a> TokenIterator<'a> { escape.clear(); result.push('\r'); } - 'x' if !escape.is_empty() => { + ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { let mut seq = escape.clone(); - seq.push('x'); + seq.push(ch); escape.clear(); + let mut out_val: u32 = 0; - for _ in 0..2 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); + let len = match ch { + 'x' => 2, + 'u' => 4, + 'U' => 8, + _ => panic!("should be 'x', 'u' or 'U'"), + }; - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } + for _ in 0..len { + let c = self.char_stream.next().ok_or_else(|| { + (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) + })?; + + seq.push(c); + self.advance(); + + out_val *= 16; + out_val += c.to_digit(16).ok_or_else(|| { + (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) + })?; } - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } + result.push( + char::from_u32(out_val) + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), self.pos))?, + ); } - 'u' if !escape.is_empty() => { - let mut seq = escape.clone(); - seq.push('u'); - escape.clear(); - let mut out_val: u32 = 0; - for _ in 0..4 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); - - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - 'U' if !escape.is_empty() => { - let mut seq = escape.clone(); - seq.push('U'); - escape.clear(); - let mut out_val: u32 = 0; - for _ in 0..8 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); - - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - x if enclosing_char == x && !escape.is_empty() => result.push(x), - x if enclosing_char == x && escape.is_empty() => break, + ch if enclosing_char == ch && !escape.is_empty() => result.push(ch), + ch if enclosing_char == ch && escape.is_empty() => break, _ if !escape.is_empty() => { return Err((LERR::MalformedEscapeSequence(escape), self.pos)) } @@ -727,9 +716,9 @@ impl<'a> TokenIterator<'a> { self.rewind(); return Err((LERR::UnterminatedString, self.pos)); } - x => { + ch => { escape.clear(); - result.push(x); + result.push(ch); } } } @@ -775,54 +764,47 @@ impl<'a> TokenIterator<'a> { } } } - 'x' | 'X' if c == '0' => { + ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' + if c == '0' => + { result.push(next_char); self.char_stream.next(); self.advance(); + + let valid = match ch { + 'x' | 'X' => [ + 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', + ], + 'o' | 'O' => [ + '0', '1', '2', '3', '4', '5', '6', '7', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + 'b' | 'B' => [ + '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + _ => panic!("unexpected character {}", ch), + }; + + radix_base = Some(match ch { + 'x' | 'X' => 16, + 'o' | 'O' => 8, + 'b' | 'B' => 2, + _ => panic!("unexpected character {}", ch), + }); + while let Some(&next_char_in_hex) = self.char_stream.peek() { - match next_char_in_hex { - '0'..='9' | 'a'..='f' | 'A'..='F' | '_' => { - result.push(next_char_in_hex); - self.char_stream.next(); - self.advance(); - } - _ => break, + if !valid.contains(&next_char_in_hex) { + break; } + + result.push(next_char_in_hex); + self.char_stream.next(); + self.advance(); } - radix_base = Some(16); - } - 'o' | 'O' if c == '0' => { - result.push(next_char); - self.char_stream.next(); - self.advance(); - while let Some(&next_char_in_oct) = self.char_stream.peek() { - match next_char_in_oct { - '0'..='8' | '_' => { - result.push(next_char_in_oct); - self.char_stream.next(); - self.advance(); - } - _ => break, - } - } - radix_base = Some(8); - } - 'b' | 'B' if c == '0' => { - result.push(next_char); - self.char_stream.next(); - self.advance(); - while let Some(&next_char_in_binary) = self.char_stream.peek() { - match next_char_in_binary { - '0' | '1' | '_' => { - result.push(next_char_in_binary); - self.char_stream.next(); - self.advance(); - } - _ => break, - } - } - radix_base = Some(2); } + _ => break, } } @@ -844,25 +826,15 @@ impl<'a> TokenIterator<'a> { )); } else { let out: String = result.iter().filter(|&&c| c != '_').collect(); - - #[cfg(feature = "no_float")] - return Some(( - INT::from_str(&out) - .map(Token::IntegerConstant) - .unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }), - pos, - )); + let num = INT::from_str(&out).map(Token::IntegerConstant); #[cfg(not(feature = "no_float"))] + let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); + return Some(( - INT::from_str(&out) - .map(Token::IntegerConstant) - .or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)) - .unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }), + num.unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), pos, )); } @@ -1217,67 +1189,6 @@ pub fn lex(input: &str) -> TokenIterator<'_> { } } -fn get_precedence(token: &Token) -> u8 { - match *token { - Token::Equals - | Token::PlusAssign - | Token::MinusAssign - | Token::MultiplyAssign - | Token::DivideAssign - | Token::LeftShiftAssign - | Token::RightShiftAssign - | Token::AndAssign - | Token::OrAssign - | Token::XOrAssign - | Token::ModuloAssign - | Token::PowerOfAssign => 10, - - Token::Or | Token::XOr | Token::Pipe => 50, - - Token::And | Token::Ampersand => 60, - - Token::LessThan - | Token::LessThanEqualsTo - | Token::GreaterThan - | Token::GreaterThanEqualsTo - | Token::EqualsTo - | Token::NotEqualsTo => 70, - - Token::Plus | Token::Minus => 80, - - Token::Divide | Token::Multiply | Token::PowerOf => 90, - - Token::LeftShift | Token::RightShift => 100, - - Token::Modulo => 110, - - Token::Period => 120, - - _ => 0, - } -} - -fn is_bind_right(token: &Token) -> bool { - match *token { - Token::Equals - | Token::PlusAssign - | Token::MinusAssign - | Token::MultiplyAssign - | Token::DivideAssign - | Token::LeftShiftAssign - | Token::RightShiftAssign - | Token::AndAssign - | Token::OrAssign - | Token::XOrAssign - | Token::ModuloAssign - | Token::PowerOfAssign => true, - - Token::Period => true, - - _ => false, - } -} - fn parse_paren_expr<'a>( input: &mut Peekable>, begin: Position, @@ -1315,7 +1226,15 @@ fn parse_call_expr<'a>( ) -> Result { let mut args_expr_list = Vec::new(); - if let Some(&(Token::RightParen, _)) = input.peek() { + if let (Token::RightParen, _) = input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the arguments to call of function '{}'", + id + )), + Position::eof(), + ) + })? { input.next(); return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); } @@ -1323,28 +1242,27 @@ fn parse_call_expr<'a>( loop { args_expr_list.push(parse_expr(input)?); - match input.peek() { - Some(&(Token::RightParen, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the arguments to call of function '{}'", + id + )), + Position::eof(), + ) + })? { + (Token::RightParen, _) => { input.next(); return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); } - Some(&(Token::Comma, _)) => (), - Some(&(_, pos)) => { + (Token::Comma, _) => (), + (_, pos) => { return Err(ParseError::new( - PERR::MissingRightParen(format!( - "closing the parameters list to function call of '{}'", + PERR::MissingComma(format!( + "separating the arguments to call of function '{}'", id )), - pos, - )) - } - None => { - return Err(ParseError::new( - PERR::MissingRightParen(format!( - "closing the parameters list to function call of '{}'", - id - )), - Position::eof(), + *pos, )) } } @@ -1419,21 +1337,20 @@ fn parse_index_expr<'a>( } // Check if there is a closing bracket - match input.peek() { - Some(&(Token::RightBracket, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBracket("index expression".into()), + Position::eof(), + ) + })? { + (Token::RightBracket, _) => { input.next(); return Ok(Expr::Index(lhs, Box::new(idx_expr), pos)); } - Some(&(_, pos)) => { + (_, pos) => { return Err(ParseError::new( PERR::MissingRightBracket("index expression".into()), - pos, - )) - } - None => { - return Err(ParseError::new( - PERR::MissingRightBracket("index expression".into()), - Position::eof(), + *pos, )) } } @@ -1445,12 +1362,13 @@ fn parse_ident_expr<'a>( begin: Position, ) -> Result { match input.peek() { - Some(&(Token::LeftParen, _)) => { + Some((Token::LeftParen, _)) => { input.next(); parse_call_expr(id, input, begin) } #[cfg(not(feature = "no_index"))] - Some(&(Token::LeftBracket, pos)) => { + Some((Token::LeftBracket, pos)) => { + let pos = *pos; input.next(); parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos) } @@ -1466,36 +1384,43 @@ fn parse_array_expr<'a>( ) -> Result { let mut arr = Vec::new(); - match input.peek() { - Some(&(Token::RightBracket, _)) => (), + if !matches!(input.peek(), Some((Token::RightBracket, _))) { + while input.peek().is_some() { + arr.push(parse_expr(input)?); - _ => { - while input.peek().is_some() { - arr.push(parse_expr(input)?); - - if let Some(&(Token::Comma, _)) = input.peek() { + match input.peek().ok_or_else(|| { + ParseError( + PERR::MissingRightBracket("separating items in array literal".into()), + Position::eof(), + ) + })? { + (Token::Comma, _) => { input.next(); } - - if let Some(&(Token::RightBracket, _)) = input.peek() { - break; + (Token::RightBracket, _) => break, + (_, pos) => { + return Err(ParseError( + PERR::MissingComma("separating items in array literal".into()), + *pos, + )) } } } } - match input.peek() { - Some(&(Token::RightBracket, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBracket("the end of array literal".into()), + Position::eof(), + ) + })? { + (Token::RightBracket, _) => { input.next(); Ok(Expr::Array(arr, begin)) } - Some(&(_, pos)) => Err(ParseError::new( + (_, pos) => Err(ParseError::new( PERR::MissingRightBracket("the end of array literal".into()), - pos, - )), - None => Err(ParseError::new( - PERR::MissingRightBracket("the end of array literal".into()), - Position::eof(), + *pos, )), } } @@ -1503,8 +1428,9 @@ fn parse_array_expr<'a>( fn parse_primary<'a>(input: &mut Peekable>) -> Result { // Block statement as expression match input.peek() { - Some(&(Token::LeftBrace, pos)) => { - return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)) + Some((Token::LeftBrace, pos)) => { + let pos = *pos; + return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)); } _ => (), } @@ -1514,45 +1440,44 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(x, pos)), + let mut root_expr = + match token.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? { + #[cfg(not(feature = "no_float"))] + (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), - Some((Token::IntegerConstant(x), pos)) => Ok(Expr::IntegerConstant(x, pos)), - Some((Token::CharConstant(c), pos)) => Ok(Expr::CharConstant(c, pos)), - Some((Token::StringConst(s), pos)) => { - can_be_indexed = true; - Ok(Expr::StringConstant(s, pos)) - } - Some((Token::Identifier(s), pos)) => { - can_be_indexed = true; - parse_ident_expr(s, input, pos) - } - Some((Token::LeftParen, pos)) => { - can_be_indexed = true; - parse_paren_expr(input, pos) - } - #[cfg(not(feature = "no_index"))] - Some((Token::LeftBracket, pos)) => { - can_be_indexed = true; - parse_array_expr(input, pos) - } - Some((Token::True, pos)) => Ok(Expr::True(pos)), - Some((Token::False, pos)) => Ok(Expr::False(pos)), - Some((Token::LexError(le), pos)) => { - Err(ParseError::new(PERR::BadInput(le.to_string()), pos)) - } - Some((token, pos)) => Err(ParseError::new( - PERR::BadInput(format!("Unexpected '{}'", token.syntax())), - pos, - )), - None => Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), - }?; + (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), + (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), + (Token::StringConst(s), pos) => { + can_be_indexed = true; + Ok(Expr::StringConstant(s, pos)) + } + (Token::Identifier(s), pos) => { + can_be_indexed = true; + parse_ident_expr(s, input, pos) + } + (Token::LeftParen, pos) => { + can_be_indexed = true; + parse_paren_expr(input, pos) + } + #[cfg(not(feature = "no_index"))] + (Token::LeftBracket, pos) => { + can_be_indexed = true; + parse_array_expr(input, pos) + } + (Token::True, pos) => Ok(Expr::True(pos)), + (Token::False, pos) => Ok(Expr::False(pos)), + (Token::LexError(le), pos) => Err(ParseError::new(PERR::BadInput(le.to_string()), pos)), + (token, pos) => Err(ParseError::new( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())), + pos, + )), + }?; if can_be_indexed { // Tail processing all possible indexing #[cfg(not(feature = "no_index"))] - while let Some(&(Token::LeftBracket, pos)) = input.peek() { + while let Some((Token::LeftBracket, pos)) = input.peek() { + let pos = *pos; input.next(); root_expr = parse_index_expr(Box::new(root_expr), input, pos)?; } @@ -1562,13 +1487,18 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - match input.peek() { - Some(&(Token::UnaryMinus, pos)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + (Token::UnaryMinus, pos) => { + let pos = *pos; + input.next(); - match parse_unary(input) { + match parse_unary(input)? { // Negative integer - Ok(Expr::IntegerConstant(i, _)) => i + Expr::IntegerConstant(i, _) => i .checked_neg() .map(|x| Expr::IntegerConstant(x, pos)) .or_else(|| { @@ -1587,19 +1517,19 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(-f, pos)), + Expr::FloatConstant(f, pos) => Ok(Expr::FloatConstant(-f, pos)), // Call negative function - Ok(expr) => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), - - err @ Err(_) => err, + expr => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), } } - Some(&(Token::UnaryPlus, _)) => { + (Token::UnaryPlus, _) => { input.next(); parse_unary(input) } - Some(&(Token::Bang, pos)) => { + (Token::Bang, pos) => { + let pos = *pos; + input.next(); Ok(Expr::FunctionCall( @@ -1678,8 +1608,6 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), Some(err) => Err(err), @@ -1709,8 +1637,8 @@ fn parse_binary_op<'a>( let mut current_lhs = lhs; loop { - let (current_precedence, bind_right) = if let Some(&(ref current_op, _)) = input.peek() { - (get_precedence(current_op), is_bind_right(current_op)) + let (current_precedence, bind_right) = if let Some((ref current_op, _)) = input.peek() { + (current_op.precedence(), current_op.is_bind_right()) } else { (0, false) }; @@ -1728,8 +1656,8 @@ fn parse_binary_op<'a>( let rhs = parse_unary(input)?; - let next_precedence = if let Some(&(ref next_op, _)) = input.peek() { - get_precedence(next_op) + let next_precedence = if let Some((next_op, _)) = input.peek() { + next_op.precedence() } else { 0 }; @@ -1863,26 +1791,21 @@ fn parse_if<'a>(input: &mut Peekable>) -> Result { - input.next(); + let else_body = if matches!(input.peek(), Some((Token::Else, _))) { + input.next(); - let else_body = if matches!(input.peek(), Some(&(Token::If, _))) { - parse_if(input)? - } else { - parse_block(input)? - }; + Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) { + parse_if(input)? + } else { + parse_block(input)? + })) + } else { + None + }; - Ok(Stmt::IfElse( - Box::new(guard), - Box::new(body), - Some(Box::new(else_body)), - )) - } - _ => Ok(Stmt::IfElse(Box::new(guard), Box::new(body), None)), - } + Ok(Stmt::IfElse(Box::new(guard), Box::new(if_body), else_body)) } fn parse_while<'a>(input: &mut Peekable>) -> Result { @@ -1905,23 +1828,26 @@ fn parse_loop<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((Token::LexError(s), pos)) => { + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (Token::LexError(s), pos) => { return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) } - Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), - None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), + (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; - match input.next() { - Some((Token::In, _)) => (), - Some((_, pos)) => return Err(ParseError::new(PERR::MissingIn, pos)), - None => return Err(ParseError::new(PERR::MissingIn, Position::eof())), + match input + .next() + .ok_or_else(|| ParseError::new(PERR::MissingIn, Position::eof()))? + { + (Token::In, _) => (), + (_, pos) => return Err(ParseError::new(PERR::MissingIn, pos)), } let expr = parse_expr(input)?; - let body = parse_block(input)?; Ok(Stmt::For(name, Box::new(expr), Box::new(body))) @@ -1931,21 +1857,23 @@ fn parse_var<'a>( input: &mut Peekable>, var_type: VariableType, ) -> Result { - let pos = match input.next() { - Some((_, tok_pos)) => tok_pos, - _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), - }; + let pos = input + .next() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + .1; - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((Token::LexError(s), pos)) => { + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (Token::LexError(s), pos) => { return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) } - Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), - None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), + (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; - if matches!(input.peek(), Some(&(Token::Equals, _))) { + if matches!(input.peek(), Some((Token::Equals, _))) { input.next(); let init_value = parse_expr(input)?; @@ -1967,19 +1895,26 @@ fn parse_var<'a>( } fn parse_block<'a>(input: &mut Peekable>) -> Result { - let pos = match input.next() { - Some((Token::LeftBrace, pos)) => pos, - Some((_, pos)) => return Err(ParseError::new(PERR::MissingLeftBrace, pos)), - None => return Err(ParseError::new(PERR::MissingLeftBrace, Position::eof())), + let pos = match input + .next() + .ok_or_else(|| ParseError::new(PERR::MissingLeftBrace, Position::eof()))? + { + (Token::LeftBrace, pos) => pos, + (_, pos) => return Err(ParseError::new(PERR::MissingLeftBrace, pos)), }; let mut statements = Vec::new(); - match input.peek() { - Some(&(Token::RightBrace, _)) => (), // empty block + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + ) + })? { + (Token::RightBrace, _) => (), // empty block #[cfg(not(feature = "no_function"))] - Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)), + (Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), _ => { while input.peek().is_some() { @@ -1987,29 +1922,30 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + ) + })? { + (Token::RightBrace, _) => { input.next(); Ok(Stmt::Block(statements, pos)) } - Some(&(_, pos)) => Err(ParseError::new( + (_, pos) => Err(ParseError::new( PERR::MissingRightBrace("end of block".into()), - pos, - )), - None => Err(ParseError::new( - PERR::MissingRightBrace("end of block".into()), - Position::eof(), + *pos, )), } } @@ -2019,16 +1955,20 @@ fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - match input.peek() { - Some(&(Token::If, _)) => parse_if(input), - Some(&(Token::While, _)) => parse_while(input), - Some(&(Token::Loop, _)) => parse_loop(input), - Some(&(Token::For, _)) => parse_for(input), - Some(&(Token::Break, pos)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + (Token::If, _) => parse_if(input), + (Token::While, _) => parse_while(input), + (Token::Loop, _) => parse_loop(input), + (Token::For, _) => parse_for(input), + (Token::Break, pos) => { + let pos = *pos; input.next(); Ok(Stmt::Break(pos)) } - Some(&(ref token @ Token::Return, _)) | Some(&(ref token @ Token::Throw, _)) => { + (token @ Token::Return, _) | (token @ Token::Throw, _) => { let return_type = match token { Token::Return => ReturnType::Return, Token::Throw => ReturnType::Exception, @@ -2038,76 +1978,109 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result Ok(Stmt::ReturnWithVal(None, return_type, pos)), - // Just a return/throw without anything at the end of script + // return/throw at EOF None => Ok(Stmt::ReturnWithVal(None, return_type, Position::eof())), + // return; or throw; + Some((Token::SemiColon, pos)) => { + let pos = *pos; + Ok(Stmt::ReturnWithVal(None, return_type, pos)) + } // return or throw with expression - Some(&(_, pos)) => { + Some((_, pos)) => { + let pos = *pos; let ret = parse_expr(input)?; Ok(Stmt::ReturnWithVal(Some(Box::new(ret)), return_type, pos)) } } } - Some(&(Token::LeftBrace, _)) => parse_block(input), - Some(&(Token::Let, _)) => parse_var(input, VariableType::Normal), - Some(&(Token::Const, _)) => parse_var(input, VariableType::Constant), + (Token::LeftBrace, _) => parse_block(input), + (Token::Let, _) => parse_var(input, VariableType::Normal), + (Token::Const, _) => parse_var(input, VariableType::Constant), _ => parse_expr_stmt(input), } } #[cfg(not(feature = "no_function"))] fn parse_fn<'a>(input: &mut Peekable>) -> Result { - let pos = match input.next() { - Some((_, tok_pos)) => tok_pos, - _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), + let pos = input + .next() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + .1; + + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::FnMissingName, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (_, pos) => return Err(ParseError::new(PERR::FnMissingName, pos)), }; - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((_, pos)) => return Err(ParseError::new(PERR::FnMissingName, pos)), - None => return Err(ParseError::new(PERR::FnMissingName, Position::eof())), - }; - - match input.peek() { - Some(&(Token::LeftParen, _)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::FnMissingParams(name.clone()), Position::eof()))? + { + (Token::LeftParen, _) => { input.next(); } - Some(&(_, pos)) => return Err(ParseError::new(PERR::FnMissingParams(name), pos)), - None => { - return Err(ParseError::new( - PERR::FnMissingParams(name), - Position::eof(), - )) - } + (_, pos) => return Err(ParseError::new(PERR::FnMissingParams(name), *pos)), } let mut params = Vec::new(); - if matches!(input.peek(), Some(&(Token::RightParen, _))) { + if matches!(input.peek(), Some((Token::RightParen, _))) { input.next(); } else { loop { - match input.next() { - Some((Token::RightParen, _)) => break, - Some((Token::Comma, _)) => (), - Some((Token::Identifier(s), _)) => { + match input.next().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + Position::eof(), + ) + })? { + (Token::Identifier(s), _) => { params.push(s.into()); } - Some((_, pos)) => { + (_, pos) => { return Err(ParseError::new( - PERR::MalformedCallExpr( - "Function call arguments missing either a ',' or a ')'".into(), - ), + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), pos, )) } - None => { + } + + match input.next().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + Position::eof(), + ) + })? { + (Token::RightParen, _) => break, + (Token::Comma, _) => (), + (Token::Identifier(_), _) => { return Err(ParseError::new( - PERR::MalformedCallExpr( - "Function call arguments missing a closing ')'".into(), - ), - Position::eof(), + PERR::MissingComma(format!( + "separating the parameters of function '{}'", + name + )), + pos, + )) + } + (_, pos) => { + return Err(ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + pos, )) } } @@ -2131,9 +2104,9 @@ fn parse_top_level<'a, 'e>( let mut functions = Vec::::new(); while input.peek().is_some() { - match input.peek() { + match input.peek().expect("should not be None") { #[cfg(not(feature = "no_function"))] - Some(&(Token::Fn, _)) => { + (Token::Fn, _) => { let f = parse_fn(input)?; // Ensure list is sorted @@ -2146,7 +2119,7 @@ fn parse_top_level<'a, 'e>( } // Notice semicolons are optional - if let Some(&(Token::SemiColon, _)) = input.peek() { + if let Some((Token::SemiColon, _)) = input.peek() { input.next(); } } From ad2601972a24655919132b1cadbbb72514bfae45 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 10:27:43 +0800 Subject: [PATCH 03/24] Optimize type_of. --- src/optimize.rs | 63 ++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/optimize.rs b/src/optimize.rs index 8a75a905..a26a5d98 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,7 +1,9 @@ #![cfg(not(feature = "no_optimize"))] -use crate::any::Dynamic; -use crate::engine::{Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT}; +use crate::any::{Any, Dynamic}; +use crate::engine::{ + Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT, KEYWORD_TYPE_OF, +}; use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; @@ -223,6 +225,8 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - } fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { + const SKIP_FUNC_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_DUMP_AST]; + match expr { Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { Stmt::Noop(_) => { @@ -275,7 +279,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); items.remove(i as usize) } - (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) + (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < s.chars().count() => { // String literal indexing - get the character @@ -343,41 +347,46 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { ), }, - // Do not optimize anything within `dump_ast` - Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => { - Expr::FunctionCall(id, args, def_value, pos) - } + // Do not optimize anything within built-in function keywords + Expr::FunctionCall(id, args, def_value, pos) if SKIP_FUNC_KEYWORDS.contains(&id.as_str())=> + Expr::FunctionCall(id, args, def_value, pos), + // Actually call function to optimize it Expr::FunctionCall(id, args, def_value, pos) - if id != KEYWORD_DEBUG // not debug - && id != KEYWORD_PRINT // not print - && state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations + if state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations && args.iter().all(|expr| expr.is_constant()) // all arguments are constants - => - { + => { let engine = state.engine.expect("engine should be Some"); let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); - engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| - r.or(def_value.clone()).and_then(|result| map_dynamic_to_expr(result, pos).0) + + // Save the typename of the first argument if it is `type_of()` + // This is to avoid `call_args` being passed into the closure + let arg_for_type_of = if id == KEYWORD_TYPE_OF && call_args.len() == 1 { + engine.map_type_name(call_args[0].type_name()) + } else { + "" + }; + + engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| + r.or_else(|| { + if !arg_for_type_of.is_empty() { + // Handle `type_of()` + Some(arg_for_type_of.to_string().into_dynamic()) + } else { + // Otherwise use the default value, if any + def_value.clone() + } + }).and_then(|result| map_dynamic_to_expr(result, pos).0) .map(|expr| { state.set_dirty(); expr - })).flatten() - .unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) + }) + ).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) } // Optimize the function call arguments - Expr::FunctionCall(id, args, def_value, pos) => { - let orig_len = args.len(); - - let args: Vec<_> = args.into_iter().map(|a| optimize_expr(a, state)).collect(); - - if orig_len != args.len() { - state.set_dirty(); - } - - Expr::FunctionCall(id, args, def_value, pos) - } + Expr::FunctionCall(id, args, def_value, pos) => + Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), Expr::Variable(ref name, _) if state.contains_constant(name) => { state.set_dirty(); From 2f7ca3935b78e5b5c0dcd54c6f3d60129a804e3c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 14:29:22 +0800 Subject: [PATCH 04/24] Mandatory semiclolons separating statements. --- src/error.rs | 9 +++-- src/optimize.rs | 4 +-- src/parser.rs | 92 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/error.rs b/src/error.rs index 6c1d3857..9408b950 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,6 +60,8 @@ pub enum ParseErrorType { MissingRightBracket(String), /// A list of expressions is missing the separating ','. MissingComma(String), + /// A statement is missing the ending ';'. + MissingSemicolon(String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. @@ -119,6 +121,7 @@ impl ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(_) => "Expecting ']'", ParseErrorType::MissingComma(_) => "Expecting ','", + ParseErrorType::MissingSemicolon(_) => "Expecting ';'", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", @@ -130,7 +133,7 @@ impl ParseError { #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", #[cfg(not(feature = "no_function"))] - ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", + ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression", ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value", ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable." @@ -168,7 +171,9 @@ impl fmt::Display for ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, - ParseErrorType::MissingComma(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::MissingSemicolon(ref s) | ParseErrorType::MissingComma(ref s) => { + write!(f, "{} for {}", self.desc(), s)? + } ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { write!(f, "{}", self.desc())? diff --git a/src/optimize.rs b/src/optimize.rs index a26a5d98..05c91304 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -451,7 +451,7 @@ pub(crate) fn optimize<'a>( if let Stmt::Const(name, value, _) = &stmt { // Load constants state.push_constant(name, value.as_ref().clone()); - stmt // Keep it in the top scope + stmt // Keep it in the global scope } else { // Keep all variable declarations at this level // and always keep the last return value @@ -470,7 +470,7 @@ pub(crate) fn optimize<'a>( // Eliminate code that is pure but always keep the last statement let last_stmt = result.pop(); - // Remove all pure statements at top level + // Remove all pure statements at global level result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); if let Some(stmt) = last_stmt { diff --git a/src/parser.rs b/src/parser.rs index 102e51b9..557e9ec9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -203,6 +203,23 @@ impl Stmt { matches!(self, Stmt::Let(_, _, _)) } + pub fn is_self_terminated(&self) -> bool { + match self { + Stmt::Noop(_) + | Stmt::IfElse(_, _, _) + | Stmt::While(_, _) + | Stmt::Loop(_) + | Stmt::For(_, _, _) + | Stmt::Block(_, _) => true, + + Stmt::Let(_, _, _) + | Stmt::Const(_, _, _) + | Stmt::Expr(_) + | Stmt::Break(_) + | Stmt::ReturnWithVal(_, _, _) => false, + } + } + pub fn position(&self) -> Position { match self { Stmt::Noop(pos) @@ -1913,22 +1930,42 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result (), // empty block - #[cfg(not(feature = "no_function"))] - (Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), - _ => { while input.peek().is_some() { // Parse statements inside the block - statements.push(parse_stmt(input)?); + let stmt = parse_stmt(input)?; - // Notice semicolons are optional - if let Some((Token::SemiColon, _)) = input.peek() { - input.next(); - } + // See if it needs a terminating semicolon + let need_semicolon = !stmt.is_self_terminated(); + statements.push(stmt); + + // End block with right brace if let Some((Token::RightBrace, _)) = input.peek() { break; } + + match input.peek() { + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), + + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); + } + + None => { + return Err(ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + )) + } + } } } } @@ -1959,6 +1996,9 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), + (Token::If, _) => parse_if(input), (Token::While, _) => parse_while(input), (Token::Loop, _) => parse_loop(input), @@ -2097,16 +2137,16 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( +fn parse_global_level<'a, 'e>( input: &mut Peekable>, ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); let mut functions = Vec::::new(); while input.peek().is_some() { - match input.peek().expect("should not be None") { - #[cfg(not(feature = "no_function"))] - (Token::Fn, _) => { + #[cfg(not(feature = "no_function"))] + { + if matches!(input.peek().expect("should not be None"), (Token::Fn, _)) { let f = parse_fn(input)?; // Ensure list is sorted @@ -2114,13 +2154,31 @@ fn parse_top_level<'a, 'e>( Ok(n) => functions[n] = f, // Override previous definition Err(n) => functions.insert(n, f), // New function definition } + + continue; } - _ => statements.push(parse_stmt(input)?), } - // Notice semicolons are optional - if let Some((Token::SemiColon, _)) = input.peek() { - input.next(); + let stmt = parse_stmt(input)?; + + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek() { + None => break, + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), + + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); + } } } @@ -2132,7 +2190,7 @@ pub fn parse<'a, 'e>( engine: &Engine<'e>, scope: &Scope, ) -> Result { - let (statements, functions) = parse_top_level(input)?; + let (statements, functions) = parse_global_level(input)?; Ok( #[cfg(not(feature = "no_optimize"))] From 706e0a0c4cd1ea1945399281acc15eac322248bf Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 16:52:06 +0800 Subject: [PATCH 05/24] Make sure return is not an error. --- src/api.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/api.rs b/src/api.rs index 3c248d5b..3ead651f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -210,23 +210,19 @@ impl<'e> Engine<'e> { Ok(result) } - match eval_ast_internal(self, scope, ast) { - Err(EvalAltResult::Return(out, pos)) => out.downcast::().map(|v| *v).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).to_string(), - pos, - ) - }), - - Ok(out) => out.downcast::().map(|v| *v).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).to_string(), - Position::eof(), - ) - }), - - Err(err) => Err(err), - } + eval_ast_internal(self, scope, ast) + .or_else(|err| match err { + EvalAltResult::Return(out, _) => Ok(out), + _ => Err(err), + }) + .and_then(|out| { + out.downcast::().map(|v| *v).map_err(|a| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name((*a).type_name()).to_string(), + Position::eof(), + ) + }) + }) } /// Evaluate a file, but throw away the result and only return error (if any). @@ -324,7 +320,10 @@ impl<'e> Engine<'e> { self.clear_functions(); } - result + result.or_else(|err| match err { + EvalAltResult::Return(_, _) => Ok(()), + _ => Err(err), + }) } /// Load a list of functions into the Engine. From 6ca39a019ba6a994908c94f43398c04cdbff43c6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 17:33:37 +0800 Subject: [PATCH 06/24] Handle break and return better. --- examples/repl.rs | 17 +++- examples/rhai_runner.rs | 8 +- src/engine.rs | 10 +- src/error.rs | 15 ++- src/optimize.rs | 71 ++++++++++--- src/parser.rs | 217 ++++++++++++++++++++++------------------ src/result.rs | 14 +-- tests/looping.rs | 32 +++--- tests/throw.rs | 2 +- 9 files changed, 244 insertions(+), 142 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 1187d1c9..953ee2a9 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -36,10 +36,18 @@ fn print_error(input: &str, err: EvalAltResult) { ); println!("{}", lines[p.line().unwrap() - 1]); + + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + _ => err.to_string(), + }; + println!( "{}^ {}", padding(" ", p.position().unwrap() - 1), - err.to_string().replace(&pos_text, "") + err_text.replace(&pos_text, "") ); } } @@ -86,7 +94,12 @@ fn main() { .map_err(EvalAltResult::ErrorParsing) .and_then(|r| { ast = Some(r); - engine.consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap()) + engine + .consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap()) + .or_else(|err| match err { + EvalAltResult::Return(_, _) => Ok(()), + err => Err(err), + }) }) { println!(""); diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 4da91feb..2b9b2077 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -31,7 +31,13 @@ fn eprint_error(input: &str, err: EvalAltResult) { // EOF let line = lines.len() - 1; let pos = lines[line - 1].len(); - eprint_line(&lines, line, pos, &err.to_string()); + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + _ => err.to_string(), + }; + eprint_line(&lines, line, pos, &err_text); } p if p.is_none() => { // No position diff --git a/src/engine.rs b/src/engine.rs index 4e75b9f5..7f81befe 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1063,7 +1063,9 @@ impl Engine<'_> { if *guard_val { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), + Err(EvalAltResult::ErrorLoopBreak(_)) => { + return Ok(().into_dynamic()) + } Err(x) => return Err(x), } } else { @@ -1078,7 +1080,7 @@ impl Engine<'_> { Stmt::Loop(body) => loop { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), + Err(EvalAltResult::ErrorLoopBreak(_)) => return Ok(().into_dynamic()), Err(x) => return Err(x), } }, @@ -1097,7 +1099,7 @@ impl Engine<'_> { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => break, + Err(EvalAltResult::ErrorLoopBreak(_)) => break, Err(x) => return Err(x), } } @@ -1109,7 +1111,7 @@ impl Engine<'_> { } // Break statement - Stmt::Break(_) => Err(EvalAltResult::LoopBreak), + Stmt::Break(pos) => Err(EvalAltResult::ErrorLoopBreak(*pos)), // Empty return Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { diff --git a/src/error.rs b/src/error.rs index 9408b950..951425ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -82,12 +82,17 @@ pub enum ParseErrorType { /// A function definition is missing the parameters list. Wrapped value is the function name. #[cfg(not(feature = "no_function"))] FnMissingParams(String), + /// A function definition is missing the body. Wrapped value is the function name. + #[cfg(not(feature = "no_function"))] + FnMissingBody(String), /// Assignment to an inappropriate LHS (left-hand-side) expression. AssignmentToInvalidLHS, /// Assignment to a copy of a value. AssignmentToCopy, /// Assignment to an a constant variable. AssignmentToConstant(String), + /// Break statement not inside a loop. + LoopBreak, } /// Error when parsing a script. @@ -133,10 +138,13 @@ impl ParseError { #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", #[cfg(not(feature = "no_function"))] + ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", + #[cfg(not(feature = "no_function"))] ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression", ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value", - ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable." + ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable.", + ParseErrorType::LoopBreak => "Break statement should only be used inside a loop" } } } @@ -164,6 +172,11 @@ impl fmt::Display for ParseError { write!(f, "Expecting parameters for function '{}'", s)? } + #[cfg(not(feature = "no_function"))] + ParseErrorType::FnMissingBody(ref s) => { + write!(f, "Expecting body statement block for function '{}'", s)? + } + ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) => { write!(f, "{} for {}", self.desc(), s)? } diff --git a/src/optimize.rs b/src/optimize.rs index 05c91304..7f201df5 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -4,7 +4,7 @@ use crate::any::{Any, Dynamic}; use crate::engine::{ Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; -use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; use std::sync::Arc; @@ -117,18 +117,40 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - Stmt::Noop(pos) } Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), - expr => Stmt::While( - Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt, state, false)), - ), + expr => match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement - turn into running the guard expression once + state.set_dirty(); + let mut statements = vec![Stmt::Expr(Box::new(optimize_expr(expr, state)))]; + if preserve_result { + statements.push(Stmt::Noop(pos)) + } + Stmt::Block(statements, pos) + } + stmt => Stmt::While(Box::new(optimize_expr(expr, state)), Box::new(stmt)), + }, + }, + Stmt::Loop(stmt) => match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement + state.set_dirty(); + Stmt::Noop(pos) + } + stmt => Stmt::Loop(Box::new(stmt)), }, - - Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), Stmt::For(id, expr, stmt) => Stmt::For( id, Box::new(optimize_expr(*expr, state)), - Box::new(optimize_stmt(*stmt, state, false)), + Box::new(match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement + state.set_dirty(); + Stmt::Noop(pos) + } + stmt => stmt, + }), ), + Stmt::Let(id, Some(expr), pos) => { Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos) } @@ -149,15 +171,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - optimize_stmt(stmt, state, preserve_result) // Optimize the statement } }) - .enumerate() - .filter(|(i, stmt)| stmt.is_op() || (preserve_result && *i == orig_len - 1)) // Remove no-op's but leave the last one if we need the result - .map(|(_, stmt)| stmt) .collect(); // Remove all raw expression statements that are pure except for the very last statement let last_stmt = if preserve_result { result.pop() } else { None }; - result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); + result.retain(|stmt| !stmt.is_pure()); if let Some(stmt) = last_stmt { result.push(stmt); @@ -193,6 +212,24 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - .collect(); } + // Remove everything following the the first return/throw + let mut dead_code = false; + + result.retain(|stmt| { + if dead_code { + return false; + } + + match stmt { + Stmt::ReturnWithVal(_, _, _) | Stmt::Break(_) => { + dead_code = true; + } + _ => (), + } + + true + }); + if orig_len != result.len() { state.set_dirty(); } @@ -500,7 +537,15 @@ pub fn optimize_ast( OptimizationLevel::Simple | OptimizationLevel::Full => { let pos = fn_def.body.position(); let mut body = optimize(vec![fn_def.body], None, &Scope::new()); - fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); + fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + Stmt::ReturnWithVal(Some(val), ReturnType::Return, _) => { + Stmt::Expr(val) + } + Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { + Stmt::Expr(Box::new(Expr::Unit(pos))) + } + stmt => stmt, + }; } } Arc::new(fn_def) diff --git a/src/parser.rs b/src/parser.rs index 557e9ec9..a96b509f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -191,6 +191,19 @@ pub enum Stmt { } impl Stmt { + pub fn position(&self) -> Position { + match self { + Stmt::Noop(pos) + | Stmt::Let(_, _, pos) + | Stmt::Const(_, _, pos) + | Stmt::Block(_, pos) + | Stmt::Break(pos) + | Stmt::ReturnWithVal(_, _, pos) => *pos, + Stmt::IfElse(expr, _, _) | Stmt::Expr(expr) => expr.position(), + Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(), + } + } + pub fn is_noop(&self) -> bool { matches!(self, Stmt::Noop(_)) } @@ -220,16 +233,21 @@ impl Stmt { } } - pub fn position(&self) -> Position { + pub fn is_pure(&self) -> bool { match self { - Stmt::Noop(pos) - | Stmt::Let(_, _, pos) - | Stmt::Const(_, _, pos) - | Stmt::Block(_, pos) - | Stmt::Break(pos) - | Stmt::ReturnWithVal(_, _, pos) => *pos, - Stmt::IfElse(expr, _, _) | Stmt::Expr(expr) => expr.position(), - Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(), + Stmt::Noop(_) => true, + Stmt::Expr(expr) => expr.is_pure(), + Stmt::IfElse(guard, if_block, Some(else_block)) => { + guard.is_pure() && if_block.is_pure() && else_block.is_pure() + } + Stmt::IfElse(guard, block, None) | Stmt::While(guard, block) => { + guard.is_pure() && block.is_pure() + } + Stmt::Loop(block) => block.is_pure(), + Stmt::For(_, range, block) => range.is_pure() && block.is_pure(), + Stmt::Let(_, _, _) | Stmt::Const(_, _, _) => false, + Stmt::Block(statements, _) => statements.iter().all(Stmt::is_pure), + Stmt::Break(_) | Stmt::ReturnWithVal(_, _, _) => false, } } } @@ -343,6 +361,8 @@ impl Expr { Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(), + Expr::Stmt(stmt, _) => stmt.is_pure(), + expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)), } } @@ -1447,7 +1467,7 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result { let pos = *pos; - return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)); + return parse_block(input, false).map(|block| Expr::Stmt(Box::new(block), pos)); } _ => (), } @@ -1457,38 +1477,39 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(x, pos)), + let mut root_expr = match token + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + #[cfg(not(feature = "no_float"))] + (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), - (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), - (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), - (Token::StringConst(s), pos) => { - can_be_indexed = true; - Ok(Expr::StringConstant(s, pos)) - } - (Token::Identifier(s), pos) => { - can_be_indexed = true; - parse_ident_expr(s, input, pos) - } - (Token::LeftParen, pos) => { - can_be_indexed = true; - parse_paren_expr(input, pos) - } - #[cfg(not(feature = "no_index"))] - (Token::LeftBracket, pos) => { - can_be_indexed = true; - parse_array_expr(input, pos) - } - (Token::True, pos) => Ok(Expr::True(pos)), - (Token::False, pos) => Ok(Expr::False(pos)), - (Token::LexError(le), pos) => Err(ParseError::new(PERR::BadInput(le.to_string()), pos)), - (token, pos) => Err(ParseError::new( - PERR::BadInput(format!("Unexpected '{}'", token.syntax())), - pos, - )), - }?; + (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), + (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), + (Token::StringConst(s), pos) => { + can_be_indexed = true; + Ok(Expr::StringConstant(s, pos)) + } + (Token::Identifier(s), pos) => { + can_be_indexed = true; + parse_ident_expr(s, input, pos) + } + (Token::LeftParen, pos) => { + can_be_indexed = true; + parse_paren_expr(input, pos) + } + #[cfg(not(feature = "no_index"))] + (Token::LeftBracket, pos) => { + can_be_indexed = true; + parse_array_expr(input, pos) + } + (Token::True, pos) => Ok(Expr::True(pos)), + (Token::False, pos) => Ok(Expr::False(pos)), + (Token::LexError(err), pos) => Err(ParseError::new(PERR::BadInput(err.to_string()), pos)), + (token, pos) => Err(ParseError::new( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())), + pos, + )), + }?; if can_be_indexed { // Tail processing all possible indexing @@ -1804,19 +1825,22 @@ fn parse_expr<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_if<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { input.next(); let guard = parse_expr(input)?; - let if_body = parse_block(input)?; + let if_body = parse_block(input, breakable)?; let else_body = if matches!(input.peek(), Some((Token::Else, _))) { input.next(); Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) { - parse_if(input)? + parse_if(input, breakable)? } else { - parse_block(input)? + parse_block(input, breakable)? })) } else { None @@ -1829,7 +1853,7 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); - let body = parse_block(input)?; + let body = parse_block(input, true)?; Ok(Stmt::Loop(Box::new(body))) } @@ -1850,8 +1874,8 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result s, - (Token::LexError(s), pos) => { - return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + (Token::LexError(err), pos) => { + return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; @@ -1865,7 +1889,7 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result( .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? { (Token::Identifier(s), _) => s, - (Token::LexError(s), pos) => { - return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + (Token::LexError(err), pos) => { + return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; @@ -1911,7 +1935,10 @@ fn parse_var<'a>( } } -fn parse_block<'a>(input: &mut Peekable>) -> Result { +fn parse_block<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { let pos = match input .next() .ok_or_else(|| ParseError::new(PERR::MissingLeftBrace, Position::eof()))? @@ -1922,50 +1949,31 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result (), // empty block + while !matches!(input.peek(), Some((Token::RightBrace, _))) { + // Parse statements inside the block + let stmt = parse_stmt(input, breakable)?; - _ => { - while input.peek().is_some() { - // Parse statements inside the block - let stmt = parse_stmt(input)?; + // See if it needs a terminating semicolon + let need_semicolon = !stmt.is_self_terminated(); - // See if it needs a terminating semicolon - let need_semicolon = !stmt.is_self_terminated(); + statements.push(stmt); - statements.push(stmt); + match input.peek() { + None => break, - // End block with right brace - if let Some((Token::RightBrace, _)) = input.peek() { - break; - } + Some((Token::RightBrace, _)) => break, - match input.peek() { - Some((Token::SemiColon, _)) => { - input.next(); - } - Some((_, _)) if !need_semicolon => (), + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), - Some((_, pos)) => { - // Semicolons are not optional between statements - return Err(ParseError::new( - PERR::MissingSemicolon("terminating a statement".into()), - *pos, - )); - } - - None => { - return Err(ParseError::new( - PERR::MissingRightBrace("end of block".into()), - Position::eof(), - )) - } - } + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); } } } @@ -1991,7 +1999,10 @@ fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_stmt<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { match input .peek() .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? @@ -1999,15 +2010,16 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), - (Token::If, _) => parse_if(input), + (Token::If, _) => parse_if(input, breakable), (Token::While, _) => parse_while(input), (Token::Loop, _) => parse_loop(input), (Token::For, _) => parse_for(input), - (Token::Break, pos) => { + (Token::Break, pos) if breakable => { let pos = *pos; input.next(); Ok(Stmt::Break(pos)) } + (Token::Break, pos) => return Err(ParseError::new(PERR::LoopBreak, *pos)), (token @ Token::Return, _) | (token @ Token::Throw, _) => { let return_type = match token { Token::Return => ReturnType::Return, @@ -2028,12 +2040,15 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result { let pos = *pos; - let ret = parse_expr(input)?; - Ok(Stmt::ReturnWithVal(Some(Box::new(ret)), return_type, pos)) + Ok(Stmt::ReturnWithVal( + Some(Box::new(parse_expr(input)?)), + return_type, + pos, + )) } } } - (Token::LeftBrace, _) => parse_block(input), + (Token::LeftBrace, _) => parse_block(input, breakable), (Token::Let, _) => parse_var(input, VariableType::Normal), (Token::Const, _) => parse_var(input, VariableType::Constant), _ => parse_expr_stmt(input), @@ -2127,7 +2142,11 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result parse_block(input, false)?, + Some((_, pos)) => return Err(ParseError::new(PERR::FnMissingBody(name), *pos)), + None => return Err(ParseError::new(PERR::FnMissingBody(name), Position::eof())), + }; Ok(FnDef { name, @@ -2159,7 +2178,7 @@ fn parse_global_level<'a, 'e>( } } - let stmt = parse_stmt(input)?; + let stmt = parse_stmt(input, false)?; let need_semicolon = !stmt.is_self_terminated(); diff --git a/src/result.rs b/src/result.rs index faa8db7c..82487311 100644 --- a/src/result.rs +++ b/src/result.rs @@ -54,8 +54,8 @@ pub enum EvalAltResult { ErrorArithmetic(String, Position), /// Run-time error encountered. Wrapped value is the error message. ErrorRuntime(String, Position), - /// Internal use: Breaking out of loops. - LoopBreak, + /// Breaking out of loops - not an error if within a loop. + ErrorLoopBreak(Position), /// Not an error: Value returned from a script via the `return` keyword. /// Wrapped value is the result value. Return(Dynamic, Position), @@ -97,7 +97,7 @@ impl EvalAltResult { Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorRuntime(_, _) => "Runtime error", - Self::LoopBreak => "[Not Error] Breaks out of loop", + Self::ErrorLoopBreak(_) => "Break statement not inside a loop", Self::Return(_, _) => "[Not Error] Function returns value", } } @@ -125,7 +125,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorRuntime(s, pos) => { write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) } - Self::LoopBreak => write!(f, "{}", desc), + Self::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), Self::ErrorReadingScriptFile(path, err) => { write!(f, "{} '{}': {}", desc, path.display(), err) @@ -199,7 +199,7 @@ impl> From for EvalAltResult { impl EvalAltResult { pub fn position(&self) -> Position { match self { - Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => Position::none(), + Self::ErrorReadingScriptFile(_, _) => Position::none(), Self::ErrorParsing(err) => err.position(), @@ -220,13 +220,14 @@ impl EvalAltResult { | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorRuntime(_, pos) + | Self::ErrorLoopBreak(pos) | Self::Return(_, pos) => *pos, } } pub(crate) fn set_position(&mut self, new_position: Position) { match self { - Self::ErrorReadingScriptFile(_, _) | Self::LoopBreak => (), + Self::ErrorReadingScriptFile(_, _) => (), Self::ErrorParsing(ParseError(_, ref mut pos)) | Self::ErrorFunctionNotFound(_, ref mut pos) @@ -246,6 +247,7 @@ impl EvalAltResult { | Self::ErrorDotExpr(_, ref mut pos) | Self::ErrorArithmetic(_, ref mut pos) | Self::ErrorRuntime(_, ref mut pos) + | Self::ErrorLoopBreak(ref mut pos) | Self::Return(_, ref mut pos) => *pos = new_position, } } diff --git a/tests/looping.rs b/tests/looping.rs index 4baa0aab..651cbe32 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -1,27 +1,29 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_loop() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - assert!(engine.eval::( - r" - let x = 0; - let i = 0; + assert_eq!( + engine.eval::( + r" + let x = 0; + let i = 0; - loop { - if i < 10 { - x = x + i; - i = i + 1; + loop { + if i < 10 { + x = x + i; + i = i + 1; + } else { + break; + } } - else { - break; - } - } - x == 45 + return x; " - )?); + )?, + 45 + ); Ok(()) } diff --git a/tests/throw.rs b/tests/throw.rs index 518f56d8..690d9c6e 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -9,6 +9,6 @@ fn test_throw() { EvalAltResult::ErrorRuntime(s, _) if s == "hello")); assert!(matches!( - engine.eval::<()>(r#"throw;"#).expect_err("expects error"), + engine.eval::<()>(r#"throw"#).expect_err("expects error"), EvalAltResult::ErrorRuntime(s, _) if s == "")); } From dca2a927b5c0498c80e460255bcfe902462a4368 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 17:37:28 +0800 Subject: [PATCH 07/24] Add section on statements and variables. --- README.md | 87 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 09a5d6d4..5032e531 100644 --- a/README.md +++ b/README.md @@ -611,6 +611,27 @@ let /* intruder comment */ name = "Bob"; */ ``` +Statements +---------- + +Statements are terminated by semicolons '`;`' - they are mandatory, except for the _last_ statement where it can be omitted. + +A statement can be used anywhere where an expression is expected. The _last_ statement of a statement block +(enclosed by '`{`' .. '`}`' pairs) is always the return value of the statement. If a statement has no return value +(e.g. variable definitions, assignments) then the value will be `()`. + +```rust +let a = 42; // normal assignment statement +let a = foo(42); // normal function call statement +foo < 42; // normal expression as statement + +let a = { 40 + 2 }; // the value of 'a' is the value of the statement block, which is the value of the last statement +// ^ notice that the last statement does not require an ending semicolon + +4 * 10 + 2 // this is also a statement, which is an expression, with no ending semicolon because + // it is the last statement of the whole block +``` + Variables --------- @@ -619,21 +640,27 @@ Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII let Variable names must start with an ASCII letter or an underscore '`_`', and must contain at least one ASCII letter within. Therefore, names like '`_`', '`_42`' etc. are not legal variable names. Variable names are also case _sensitive_. -Variables are defined using the `let` keyword. +Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. ```rust -let x = 3; // ok -let _x = 42; // ok -let x_ = 42; // also ok -let _x_ = 42; // still ok +let x = 3; // ok +let _x = 42; // ok +let x_ = 42; // also ok +let _x_ = 42; // still ok -let _ = 123; // syntax error - illegal variable name -let _9 = 9; // syntax error - illegal variable name +let _ = 123; // syntax error - illegal variable name +let _9 = 9; // syntax error - illegal variable name -let x = 42; // variable is 'x', lower case -let X = 123; // variable is 'X', upper case +let x = 42; // variable is 'x', lower case +let X = 123; // variable is 'X', upper case x == 42; X == 123; + +{ + let x = 999; // local variable 'x' shadows the 'x' in parent block + x == 999; // access to local 'x' +} +x == 42; // the parent block's 'x' is not changed ``` Constants @@ -1118,7 +1145,7 @@ regardless of whether it is terminated with a semicolon `;`. This is different f ```rust fn add(x, y) { - x + y; // value of the last statement is used as the function's return value + x + y; // value of the last statement (no need for ending semicolon) is used as the return value } fn add2(x) { @@ -1345,6 +1372,10 @@ An engine's optimization level is set via a call to `set_optimization_level`: engine.set_optimization_level(rhai::OptimizationLevel::Full); ``` +When the optimization level is [`OptimizationLevel::Full`], the engine assumes all functions to be _pure_ and will _eagerly_ +evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators +(which are implemented as functions). For instance, the same example above: + ```rust // When compiling the following with OptimizationLevel::Full... @@ -1356,18 +1387,38 @@ if DECISION == 1 { // is a function call to the '==' function, and it retur print("boo!"); // this block is eliminated because it is never reached } -print("hello!"); // <- the above is equivalent to this +print("hello!"); // <- the above is equivalent to this ('print' and 'debug' are handled specially) ``` -### Side effect considerations +Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result. +This does not happen with `OptimizationLevel::Simple` which doesn't assume all functions to be _pure_. -All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe. -Beware, however, that if custom functions are registered, they'll also be called. -If custom functions are registered to replace built-in operator functions, the custom functions will be called -and _may_ cause side-effects. +```rust +// When compiling the following with OptimizationLevel::Full... -Therefore, when using [`OptimizationLevel::Full`], it is recommended that registrations of custom functions be held off -until _after_ the compilation process. +let x = (1 + 2) * 3 - 4 / 5 % 6; // <- will be replaced by 'let x = 9' +let y = (1 > 2) || (3 <= 4); // <- will be replaced by 'let y = true' +``` + +### Function side effect considerations + +All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state +nor cause side any effects, with the exception of `print` and `debug` which are handled specially) so using [`OptimizationLevel::Full`] +is usually quite safe _unless_ you register your own types and functions. + +If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block). +If custom functions are registered to replace built-in operators, they will also be called when the operators are used (in an `if` +statement, for example) and cause side-effects. + +### Function volatility considerations + +Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ on the external +environment and not _pure_. A perfect example is a function that gets the current time - obviously each run will return a different value! +The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_, so when it finds constant arguments. +This may cause the script to behave differently from the intended semantics because essentially the result of each function call will +always be the same value. + +Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions. ### Subtle semantic changes From ef8d428f42164c537122cb6f7630d95132d31f05 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 10:36:50 +0800 Subject: [PATCH 08/24] Add code comments. --- src/any.rs | 10 +-- src/builtin.rs | 74 +++++++++++---- src/optimize.rs | 235 ++++++++++++++++++++++++++++-------------------- src/parser.rs | 218 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 385 insertions(+), 152 deletions(-) diff --git a/src/any.rs b/src/any.rs index 96fe099b..5f22231f 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,7 +1,7 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. use std::{ - any::{type_name, TypeId}, + any::{type_name, Any as StdAny, TypeId}, fmt, }; @@ -12,7 +12,7 @@ pub type Variant = dyn Any; pub type Dynamic = Box; /// A trait covering any type. -pub trait Any: std::any::Any { +pub trait Any: StdAny { /// Get the `TypeId` of this type. fn type_id(&self) -> TypeId; @@ -22,12 +22,12 @@ pub trait Any: std::any::Any { /// Convert into `Dynamic`. fn into_dynamic(&self) -> Dynamic; - /// This type may only be implemented by `rhai`. + /// This trait may only be implemented by `rhai`. #[doc(hidden)] fn _closed(&self) -> _Private; } -impl Any for T { +impl Any for T { fn type_id(&self) -> TypeId { TypeId::of::() } @@ -90,7 +90,7 @@ pub trait AnyExt: Sized { /// Get a copy of a `Dynamic` value as a specific type. fn downcast(self) -> Result, Self>; - /// This type may only be implemented by `rhai`. + /// This trait may only be implemented by `rhai`. #[doc(hidden)] fn _closed(&self) -> _Private; } diff --git a/src/builtin.rs b/src/builtin.rs index b85cf306..9679ddf5 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -5,7 +5,7 @@ use crate::any::Any; #[cfg(not(feature = "no_index"))] use crate::engine::Array; use crate::engine::Engine; -use crate::fn_register::{RegisterFn, RegisterResultFn}; +use crate::fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; use crate::parser::{Position, INT}; use crate::result::EvalAltResult; @@ -52,6 +52,7 @@ macro_rules! reg_op_result1 { impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { + /// Checked add #[cfg(not(feature = "unchecked"))] fn add(x: T, y: T) -> Result { x.checked_add(&y).ok_or_else(|| { @@ -61,6 +62,7 @@ impl Engine<'_> { ) }) } + /// Checked subtract #[cfg(not(feature = "unchecked"))] fn sub(x: T, y: T) -> Result { x.checked_sub(&y).ok_or_else(|| { @@ -70,6 +72,7 @@ impl Engine<'_> { ) }) } + /// Checked multiply #[cfg(not(feature = "unchecked"))] fn mul(x: T, y: T) -> Result { x.checked_mul(&y).ok_or_else(|| { @@ -79,11 +82,13 @@ impl Engine<'_> { ) }) } + /// Checked divide #[cfg(not(feature = "unchecked"))] fn div(x: T, y: T) -> Result where T: Display + CheckedDiv + PartialEq + Zero, { + // Detect division by zero if y == T::zero() { return Err(EvalAltResult::ErrorArithmetic( format!("Division by zero: {} / {}", x, y), @@ -98,6 +103,7 @@ impl Engine<'_> { ) }) } + /// Checked negative - e.g. -(i32::MIN) will overflow i32::MAX #[cfg(not(feature = "unchecked"))] fn neg(x: T) -> Result { x.checked_neg().ok_or_else(|| { @@ -107,6 +113,7 @@ impl Engine<'_> { ) }) } + /// Checked absolute #[cfg(not(feature = "unchecked"))] fn abs(x: T) -> Result { // FIX - We don't use Signed::abs() here because, contrary to documentation, it panics @@ -122,26 +129,32 @@ impl Engine<'_> { }) } } + /// Unchecked add - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn add_u(x: T, y: T) -> ::Output { x + y } + /// Unchecked subtract - may panic on underflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn sub_u(x: T, y: T) -> ::Output { x - y } + /// Unchecked multiply - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn mul_u(x: T, y: T) -> ::Output { x * y } + /// Unchecked divide - may panic when dividing by zero #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn div_u(x: T, y: T) -> ::Output { x / y } + /// Unchecked negative - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn neg_u(x: T) -> ::Output { -x } + /// Unchecked absolute - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn abs_u(x: T) -> ::Output where @@ -154,6 +167,9 @@ impl Engine<'_> { x.into() } } + + // Comparison operators + fn lt(x: T, y: T) -> bool { x < y } @@ -172,6 +188,9 @@ impl Engine<'_> { fn ne(x: T, y: T) -> bool { x != y } + + // Logic operators + fn and(x: bool, y: bool) -> bool { x && y } @@ -181,6 +200,9 @@ impl Engine<'_> { fn not(x: bool) -> bool { !x } + + // Bit operators + fn binary_and(x: T, y: T) -> ::Output { x & y } @@ -190,8 +212,11 @@ impl Engine<'_> { fn binary_xor(x: T, y: T) -> ::Output { x ^ y } + + /// Checked left-shift #[cfg(not(feature = "unchecked"))] fn shl(x: T, y: INT) -> Result { + // Cannot shift by a negative number of bits if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Left-shift by a negative number: {} << {}", x, y), @@ -201,13 +226,15 @@ impl Engine<'_> { CheckedShl::checked_shl(&x, y as u32).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Left-shift overflow: {} << {}", x, y), + format!("Left-shift by too many bits: {} << {}", x, y), Position::none(), ) }) } + /// Checked right-shift #[cfg(not(feature = "unchecked"))] fn shr(x: T, y: INT) -> Result { + // Cannot shift by a negative number of bits if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Right-shift by a negative number: {} >> {}", x, y), @@ -217,44 +244,49 @@ impl Engine<'_> { CheckedShr::checked_shr(&x, y as u32).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Right-shift overflow: {} % {}", x, y), + format!("Right-shift by too many bits: {} % {}", x, y), Position::none(), ) }) } + /// Unchecked left-shift - may panic if shifting by a negative number of bits #[cfg(feature = "unchecked")] fn shl_u>(x: T, y: T) -> >::Output { x.shl(y) } + /// Unchecked right-shift - may panic if shifting by a negative number of bits #[cfg(feature = "unchecked")] fn shr_u>(x: T, y: T) -> >::Output { x.shr(y) } + /// Checked modulo #[cfg(not(feature = "unchecked"))] fn modulo(x: T, y: T) -> Result { x.checked_rem(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Modulo division overflow: {} % {}", x, y), + format!("Modulo division by zero or overflow: {} % {}", x, y), Position::none(), ) }) } + /// Unchecked modulo - may panic if dividing by zero #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn modulo_u(x: T, y: T) -> ::Output { x % y } + /// Checked power #[cfg(not(feature = "unchecked"))] - fn pow_i_i_u(x: INT, y: INT) -> Result { + fn pow_i_i(x: INT, y: INT) -> Result { #[cfg(not(feature = "only_i32"))] { if y > (u32::MAX as INT) { Err(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), + format!("Integer raised to too large an index: {} ~ {}", x, y), Position::none(), )) } else if y < 0 { Err(EvalAltResult::ErrorArithmetic( - format!("Power underflow: {} ~ {}", x, y), + format!("Integer raised to a negative index: {} ~ {}", x, y), Position::none(), )) } else { @@ -271,7 +303,7 @@ impl Engine<'_> { { if y < 0 { Err(EvalAltResult::ErrorArithmetic( - format!("Power underflow: {} ~ {}", x, y), + format!("Integer raised to a negative index: {} ~ {}", x, y), Position::none(), )) } else { @@ -284,29 +316,34 @@ impl Engine<'_> { } } } + /// Unchecked integer power - may panic on overflow or if the power index is too high (> u32::MAX) #[cfg(feature = "unchecked")] - fn pow_i_i(x: INT, y: INT) -> INT { + fn pow_i_i_u(x: INT, y: INT) -> INT { x.pow(y as u32) } + /// Floating-point power - always well-defined #[cfg(not(feature = "no_float"))] fn pow_f_f(x: FLOAT, y: FLOAT) -> FLOAT { x.powf(y) } + /// Checked power #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_float"))] - fn pow_f_i_u(x: FLOAT, y: INT) -> Result { + fn pow_f_i(x: FLOAT, y: INT) -> Result { + // Raise to power that is larger than an i32 if y > (i32::MAX as INT) { return Err(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), + format!("Number raised to too large an index: {} ~ {}", x, y), Position::none(), )); } Ok(x.powi(y as i32)) } + /// Unchecked power - may be incorrect if the power index is too high (> i32::MAX) #[cfg(feature = "unchecked")] #[cfg(not(feature = "no_float"))] - fn pow_f_i(x: FLOAT, y: INT) -> FLOAT { + fn pow_f_i_u(x: FLOAT, y: INT) -> FLOAT { x.powi(y as i32) } @@ -390,8 +427,10 @@ impl Engine<'_> { } } + // `&&` and `||` are treated specially as they short-circuit //reg_op!(self, "||", or, bool); //reg_op!(self, "&&", and, bool); + reg_op!(self, "|", or, bool); reg_op!(self, "&", and, bool); @@ -445,18 +484,18 @@ impl Engine<'_> { #[cfg(not(feature = "unchecked"))] { - self.register_result_fn("~", pow_i_i_u); + self.register_result_fn("~", pow_i_i); #[cfg(not(feature = "no_float"))] - self.register_result_fn("~", pow_f_i_u); + self.register_result_fn("~", pow_f_i); } #[cfg(feature = "unchecked")] { - self.register_fn("~", pow_i_i); + self.register_fn("~", pow_i_i_u); #[cfg(not(feature = "no_float"))] - self.register_fn("~", pow_f_i); + self.register_fn("~", pow_f_i_u); } { @@ -625,9 +664,6 @@ macro_rules! reg_fn2y { impl Engine<'_> { #[cfg(not(feature = "no_stdlib"))] pub(crate) fn register_stdlib(&mut self) { - #[cfg(not(feature = "no_index"))] - use crate::fn_register::RegisterDynamicFn; - #[cfg(not(feature = "no_float"))] { // Advanced math functions diff --git a/src/optimize.rs b/src/optimize.rs index 7f201df5..30d89abf 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -9,50 +9,54 @@ use crate::scope::{Scope, ScopeEntry, VariableType}; use std::sync::Arc; -/// Level of optimization performed +/// Level of optimization performed. #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] pub enum OptimizationLevel { - /// No optimization performed + /// No optimization performed. None, - /// Only perform simple optimizations without evaluating functions + /// Only perform simple optimizations without evaluating functions. Simple, /// Full optimizations performed, including evaluating functions. - /// Take care that this may cause side effects. + /// Take care that this may cause side effects as it essentially assumes that all functions are pure. Full, } +/// Mutable state throughout an optimization pass. struct State<'a> { + /// Has the AST been changed during this pass? changed: bool, + /// Collection of constants to use for eager function evaluations. constants: Vec<(String, Expr)>, - engine: Option<&'a Engine<'a>>, + /// An `Engine` instance for eager function evaluation. + engine: &'a Engine<'a>, } impl State<'_> { - pub fn new() -> Self { - State { - changed: false, - constants: vec![], - engine: None, - } - } + /// Reset the state from dirty to clean. pub fn reset(&mut self) { self.changed = false; } + /// Set the AST state to be dirty (i.e. changed). pub fn set_dirty(&mut self) { self.changed = true; } + /// Is the AST dirty (i.e. changed)? pub fn is_dirty(&self) -> bool { self.changed } + /// Does a constant exist? pub fn contains_constant(&self, name: &str) -> bool { self.constants.iter().any(|(n, _)| n == name) } + /// Prune the list of constants back to a specified size. pub fn restore_constants(&mut self, len: usize) { self.constants.truncate(len) } + /// Add a new constant to the list. pub fn push_constant(&mut self, name: &str, value: Expr) { self.constants.push((name.to_string(), value)) } + /// Look up a constant from the list. pub fn find_constant(&self, name: &str) -> Option<&Expr> { for (n, expr) in self.constants.iter().rev() { if n == name { @@ -64,60 +68,68 @@ impl State<'_> { } } +/// Optimize a statement. fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt { match stmt { - Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => { + // if expr { Noop } + Stmt::IfElse(expr, if_block, None) if matches!(*if_block, Stmt::Noop(_)) => { state.set_dirty(); let pos = expr.position(); let expr = optimize_expr(*expr, state); - if matches!(expr, Expr::False(_) | Expr::True(_)) { - Stmt::Noop(stmt1.position()) + if preserve_result { + // -> { expr, Noop } + Stmt::Block(vec![Stmt::Expr(Box::new(expr)), *if_block], pos) } else { - let stmt = Stmt::Expr(Box::new(expr)); - - if preserve_result { - Stmt::Block(vec![stmt, *stmt1], pos) - } else { - stmt - } + // -> expr + Stmt::Expr(Box::new(expr)) } } - - Stmt::IfElse(expr, stmt1, None) => match *expr { + // if expr { if_block } + Stmt::IfElse(expr, if_block, None) => match *expr { + // if false { if_block } -> Noop Expr::False(pos) => { state.set_dirty(); Stmt::Noop(pos) } - Expr::True(_) => optimize_stmt(*stmt1, state, true), + // if true { if_block } -> if_block + Expr::True(_) => optimize_stmt(*if_block, state, true), + // if expr { if_block } expr => Stmt::IfElse( Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt1, state, true)), + Box::new(optimize_stmt(*if_block, state, true)), None, ), }, - - Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { - Expr::False(_) => optimize_stmt(*stmt2, state, true), - Expr::True(_) => optimize_stmt(*stmt1, state, true), + // if expr { if_block } else { else_block } + Stmt::IfElse(expr, if_block, Some(else_block)) => match *expr { + // if false { if_block } else { else_block } -> else_block + Expr::False(_) => optimize_stmt(*else_block, state, true), + // if true { if_block } else { else_block } -> if_block + Expr::True(_) => optimize_stmt(*if_block, state, true), + // if expr { if_block } else { else_block } expr => Stmt::IfElse( Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt1, state, true)), - match optimize_stmt(*stmt2, state, true) { - stmt if stmt.is_noop() => None, + Box::new(optimize_stmt(*if_block, state, true)), + match optimize_stmt(*else_block, state, true) { + stmt if matches!(stmt, Stmt::Noop(_)) => None, // Noop -> no else block stmt => Some(Box::new(stmt)), }, ), }, - - Stmt::While(expr, stmt) => match *expr { + // while expr { block } + Stmt::While(expr, block) => match *expr { + // while false { block } -> Noop Expr::False(pos) => { state.set_dirty(); Stmt::Noop(pos) } - Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), - expr => match optimize_stmt(*stmt, state, false) { + // while true { block } -> loop { block } + Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*block, state, false))), + // while expr { block } + expr => match optimize_stmt(*block, state, false) { + // while expr { break; } -> { expr; } Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); @@ -127,48 +139,50 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - } Stmt::Block(statements, pos) } + // while expr { block } stmt => Stmt::While(Box::new(optimize_expr(expr, state)), Box::new(stmt)), }, }, - Stmt::Loop(stmt) => match optimize_stmt(*stmt, state, false) { + // loop { block } + Stmt::Loop(block) => match optimize_stmt(*block, state, false) { + // loop { break; } -> Noop Stmt::Break(pos) => { // Only a single break statement state.set_dirty(); Stmt::Noop(pos) } + // loop { block } stmt => Stmt::Loop(Box::new(stmt)), }, - Stmt::For(id, expr, stmt) => Stmt::For( + // for id in expr { block } + Stmt::For(id, expr, block) => Stmt::For( id, Box::new(optimize_expr(*expr, state)), - Box::new(match optimize_stmt(*stmt, state, false) { - Stmt::Break(pos) => { - // Only a single break statement - state.set_dirty(); - Stmt::Noop(pos) - } - stmt => stmt, - }), + Box::new(optimize_stmt(*block, state, false)), ), - + // let id = expr; Stmt::Let(id, Some(expr), pos) => { Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos) } + // let id; Stmt::Let(_, None, _) => stmt, + // { block } + Stmt::Block(block, pos) => { + let orig_len = block.len(); // Original number of statements in the block, for change detection + let orig_constants_len = state.constants.len(); // Original number of constants in the state, for restore later - Stmt::Block(statements, pos) => { - let orig_len = statements.len(); - let orig_constants_len = state.constants.len(); - - let mut result: Vec<_> = statements - .into_iter() // For each statement + // Optimize each statement in the block + let mut result: Vec<_> = block + .into_iter() .map(|stmt| { if let Stmt::Const(name, value, pos) = stmt { + // Add constant into the state state.push_constant(&name, *value); state.set_dirty(); Stmt::Noop(pos) // No need to keep constants } else { - optimize_stmt(stmt, state, preserve_result) // Optimize the statement + // Optimize the statement + optimize_stmt(stmt, state, preserve_result) } }) .collect(); @@ -203,11 +217,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - result.push(Stmt::Noop(pos)) } + // Optimize all the statements again result = result .into_iter() .rev() .enumerate() - .map(|(i, s)| optimize_stmt(s, state, i == 0)) // Optimize all other statements again + .map(|(i, s)| optimize_stmt(s, state, i == 0)) .rev() .collect(); } @@ -230,10 +245,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - true }); + // Change detection if orig_len != result.len() { state.set_dirty(); } + // Pop the stack and remove all the local constants state.restore_constants(orig_constants_len); match result[..] { @@ -250,44 +267,54 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - _ => Stmt::Block(result, pos), } } - + // expr; Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))), - + // return expr; Stmt::ReturnWithVal(Some(expr), is_return, pos) => { Stmt::ReturnWithVal(Some(Box::new(optimize_expr(*expr, state))), is_return, pos) } - + // All other statements - skip stmt => stmt, } } +/// Optimize an expression. fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { + // These keywords are handled specially const SKIP_FUNC_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_DUMP_AST]; match expr { + // ( stmt ) Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { + // ( Noop ) -> () Stmt::Noop(_) => { state.set_dirty(); Expr::Unit(pos) } + // ( expr ) -> expr Stmt::Expr(expr) => { state.set_dirty(); *expr } + // ( stmt ) stmt => Expr::Stmt(Box::new(stmt), pos), }, - Expr::Assignment(id1, expr1, pos1) => match *expr1 { - Expr::Assignment(id2, expr2, pos2) => match (*id1, *id2) { - (Expr::Variable(var1, _), Expr::Variable(var2, _)) if var1 == var2 => { + // id = expr + Expr::Assignment(id, expr, pos) => match *expr { + //id = id2 = expr2 + Expr::Assignment(id2, expr2, pos2) => match (*id, *id2) { + // var = var = expr2 -> var = expr2 + (Expr::Variable(var, _), Expr::Variable(var2, _)) if var == var2 => { // Assignment to the same variable - fold state.set_dirty(); Expr::Assignment( - Box::new(Expr::Variable(var1, pos1)), + Box::new(Expr::Variable(var, pos)), Box::new(optimize_expr(*expr2, state)), - pos1, + pos, ) } + // id1 = id2 = expr2 (id1, id2) => Expr::Assignment( Box::new(id1), Box::new(Expr::Assignment( @@ -295,19 +322,23 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Box::new(optimize_expr(*expr2, state)), pos2, )), - pos1, + pos, ), }, - expr => Expr::Assignment(id1, Box::new(optimize_expr(expr, state)), pos1), + // id = expr + expr => Expr::Assignment(id, Box::new(optimize_expr(expr, state)), pos), }, + // lhs.rhs Expr::Dot(lhs, rhs, pos) => Expr::Dot( Box::new(optimize_expr(*lhs, state)), Box::new(optimize_expr(*rhs, state)), pos, ), + // lhs[rhs] #[cfg(not(feature = "no_index"))] Expr::Index(lhs, rhs, pos) => match (*lhs, *rhs) { + // array[int] (Expr::Array(mut items, _), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < items.len() && items.iter().all(|x| x.is_pure()) => { @@ -316,6 +347,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); items.remove(i as usize) } + // string[int] (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < s.chars().count() => { @@ -323,14 +355,14 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); Expr::CharConstant(s.chars().nth(i as usize).expect("should get char"), pos) } - + // lhs[rhs] (lhs, rhs) => Expr::Index( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), pos, ), }, - + // [ items .. ] #[cfg(not(feature = "no_index"))] Expr::Array(items, pos) => { let orig_len = items.len(); @@ -346,38 +378,47 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::Array(items, pos) } - + // lhs && rhs Expr::And(lhs, rhs) => match (*lhs, *rhs) { + // true && rhs -> rhs (Expr::True(_), rhs) => { state.set_dirty(); rhs } + // false && rhs -> false (Expr::False(pos), _) => { state.set_dirty(); Expr::False(pos) } + // lhs && true -> lhs (lhs, Expr::True(_)) => { state.set_dirty(); - lhs + optimize_expr(lhs, state) } + // lhs && rhs (lhs, rhs) => Expr::And( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), ), }, + // lhs || rhs Expr::Or(lhs, rhs) => match (*lhs, *rhs) { + // false || rhs -> rhs (Expr::False(_), rhs) => { state.set_dirty(); rhs } + // true || rhs -> true (Expr::True(pos), _) => { state.set_dirty(); Expr::True(pos) } + // lhs || false (lhs, Expr::False(_)) => { state.set_dirty(); - lhs + optimize_expr(lhs, state) } + // lhs || rhs (lhs, rhs) => Expr::Or( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), @@ -388,24 +429,23 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::FunctionCall(id, args, def_value, pos) if SKIP_FUNC_KEYWORDS.contains(&id.as_str())=> Expr::FunctionCall(id, args, def_value, pos), - // Actually call function to optimize it + // Eagerly call functions Expr::FunctionCall(id, args, def_value, pos) - if state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations + if state.engine.optimization_level == OptimizationLevel::Full // full optimizations && args.iter().all(|expr| expr.is_constant()) // all arguments are constants => { - let engine = state.engine.expect("engine should be Some"); let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure let arg_for_type_of = if id == KEYWORD_TYPE_OF && call_args.len() == 1 { - engine.map_type_name(call_args[0].type_name()) + state.engine.map_type_name(call_args[0].type_name()) } else { "" }; - engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| + state.engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| r.or_else(|| { if !arg_for_type_of.is_empty() { // Handle `type_of()` @@ -421,10 +461,10 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { }) ).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) } - // Optimize the function call arguments + // id(args ..) -> optimize function call arguments Expr::FunctionCall(id, args, def_value, pos) => Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), - + // constant-name Expr::Variable(ref name, _) if state.contains_constant(name) => { state.set_dirty(); @@ -434,28 +474,25 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { .expect("should find constant in scope!") .clone() } - + // All other expressions - skip expr => expr, } } -pub(crate) fn optimize<'a>( - statements: Vec, - engine: Option<&Engine<'a>>, - scope: &Scope, -) -> Vec { +pub(crate) fn optimize<'a>(statements: Vec, engine: &Engine<'a>, scope: &Scope) -> Vec { // If optimization level is None then skip optimizing - if engine - .map(|eng| eng.optimization_level == OptimizationLevel::None) - .unwrap_or(false) - { + if engine.optimization_level == OptimizationLevel::None { return statements; } // Set up the state - let mut state = State::new(); - state.engine = engine; + let mut state = State { + changed: false, + constants: vec![], + engine, + }; + // Add constants from the scope into the state scope .iter() .filter(|ScopeEntry { var_type, expr, .. }| { @@ -472,9 +509,9 @@ pub(crate) fn optimize<'a>( let orig_constants_len = state.constants.len(); - // Optimization loop let mut result = statements; + // Optimization loop loop { state.reset(); state.restore_constants(orig_constants_len); @@ -492,7 +529,7 @@ pub(crate) fn optimize<'a>( } else { // Keep all variable declarations at this level // and always keep the last return value - let keep = stmt.is_var() || i == num_statements - 1; + let keep = matches!(stmt, Stmt::Let(_, _, _)) || i == num_statements - 1; optimize_stmt(stmt, &mut state, keep) } @@ -517,6 +554,7 @@ pub(crate) fn optimize<'a>( result } +/// Optimize an AST. pub fn optimize_ast( engine: &Engine, scope: &Scope, @@ -526,8 +564,8 @@ pub fn optimize_ast( AST( match engine.optimization_level { OptimizationLevel::None => statements, - OptimizationLevel::Simple => optimize(statements, None, &scope), - OptimizationLevel::Full => optimize(statements, Some(engine), &scope), + OptimizationLevel::Simple => optimize(statements, engine, &scope), + OptimizationLevel::Full => optimize(statements, engine, &scope), }, functions .into_iter() @@ -536,14 +574,21 @@ pub fn optimize_ast( OptimizationLevel::None => (), OptimizationLevel::Simple | OptimizationLevel::Full => { let pos = fn_def.body.position(); - let mut body = optimize(vec![fn_def.body], None, &Scope::new()); + + // Optimize the function body + let mut body = optimize(vec![fn_def.body], engine, &Scope::new()); + + // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + // { return val; } -> val Stmt::ReturnWithVal(Some(val), ReturnType::Return, _) => { Stmt::Expr(val) } + // { return; } -> () Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { Stmt::Expr(Box::new(Expr::Unit(pos))) } + // All others stmt => stmt, }; } diff --git a/src/parser.rs b/src/parser.rs index a96b509f..5f7da436 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -19,13 +19,13 @@ use std::{ #[cfg(not(feature = "only_i32"))] pub type INT = i64; -/// The system integer type +/// The system integer type. /// /// If the `only_i32` feature is not enabled, this will be `i64` instead. #[cfg(feature = "only_i32")] pub type INT = i32; -/// The system floating-point type +/// The system floating-point type. #[cfg(not(feature = "no_float"))] pub type FLOAT = f64; @@ -35,7 +35,9 @@ type PERR = ParseErrorType; /// A location (line number + character position) in the input script. #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] pub struct Position { + /// Line number - 0 = none, MAX = EOF line: usize, + /// Character position - 0 = BOL, MAX = EOF pos: usize, } @@ -152,45 +154,69 @@ impl fmt::Debug for Position { #[derive(Debug, Clone)] pub struct AST(pub(crate) Vec, pub(crate) Vec>); +/// A script-function definition. #[derive(Debug, Clone)] pub struct FnDef { + /// Function name. pub name: String, + /// Names of function parameters. pub params: Vec, + /// Function body. pub body: Stmt, + /// Position of the function definition. pub pos: Position, } impl FnDef { + /// Function to order two FnDef records, for binary search. pub fn compare(&self, name: &str, params_len: usize) -> Ordering { + // First order by name match self.name.as_str().cmp(name) { + // Then by number of parameters Ordering::Equal => self.params.len().cmp(¶ms_len), order => order, } } } +/// `return`/`throw` statement. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum ReturnType { + /// `return` statement. Return, + /// `throw` statement. Exception, } +/// A statement. #[derive(Debug, Clone)] pub enum Stmt { + /// No-op. Noop(Position), + /// if expr { stmt } else { stmt } IfElse(Box, Box, Option>), + /// while expr { stmt } While(Box, Box), + /// loop { stmt } Loop(Box), + /// for id in expr { stmt } For(String, Box, Box), + /// let id = expr Let(String, Option>, Position), + /// const id = expr Const(String, Box, Position), + /// { stmt; ... } Block(Vec, Position), + /// { stmt } Expr(Box), + /// break Break(Position), + /// `return`/`throw` ReturnWithVal(Option>, ReturnType, Position), } impl Stmt { + /// Get the `Position` of this statement. pub fn position(&self) -> Position { match self { Stmt::Noop(pos) @@ -204,18 +230,7 @@ impl Stmt { } } - pub fn is_noop(&self) -> bool { - matches!(self, Stmt::Noop(_)) - } - - pub fn is_op(&self) -> bool { - !matches!(self, Stmt::Noop(_)) - } - - pub fn is_var(&self) -> bool { - matches!(self, Stmt::Let(_, _, _)) - } - + /// Is this statement self-terminated (i.e. no need for a semicolon terminator)? pub fn is_self_terminated(&self) -> bool { match self { Stmt::Noop(_) @@ -233,6 +248,7 @@ impl Stmt { } } + /// Is this statement _pure_? pub fn is_pure(&self) -> bool { match self { Stmt::Noop(_) => true, @@ -252,31 +268,54 @@ impl Stmt { } } +/// An expression. #[derive(Debug, Clone)] pub enum Expr { + /// Integer constant. IntegerConstant(INT, Position), + /// Floating-point constant. #[cfg(not(feature = "no_float"))] FloatConstant(FLOAT, Position), - Variable(String, Position), - Property(String, Position), + /// Character constant. CharConstant(char, Position), + /// String constant. StringConstant(String, Position), + /// Variable access. + Variable(String, Position), + /// Property access. + Property(String, Position), + /// { stmt } Stmt(Box, Position), + /// func(expr, ... ) FunctionCall(String, Vec, Option, Position), + /// expr = expr Assignment(Box, Box, Position), + /// lhs.rhs Dot(Box, Box, Position), + /// expr[expr] #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), #[cfg(not(feature = "no_index"))] + /// [ expr, ... ] Array(Vec, Position), + /// lhs && rhs And(Box, Box), + /// lhs || rhs Or(Box, Box), + /// true True(Position), + /// false False(Position), + /// () Unit(Position), } impl Expr { + /// Get the `Dynamic` value of a constant expression. + /// + /// # Panics + /// + /// Panics when the expression is not constant. pub fn get_constant_value(&self) -> Dynamic { match self { Expr::IntegerConstant(i, _) => i.into_dynamic(), @@ -300,6 +339,11 @@ impl Expr { } } + /// Get the display value of a constant expression. + /// + /// # Panics + /// + /// Panics when the expression is not constant. pub fn get_constant_str(&self) -> String { match self { Expr::IntegerConstant(i, _) => i.to_string(), @@ -319,6 +363,7 @@ impl Expr { } } + /// Get the `Position` of the expression. pub fn position(&self) -> Position { match self { Expr::IntegerConstant(_, pos) @@ -348,7 +393,7 @@ impl Expr { } } - /// Is this expression pure? + /// Is the expression pure? /// /// A pure expression has no side effects. pub fn is_pure(&self) -> bool { @@ -367,6 +412,7 @@ impl Expr { } } + /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { Expr::IntegerConstant(_, _) @@ -379,6 +425,7 @@ impl Expr { #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, _) => true, + // An array literal is constant if all items are constant #[cfg(not(feature = "no_index"))] Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), @@ -387,6 +434,7 @@ impl Expr { } } +/// Tokens. #[derive(Debug, PartialEq, Clone)] pub enum Token { IntegerConstant(INT), @@ -460,6 +508,7 @@ pub enum Token { } impl Token { + /// Get the syntax of the token. pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; @@ -540,8 +589,8 @@ impl Token { } } - // if another operator is after these, it's probably an unary operator - // not sure about fn's name + // If another operator is after these, it's probably an unary operator + // (not sure about fn name). pub fn is_next_unary(&self) -> bool { use self::Token::*; @@ -602,6 +651,7 @@ impl Token { } } + /// Get the precedence number of the token. pub fn precedence(&self) -> u8 { match self { Self::Equals @@ -642,8 +692,10 @@ impl Token { } } + /// Does an expression bind to the right (instead of left)? pub fn is_bind_right(&self) -> bool { match self { + // Assignments bind to the right Self::Equals | Self::PlusAssign | Self::MinusAssign @@ -657,6 +709,7 @@ impl Token { | Self::ModuloAssign | Self::PowerOfAssign => true, + // Property access binds to the right Self::Period => true, _ => false, @@ -664,24 +717,36 @@ impl Token { } } +/// An iterator on a `Token` stream. pub struct TokenIterator<'a> { + /// The last token seen. last: Token, + /// Current position. pos: Position, + /// The input characters stream. char_stream: Peekable>, } impl<'a> TokenIterator<'a> { + /// Move the current position one character ahead. fn advance(&mut self) { self.pos.advance(); } + /// Move the current position back one character. + /// + /// # Panics + /// + /// Panics if already at the beginning of a line - cannot rewind to the previous line. fn rewind(&mut self) { self.pos.rewind(); } + /// Move the current position to the next line. fn new_line(&mut self) { self.pos.new_line() } - pub fn parse_string_const( + /// Parse a string literal wrapped by `enclosing_char`. + pub fn parse_string_literal( &mut self, enclosing_char: char, ) -> Result { @@ -693,25 +758,31 @@ impl<'a> TokenIterator<'a> { self.advance(); match next_char.ok_or((LERR::UnterminatedString, Position::eof()))? { + // \... '\\' if escape.is_empty() => { escape.push('\\'); } + // \\ '\\' if !escape.is_empty() => { escape.clear(); result.push('\\'); } + // \t 't' if !escape.is_empty() => { escape.clear(); result.push('\t'); } + // \n 'n' if !escape.is_empty() => { escape.clear(); result.push('\n'); } + // \r 'r' if !escape.is_empty() => { escape.clear(); result.push('\r'); } + // \x??, \u????, \U???????? ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { let mut seq = escape.clone(); seq.push(ch); @@ -744,15 +815,25 @@ impl<'a> TokenIterator<'a> { .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), self.pos))?, ); } + + // \{enclosing_char} - escaped ch if enclosing_char == ch && !escape.is_empty() => result.push(ch), + + // Close wrapper ch if enclosing_char == ch && escape.is_empty() => break, + + // Unknown escape sequence _ if !escape.is_empty() => { return Err((LERR::MalformedEscapeSequence(escape), self.pos)) } + + // Cannot have new-lines inside string literals '\n' => { self.rewind(); return Err((LERR::UnterminatedString, self.pos)); } + + // All other characters ch => { escape.clear(); result.push(ch); @@ -763,6 +844,7 @@ impl<'a> TokenIterator<'a> { Ok(result.iter().collect()) } + /// Get the next token. fn inner_next(&mut self) -> Option<(Token, Position)> { let mut negated = false; @@ -772,7 +854,9 @@ impl<'a> TokenIterator<'a> { let pos = self.pos; match c { + // \n '\n' => self.new_line(), + // digit ... '0'..='9' => { let mut result = Vec::new(); let mut radix_base: Option = None; @@ -801,6 +885,7 @@ impl<'a> TokenIterator<'a> { } } } + // 0x????, 0o????, 0b???? ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' if c == '0' => { @@ -850,6 +935,7 @@ impl<'a> TokenIterator<'a> { result.insert(0, '-'); } + // Parse number if let Some(radix) = radix_base { let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); @@ -865,6 +951,7 @@ impl<'a> TokenIterator<'a> { let out: String = result.iter().filter(|&&c| c != '_').collect(); let num = INT::from_str(&out).map(Token::IntegerConstant); + // If integer parsing is unnecessary, try float instead #[cfg(not(feature = "no_float"))] let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); @@ -876,6 +963,7 @@ impl<'a> TokenIterator<'a> { )); } } + // letter ... 'A'..='Z' | 'a'..='z' | '_' => { let mut result = Vec::new(); result.push(c); @@ -920,13 +1008,15 @@ impl<'a> TokenIterator<'a> { pos, )); } + // " - string literal '"' => { - return match self.parse_string_const('"') { + return match self.parse_string_literal('"') { Ok(out) => Some((Token::StringConst(out), pos)), Err(e) => Some((Token::LexError(e.0), e.1)), } } - '\'' => match self.parse_string_const('\'') { + // ' - character literal + '\'' => match self.parse_string_literal('\'') { Ok(result) => { let mut chars = result.chars(); @@ -945,16 +1035,22 @@ impl<'a> TokenIterator<'a> { } Err(e) => return Some((Token::LexError(e.0), e.1)), }, + + // Braces '{' => return Some((Token::LeftBrace, pos)), '}' => return Some((Token::RightBrace, pos)), + + // Parentheses '(' => return Some((Token::LeftParen, pos)), ')' => return Some((Token::RightParen, pos)), + // Indexing #[cfg(not(feature = "no_index"))] '[' => return Some((Token::LeftBracket, pos)), #[cfg(not(feature = "no_index"))] ']' => return Some((Token::RightBracket, pos)), + // Operators '+' => { return Some(( match self.char_stream.peek() { @@ -1218,6 +1314,7 @@ impl<'a> Iterator for TokenIterator<'a> { } } +/// Tokenize an input text stream. pub fn lex(input: &str) -> TokenIterator<'_> { TokenIterator { last: Token::LexError(LERR::InputError("".into())), @@ -1226,6 +1323,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> { } } +/// Parse ( expr ) fn parse_paren_expr<'a>( input: &mut Peekable>, begin: Position, @@ -1242,13 +1340,16 @@ fn parse_paren_expr<'a>( let expr = parse_expr(input)?; match input.next() { + // ( xxx ) Some((Token::RightParen, _)) => Ok(expr), + // ( xxx ??? Some((_, pos)) => { return Err(ParseError::new( PERR::MissingRightParen("a matching ( in the expression".into()), pos, )) } + // ( xxx None => Err(ParseError::new( PERR::MissingRightParen("a matching ( in the expression".into()), Position::eof(), @@ -1256,6 +1357,7 @@ fn parse_paren_expr<'a>( } } +/// Parse a function call. fn parse_call_expr<'a>( id: String, input: &mut Peekable>, @@ -1263,6 +1365,7 @@ fn parse_call_expr<'a>( ) -> Result { let mut args_expr_list = Vec::new(); + // id() if let (Token::RightParen, _) = input.peek().ok_or_else(|| { ParseError::new( PERR::MissingRightParen(format!( @@ -1308,6 +1411,7 @@ fn parse_call_expr<'a>( } } +/// Parse an indexing expression.s #[cfg(not(feature = "no_index"))] fn parse_index_expr<'a>( lhs: Box, @@ -1318,6 +1422,7 @@ fn parse_index_expr<'a>( // Check type of indexing - must be integer match &idx_expr { + // lhs[int] Expr::IntegerConstant(i, pos) if *i < 0 => { return Err(ParseError::new( PERR::MalformedIndexExpr(format!( @@ -1327,6 +1432,7 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[float] #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => { return Err(ParseError::new( @@ -1334,6 +1440,7 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[char] Expr::CharConstant(_, pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1342,18 +1449,21 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[string] Expr::StringConstant(_, pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr("Array access expects integer index, not a string".into()), *pos, )) } + // lhs[??? = ??? ], lhs[()] Expr::Assignment(_, _, pos) | Expr::Unit(pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr("Array access expects integer index, not ()".into()), *pos, )) } + // lhs[??? && ???], lhs[??? || ???] Expr::And(lhs, _) | Expr::Or(lhs, _) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1362,6 +1472,7 @@ fn parse_index_expr<'a>( lhs.position(), )) } + // lhs[true], lhs[false] Expr::True(pos) | Expr::False(pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1370,6 +1481,7 @@ fn parse_index_expr<'a>( *pos, )) } + // All other expressions _ => (), } @@ -1393,29 +1505,35 @@ fn parse_index_expr<'a>( } } +/// Parse an expression that begins with an identifier. fn parse_ident_expr<'a>( id: String, input: &mut Peekable>, begin: Position, ) -> Result { match input.peek() { + // id(...) - function call Some((Token::LeftParen, _)) => { input.next(); parse_call_expr(id, input, begin) } + // id[...] - indexing #[cfg(not(feature = "no_index"))] Some((Token::LeftBracket, pos)) => { let pos = *pos; input.next(); parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos) } + // id - variable Some(_) => Ok(Expr::Variable(id, begin)), + // EOF None => Ok(Expr::Variable(id, Position::eof())), } } +/// Parse an array literal. #[cfg(not(feature = "no_index"))] -fn parse_array_expr<'a>( +fn parse_array_literal<'a>( input: &mut Peekable>, begin: Position, ) -> Result { @@ -1462,8 +1580,9 @@ fn parse_array_expr<'a>( } } +/// Parse a primary expression. fn parse_primary<'a>(input: &mut Peekable>) -> Result { - // Block statement as expression + // { - block statement as expression match input.peek() { Some((Token::LeftBrace, pos)) => { let pos = *pos; @@ -1500,7 +1619,7 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result { can_be_indexed = true; - parse_array_expr(input, pos) + parse_array_literal(input, pos) } (Token::True, pos) => Ok(Expr::True(pos)), (Token::False, pos) => Ok(Expr::False(pos)), @@ -1524,11 +1643,13 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { match input .peek() .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? { + // -expr (Token::UnaryMinus, pos) => { let pos = *pos; @@ -1561,10 +1682,12 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), } } + // +expr (Token::UnaryPlus, _) => { input.next(); parse_unary(input) } + // !expr (Token::Bang, pos) => { let pos = *pos; @@ -1577,34 +1700,41 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result parse_primary(input), } } +/// Parse an assignment. fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result { + // Is the LHS in a valid format? fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option { match expr { + // var Expr::Variable(_, _) => { assert!(is_top, "property expected but gets variable"); None } + // property Expr::Property(_, _) => { assert!(!is_top, "variable expected but gets property"); None } + // var[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) => { assert!(is_top, "property expected but gets variable"); None } - + // property[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) => { assert!(!is_top, "variable expected but gets property"); None } + // idx_lhs[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, pos) => Some(ParseError::new( match idx_lhs.as_ref() { @@ -1614,22 +1744,27 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result match dot_lhs.as_ref() { + // var.dot_rhs Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false), + // property.dot_rhs Expr::Property(_, _) if !is_top => valid_assignment_chain(dot_rhs, false), - + // var[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) && is_top => { valid_assignment_chain(dot_rhs, false) } + // property[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) && !is_top => { valid_assignment_chain(dot_rhs, false) } + // idx_lhs[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) => Some(ParseError::new( ParseErrorType::AssignmentToCopy, @@ -1652,6 +1787,7 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result( input: &mut Peekable>, parent_precedence: u8, @@ -1820,11 +1957,13 @@ fn parse_binary_op<'a>( } } +/// Parse an expression. fn parse_expr<'a>(input: &mut Peekable>) -> Result { let lhs = parse_unary(input)?; parse_binary_op(input, 1, lhs) } +/// Parse an if statement. fn parse_if<'a>( input: &mut Peekable>, breakable: bool, @@ -1849,6 +1988,7 @@ fn parse_if<'a>( Ok(Stmt::IfElse(Box::new(guard), Box::new(if_body), else_body)) } +/// Parse a while loop. fn parse_while<'a>(input: &mut Peekable>) -> Result { input.next(); @@ -1858,6 +1998,7 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); @@ -1866,6 +2007,7 @@ fn parse_loop<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); @@ -1894,7 +2036,8 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result( +/// Parse a variable definition statement. +fn parse_let<'a>( input: &mut Peekable>, var_type: VariableType, ) -> Result { @@ -1935,6 +2078,7 @@ fn parse_var<'a>( } } +/// Parse a statement block. fn parse_block<'a>( input: &mut Peekable>, breakable: bool, @@ -1995,10 +2139,12 @@ fn parse_block<'a>( } } +/// Parse an expression as a statement. fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result { Ok(Stmt::Expr(Box::new(parse_expr(input)?))) } +/// Parse a single statement. fn parse_stmt<'a>( input: &mut Peekable>, breakable: bool, @@ -2030,14 +2176,14 @@ fn parse_stmt<'a>( input.next(); match input.peek() { - // return/throw at EOF + // `return`/`throw` at EOF None => Ok(Stmt::ReturnWithVal(None, return_type, Position::eof())), - // return; or throw; + // `return;` or `throw;` Some((Token::SemiColon, pos)) => { let pos = *pos; Ok(Stmt::ReturnWithVal(None, return_type, pos)) } - // return or throw with expression + // `return` or `throw` with expression Some((_, pos)) => { let pos = *pos; Ok(Stmt::ReturnWithVal( @@ -2049,12 +2195,13 @@ fn parse_stmt<'a>( } } (Token::LeftBrace, _) => parse_block(input, breakable), - (Token::Let, _) => parse_var(input, VariableType::Normal), - (Token::Const, _) => parse_var(input, VariableType::Constant), + (Token::Let, _) => parse_let(input, VariableType::Normal), + (Token::Const, _) => parse_let(input, VariableType::Constant), _ => parse_expr_stmt(input), } } +/// Parse a function definition. #[cfg(not(feature = "no_function"))] fn parse_fn<'a>(input: &mut Peekable>) -> Result { let pos = input @@ -2156,6 +2303,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( input: &mut Peekable>, ) -> Result<(Vec, Vec), ParseError> { @@ -2204,6 +2352,7 @@ fn parse_global_level<'a, 'e>( Ok((statements, functions)) } +/// Run the parser on an input stream, returning an AST. pub fn parse<'a, 'e>( input: &mut Peekable>, engine: &Engine<'e>, @@ -2219,6 +2368,9 @@ pub fn parse<'a, 'e>( ) } +/// Map a `Dynamic` value to an expression. +/// +/// Returns Some(expression) if conversion is successful. Otherwise None. pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dynamic) { if value.is::() { let value2 = value.clone(); From d4311bddb03054fb43d5ae9c8e49fcc26411f680 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 14:50:12 +0800 Subject: [PATCH 09/24] Put comments into example scripts. --- README.md | 50 +++++++++++++++++++------------------ scripts/array.rhai | 7 ++++-- scripts/assignment.rhai | 2 +- scripts/comments.rhai | 6 ++--- scripts/for1.rhai | 30 +++++++++++----------- scripts/function_decl1.rhai | 6 +++-- scripts/function_decl2.rhai | 11 ++++++-- scripts/function_decl3.rhai | 4 ++- scripts/if1.rhai | 19 ++++++++++---- scripts/loop.rhai | 4 +++ scripts/op1.rhai | 2 +- scripts/op2.rhai | 3 ++- scripts/op3.rhai | 3 ++- scripts/primes.rhai | 5 ++-- scripts/speed_test.rhai | 11 ++++++-- scripts/string.rhai | 22 +++++++++++----- scripts/while.rhai | 2 ++ 17 files changed, 119 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f93b6eba..5a406fc6 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ Optional features | `no_index` | Disable arrays and indexing features if not needed. | | `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | -| `only_i32` | Set the system integer type to `i32` and disable all other integer types. | -| `only_i64` | Set the system integer type to `i64` and disable all other integer types. | +| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | +| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. @@ -93,27 +93,27 @@ Example Scripts There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: -| Language feature scripts | Description | -| ------------------------ | ------------------------------------------------------------- | -| `array.rhai` | arrays in Rhai | -| `assignment.rhai` | variable declarations | -| `comments.rhai` | just comments | -| `for1.rhai` | for loops | -| `function_decl1.rhai` | a function without parameters | -| `function_decl2.rhai` | a function with two parameters | -| `function_decl3.rhai` | a function with many parameters | -| `if1.rhai` | if example | -| `loop.rhai` | endless loop in Rhai, this example emulates a do..while cycle | -| `op1.rhai` | just a simple addition | -| `op2.rhai` | simple addition and multiplication | -| `op3.rhai` | change evaluation order with parenthesis | -| `string.rhai` | string operations | -| `while.rhai` | while loop | +| Language feature scripts | Description | +| ---------------------------------------------------- | ------------------------------------------------------------- | +| [`array.rhai`](scripts/array.rhai) | arrays in Rhai | +| [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | +| [`comments.rhai`](scripts/comments.rhai) | just comments | +| [`for1.rhai`](scripts/for1.rhai) | for loops | +| [`function_decl1.rhai`](scripts/function_decl1.rhai) | a function without parameters | +| [`function_decl2.rhai`](scripts/function_decl2.rhai) | a function with two parameters | +| [`function_decl3.rhai`](scripts/function_decl3.rhai) | a function with many parameters | +| [`if1.rhai`](scripts/if1.rhai) | if example | +| [`loop.rhai`](scripts/loop.rhai) | endless loop in Rhai, this example emulates a do..while cycle | +| [`op1.rhai`](scripts/op1.rhai) | just a simple addition | +| [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | +| [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | +| [`string.rhai`](scripts/string.rhai) | string operations | +| [`while.rhai`](scripts/while.rhai) | while loop | -| Example scripts | Description | -| ----------------- | ---------------------------------------------------------------------------------- | -| `speed_test.rhai` | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | -| `primes.rhai` | use Sieve of Eratosthenes to find all primes smaller than a limit | +| Example scripts | Description | +| -------------------------------------------- | ---------------------------------------------------------------------------------- | +| [`speed_test.rhai`](scripts/speed_test.rhai) | a simple program to measure the speed of Rhai's interpreter (1 million iterations) | +| [`primes.rhai`](scripts/primes.rhai) | use Sieve of Eratosthenes to find all primes smaller than a limit | To run the scripts, either make a tiny program or use of the `rhai_runner` example: @@ -740,7 +740,9 @@ The following standard functions (defined in the standard library but excluded i Strings and Chars ----------------- -String and char literals follow C-style formatting, with support for Unicode ('`\u`') and hex ('`\x`') escape sequences. +String and char literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. + +Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicode code points and '`\U`' maps the full, 32-bit extended Unicode code points. Although internally Rhai strings are stored as UTF-8 just like in Rust (they _are_ Rust `String`s), in the Rhai language they can be considered a stream of Unicode characters, and can be directly indexed (unlike Rust). @@ -1051,7 +1053,7 @@ for x in array { if x == 42 { break; } } -// The 'range' function allows iterating from first..last +// The 'range' function allows iterating from first to last-1 for x in range(0, 50) { print(x); if x == 42 { break; } diff --git a/scripts/array.rhai b/scripts/array.rhai index 87343a6a..1d0768bd 100644 --- a/scripts/array.rhai +++ b/scripts/array.rhai @@ -1,4 +1,7 @@ let x = [1, 2, 3]; -print(x[1]); + +print(x[1]); // prints 2 + x[1] = 5; -print(x[1]); + +print(x[1]); // prints 5 diff --git a/scripts/assignment.rhai b/scripts/assignment.rhai index 8e8d6047..9554a7e5 100644 --- a/scripts/assignment.rhai +++ b/scripts/assignment.rhai @@ -1,2 +1,2 @@ let x = 78; -print(x) +print(x); diff --git a/scripts/comments.rhai b/scripts/comments.rhai index 50f15278..645105bb 100644 --- a/scripts/comments.rhai +++ b/scripts/comments.rhai @@ -3,8 +3,8 @@ let /* I am a spy in a variable declaration! */ x = 5; /* I am a simple - multiline comment */ + multi-line comment */ -/* look /* at /* that, /* multiline */ comments */ can be */ nested */ +/* look /* at /* that, /* multi-line */ comments */ can be */ nested */ -/* sorrounded by */ x // comments +/* surrounded by */ x // comments diff --git a/scripts/for1.rhai b/scripts/for1.rhai index ea3675b7..8eafedde 100644 --- a/scripts/for1.rhai +++ b/scripts/for1.rhai @@ -1,15 +1,17 @@ -let arr = [1,2,3,4] -for a in arr { - for b in [10,20] { - print(a) - print(b) - } - if a == 3 { - break; - } -} -//print(a) +// This script runs for-loops -for i in range(0,5) { - print(i) -} \ No newline at end of file +let arr = [1,2,3,4]; + +for a in arr { + for b in [10, 20] { + print(a + "," + b); + } + + if a == 3 { break; } +} +//print(a); // <- if you uncomment this line, the script will fail to run + // because 'a' is not defined here + +for i in range(0, 5) { // runs through a range from 1 to 5 exclusive + print(i); +} diff --git a/scripts/function_decl1.rhai b/scripts/function_decl1.rhai index c23357b8..4e97e47c 100644 --- a/scripts/function_decl1.rhai +++ b/scripts/function_decl1.rhai @@ -1,5 +1,7 @@ +// This script defines a function and calls it + fn bob() { - 3 + return 3; } -print(bob()) +print(bob()); // should print 3 diff --git a/scripts/function_decl2.rhai b/scripts/function_decl2.rhai index b341688b..201dcea4 100644 --- a/scripts/function_decl2.rhai +++ b/scripts/function_decl2.rhai @@ -1,5 +1,12 @@ +// This script defines a function with two parameters + +let a = 3; + fn addme(a, b) { - a+b + a = 42; // notice that 'a' is passed by value + a + b; // notice that the last value is returned even if terminated by a semicolon } -print(addme(3, 4)) +print(addme(a, 4)); // should print 46 + +print(a); // should print 3 - 'a' is never changed diff --git a/scripts/function_decl3.rhai b/scripts/function_decl3.rhai index b23846f4..339c4b61 100644 --- a/scripts/function_decl3.rhai +++ b/scripts/function_decl3.rhai @@ -1,5 +1,7 @@ +// This script defines a function with many parameters and calls it + fn f(a, b, c, d, e, f) { a - b * c - d * e - f } -print(f(100, 5, 2, 9, 6, 32)) +print(f(100, 5, 2, 9, 6, 32)); // should print 4 diff --git a/scripts/if1.rhai b/scripts/if1.rhai index b45cbde2..6f414a78 100644 --- a/scripts/if1.rhai +++ b/scripts/if1.rhai @@ -1,5 +1,14 @@ -let a = true; -if (a) { - let x = 56; - print(x); -} +let a = 42; +let b = 123; +let x = 999; + +if a > b { + print("a > b"); +} else if a < b { + print("a < b"); + + let x = 0; // this 'x' shadows the global 'x' + print(x); // should print 0 +} else { + print("a == b"); +} \ No newline at end of file diff --git a/scripts/loop.rhai b/scripts/loop.rhai index 91b7a5d8..d173b86f 100644 --- a/scripts/loop.rhai +++ b/scripts/loop.rhai @@ -1,8 +1,12 @@ +// This script runs an infinite loop, ending it with a break statement + let x = 10; // simulate do..while using loop loop { print(x); + x = x - 1; + if x <= 0 { break; } } diff --git a/scripts/op1.rhai b/scripts/op1.rhai index 3f25f040..5351f999 100644 --- a/scripts/op1.rhai +++ b/scripts/op1.rhai @@ -1 +1 @@ -print(34 + 12) +print(34 + 12); // should be 46 diff --git a/scripts/op2.rhai b/scripts/op2.rhai index c7767bc8..fedbd0aa 100644 --- a/scripts/op2.rhai +++ b/scripts/op2.rhai @@ -1 +1,2 @@ -print(12 + 34 * 5) +let x = 12 + 34 * 5; +print(x); // should be 182 diff --git a/scripts/op3.rhai b/scripts/op3.rhai index 7e5eb4e2..7811dbac 100644 --- a/scripts/op3.rhai +++ b/scripts/op3.rhai @@ -1 +1,2 @@ -print(0 + (12 + 34) * 5) +let x = (12 + 34) * 5; +print(x); // should be 230 diff --git a/scripts/primes.rhai b/scripts/primes.rhai index 4f76291f..d09418e8 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -1,6 +1,6 @@ -// This is a script to calculate prime numbers. +// This script uses the Sieve of Eratosthenes to calculate prime numbers. -const MAX_NUMBER_TO_CHECK = 10000; // 1229 primes +const MAX_NUMBER_TO_CHECK = 10_000; // 1229 primes <= 10000 let prime_mask = []; prime_mask.pad(MAX_NUMBER_TO_CHECK, true); @@ -24,4 +24,3 @@ for p in range(2, MAX_NUMBER_TO_CHECK) { } print("Total " + total_primes_found + " primes."); - diff --git a/scripts/speed_test.rhai b/scripts/speed_test.rhai index 505b6f52..1aa67276 100644 --- a/scripts/speed_test.rhai +++ b/scripts/speed_test.rhai @@ -1,5 +1,12 @@ -let x = 1000000; +// This script runs 1 million iterations +// to test the speed of the scripting engine. + +let x = 1_000_000; + +print("Ready... Go!"); + while x > 0 { x = x - 1; } -print(x); + +print("Finished."); diff --git a/scripts/string.rhai b/scripts/string.rhai index ecf418ec..ab30a29a 100644 --- a/scripts/string.rhai +++ b/scripts/string.rhai @@ -1,7 +1,17 @@ +// This script tests string operations + print("hello"); -print("this\nis \\ nice"); -print("40 hex is \x40"); -print("fun with unicode: \u2764 and \U0001F603"); -print("foo" + " " + "bar"); -print("foo" < "bar"); -print("foo" >= "bar"); \ No newline at end of file +print("this\nis \\ nice"); // escape sequences +print("40 hex is \x40"); // hex escape sequence +print("unicode fun: \u2764"); // Unicode escape sequence +print("more fun: \U0001F603"); // Unicode escape sequence +print("foo" + " " + "bar"); // string building using strings +print("foo" < "bar"); // string comparison +print("foo" >= "bar"); // string comparison +print("the answer is " + 42); // string building using non-string types + +let s = "hello, world!"; // string variable +print("length=" + s.len()); // should be 13 + +s[s.len()-1] = '?'; // change the string +print(s); // should print 'hello, world?' diff --git a/scripts/while.rhai b/scripts/while.rhai index d0f788f3..3f58f6b6 100644 --- a/scripts/while.rhai +++ b/scripts/while.rhai @@ -1,3 +1,5 @@ +// This script runs a while loop + let x = 10; while x > 0 { From abe5365bfdc7a2897a388a86e0e70646defea6f6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 23:51:32 +0800 Subject: [PATCH 10/24] Improve error messages to lists. --- Cargo.toml | 1 + README.md | 36 +-- src/engine.rs | 15 +- src/error.rs | 5 + src/parser.rs | 829 ++++++++++++++++++++++++-------------------------- 5 files changed, 433 insertions(+), 453 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f495d608..11015a39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ include = [ "scripts/*.rhai", "Cargo.toml" ] +keywords = [ "scripting" ] [dependencies] num-traits = "0.2.11" diff --git a/README.md b/README.md index 5a406fc6..09a5d6d4 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ let ast = engine.compile("40 + 2")?; for _ in 0..42 { let result: i64 = engine.eval_ast(&ast)?; - println!("Answer: {}", result); // prints 42 + println!("Answer #{}: {}", i, result); // prints 42 } ``` @@ -193,14 +193,17 @@ use rhai::Engine; let mut engine = Engine::new(); // Define a function in a script and load it into the Engine. -engine.consume(true, // pass true to 'retain_functions' otherwise these functions - r" // will be cleared at the end of consume() - fn hello(x, y) { // a function with two parameters: String and i64 - x.len() + y // returning i64 +// Pass true to 'retain_functions' otherwise these functions will be cleared at the end of consume() +engine.consume(true, + r" + // a function with two parameters: String and i64 + fn hello(x, y) { + x.len() + y } - fn hello(x) { // functions can be overloaded: this one takes only one parameter - x * 2 // returning i64 + // functions can be overloaded: this one takes only one parameter + fn hello(x) { + x * 2 } ")?; @@ -495,22 +498,21 @@ let result = engine.eval::("let x = new_ts(); x.foo()")?; println!("result: {}", result); // prints 1 ``` -`type_of` works fine with custom types and returns the name of the type: +`type_of` works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type +with a special "pretty-print" name, `type_of` will return that name instead. ```rust let x = new_ts(); print(x.type_of()); // prints "foo::bar::TestStruct" + // prints "Hello" if TestStruct is registered with + // engine.register_type_with_name::("Hello")?; ``` -If `register_type_with_name` is used to register the custom type with a special "pretty-print" name, `type_of` will return that name instead. - Getters and setters ------------------- Similarly, custom types can expose members by registering a `get` and/or `set` function. -For example: - ```rust #[derive(Clone)] struct TestStruct { @@ -565,8 +567,8 @@ fn main() -> Result<(), EvalAltResult> // Then push some initialized variables into the state // NOTE: Remember the system number types in Rhai are i64 (i32 if 'only_i32') ond f64. // Better stick to them or it gets hard working with the script. - scope.push("y".into(), 42_i64); - scope.push("z".into(), 999_i64); + scope.push("y", 42_i64); + scope.push("z", 999_i64); // First invocation engine.eval_with_scope::<()>(&mut scope, r" @@ -684,7 +686,7 @@ Numeric operators generally follow C styles. | `%` | Modulo (remainder) | | | `~` | Power | | | `&` | Binary _And_ bit-mask | Yes | -| `|` | Binary _Or_ bit-mask | Yes | +| `\|` | Binary _Or_ bit-mask | Yes | | `^` | Binary _Xor_ bit-mask | Yes | | `<<` | Left bit-shift | Yes | | `>>` | Right bit-shift | Yes | @@ -948,9 +950,9 @@ Boolean operators | -------- | ------------------------------- | | `!` | Boolean _Not_ | | `&&` | Boolean _And_ (short-circuits) | -| `||` | Boolean _Or_ (short-circuits) | +| `\|\|` | Boolean _Or_ (short-circuits) | | `&` | Boolean _And_ (full evaluation) | -| `|` | Boolean _Or_ (full evaluation) | +| `\|` | Boolean _Or_ (full evaluation) | Double boolean operators `&&` and `||` _short-circuit_, meaning that the second operand will not be evaluated if the first one already proves the condition wrong. diff --git a/src/engine.rs b/src/engine.rs index 9b239e96..9362dd30 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1031,30 +1031,29 @@ impl Engine<'_> { // Block scope Stmt::Block(block, _) => { let prev_len = scope.len(); - let mut last_result: Result = Ok(().into_dynamic()); + let mut result: Result = Ok(().into_dynamic()); - for block_stmt in block.iter() { - last_result = self.eval_stmt(scope, block_stmt); + for stmt in block.iter() { + result = self.eval_stmt(scope, stmt); - if let Err(x) = last_result { - last_result = Err(x); + if result.is_err() { break; } } scope.rewind(prev_len); - last_result + result } // If-else statement - Stmt::IfElse(guard, body, else_body) => self + Stmt::IfElse(guard, if_body, else_body) => self .eval_expr(scope, guard)? .downcast::() .map_err(|_| EvalAltResult::ErrorIfGuard(guard.position())) .and_then(|guard_val| { if *guard_val { - self.eval_stmt(scope, body) + self.eval_stmt(scope, if_body) } else if let Some(stmt) = else_body { self.eval_stmt(scope, stmt.as_ref()) } else { diff --git a/src/error.rs b/src/error.rs index 166108c8..53a0091d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,6 +58,8 @@ pub enum ParseErrorType { /// An open `[` is missing the corresponding closing `]`. #[cfg(not(feature = "no_index"))] MissingRightBracket(String), + /// A list of expressions is missing the separating ','. + MissingComma(String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. @@ -116,6 +118,7 @@ impl ParseError { ParseErrorType::MissingRightBrace(_) => "Expecting '}'", #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(_) => "Expecting ']'", + ParseErrorType::MissingComma(_) => "Expecting ','", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", @@ -165,6 +168,8 @@ impl fmt::Display for ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::MissingComma(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { write!(f, "{}", self.desc())? } diff --git a/src/parser.rs b/src/parser.rs index a66990b6..c9dfe5e8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -52,9 +52,11 @@ pub struct Position { impl Position { /// Create a new `Position`. pub fn new(line: usize, position: usize) -> Self { - if line == 0 || (line == usize::MAX && position == usize::MAX) { - panic!("invalid position: ({}, {})", line, position); - } + assert!(line != 0, "line cannot be zero"); + assert!( + line != usize::MAX || position != usize::MAX, + "invalid position" + ); Self { line, @@ -305,9 +307,10 @@ impl Expr { | Expr::False(pos) | Expr::Unit(pos) => *pos, - Expr::Assignment(e, _, _) | Expr::Dot(e, _, _) | Expr::And(e, _) | Expr::Or(e, _) => { - e.position() - } + Expr::Assignment(expr, _, _) + | Expr::Dot(expr, _, _) + | Expr::And(expr, _) + | Expr::Or(expr, _) => expr.position(), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => *pos, @@ -316,7 +319,7 @@ impl Expr { Expr::Array(_, pos) => *pos, #[cfg(not(feature = "no_index"))] - Expr::Index(e, _, _) => e.position(), + Expr::Index(expr, _, _) => expr.position(), } } @@ -433,15 +436,15 @@ impl Token { pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; - match *self { - IntegerConstant(ref i) => i.to_string().into(), + match self { + IntegerConstant(i) => i.to_string().into(), #[cfg(not(feature = "no_float"))] - FloatConstant(ref f) => f.to_string().into(), - Identifier(ref s) => s.into(), - CharConstant(ref c) => c.to_string().into(), - LexError(ref err) => err.to_string().into(), + FloatConstant(f) => f.to_string().into(), + Identifier(s) => s.into(), + CharConstant(c) => c.to_string().into(), + LexError(err) => err.to_string().into(), - ref token => (match token { + token => (match token { StringConst(_) => "string", LeftBrace => "{", RightBrace => "}", @@ -515,7 +518,7 @@ impl Token { pub fn is_next_unary(&self) -> bool { use self::Token::*; - match *self { + match self { LexError(_) | LeftBrace | // (+expr) - is unary // RightBrace | {expr} - expr not unary & is closing @@ -572,28 +575,63 @@ impl Token { } } - #[allow(dead_code)] - pub fn is_binary_op(&self) -> bool { - use self::Token::*; + pub fn precedence(&self) -> u8 { + match self { + Self::Equals + | Self::PlusAssign + | Self::MinusAssign + | Self::MultiplyAssign + | Self::DivideAssign + | Self::LeftShiftAssign + | Self::RightShiftAssign + | Self::AndAssign + | Self::OrAssign + | Self::XOrAssign + | Self::ModuloAssign + | Self::PowerOfAssign => 10, - match *self { - RightParen | Plus | Minus | Multiply | Divide | Comma | Equals | LessThan - | GreaterThan | LessThanEqualsTo | GreaterThanEqualsTo | EqualsTo | NotEqualsTo - | Pipe | Or | Ampersand | And | PowerOf => true, + Self::Or | Self::XOr | Self::Pipe => 50, - #[cfg(not(feature = "no_index"))] - RightBrace | RightBracket => true, + Self::And | Self::Ampersand => 60, - _ => false, + Self::LessThan + | Self::LessThanEqualsTo + | Self::GreaterThan + | Self::GreaterThanEqualsTo + | Self::EqualsTo + | Self::NotEqualsTo => 70, + + Self::Plus | Self::Minus => 80, + + Self::Divide | Self::Multiply | Self::PowerOf => 90, + + Self::LeftShift | Self::RightShift => 100, + + Self::Modulo => 110, + + Self::Period => 120, + + _ => 0, } } - #[allow(dead_code)] - pub fn is_unary_op(&self) -> bool { - use self::Token::*; + pub fn is_bind_right(&self) -> bool { + match self { + Self::Equals + | Self::PlusAssign + | Self::MinusAssign + | Self::MultiplyAssign + | Self::DivideAssign + | Self::LeftShiftAssign + | Self::RightShiftAssign + | Self::AndAssign + | Self::OrAssign + | Self::XOrAssign + | Self::ModuloAssign + | Self::PowerOfAssign => true, + + Self::Period => true, - match *self { - UnaryPlus | UnaryMinus | Equals | Bang | Return | Throw => true, _ => false, } } @@ -647,89 +685,40 @@ impl<'a> TokenIterator<'a> { escape.clear(); result.push('\r'); } - 'x' if !escape.is_empty() => { + ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { let mut seq = escape.clone(); - seq.push('x'); + seq.push(ch); escape.clear(); + let mut out_val: u32 = 0; - for _ in 0..2 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); + let len = match ch { + 'x' => 2, + 'u' => 4, + 'U' => 8, + _ => panic!("should be 'x', 'u' or 'U'"), + }; - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } + for _ in 0..len { + let c = self.char_stream.next().ok_or_else(|| { + (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) + })?; + + seq.push(c); + self.advance(); + + out_val *= 16; + out_val += c.to_digit(16).ok_or_else(|| { + (LERR::MalformedEscapeSequence(seq.to_string()), self.pos) + })?; } - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } + result.push( + char::from_u32(out_val) + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), self.pos))?, + ); } - 'u' if !escape.is_empty() => { - let mut seq = escape.clone(); - seq.push('u'); - escape.clear(); - let mut out_val: u32 = 0; - for _ in 0..4 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); - - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - 'U' if !escape.is_empty() => { - let mut seq = escape.clone(); - seq.push('U'); - escape.clear(); - let mut out_val: u32 = 0; - for _ in 0..8 { - if let Some(c) = self.char_stream.next() { - seq.push(c); - self.advance(); - - if let Some(d1) = c.to_digit(16) { - out_val *= 16; - out_val += d1; - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - - if let Some(r) = char::from_u32(out_val) { - result.push(r); - } else { - return Err((LERR::MalformedEscapeSequence(seq), self.pos)); - } - } - x if enclosing_char == x && !escape.is_empty() => result.push(x), - x if enclosing_char == x && escape.is_empty() => break, + ch if enclosing_char == ch && !escape.is_empty() => result.push(ch), + ch if enclosing_char == ch && escape.is_empty() => break, _ if !escape.is_empty() => { return Err((LERR::MalformedEscapeSequence(escape), self.pos)) } @@ -737,9 +726,9 @@ impl<'a> TokenIterator<'a> { self.rewind(); return Err((LERR::UnterminatedString, self.pos)); } - x => { + ch => { escape.clear(); - result.push(x); + result.push(ch); } } } @@ -785,54 +774,47 @@ impl<'a> TokenIterator<'a> { } } } - 'x' | 'X' if c == '0' => { + ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' + if c == '0' => + { result.push(next_char); self.char_stream.next(); self.advance(); + + let valid = match ch { + 'x' | 'X' => [ + 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', + ], + 'o' | 'O' => [ + '0', '1', '2', '3', '4', '5', '6', '7', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + 'b' | 'B' => [ + '0', '1', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', + ], + _ => panic!("unexpected character {}", ch), + }; + + radix_base = Some(match ch { + 'x' | 'X' => 16, + 'o' | 'O' => 8, + 'b' | 'B' => 2, + _ => panic!("unexpected character {}", ch), + }); + while let Some(&next_char_in_hex) = self.char_stream.peek() { - match next_char_in_hex { - '0'..='9' | 'a'..='f' | 'A'..='F' | '_' => { - result.push(next_char_in_hex); - self.char_stream.next(); - self.advance(); - } - _ => break, + if !valid.contains(&next_char_in_hex) { + break; } + + result.push(next_char_in_hex); + self.char_stream.next(); + self.advance(); } - radix_base = Some(16); - } - 'o' | 'O' if c == '0' => { - result.push(next_char); - self.char_stream.next(); - self.advance(); - while let Some(&next_char_in_oct) = self.char_stream.peek() { - match next_char_in_oct { - '0'..='8' | '_' => { - result.push(next_char_in_oct); - self.char_stream.next(); - self.advance(); - } - _ => break, - } - } - radix_base = Some(8); - } - 'b' | 'B' if c == '0' => { - result.push(next_char); - self.char_stream.next(); - self.advance(); - while let Some(&next_char_in_binary) = self.char_stream.peek() { - match next_char_in_binary { - '0' | '1' | '_' => { - result.push(next_char_in_binary); - self.char_stream.next(); - self.advance(); - } - _ => break, - } - } - radix_base = Some(2); } + _ => break, } } @@ -854,25 +836,15 @@ impl<'a> TokenIterator<'a> { )); } else { let out: String = result.iter().filter(|&&c| c != '_').collect(); - - #[cfg(feature = "no_float")] - return Some(( - INT::from_str(&out) - .map(Token::IntegerConstant) - .unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }), - pos, - )); + let num = INT::from_str(&out).map(Token::IntegerConstant); #[cfg(not(feature = "no_float"))] + let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); + return Some(( - INT::from_str(&out) - .map(Token::IntegerConstant) - .or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)) - .unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.iter().collect())) - }), + num.unwrap_or_else(|_| { + Token::LexError(LERR::MalformedNumber(result.iter().collect())) + }), pos, )); } @@ -1227,67 +1199,6 @@ pub fn lex(input: &str) -> TokenIterator<'_> { } } -fn get_precedence(token: &Token) -> u8 { - match *token { - Token::Equals - | Token::PlusAssign - | Token::MinusAssign - | Token::MultiplyAssign - | Token::DivideAssign - | Token::LeftShiftAssign - | Token::RightShiftAssign - | Token::AndAssign - | Token::OrAssign - | Token::XOrAssign - | Token::ModuloAssign - | Token::PowerOfAssign => 10, - - Token::Or | Token::XOr | Token::Pipe => 50, - - Token::And | Token::Ampersand => 60, - - Token::LessThan - | Token::LessThanEqualsTo - | Token::GreaterThan - | Token::GreaterThanEqualsTo - | Token::EqualsTo - | Token::NotEqualsTo => 70, - - Token::Plus | Token::Minus => 80, - - Token::Divide | Token::Multiply | Token::PowerOf => 90, - - Token::LeftShift | Token::RightShift => 100, - - Token::Modulo => 110, - - Token::Period => 120, - - _ => 0, - } -} - -fn is_bind_right(token: &Token) -> bool { - match *token { - Token::Equals - | Token::PlusAssign - | Token::MinusAssign - | Token::MultiplyAssign - | Token::DivideAssign - | Token::LeftShiftAssign - | Token::RightShiftAssign - | Token::AndAssign - | Token::OrAssign - | Token::XOrAssign - | Token::ModuloAssign - | Token::PowerOfAssign => true, - - Token::Period => true, - - _ => false, - } -} - fn parse_paren_expr<'a>( input: &mut Peekable>, begin: Position, @@ -1325,7 +1236,15 @@ fn parse_call_expr<'a>( ) -> Result { let mut args_expr_list = Vec::new(); - if let Some(&(Token::RightParen, _)) = input.peek() { + if let (Token::RightParen, _) = input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the arguments to call of function '{}'", + id + )), + Position::eof(), + ) + })? { input.next(); return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); } @@ -1333,28 +1252,27 @@ fn parse_call_expr<'a>( loop { args_expr_list.push(parse_expr(input)?); - match input.peek() { - Some(&(Token::RightParen, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the arguments to call of function '{}'", + id + )), + Position::eof(), + ) + })? { + (Token::RightParen, _) => { input.next(); return Ok(Expr::FunctionCall(id, args_expr_list, None, begin)); } - Some(&(Token::Comma, _)) => (), - Some(&(_, pos)) => { + (Token::Comma, _) => (), + (_, pos) => { return Err(ParseError::new( - PERR::MissingRightParen(format!( - "closing the parameters list to function call of '{}'", + PERR::MissingComma(format!( + "separating the arguments to call of function '{}'", id )), - pos, - )) - } - None => { - return Err(ParseError::new( - PERR::MissingRightParen(format!( - "closing the parameters list to function call of '{}'", - id - )), - Position::eof(), + *pos, )) } } @@ -1429,21 +1347,20 @@ fn parse_index_expr<'a>( } // Check if there is a closing bracket - match input.peek() { - Some(&(Token::RightBracket, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBracket("index expression".into()), + Position::eof(), + ) + })? { + (Token::RightBracket, _) => { input.next(); return Ok(Expr::Index(lhs, Box::new(idx_expr), pos)); } - Some(&(_, pos)) => { + (_, pos) => { return Err(ParseError::new( PERR::MissingRightBracket("index expression".into()), - pos, - )) - } - None => { - return Err(ParseError::new( - PERR::MissingRightBracket("index expression".into()), - Position::eof(), + *pos, )) } } @@ -1455,12 +1372,13 @@ fn parse_ident_expr<'a>( begin: Position, ) -> Result { match input.peek() { - Some(&(Token::LeftParen, _)) => { + Some((Token::LeftParen, _)) => { input.next(); parse_call_expr(id, input, begin) } #[cfg(not(feature = "no_index"))] - Some(&(Token::LeftBracket, pos)) => { + Some((Token::LeftBracket, pos)) => { + let pos = *pos; input.next(); parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos) } @@ -1476,36 +1394,43 @@ fn parse_array_expr<'a>( ) -> Result { let mut arr = Vec::new(); - match input.peek() { - Some(&(Token::RightBracket, _)) => (), + if !matches!(input.peek(), Some((Token::RightBracket, _))) { + while input.peek().is_some() { + arr.push(parse_expr(input)?); - _ => { - while input.peek().is_some() { - arr.push(parse_expr(input)?); - - if let Some(&(Token::Comma, _)) = input.peek() { + match input.peek().ok_or_else(|| { + ParseError( + PERR::MissingRightBracket("separating items in array literal".into()), + Position::eof(), + ) + })? { + (Token::Comma, _) => { input.next(); } - - if let Some(&(Token::RightBracket, _)) = input.peek() { - break; + (Token::RightBracket, _) => break, + (_, pos) => { + return Err(ParseError( + PERR::MissingComma("separating items in array literal".into()), + *pos, + )) } } } } - match input.peek() { - Some(&(Token::RightBracket, _)) => { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBracket("the end of array literal".into()), + Position::eof(), + ) + })? { + (Token::RightBracket, _) => { input.next(); Ok(Expr::Array(arr, begin)) } - Some(&(_, pos)) => Err(ParseError::new( + (_, pos) => Err(ParseError::new( PERR::MissingRightBracket("the end of array literal".into()), - pos, - )), - None => Err(ParseError::new( - PERR::MissingRightBracket("the end of array literal".into()), - Position::eof(), + *pos, )), } } @@ -1513,8 +1438,9 @@ fn parse_array_expr<'a>( fn parse_primary<'a>(input: &mut Peekable>) -> Result { // Block statement as expression match input.peek() { - Some(&(Token::LeftBrace, pos)) => { - return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)) + Some((Token::LeftBrace, pos)) => { + let pos = *pos; + return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)); } _ => (), } @@ -1524,45 +1450,44 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(x, pos)), + let mut root_expr = + match token.ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? { + #[cfg(not(feature = "no_float"))] + (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), - Some((Token::IntegerConstant(x), pos)) => Ok(Expr::IntegerConstant(x, pos)), - Some((Token::CharConstant(c), pos)) => Ok(Expr::CharConstant(c, pos)), - Some((Token::StringConst(s), pos)) => { - can_be_indexed = true; - Ok(Expr::StringConstant(s, pos)) - } - Some((Token::Identifier(s), pos)) => { - can_be_indexed = true; - parse_ident_expr(s, input, pos) - } - Some((Token::LeftParen, pos)) => { - can_be_indexed = true; - parse_paren_expr(input, pos) - } - #[cfg(not(feature = "no_index"))] - Some((Token::LeftBracket, pos)) => { - can_be_indexed = true; - parse_array_expr(input, pos) - } - Some((Token::True, pos)) => Ok(Expr::True(pos)), - Some((Token::False, pos)) => Ok(Expr::False(pos)), - Some((Token::LexError(le), pos)) => { - Err(ParseError::new(PERR::BadInput(le.to_string()), pos)) - } - Some((token, pos)) => Err(ParseError::new( - PERR::BadInput(format!("Unexpected '{}'", token.syntax())), - pos, - )), - None => Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), - }?; + (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), + (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), + (Token::StringConst(s), pos) => { + can_be_indexed = true; + Ok(Expr::StringConstant(s, pos)) + } + (Token::Identifier(s), pos) => { + can_be_indexed = true; + parse_ident_expr(s, input, pos) + } + (Token::LeftParen, pos) => { + can_be_indexed = true; + parse_paren_expr(input, pos) + } + #[cfg(not(feature = "no_index"))] + (Token::LeftBracket, pos) => { + can_be_indexed = true; + parse_array_expr(input, pos) + } + (Token::True, pos) => Ok(Expr::True(pos)), + (Token::False, pos) => Ok(Expr::False(pos)), + (Token::LexError(le), pos) => Err(ParseError::new(PERR::BadInput(le.to_string()), pos)), + (token, pos) => Err(ParseError::new( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())), + pos, + )), + }?; if can_be_indexed { // Tail processing all possible indexing #[cfg(not(feature = "no_index"))] - while let Some(&(Token::LeftBracket, pos)) = input.peek() { + while let Some((Token::LeftBracket, pos)) = input.peek() { + let pos = *pos; input.next(); root_expr = parse_index_expr(Box::new(root_expr), input, pos)?; } @@ -1572,13 +1497,18 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - match input.peek() { - Some(&(Token::UnaryMinus, pos)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + (Token::UnaryMinus, pos) => { + let pos = *pos; + input.next(); - match parse_unary(input) { + match parse_unary(input)? { // Negative integer - Ok(Expr::IntegerConstant(i, _)) => i + Expr::IntegerConstant(i, _) => i .checked_neg() .map(|x| Expr::IntegerConstant(x, pos)) .or_else(|| { @@ -1597,19 +1527,19 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(-f, pos)), + Expr::FloatConstant(f, pos) => Ok(Expr::FloatConstant(-f, pos)), // Call negative function - Ok(expr) => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), - - err @ Err(_) => err, + expr => Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), } } - Some(&(Token::UnaryPlus, _)) => { + (Token::UnaryPlus, _) => { input.next(); parse_unary(input) } - Some(&(Token::Bang, pos)) => { + (Token::Bang, pos) => { + let pos = *pos; + input.next(); Ok(Expr::FunctionCall( @@ -1688,8 +1618,6 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result Ok(Expr::Assignment(Box::new(lhs), Box::new(rhs), pos)), Some(err) => Err(err), @@ -1719,8 +1647,8 @@ fn parse_binary_op<'a>( let mut current_lhs = lhs; loop { - let (current_precedence, bind_right) = if let Some(&(ref current_op, _)) = input.peek() { - (get_precedence(current_op), is_bind_right(current_op)) + let (current_precedence, bind_right) = if let Some((ref current_op, _)) = input.peek() { + (current_op.precedence(), current_op.is_bind_right()) } else { (0, false) }; @@ -1738,8 +1666,8 @@ fn parse_binary_op<'a>( let rhs = parse_unary(input)?; - let next_precedence = if let Some(&(ref next_op, _)) = input.peek() { - get_precedence(next_op) + let next_precedence = if let Some((next_op, _)) = input.peek() { + next_op.precedence() } else { 0 }; @@ -1873,26 +1801,21 @@ fn parse_if<'a>(input: &mut Peekable>) -> Result { - input.next(); + let else_body = if matches!(input.peek(), Some((Token::Else, _))) { + input.next(); - let else_body = if matches!(input.peek(), Some(&(Token::If, _))) { - parse_if(input)? - } else { - parse_block(input)? - }; + Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) { + parse_if(input)? + } else { + parse_block(input)? + })) + } else { + None + }; - Ok(Stmt::IfElse( - Box::new(guard), - Box::new(body), - Some(Box::new(else_body)), - )) - } - _ => Ok(Stmt::IfElse(Box::new(guard), Box::new(body), None)), - } + Ok(Stmt::IfElse(Box::new(guard), Box::new(if_body), else_body)) } fn parse_while<'a>(input: &mut Peekable>) -> Result { @@ -1915,23 +1838,26 @@ fn parse_loop<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((Token::LexError(s), pos)) => { + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (Token::LexError(s), pos) => { return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) } - Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), - None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), + (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; - match input.next() { - Some((Token::In, _)) => (), - Some((_, pos)) => return Err(ParseError::new(PERR::MissingIn, pos)), - None => return Err(ParseError::new(PERR::MissingIn, Position::eof())), + match input + .next() + .ok_or_else(|| ParseError::new(PERR::MissingIn, Position::eof()))? + { + (Token::In, _) => (), + (_, pos) => return Err(ParseError::new(PERR::MissingIn, pos)), } let expr = parse_expr(input)?; - let body = parse_block(input)?; Ok(Stmt::For(name, Box::new(expr), Box::new(body))) @@ -1941,21 +1867,23 @@ fn parse_var<'a>( input: &mut Peekable>, var_type: VariableType, ) -> Result { - let pos = match input.next() { - Some((_, tok_pos)) => tok_pos, - _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), - }; + let pos = input + .next() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + .1; - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((Token::LexError(s), pos)) => { + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (Token::LexError(s), pos) => { return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) } - Some((_, pos)) => return Err(ParseError::new(PERR::VariableExpected, pos)), - None => return Err(ParseError::new(PERR::VariableExpected, Position::eof())), + (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; - if matches!(input.peek(), Some(&(Token::Equals, _))) { + if matches!(input.peek(), Some((Token::Equals, _))) { input.next(); let init_value = parse_expr(input)?; @@ -1977,19 +1905,26 @@ fn parse_var<'a>( } fn parse_block<'a>(input: &mut Peekable>) -> Result { - let pos = match input.next() { - Some((Token::LeftBrace, pos)) => pos, - Some((_, pos)) => return Err(ParseError::new(PERR::MissingLeftBrace, pos)), - None => return Err(ParseError::new(PERR::MissingLeftBrace, Position::eof())), + let pos = match input + .next() + .ok_or_else(|| ParseError::new(PERR::MissingLeftBrace, Position::eof()))? + { + (Token::LeftBrace, pos) => pos, + (_, pos) => return Err(ParseError::new(PERR::MissingLeftBrace, pos)), }; let mut statements = Vec::new(); - match input.peek() { - Some(&(Token::RightBrace, _)) => (), // empty block + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + ) + })? { + (Token::RightBrace, _) => (), // empty block #[cfg(not(feature = "no_function"))] - Some(&(Token::Fn, pos)) => return Err(ParseError::new(PERR::WrongFnDefinition, pos)), + (Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), _ => { while input.peek().is_some() { @@ -1997,29 +1932,30 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result { + match input.peek().ok_or_else(|| { + ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + ) + })? { + (Token::RightBrace, _) => { input.next(); Ok(Stmt::Block(statements, pos)) } - Some(&(_, pos)) => Err(ParseError::new( + (_, pos) => Err(ParseError::new( PERR::MissingRightBrace("end of block".into()), - pos, - )), - None => Err(ParseError::new( - PERR::MissingRightBrace("end of block".into()), - Position::eof(), + *pos, )), } } @@ -2029,16 +1965,20 @@ fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { - match input.peek() { - Some(&(Token::If, _)) => parse_if(input), - Some(&(Token::While, _)) => parse_while(input), - Some(&(Token::Loop, _)) => parse_loop(input), - Some(&(Token::For, _)) => parse_for(input), - Some(&(Token::Break, pos)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + (Token::If, _) => parse_if(input), + (Token::While, _) => parse_while(input), + (Token::Loop, _) => parse_loop(input), + (Token::For, _) => parse_for(input), + (Token::Break, pos) => { + let pos = *pos; input.next(); Ok(Stmt::Break(pos)) } - Some(&(ref token @ Token::Return, _)) | Some(&(ref token @ Token::Throw, _)) => { + (token @ Token::Return, _) | (token @ Token::Throw, _) => { let return_type = match token { Token::Return => ReturnType::Return, Token::Throw => ReturnType::Exception, @@ -2048,76 +1988,109 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result Ok(Stmt::ReturnWithVal(None, return_type, pos)), - // Just a return/throw without anything at the end of script + // return/throw at EOF None => Ok(Stmt::ReturnWithVal(None, return_type, Position::eof())), + // return; or throw; + Some((Token::SemiColon, pos)) => { + let pos = *pos; + Ok(Stmt::ReturnWithVal(None, return_type, pos)) + } // return or throw with expression - Some(&(_, pos)) => { + Some((_, pos)) => { + let pos = *pos; let ret = parse_expr(input)?; Ok(Stmt::ReturnWithVal(Some(Box::new(ret)), return_type, pos)) } } } - Some(&(Token::LeftBrace, _)) => parse_block(input), - Some(&(Token::Let, _)) => parse_var(input, VariableType::Normal), - Some(&(Token::Const, _)) => parse_var(input, VariableType::Constant), + (Token::LeftBrace, _) => parse_block(input), + (Token::Let, _) => parse_var(input, VariableType::Normal), + (Token::Const, _) => parse_var(input, VariableType::Constant), _ => parse_expr_stmt(input), } } #[cfg(not(feature = "no_function"))] fn parse_fn<'a>(input: &mut Peekable>) -> Result { - let pos = match input.next() { - Some((_, tok_pos)) => tok_pos, - _ => return Err(ParseError::new(PERR::InputPastEndOfFile, Position::eof())), + let pos = input + .next() + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + .1; + + let name = match input + .next() + .ok_or_else(|| ParseError::new(PERR::FnMissingName, Position::eof()))? + { + (Token::Identifier(s), _) => s, + (_, pos) => return Err(ParseError::new(PERR::FnMissingName, pos)), }; - let name = match input.next() { - Some((Token::Identifier(s), _)) => s, - Some((_, pos)) => return Err(ParseError::new(PERR::FnMissingName, pos)), - None => return Err(ParseError::new(PERR::FnMissingName, Position::eof())), - }; - - match input.peek() { - Some(&(Token::LeftParen, _)) => { + match input + .peek() + .ok_or_else(|| ParseError::new(PERR::FnMissingParams(name.clone()), Position::eof()))? + { + (Token::LeftParen, _) => { input.next(); } - Some(&(_, pos)) => return Err(ParseError::new(PERR::FnMissingParams(name), pos)), - None => { - return Err(ParseError::new( - PERR::FnMissingParams(name), - Position::eof(), - )) - } + (_, pos) => return Err(ParseError::new(PERR::FnMissingParams(name), *pos)), } let mut params = Vec::new(); - if matches!(input.peek(), Some(&(Token::RightParen, _))) { + if matches!(input.peek(), Some((Token::RightParen, _))) { input.next(); } else { loop { - match input.next() { - Some((Token::RightParen, _)) => break, - Some((Token::Comma, _)) => (), - Some((Token::Identifier(s), _)) => { + match input.next().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + Position::eof(), + ) + })? { + (Token::Identifier(s), _) => { params.push(s.into()); } - Some((_, pos)) => { + (_, pos) => { return Err(ParseError::new( - PERR::MalformedCallExpr( - "Function call arguments missing either a ',' or a ')'".into(), - ), + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), pos, )) } - None => { + } + + match input.next().ok_or_else(|| { + ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + Position::eof(), + ) + })? { + (Token::RightParen, _) => break, + (Token::Comma, _) => (), + (Token::Identifier(_), _) => { return Err(ParseError::new( - PERR::MalformedCallExpr( - "Function call arguments missing a closing ')'".into(), - ), - Position::eof(), + PERR::MissingComma(format!( + "separating the parameters of function '{}'", + name + )), + pos, + )) + } + (_, pos) => { + return Err(ParseError::new( + PERR::MissingRightParen(format!( + "closing the parameters list of function '{}'", + name + )), + pos, )) } } @@ -2141,9 +2114,9 @@ fn parse_top_level<'a, 'e>( let mut functions = Vec::::new(); while input.peek().is_some() { - match input.peek() { + match input.peek().expect("should not be None") { #[cfg(not(feature = "no_function"))] - Some(&(Token::Fn, _)) => { + (Token::Fn, _) => { let f = parse_fn(input)?; // Ensure list is sorted @@ -2156,7 +2129,7 @@ fn parse_top_level<'a, 'e>( } // Notice semicolons are optional - if let Some(&(Token::SemiColon, _)) = input.peek() { + if let Some((Token::SemiColon, _)) = input.peek() { input.next(); } } From c3d4a1b5e91be86009eb99c2ec1e5be03f524cdd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 10:27:43 +0800 Subject: [PATCH 11/24] Optimize type_of. --- src/optimize.rs | 63 ++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/optimize.rs b/src/optimize.rs index b3e11046..26efed79 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,7 +1,9 @@ #![cfg(not(feature = "no_optimize"))] -use crate::any::Dynamic; -use crate::engine::{Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT}; +use crate::any::{Any, Dynamic}; +use crate::engine::{ + Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT, KEYWORD_TYPE_OF, +}; use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; @@ -227,6 +229,8 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - } fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { + const SKIP_FUNC_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_DUMP_AST]; + match expr { Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { Stmt::Noop(_) => { @@ -279,7 +283,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); items.remove(i as usize) } - (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) + (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < s.chars().count() => { // String literal indexing - get the character @@ -347,41 +351,46 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { ), }, - // Do not optimize anything within `dump_ast` - Expr::FunctionCall(id, args, def_value, pos) if id == KEYWORD_DUMP_AST => { - Expr::FunctionCall(id, args, def_value, pos) - } + // Do not optimize anything within built-in function keywords + Expr::FunctionCall(id, args, def_value, pos) if SKIP_FUNC_KEYWORDS.contains(&id.as_str())=> + Expr::FunctionCall(id, args, def_value, pos), + // Actually call function to optimize it Expr::FunctionCall(id, args, def_value, pos) - if id != KEYWORD_DEBUG // not debug - && id != KEYWORD_PRINT // not print - && state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations + if state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations && args.iter().all(|expr| expr.is_constant()) // all arguments are constants - => - { + => { let engine = state.engine.expect("engine should be Some"); let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); - engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| - r.or(def_value.clone()).and_then(|result| map_dynamic_to_expr(result, pos).0) + + // Save the typename of the first argument if it is `type_of()` + // This is to avoid `call_args` being passed into the closure + let arg_for_type_of = if id == KEYWORD_TYPE_OF && call_args.len() == 1 { + engine.map_type_name(call_args[0].type_name()) + } else { + "" + }; + + engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| + r.or_else(|| { + if !arg_for_type_of.is_empty() { + // Handle `type_of()` + Some(arg_for_type_of.to_string().into_dynamic()) + } else { + // Otherwise use the default value, if any + def_value.clone() + } + }).and_then(|result| map_dynamic_to_expr(result, pos).0) .map(|expr| { state.set_dirty(); expr - })).flatten() - .unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) + }) + ).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) } // Optimize the function call arguments - Expr::FunctionCall(id, args, def_value, pos) => { - let orig_len = args.len(); - - let args: Vec<_> = args.into_iter().map(|a| optimize_expr(a, state)).collect(); - - if orig_len != args.len() { - state.set_dirty(); - } - - Expr::FunctionCall(id, args, def_value, pos) - } + Expr::FunctionCall(id, args, def_value, pos) => + Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), Expr::Variable(ref name, _) if state.contains_constant(name) => { state.set_dirty(); From b26ca753c2387355108e966ae0e7d410d1f44a09 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 14:29:22 +0800 Subject: [PATCH 12/24] Mandatory semiclolons separating statements. --- src/error.rs | 9 +++-- src/optimize.rs | 4 +-- src/parser.rs | 92 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/error.rs b/src/error.rs index 53a0091d..c04e2c32 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,6 +60,8 @@ pub enum ParseErrorType { MissingRightBracket(String), /// A list of expressions is missing the separating ','. MissingComma(String), + /// A statement is missing the ending ';'. + MissingSemicolon(String), /// An expression in function call arguments `()` has syntax error. MalformedCallExpr(String), /// An expression in indexing brackets `[]` has syntax error. @@ -119,6 +121,7 @@ impl ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(_) => "Expecting ']'", ParseErrorType::MissingComma(_) => "Expecting ','", + ParseErrorType::MissingSemicolon(_) => "Expecting ';'", ParseErrorType::MalformedCallExpr(_) => "Invalid expression in function call arguments", #[cfg(not(feature = "no_index"))] ParseErrorType::MalformedIndexExpr(_) => "Invalid index in indexing expression", @@ -130,7 +133,7 @@ impl ParseError { #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", #[cfg(not(feature = "no_function"))] - ParseErrorType::WrongFnDefinition => "Function definitions must be at top level and cannot be inside a block or another function", + ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression", ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value", ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable." @@ -168,7 +171,9 @@ impl fmt::Display for ParseError { #[cfg(not(feature = "no_index"))] ParseErrorType::MissingRightBracket(ref s) => write!(f, "{} for {}", self.desc(), s)?, - ParseErrorType::MissingComma(ref s) => write!(f, "{} for {}", self.desc(), s)?, + ParseErrorType::MissingSemicolon(ref s) | ParseErrorType::MissingComma(ref s) => { + write!(f, "{} for {}", self.desc(), s)? + } ParseErrorType::AssignmentToConstant(ref s) if s.is_empty() => { write!(f, "{}", self.desc())? diff --git a/src/optimize.rs b/src/optimize.rs index 26efed79..0c7d2c5b 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -455,7 +455,7 @@ pub(crate) fn optimize<'a>( if let Stmt::Const(name, value, _) = &stmt { // Load constants state.push_constant(name, value.as_ref().clone()); - stmt // Keep it in the top scope + stmt // Keep it in the global scope } else { // Keep all variable declarations at this level // and always keep the last return value @@ -474,7 +474,7 @@ pub(crate) fn optimize<'a>( // Eliminate code that is pure but always keep the last statement let last_stmt = result.pop(); - // Remove all pure statements at top level + // Remove all pure statements at global level result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); if let Some(stmt) = last_stmt { diff --git a/src/parser.rs b/src/parser.rs index c9dfe5e8..bdfa1f6c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -213,6 +213,23 @@ impl Stmt { matches!(self, Stmt::Let(_, _, _)) } + pub fn is_self_terminated(&self) -> bool { + match self { + Stmt::Noop(_) + | Stmt::IfElse(_, _, _) + | Stmt::While(_, _) + | Stmt::Loop(_) + | Stmt::For(_, _, _) + | Stmt::Block(_, _) => true, + + Stmt::Let(_, _, _) + | Stmt::Const(_, _, _) + | Stmt::Expr(_) + | Stmt::Break(_) + | Stmt::ReturnWithVal(_, _, _) => false, + } + } + pub fn position(&self) -> Position { match self { Stmt::Noop(pos) @@ -1923,22 +1940,42 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result (), // empty block - #[cfg(not(feature = "no_function"))] - (Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), - _ => { while input.peek().is_some() { // Parse statements inside the block - statements.push(parse_stmt(input)?); + let stmt = parse_stmt(input)?; - // Notice semicolons are optional - if let Some((Token::SemiColon, _)) = input.peek() { - input.next(); - } + // See if it needs a terminating semicolon + let need_semicolon = !stmt.is_self_terminated(); + statements.push(stmt); + + // End block with right brace if let Some((Token::RightBrace, _)) = input.peek() { break; } + + match input.peek() { + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), + + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); + } + + None => { + return Err(ParseError::new( + PERR::MissingRightBrace("end of block".into()), + Position::eof(), + )) + } + } } } } @@ -1969,6 +2006,9 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), + (Token::If, _) => parse_if(input), (Token::While, _) => parse_while(input), (Token::Loop, _) => parse_loop(input), @@ -2107,16 +2147,16 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( +fn parse_global_level<'a, 'e>( input: &mut Peekable>, ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); let mut functions = Vec::::new(); while input.peek().is_some() { - match input.peek().expect("should not be None") { - #[cfg(not(feature = "no_function"))] - (Token::Fn, _) => { + #[cfg(not(feature = "no_function"))] + { + if matches!(input.peek().expect("should not be None"), (Token::Fn, _)) { let f = parse_fn(input)?; // Ensure list is sorted @@ -2124,13 +2164,31 @@ fn parse_top_level<'a, 'e>( Ok(n) => functions[n] = f, // Override previous definition Err(n) => functions.insert(n, f), // New function definition } + + continue; } - _ => statements.push(parse_stmt(input)?), } - // Notice semicolons are optional - if let Some((Token::SemiColon, _)) = input.peek() { - input.next(); + let stmt = parse_stmt(input)?; + + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek() { + None => break, + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), + + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); + } } } @@ -2142,7 +2200,7 @@ pub fn parse<'a, 'e>( engine: &Engine<'e>, scope: &Scope, ) -> Result { - let (statements, functions) = parse_top_level(input)?; + let (statements, functions) = parse_global_level(input)?; Ok( #[cfg(not(feature = "no_optimize"))] From d2951bfb6be45d35b898436f1c42bc70a3b4b32c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 16:52:06 +0800 Subject: [PATCH 13/24] Make sure return is not an error. --- src/api.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/api.rs b/src/api.rs index 172e55d4..4f91d96f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -217,23 +217,19 @@ impl<'e> Engine<'e> { Ok(result) } - match eval_ast_internal(self, scope, ast) { - Err(EvalAltResult::Return(out, pos)) => out.downcast::().map(|v| *v).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).to_string(), - pos, - ) - }), - - Ok(out) => out.downcast::().map(|v| *v).map_err(|a| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name((*a).type_name()).to_string(), - Position::eof(), - ) - }), - - Err(err) => Err(err), - } + eval_ast_internal(self, scope, ast) + .or_else(|err| match err { + EvalAltResult::Return(out, _) => Ok(out), + _ => Err(err), + }) + .and_then(|out| { + out.downcast::().map(|v| *v).map_err(|a| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name((*a).type_name()).to_string(), + Position::eof(), + ) + }) + }) } /// Evaluate a file, but throw away the result and only return error (if any). @@ -333,7 +329,10 @@ impl<'e> Engine<'e> { self.clear_functions(); } - result + result.or_else(|err| match err { + EvalAltResult::Return(_, _) => Ok(()), + _ => Err(err), + }) } /// Load a list of functions into the Engine. From 8efe08041232e55e199bd3725dc5fff60753843a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 17:33:37 +0800 Subject: [PATCH 14/24] Handle break and return better. --- examples/repl.rs | 17 +++- examples/rhai_runner.rs | 8 +- src/engine.rs | 10 +- src/error.rs | 15 ++- src/optimize.rs | 71 ++++++++++--- src/parser.rs | 217 ++++++++++++++++++++++------------------ src/result.rs | 12 +-- tests/looping.rs | 32 +++--- tests/throw.rs | 2 +- 9 files changed, 242 insertions(+), 142 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 1187d1c9..953ee2a9 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -36,10 +36,18 @@ fn print_error(input: &str, err: EvalAltResult) { ); println!("{}", lines[p.line().unwrap() - 1]); + + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + _ => err.to_string(), + }; + println!( "{}^ {}", padding(" ", p.position().unwrap() - 1), - err.to_string().replace(&pos_text, "") + err_text.replace(&pos_text, "") ); } } @@ -86,7 +94,12 @@ fn main() { .map_err(EvalAltResult::ErrorParsing) .and_then(|r| { ast = Some(r); - engine.consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap()) + engine + .consume_ast_with_scope(&mut scope, true, ast.as_ref().unwrap()) + .or_else(|err| match err { + EvalAltResult::Return(_, _) => Ok(()), + err => Err(err), + }) }) { println!(""); diff --git a/examples/rhai_runner.rs b/examples/rhai_runner.rs index 4da91feb..2b9b2077 100644 --- a/examples/rhai_runner.rs +++ b/examples/rhai_runner.rs @@ -31,7 +31,13 @@ fn eprint_error(input: &str, err: EvalAltResult) { // EOF let line = lines.len() - 1; let pos = lines[line - 1].len(); - eprint_line(&lines, line, pos, &err.to_string()); + let err_text = match err { + EvalAltResult::ErrorRuntime(err, _) if !err.is_empty() => { + format!("Runtime error: {}", err) + } + _ => err.to_string(), + }; + eprint_line(&lines, line, pos, &err_text); } p if p.is_none() => { // No position diff --git a/src/engine.rs b/src/engine.rs index 9362dd30..e1290e07 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1068,7 +1068,9 @@ impl Engine<'_> { if *guard_val { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), + Err(EvalAltResult::ErrorLoopBreak(_)) => { + return Ok(().into_dynamic()) + } Err(x) => return Err(x), } } else { @@ -1083,7 +1085,7 @@ impl Engine<'_> { Stmt::Loop(body) => loop { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => return Ok(().into_dynamic()), + Err(EvalAltResult::ErrorLoopBreak(_)) => return Ok(().into_dynamic()), Err(x) => return Err(x), } }, @@ -1102,7 +1104,7 @@ impl Engine<'_> { match self.eval_stmt(scope, body) { Ok(_) => (), - Err(EvalAltResult::LoopBreak) => break, + Err(EvalAltResult::ErrorLoopBreak(_)) => break, Err(x) => return Err(x), } } @@ -1114,7 +1116,7 @@ impl Engine<'_> { } // Break statement - Stmt::Break(_) => Err(EvalAltResult::LoopBreak), + Stmt::Break(pos) => Err(EvalAltResult::ErrorLoopBreak(*pos)), // Empty return Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { diff --git a/src/error.rs b/src/error.rs index c04e2c32..59816e96 100644 --- a/src/error.rs +++ b/src/error.rs @@ -82,12 +82,17 @@ pub enum ParseErrorType { /// A function definition is missing the parameters list. Wrapped value is the function name. #[cfg(not(feature = "no_function"))] FnMissingParams(String), + /// A function definition is missing the body. Wrapped value is the function name. + #[cfg(not(feature = "no_function"))] + FnMissingBody(String), /// Assignment to an inappropriate LHS (left-hand-side) expression. AssignmentToInvalidLHS, /// Assignment to a copy of a value. AssignmentToCopy, /// Assignment to an a constant variable. AssignmentToConstant(String), + /// Break statement not inside a loop. + LoopBreak, } /// Error when parsing a script. @@ -133,10 +138,13 @@ impl ParseError { #[cfg(not(feature = "no_function"))] ParseErrorType::FnMissingParams(_) => "Expecting parameters in function declaration", #[cfg(not(feature = "no_function"))] + ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration", + #[cfg(not(feature = "no_function"))] ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", ParseErrorType::AssignmentToInvalidLHS => "Cannot assign to this expression", ParseErrorType::AssignmentToCopy => "Cannot assign to this expression because it will only be changing a copy of the value", - ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable." + ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant variable.", + ParseErrorType::LoopBreak => "Break statement should only be used inside a loop" } } } @@ -164,6 +172,11 @@ impl fmt::Display for ParseError { write!(f, "Expecting parameters for function '{}'", s)? } + #[cfg(not(feature = "no_function"))] + ParseErrorType::FnMissingBody(ref s) => { + write!(f, "Expecting body statement block for function '{}'", s)? + } + ParseErrorType::MissingRightParen(ref s) | ParseErrorType::MissingRightBrace(ref s) => { write!(f, "{} for {}", self.desc(), s)? } diff --git a/src/optimize.rs b/src/optimize.rs index 0c7d2c5b..227110ee 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -4,7 +4,7 @@ use crate::any::{Any, Dynamic}; use crate::engine::{ Engine, FnCallArgs, KEYWORD_DEBUG, KEYWORD_DUMP_AST, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; -use crate::parser::{map_dynamic_to_expr, Expr, FnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; use crate::stdlib::{ @@ -121,18 +121,40 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - Stmt::Noop(pos) } Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), - expr => Stmt::While( - Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt, state, false)), - ), + expr => match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement - turn into running the guard expression once + state.set_dirty(); + let mut statements = vec![Stmt::Expr(Box::new(optimize_expr(expr, state)))]; + if preserve_result { + statements.push(Stmt::Noop(pos)) + } + Stmt::Block(statements, pos) + } + stmt => Stmt::While(Box::new(optimize_expr(expr, state)), Box::new(stmt)), + }, + }, + Stmt::Loop(stmt) => match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement + state.set_dirty(); + Stmt::Noop(pos) + } + stmt => Stmt::Loop(Box::new(stmt)), }, - - Stmt::Loop(stmt) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), Stmt::For(id, expr, stmt) => Stmt::For( id, Box::new(optimize_expr(*expr, state)), - Box::new(optimize_stmt(*stmt, state, false)), + Box::new(match optimize_stmt(*stmt, state, false) { + Stmt::Break(pos) => { + // Only a single break statement + state.set_dirty(); + Stmt::Noop(pos) + } + stmt => stmt, + }), ), + Stmt::Let(id, Some(expr), pos) => { Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos) } @@ -153,15 +175,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - optimize_stmt(stmt, state, preserve_result) // Optimize the statement } }) - .enumerate() - .filter(|(i, stmt)| stmt.is_op() || (preserve_result && *i == orig_len - 1)) // Remove no-op's but leave the last one if we need the result - .map(|(_, stmt)| stmt) .collect(); // Remove all raw expression statements that are pure except for the very last statement let last_stmt = if preserve_result { result.pop() } else { None }; - result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); + result.retain(|stmt| !stmt.is_pure()); if let Some(stmt) = last_stmt { result.push(stmt); @@ -197,6 +216,24 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - .collect(); } + // Remove everything following the the first return/throw + let mut dead_code = false; + + result.retain(|stmt| { + if dead_code { + return false; + } + + match stmt { + Stmt::ReturnWithVal(_, _, _) | Stmt::Break(_) => { + dead_code = true; + } + _ => (), + } + + true + }); + if orig_len != result.len() { state.set_dirty(); } @@ -504,7 +541,15 @@ pub fn optimize_ast( OptimizationLevel::Simple | OptimizationLevel::Full => { let pos = fn_def.body.position(); let mut body = optimize(vec![fn_def.body], None, &Scope::new()); - fn_def.body = body.pop().unwrap_or_else(|| Stmt::Noop(pos)); + fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + Stmt::ReturnWithVal(Some(val), ReturnType::Return, _) => { + Stmt::Expr(val) + } + Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { + Stmt::Expr(Box::new(Expr::Unit(pos))) + } + stmt => stmt, + }; } } Arc::new(fn_def) diff --git a/src/parser.rs b/src/parser.rs index bdfa1f6c..702f0fed 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -201,6 +201,19 @@ pub enum Stmt { } impl Stmt { + pub fn position(&self) -> Position { + match self { + Stmt::Noop(pos) + | Stmt::Let(_, _, pos) + | Stmt::Const(_, _, pos) + | Stmt::Block(_, pos) + | Stmt::Break(pos) + | Stmt::ReturnWithVal(_, _, pos) => *pos, + Stmt::IfElse(expr, _, _) | Stmt::Expr(expr) => expr.position(), + Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(), + } + } + pub fn is_noop(&self) -> bool { matches!(self, Stmt::Noop(_)) } @@ -230,16 +243,21 @@ impl Stmt { } } - pub fn position(&self) -> Position { + pub fn is_pure(&self) -> bool { match self { - Stmt::Noop(pos) - | Stmt::Let(_, _, pos) - | Stmt::Const(_, _, pos) - | Stmt::Block(_, pos) - | Stmt::Break(pos) - | Stmt::ReturnWithVal(_, _, pos) => *pos, - Stmt::IfElse(expr, _, _) | Stmt::Expr(expr) => expr.position(), - Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(), + Stmt::Noop(_) => true, + Stmt::Expr(expr) => expr.is_pure(), + Stmt::IfElse(guard, if_block, Some(else_block)) => { + guard.is_pure() && if_block.is_pure() && else_block.is_pure() + } + Stmt::IfElse(guard, block, None) | Stmt::While(guard, block) => { + guard.is_pure() && block.is_pure() + } + Stmt::Loop(block) => block.is_pure(), + Stmt::For(_, range, block) => range.is_pure() && block.is_pure(), + Stmt::Let(_, _, _) | Stmt::Const(_, _, _) => false, + Stmt::Block(statements, _) => statements.iter().all(Stmt::is_pure), + Stmt::Break(_) | Stmt::ReturnWithVal(_, _, _) => false, } } } @@ -353,6 +371,8 @@ impl Expr { Expr::And(x, y) | Expr::Or(x, y) => x.is_pure() && y.is_pure(), + Expr::Stmt(stmt, _) => stmt.is_pure(), + expr => expr.is_constant() || matches!(expr, Expr::Variable(_, _)), } } @@ -1457,7 +1477,7 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result { let pos = *pos; - return parse_block(input).map(|block| Expr::Stmt(Box::new(block), pos)); + return parse_block(input, false).map(|block| Expr::Stmt(Box::new(block), pos)); } _ => (), } @@ -1467,38 +1487,39 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FloatConstant(x, pos)), + let mut root_expr = match token + .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? + { + #[cfg(not(feature = "no_float"))] + (Token::FloatConstant(x), pos) => Ok(Expr::FloatConstant(x, pos)), - (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), - (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), - (Token::StringConst(s), pos) => { - can_be_indexed = true; - Ok(Expr::StringConstant(s, pos)) - } - (Token::Identifier(s), pos) => { - can_be_indexed = true; - parse_ident_expr(s, input, pos) - } - (Token::LeftParen, pos) => { - can_be_indexed = true; - parse_paren_expr(input, pos) - } - #[cfg(not(feature = "no_index"))] - (Token::LeftBracket, pos) => { - can_be_indexed = true; - parse_array_expr(input, pos) - } - (Token::True, pos) => Ok(Expr::True(pos)), - (Token::False, pos) => Ok(Expr::False(pos)), - (Token::LexError(le), pos) => Err(ParseError::new(PERR::BadInput(le.to_string()), pos)), - (token, pos) => Err(ParseError::new( - PERR::BadInput(format!("Unexpected '{}'", token.syntax())), - pos, - )), - }?; + (Token::IntegerConstant(x), pos) => Ok(Expr::IntegerConstant(x, pos)), + (Token::CharConstant(c), pos) => Ok(Expr::CharConstant(c, pos)), + (Token::StringConst(s), pos) => { + can_be_indexed = true; + Ok(Expr::StringConstant(s, pos)) + } + (Token::Identifier(s), pos) => { + can_be_indexed = true; + parse_ident_expr(s, input, pos) + } + (Token::LeftParen, pos) => { + can_be_indexed = true; + parse_paren_expr(input, pos) + } + #[cfg(not(feature = "no_index"))] + (Token::LeftBracket, pos) => { + can_be_indexed = true; + parse_array_expr(input, pos) + } + (Token::True, pos) => Ok(Expr::True(pos)), + (Token::False, pos) => Ok(Expr::False(pos)), + (Token::LexError(err), pos) => Err(ParseError::new(PERR::BadInput(err.to_string()), pos)), + (token, pos) => Err(ParseError::new( + PERR::BadInput(format!("Unexpected '{}'", token.syntax())), + pos, + )), + }?; if can_be_indexed { // Tail processing all possible indexing @@ -1814,19 +1835,22 @@ fn parse_expr<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_if<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { input.next(); let guard = parse_expr(input)?; - let if_body = parse_block(input)?; + let if_body = parse_block(input, breakable)?; let else_body = if matches!(input.peek(), Some((Token::Else, _))) { input.next(); Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) { - parse_if(input)? + parse_if(input, breakable)? } else { - parse_block(input)? + parse_block(input, breakable)? })) } else { None @@ -1839,7 +1863,7 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); - let body = parse_block(input)?; + let body = parse_block(input, true)?; Ok(Stmt::Loop(Box::new(body))) } @@ -1860,8 +1884,8 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result s, - (Token::LexError(s), pos) => { - return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + (Token::LexError(err), pos) => { + return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; @@ -1875,7 +1899,7 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result( .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? { (Token::Identifier(s), _) => s, - (Token::LexError(s), pos) => { - return Err(ParseError::new(PERR::BadInput(s.to_string()), pos)) + (Token::LexError(err), pos) => { + return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; @@ -1921,7 +1945,10 @@ fn parse_var<'a>( } } -fn parse_block<'a>(input: &mut Peekable>) -> Result { +fn parse_block<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { let pos = match input .next() .ok_or_else(|| ParseError::new(PERR::MissingLeftBrace, Position::eof()))? @@ -1932,50 +1959,31 @@ fn parse_block<'a>(input: &mut Peekable>) -> Result (), // empty block + while !matches!(input.peek(), Some((Token::RightBrace, _))) { + // Parse statements inside the block + let stmt = parse_stmt(input, breakable)?; - _ => { - while input.peek().is_some() { - // Parse statements inside the block - let stmt = parse_stmt(input)?; + // See if it needs a terminating semicolon + let need_semicolon = !stmt.is_self_terminated(); - // See if it needs a terminating semicolon - let need_semicolon = !stmt.is_self_terminated(); + statements.push(stmt); - statements.push(stmt); + match input.peek() { + None => break, - // End block with right brace - if let Some((Token::RightBrace, _)) = input.peek() { - break; - } + Some((Token::RightBrace, _)) => break, - match input.peek() { - Some((Token::SemiColon, _)) => { - input.next(); - } - Some((_, _)) if !need_semicolon => (), + Some((Token::SemiColon, _)) => { + input.next(); + } + Some((_, _)) if !need_semicolon => (), - Some((_, pos)) => { - // Semicolons are not optional between statements - return Err(ParseError::new( - PERR::MissingSemicolon("terminating a statement".into()), - *pos, - )); - } - - None => { - return Err(ParseError::new( - PERR::MissingRightBrace("end of block".into()), - Position::eof(), - )) - } - } + Some((_, pos)) => { + // Semicolons are not optional between statements + return Err(ParseError::new( + PERR::MissingSemicolon("terminating a statement".into()), + *pos, + )); } } } @@ -2001,7 +2009,10 @@ fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { +fn parse_stmt<'a>( + input: &mut Peekable>, + breakable: bool, +) -> Result { match input .peek() .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? @@ -2009,15 +2020,16 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), - (Token::If, _) => parse_if(input), + (Token::If, _) => parse_if(input, breakable), (Token::While, _) => parse_while(input), (Token::Loop, _) => parse_loop(input), (Token::For, _) => parse_for(input), - (Token::Break, pos) => { + (Token::Break, pos) if breakable => { let pos = *pos; input.next(); Ok(Stmt::Break(pos)) } + (Token::Break, pos) => return Err(ParseError::new(PERR::LoopBreak, *pos)), (token @ Token::Return, _) | (token @ Token::Throw, _) => { let return_type = match token { Token::Return => ReturnType::Return, @@ -2038,12 +2050,15 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result { let pos = *pos; - let ret = parse_expr(input)?; - Ok(Stmt::ReturnWithVal(Some(Box::new(ret)), return_type, pos)) + Ok(Stmt::ReturnWithVal( + Some(Box::new(parse_expr(input)?)), + return_type, + pos, + )) } } } - (Token::LeftBrace, _) => parse_block(input), + (Token::LeftBrace, _) => parse_block(input, breakable), (Token::Let, _) => parse_var(input, VariableType::Normal), (Token::Const, _) => parse_var(input, VariableType::Constant), _ => parse_expr_stmt(input), @@ -2137,7 +2152,11 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result parse_block(input, false)?, + Some((_, pos)) => return Err(ParseError::new(PERR::FnMissingBody(name), *pos)), + None => return Err(ParseError::new(PERR::FnMissingBody(name), Position::eof())), + }; Ok(FnDef { name, @@ -2169,7 +2188,7 @@ fn parse_global_level<'a, 'e>( } } - let stmt = parse_stmt(input)?; + let stmt = parse_stmt(input, false)?; let need_semicolon = !stmt.is_self_terminated(); diff --git a/src/result.rs b/src/result.rs index 12eb9d78..75bd92a2 100644 --- a/src/result.rs +++ b/src/result.rs @@ -62,8 +62,8 @@ pub enum EvalAltResult { ErrorArithmetic(String, Position), /// Run-time error encountered. Wrapped value is the error message. ErrorRuntime(String, Position), - /// Internal use: Breaking out of loops. - LoopBreak, + /// Breaking out of loops - not an error if within a loop. + ErrorLoopBreak(Position), /// Not an error: Value returned from a script via the `return` keyword. /// Wrapped value is the result value. Return(Dynamic, Position), @@ -106,7 +106,7 @@ impl EvalAltResult { Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorRuntime(_, _) => "Runtime error", - Self::LoopBreak => "[Not Error] Breaks out of loop", + Self::ErrorLoopBreak(_) => "Break statement not inside a loop", Self::Return(_, _) => "[Not Error] Function returns value", } } @@ -134,7 +134,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorRuntime(s, pos) => { write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) } - Self::LoopBreak => write!(f, "{}", desc), + Self::ErrorLoopBreak(pos) => write!(f, "{} ({})", desc, pos), Self::Return(_, pos) => write!(f, "{} ({})", desc, pos), #[cfg(not(feature = "no_stdlib"))] Self::ErrorReadingScriptFile(path, err) => { @@ -211,7 +211,6 @@ impl EvalAltResult { match self { #[cfg(not(feature = "no_stdlib"))] Self::ErrorReadingScriptFile(_, _) => Position::none(), - Self::LoopBreak => Position::none(), Self::ErrorParsing(err) => err.position(), @@ -232,6 +231,7 @@ impl EvalAltResult { | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorRuntime(_, pos) + | Self::ErrorLoopBreak(pos) | Self::Return(_, pos) => *pos, } } @@ -240,7 +240,6 @@ impl EvalAltResult { match self { #[cfg(not(feature = "no_stdlib"))] Self::ErrorReadingScriptFile(_, _) => (), - Self::LoopBreak => (), Self::ErrorParsing(ParseError(_, ref mut pos)) | Self::ErrorFunctionNotFound(_, ref mut pos) @@ -260,6 +259,7 @@ impl EvalAltResult { | Self::ErrorDotExpr(_, ref mut pos) | Self::ErrorArithmetic(_, ref mut pos) | Self::ErrorRuntime(_, ref mut pos) + | Self::ErrorLoopBreak(ref mut pos) | Self::Return(_, ref mut pos) => *pos = new_position, } } diff --git a/tests/looping.rs b/tests/looping.rs index 4baa0aab..651cbe32 100644 --- a/tests/looping.rs +++ b/tests/looping.rs @@ -1,27 +1,29 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_loop() -> Result<(), EvalAltResult> { let mut engine = Engine::new(); - assert!(engine.eval::( - r" - let x = 0; - let i = 0; + assert_eq!( + engine.eval::( + r" + let x = 0; + let i = 0; - loop { - if i < 10 { - x = x + i; - i = i + 1; + loop { + if i < 10 { + x = x + i; + i = i + 1; + } else { + break; + } } - else { - break; - } - } - x == 45 + return x; " - )?); + )?, + 45 + ); Ok(()) } diff --git a/tests/throw.rs b/tests/throw.rs index 518f56d8..690d9c6e 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -9,6 +9,6 @@ fn test_throw() { EvalAltResult::ErrorRuntime(s, _) if s == "hello")); assert!(matches!( - engine.eval::<()>(r#"throw;"#).expect_err("expects error"), + engine.eval::<()>(r#"throw"#).expect_err("expects error"), EvalAltResult::ErrorRuntime(s, _) if s == "")); } From 777f66ff3c009d8077ba615ad64ba44fdf6fc5af Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 17 Mar 2020 17:37:28 +0800 Subject: [PATCH 15/24] Add section on statements and variables. --- README.md | 87 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 09a5d6d4..5032e531 100644 --- a/README.md +++ b/README.md @@ -611,6 +611,27 @@ let /* intruder comment */ name = "Bob"; */ ``` +Statements +---------- + +Statements are terminated by semicolons '`;`' - they are mandatory, except for the _last_ statement where it can be omitted. + +A statement can be used anywhere where an expression is expected. The _last_ statement of a statement block +(enclosed by '`{`' .. '`}`' pairs) is always the return value of the statement. If a statement has no return value +(e.g. variable definitions, assignments) then the value will be `()`. + +```rust +let a = 42; // normal assignment statement +let a = foo(42); // normal function call statement +foo < 42; // normal expression as statement + +let a = { 40 + 2 }; // the value of 'a' is the value of the statement block, which is the value of the last statement +// ^ notice that the last statement does not require an ending semicolon + +4 * 10 + 2 // this is also a statement, which is an expression, with no ending semicolon because + // it is the last statement of the whole block +``` + Variables --------- @@ -619,21 +640,27 @@ Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII let Variable names must start with an ASCII letter or an underscore '`_`', and must contain at least one ASCII letter within. Therefore, names like '`_`', '`_42`' etc. are not legal variable names. Variable names are also case _sensitive_. -Variables are defined using the `let` keyword. +Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. ```rust -let x = 3; // ok -let _x = 42; // ok -let x_ = 42; // also ok -let _x_ = 42; // still ok +let x = 3; // ok +let _x = 42; // ok +let x_ = 42; // also ok +let _x_ = 42; // still ok -let _ = 123; // syntax error - illegal variable name -let _9 = 9; // syntax error - illegal variable name +let _ = 123; // syntax error - illegal variable name +let _9 = 9; // syntax error - illegal variable name -let x = 42; // variable is 'x', lower case -let X = 123; // variable is 'X', upper case +let x = 42; // variable is 'x', lower case +let X = 123; // variable is 'X', upper case x == 42; X == 123; + +{ + let x = 999; // local variable 'x' shadows the 'x' in parent block + x == 999; // access to local 'x' +} +x == 42; // the parent block's 'x' is not changed ``` Constants @@ -1118,7 +1145,7 @@ regardless of whether it is terminated with a semicolon `;`. This is different f ```rust fn add(x, y) { - x + y; // value of the last statement is used as the function's return value + x + y; // value of the last statement (no need for ending semicolon) is used as the return value } fn add2(x) { @@ -1345,6 +1372,10 @@ An engine's optimization level is set via a call to `set_optimization_level`: engine.set_optimization_level(rhai::OptimizationLevel::Full); ``` +When the optimization level is [`OptimizationLevel::Full`], the engine assumes all functions to be _pure_ and will _eagerly_ +evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators +(which are implemented as functions). For instance, the same example above: + ```rust // When compiling the following with OptimizationLevel::Full... @@ -1356,18 +1387,38 @@ if DECISION == 1 { // is a function call to the '==' function, and it retur print("boo!"); // this block is eliminated because it is never reached } -print("hello!"); // <- the above is equivalent to this +print("hello!"); // <- the above is equivalent to this ('print' and 'debug' are handled specially) ``` -### Side effect considerations +Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result. +This does not happen with `OptimizationLevel::Simple` which doesn't assume all functions to be _pure_. -All built-in operators have _pure_ functions (i.e. they do not cause side effects) so using [`OptimizationLevel::Full`] is usually quite safe. -Beware, however, that if custom functions are registered, they'll also be called. -If custom functions are registered to replace built-in operator functions, the custom functions will be called -and _may_ cause side-effects. +```rust +// When compiling the following with OptimizationLevel::Full... -Therefore, when using [`OptimizationLevel::Full`], it is recommended that registrations of custom functions be held off -until _after_ the compilation process. +let x = (1 + 2) * 3 - 4 / 5 % 6; // <- will be replaced by 'let x = 9' +let y = (1 > 2) || (3 <= 4); // <- will be replaced by 'let y = true' +``` + +### Function side effect considerations + +All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state +nor cause side any effects, with the exception of `print` and `debug` which are handled specially) so using [`OptimizationLevel::Full`] +is usually quite safe _unless_ you register your own types and functions. + +If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block). +If custom functions are registered to replace built-in operators, they will also be called when the operators are used (in an `if` +statement, for example) and cause side-effects. + +### Function volatility considerations + +Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ on the external +environment and not _pure_. A perfect example is a function that gets the current time - obviously each run will return a different value! +The optimizer, when using [`OptimizationLevel::Full`], _assumes_ that all functions are _pure_, so when it finds constant arguments. +This may cause the script to behave differently from the intended semantics because essentially the result of each function call will +always be the same value. + +Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions. ### Subtle semantic changes From ca20faf170cc866357bd5a99dc84eaa9450c849c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 10:36:50 +0800 Subject: [PATCH 16/24] Add code comments. --- src/any.rs | 10 +-- src/builtin.rs | 74 +++++++++++---- src/optimize.rs | 235 ++++++++++++++++++++++++++++-------------------- src/parser.rs | 218 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 385 insertions(+), 152 deletions(-) diff --git a/src/any.rs b/src/any.rs index 927f2124..f804cb24 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,7 +1,7 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. use crate::stdlib::{ - any::{self, type_name, TypeId}, + any::{type_name, Any as StdAny, TypeId}, boxed::Box, fmt, }; @@ -13,7 +13,7 @@ pub type Variant = dyn Any; pub type Dynamic = Box; /// A trait covering any type. -pub trait Any: any::Any { +pub trait Any: StdAny { /// Get the `TypeId` of this type. fn type_id(&self) -> TypeId; @@ -23,12 +23,12 @@ pub trait Any: any::Any { /// Convert into `Dynamic`. fn into_dynamic(&self) -> Dynamic; - /// This type may only be implemented by `rhai`. + /// This trait may only be implemented by `rhai`. #[doc(hidden)] fn _closed(&self) -> _Private; } -impl Any for T { +impl Any for T { fn type_id(&self) -> TypeId { TypeId::of::() } @@ -91,7 +91,7 @@ pub trait AnyExt: Sized { /// Get a copy of a `Dynamic` value as a specific type. fn downcast(self) -> Result, Self>; - /// This type may only be implemented by `rhai`. + /// This trait may only be implemented by `rhai`. #[doc(hidden)] fn _closed(&self) -> _Private; } diff --git a/src/builtin.rs b/src/builtin.rs index 1214c1b0..55226af7 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -5,7 +5,7 @@ use crate::any::Any; #[cfg(not(feature = "no_index"))] use crate::engine::Array; use crate::engine::Engine; -use crate::fn_register::{RegisterFn, RegisterResultFn}; +use crate::fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; use crate::parser::{Position, INT}; use crate::result::EvalAltResult; @@ -55,6 +55,7 @@ macro_rules! reg_op_result1 { impl Engine<'_> { /// Register the core built-in library. pub(crate) fn register_core_lib(&mut self) { + /// Checked add #[cfg(not(feature = "unchecked"))] fn add(x: T, y: T) -> Result { x.checked_add(&y).ok_or_else(|| { @@ -64,6 +65,7 @@ impl Engine<'_> { ) }) } + /// Checked subtract #[cfg(not(feature = "unchecked"))] fn sub(x: T, y: T) -> Result { x.checked_sub(&y).ok_or_else(|| { @@ -73,6 +75,7 @@ impl Engine<'_> { ) }) } + /// Checked multiply #[cfg(not(feature = "unchecked"))] fn mul(x: T, y: T) -> Result { x.checked_mul(&y).ok_or_else(|| { @@ -82,11 +85,13 @@ impl Engine<'_> { ) }) } + /// Checked divide #[cfg(not(feature = "unchecked"))] fn div(x: T, y: T) -> Result where T: Display + CheckedDiv + PartialEq + Zero, { + // Detect division by zero if y == T::zero() { return Err(EvalAltResult::ErrorArithmetic( format!("Division by zero: {} / {}", x, y), @@ -101,6 +106,7 @@ impl Engine<'_> { ) }) } + /// Checked negative - e.g. -(i32::MIN) will overflow i32::MAX #[cfg(not(feature = "unchecked"))] fn neg(x: T) -> Result { x.checked_neg().ok_or_else(|| { @@ -110,6 +116,7 @@ impl Engine<'_> { ) }) } + /// Checked absolute #[cfg(not(feature = "unchecked"))] fn abs(x: T) -> Result { // FIX - We don't use Signed::abs() here because, contrary to documentation, it panics @@ -125,26 +132,32 @@ impl Engine<'_> { }) } } + /// Unchecked add - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn add_u(x: T, y: T) -> ::Output { x + y } + /// Unchecked subtract - may panic on underflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn sub_u(x: T, y: T) -> ::Output { x - y } + /// Unchecked multiply - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn mul_u(x: T, y: T) -> ::Output { x * y } + /// Unchecked divide - may panic when dividing by zero #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn div_u(x: T, y: T) -> ::Output { x / y } + /// Unchecked negative - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn neg_u(x: T) -> ::Output { -x } + /// Unchecked absolute - may panic on overflow #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn abs_u(x: T) -> ::Output where @@ -157,6 +170,9 @@ impl Engine<'_> { x.into() } } + + // Comparison operators + fn lt(x: T, y: T) -> bool { x < y } @@ -175,6 +191,9 @@ impl Engine<'_> { fn ne(x: T, y: T) -> bool { x != y } + + // Logic operators + fn and(x: bool, y: bool) -> bool { x && y } @@ -184,6 +203,9 @@ impl Engine<'_> { fn not(x: bool) -> bool { !x } + + // Bit operators + fn binary_and(x: T, y: T) -> ::Output { x & y } @@ -193,8 +215,11 @@ impl Engine<'_> { fn binary_xor(x: T, y: T) -> ::Output { x ^ y } + + /// Checked left-shift #[cfg(not(feature = "unchecked"))] fn shl(x: T, y: INT) -> Result { + // Cannot shift by a negative number of bits if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Left-shift by a negative number: {} << {}", x, y), @@ -204,13 +229,15 @@ impl Engine<'_> { CheckedShl::checked_shl(&x, y as u32).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Left-shift overflow: {} << {}", x, y), + format!("Left-shift by too many bits: {} << {}", x, y), Position::none(), ) }) } + /// Checked right-shift #[cfg(not(feature = "unchecked"))] fn shr(x: T, y: INT) -> Result { + // Cannot shift by a negative number of bits if y < 0 { return Err(EvalAltResult::ErrorArithmetic( format!("Right-shift by a negative number: {} >> {}", x, y), @@ -220,44 +247,49 @@ impl Engine<'_> { CheckedShr::checked_shr(&x, y as u32).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Right-shift overflow: {} % {}", x, y), + format!("Right-shift by too many bits: {} % {}", x, y), Position::none(), ) }) } + /// Unchecked left-shift - may panic if shifting by a negative number of bits #[cfg(feature = "unchecked")] fn shl_u>(x: T, y: T) -> >::Output { x.shl(y) } + /// Unchecked right-shift - may panic if shifting by a negative number of bits #[cfg(feature = "unchecked")] fn shr_u>(x: T, y: T) -> >::Output { x.shr(y) } + /// Checked modulo #[cfg(not(feature = "unchecked"))] fn modulo(x: T, y: T) -> Result { x.checked_rem(&y).ok_or_else(|| { EvalAltResult::ErrorArithmetic( - format!("Modulo division overflow: {} % {}", x, y), + format!("Modulo division by zero or overflow: {} % {}", x, y), Position::none(), ) }) } + /// Unchecked modulo - may panic if dividing by zero #[cfg(any(feature = "unchecked", not(feature = "no_float")))] fn modulo_u(x: T, y: T) -> ::Output { x % y } + /// Checked power #[cfg(not(feature = "unchecked"))] - fn pow_i_i_u(x: INT, y: INT) -> Result { + fn pow_i_i(x: INT, y: INT) -> Result { #[cfg(not(feature = "only_i32"))] { if y > (u32::MAX as INT) { Err(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), + format!("Integer raised to too large an index: {} ~ {}", x, y), Position::none(), )) } else if y < 0 { Err(EvalAltResult::ErrorArithmetic( - format!("Power underflow: {} ~ {}", x, y), + format!("Integer raised to a negative index: {} ~ {}", x, y), Position::none(), )) } else { @@ -274,7 +306,7 @@ impl Engine<'_> { { if y < 0 { Err(EvalAltResult::ErrorArithmetic( - format!("Power underflow: {} ~ {}", x, y), + format!("Integer raised to a negative index: {} ~ {}", x, y), Position::none(), )) } else { @@ -287,29 +319,34 @@ impl Engine<'_> { } } } + /// Unchecked integer power - may panic on overflow or if the power index is too high (> u32::MAX) #[cfg(feature = "unchecked")] - fn pow_i_i(x: INT, y: INT) -> INT { + fn pow_i_i_u(x: INT, y: INT) -> INT { x.pow(y as u32) } + /// Floating-point power - always well-defined #[cfg(not(feature = "no_float"))] fn pow_f_f(x: FLOAT, y: FLOAT) -> FLOAT { x.powf(y) } + /// Checked power #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_float"))] - fn pow_f_i_u(x: FLOAT, y: INT) -> Result { + fn pow_f_i(x: FLOAT, y: INT) -> Result { + // Raise to power that is larger than an i32 if y > (i32::MAX as INT) { return Err(EvalAltResult::ErrorArithmetic( - format!("Power overflow: {} ~ {}", x, y), + format!("Number raised to too large an index: {} ~ {}", x, y), Position::none(), )); } Ok(x.powi(y as i32)) } + /// Unchecked power - may be incorrect if the power index is too high (> i32::MAX) #[cfg(feature = "unchecked")] #[cfg(not(feature = "no_float"))] - fn pow_f_i(x: FLOAT, y: INT) -> FLOAT { + fn pow_f_i_u(x: FLOAT, y: INT) -> FLOAT { x.powi(y as i32) } @@ -393,8 +430,10 @@ impl Engine<'_> { } } + // `&&` and `||` are treated specially as they short-circuit //reg_op!(self, "||", or, bool); //reg_op!(self, "&&", and, bool); + reg_op!(self, "|", or, bool); reg_op!(self, "&", and, bool); @@ -448,18 +487,18 @@ impl Engine<'_> { #[cfg(not(feature = "unchecked"))] { - self.register_result_fn("~", pow_i_i_u); + self.register_result_fn("~", pow_i_i); #[cfg(not(feature = "no_float"))] - self.register_result_fn("~", pow_f_i_u); + self.register_result_fn("~", pow_f_i); } #[cfg(feature = "unchecked")] { - self.register_fn("~", pow_i_i); + self.register_fn("~", pow_i_i_u); #[cfg(not(feature = "no_float"))] - self.register_fn("~", pow_f_i); + self.register_fn("~", pow_f_i_u); } { @@ -628,9 +667,6 @@ macro_rules! reg_fn2y { impl Engine<'_> { #[cfg(not(feature = "no_stdlib"))] pub(crate) fn register_stdlib(&mut self) { - #[cfg(not(feature = "no_index"))] - use crate::fn_register::RegisterDynamicFn; - #[cfg(not(feature = "no_float"))] { // Advanced math functions diff --git a/src/optimize.rs b/src/optimize.rs index 227110ee..79c922f6 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -13,50 +13,54 @@ use crate::stdlib::{ boxed::Box, vec, }; -/// Level of optimization performed +/// Level of optimization performed. #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] pub enum OptimizationLevel { - /// No optimization performed + /// No optimization performed. None, - /// Only perform simple optimizations without evaluating functions + /// Only perform simple optimizations without evaluating functions. Simple, /// Full optimizations performed, including evaluating functions. - /// Take care that this may cause side effects. + /// Take care that this may cause side effects as it essentially assumes that all functions are pure. Full, } +/// Mutable state throughout an optimization pass. struct State<'a> { + /// Has the AST been changed during this pass? changed: bool, + /// Collection of constants to use for eager function evaluations. constants: Vec<(String, Expr)>, - engine: Option<&'a Engine<'a>>, + /// An `Engine` instance for eager function evaluation. + engine: &'a Engine<'a>, } impl State<'_> { - pub fn new() -> Self { - State { - changed: false, - constants: vec![], - engine: None, - } - } + /// Reset the state from dirty to clean. pub fn reset(&mut self) { self.changed = false; } + /// Set the AST state to be dirty (i.e. changed). pub fn set_dirty(&mut self) { self.changed = true; } + /// Is the AST dirty (i.e. changed)? pub fn is_dirty(&self) -> bool { self.changed } + /// Does a constant exist? pub fn contains_constant(&self, name: &str) -> bool { self.constants.iter().any(|(n, _)| n == name) } + /// Prune the list of constants back to a specified size. pub fn restore_constants(&mut self, len: usize) { self.constants.truncate(len) } + /// Add a new constant to the list. pub fn push_constant(&mut self, name: &str, value: Expr) { self.constants.push((name.to_string(), value)) } + /// Look up a constant from the list. pub fn find_constant(&self, name: &str) -> Option<&Expr> { for (n, expr) in self.constants.iter().rev() { if n == name { @@ -68,60 +72,68 @@ impl State<'_> { } } +/// Optimize a statement. fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt { match stmt { - Stmt::IfElse(expr, stmt1, None) if stmt1.is_noop() => { + // if expr { Noop } + Stmt::IfElse(expr, if_block, None) if matches!(*if_block, Stmt::Noop(_)) => { state.set_dirty(); let pos = expr.position(); let expr = optimize_expr(*expr, state); - if matches!(expr, Expr::False(_) | Expr::True(_)) { - Stmt::Noop(stmt1.position()) + if preserve_result { + // -> { expr, Noop } + Stmt::Block(vec![Stmt::Expr(Box::new(expr)), *if_block], pos) } else { - let stmt = Stmt::Expr(Box::new(expr)); - - if preserve_result { - Stmt::Block(vec![stmt, *stmt1], pos) - } else { - stmt - } + // -> expr + Stmt::Expr(Box::new(expr)) } } - - Stmt::IfElse(expr, stmt1, None) => match *expr { + // if expr { if_block } + Stmt::IfElse(expr, if_block, None) => match *expr { + // if false { if_block } -> Noop Expr::False(pos) => { state.set_dirty(); Stmt::Noop(pos) } - Expr::True(_) => optimize_stmt(*stmt1, state, true), + // if true { if_block } -> if_block + Expr::True(_) => optimize_stmt(*if_block, state, true), + // if expr { if_block } expr => Stmt::IfElse( Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt1, state, true)), + Box::new(optimize_stmt(*if_block, state, true)), None, ), }, - - Stmt::IfElse(expr, stmt1, Some(stmt2)) => match *expr { - Expr::False(_) => optimize_stmt(*stmt2, state, true), - Expr::True(_) => optimize_stmt(*stmt1, state, true), + // if expr { if_block } else { else_block } + Stmt::IfElse(expr, if_block, Some(else_block)) => match *expr { + // if false { if_block } else { else_block } -> else_block + Expr::False(_) => optimize_stmt(*else_block, state, true), + // if true { if_block } else { else_block } -> if_block + Expr::True(_) => optimize_stmt(*if_block, state, true), + // if expr { if_block } else { else_block } expr => Stmt::IfElse( Box::new(optimize_expr(expr, state)), - Box::new(optimize_stmt(*stmt1, state, true)), - match optimize_stmt(*stmt2, state, true) { - stmt if stmt.is_noop() => None, + Box::new(optimize_stmt(*if_block, state, true)), + match optimize_stmt(*else_block, state, true) { + stmt if matches!(stmt, Stmt::Noop(_)) => None, // Noop -> no else block stmt => Some(Box::new(stmt)), }, ), }, - - Stmt::While(expr, stmt) => match *expr { + // while expr { block } + Stmt::While(expr, block) => match *expr { + // while false { block } -> Noop Expr::False(pos) => { state.set_dirty(); Stmt::Noop(pos) } - Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*stmt, state, false))), - expr => match optimize_stmt(*stmt, state, false) { + // while true { block } -> loop { block } + Expr::True(_) => Stmt::Loop(Box::new(optimize_stmt(*block, state, false))), + // while expr { block } + expr => match optimize_stmt(*block, state, false) { + // while expr { break; } -> { expr; } Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); @@ -131,48 +143,50 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - } Stmt::Block(statements, pos) } + // while expr { block } stmt => Stmt::While(Box::new(optimize_expr(expr, state)), Box::new(stmt)), }, }, - Stmt::Loop(stmt) => match optimize_stmt(*stmt, state, false) { + // loop { block } + Stmt::Loop(block) => match optimize_stmt(*block, state, false) { + // loop { break; } -> Noop Stmt::Break(pos) => { // Only a single break statement state.set_dirty(); Stmt::Noop(pos) } + // loop { block } stmt => Stmt::Loop(Box::new(stmt)), }, - Stmt::For(id, expr, stmt) => Stmt::For( + // for id in expr { block } + Stmt::For(id, expr, block) => Stmt::For( id, Box::new(optimize_expr(*expr, state)), - Box::new(match optimize_stmt(*stmt, state, false) { - Stmt::Break(pos) => { - // Only a single break statement - state.set_dirty(); - Stmt::Noop(pos) - } - stmt => stmt, - }), + Box::new(optimize_stmt(*block, state, false)), ), - + // let id = expr; Stmt::Let(id, Some(expr), pos) => { Stmt::Let(id, Some(Box::new(optimize_expr(*expr, state))), pos) } + // let id; Stmt::Let(_, None, _) => stmt, + // { block } + Stmt::Block(block, pos) => { + let orig_len = block.len(); // Original number of statements in the block, for change detection + let orig_constants_len = state.constants.len(); // Original number of constants in the state, for restore later - Stmt::Block(statements, pos) => { - let orig_len = statements.len(); - let orig_constants_len = state.constants.len(); - - let mut result: Vec<_> = statements - .into_iter() // For each statement + // Optimize each statement in the block + let mut result: Vec<_> = block + .into_iter() .map(|stmt| { if let Stmt::Const(name, value, pos) = stmt { + // Add constant into the state state.push_constant(&name, *value); state.set_dirty(); Stmt::Noop(pos) // No need to keep constants } else { - optimize_stmt(stmt, state, preserve_result) // Optimize the statement + // Optimize the statement + optimize_stmt(stmt, state, preserve_result) } }) .collect(); @@ -207,11 +221,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - result.push(Stmt::Noop(pos)) } + // Optimize all the statements again result = result .into_iter() .rev() .enumerate() - .map(|(i, s)| optimize_stmt(s, state, i == 0)) // Optimize all other statements again + .map(|(i, s)| optimize_stmt(s, state, i == 0)) .rev() .collect(); } @@ -234,10 +249,12 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - true }); + // Change detection if orig_len != result.len() { state.set_dirty(); } + // Pop the stack and remove all the local constants state.restore_constants(orig_constants_len); match result[..] { @@ -254,44 +271,54 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - _ => Stmt::Block(result, pos), } } - + // expr; Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))), - + // return expr; Stmt::ReturnWithVal(Some(expr), is_return, pos) => { Stmt::ReturnWithVal(Some(Box::new(optimize_expr(*expr, state))), is_return, pos) } - + // All other statements - skip stmt => stmt, } } +/// Optimize an expression. fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { + // These keywords are handled specially const SKIP_FUNC_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_DUMP_AST]; match expr { + // ( stmt ) Expr::Stmt(stmt, pos) => match optimize_stmt(*stmt, state, true) { + // ( Noop ) -> () Stmt::Noop(_) => { state.set_dirty(); Expr::Unit(pos) } + // ( expr ) -> expr Stmt::Expr(expr) => { state.set_dirty(); *expr } + // ( stmt ) stmt => Expr::Stmt(Box::new(stmt), pos), }, - Expr::Assignment(id1, expr1, pos1) => match *expr1 { - Expr::Assignment(id2, expr2, pos2) => match (*id1, *id2) { - (Expr::Variable(var1, _), Expr::Variable(var2, _)) if var1 == var2 => { + // id = expr + Expr::Assignment(id, expr, pos) => match *expr { + //id = id2 = expr2 + Expr::Assignment(id2, expr2, pos2) => match (*id, *id2) { + // var = var = expr2 -> var = expr2 + (Expr::Variable(var, _), Expr::Variable(var2, _)) if var == var2 => { // Assignment to the same variable - fold state.set_dirty(); Expr::Assignment( - Box::new(Expr::Variable(var1, pos1)), + Box::new(Expr::Variable(var, pos)), Box::new(optimize_expr(*expr2, state)), - pos1, + pos, ) } + // id1 = id2 = expr2 (id1, id2) => Expr::Assignment( Box::new(id1), Box::new(Expr::Assignment( @@ -299,19 +326,23 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Box::new(optimize_expr(*expr2, state)), pos2, )), - pos1, + pos, ), }, - expr => Expr::Assignment(id1, Box::new(optimize_expr(expr, state)), pos1), + // id = expr + expr => Expr::Assignment(id, Box::new(optimize_expr(expr, state)), pos), }, + // lhs.rhs Expr::Dot(lhs, rhs, pos) => Expr::Dot( Box::new(optimize_expr(*lhs, state)), Box::new(optimize_expr(*rhs, state)), pos, ), + // lhs[rhs] #[cfg(not(feature = "no_index"))] Expr::Index(lhs, rhs, pos) => match (*lhs, *rhs) { + // array[int] (Expr::Array(mut items, _), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < items.len() && items.iter().all(|x| x.is_pure()) => { @@ -320,6 +351,7 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); items.remove(i as usize) } + // string[int] (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, _)) if i >= 0 && (i as usize) < s.chars().count() => { @@ -327,14 +359,14 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { state.set_dirty(); Expr::CharConstant(s.chars().nth(i as usize).expect("should get char"), pos) } - + // lhs[rhs] (lhs, rhs) => Expr::Index( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), pos, ), }, - + // [ items .. ] #[cfg(not(feature = "no_index"))] Expr::Array(items, pos) => { let orig_len = items.len(); @@ -350,38 +382,47 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::Array(items, pos) } - + // lhs && rhs Expr::And(lhs, rhs) => match (*lhs, *rhs) { + // true && rhs -> rhs (Expr::True(_), rhs) => { state.set_dirty(); rhs } + // false && rhs -> false (Expr::False(pos), _) => { state.set_dirty(); Expr::False(pos) } + // lhs && true -> lhs (lhs, Expr::True(_)) => { state.set_dirty(); - lhs + optimize_expr(lhs, state) } + // lhs && rhs (lhs, rhs) => Expr::And( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), ), }, + // lhs || rhs Expr::Or(lhs, rhs) => match (*lhs, *rhs) { + // false || rhs -> rhs (Expr::False(_), rhs) => { state.set_dirty(); rhs } + // true || rhs -> true (Expr::True(pos), _) => { state.set_dirty(); Expr::True(pos) } + // lhs || false (lhs, Expr::False(_)) => { state.set_dirty(); - lhs + optimize_expr(lhs, state) } + // lhs || rhs (lhs, rhs) => Expr::Or( Box::new(optimize_expr(lhs, state)), Box::new(optimize_expr(rhs, state)), @@ -392,24 +433,23 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { Expr::FunctionCall(id, args, def_value, pos) if SKIP_FUNC_KEYWORDS.contains(&id.as_str())=> Expr::FunctionCall(id, args, def_value, pos), - // Actually call function to optimize it + // Eagerly call functions Expr::FunctionCall(id, args, def_value, pos) - if state.engine.map(|eng| eng.optimization_level == OptimizationLevel::Full).unwrap_or(false) // full optimizations + if state.engine.optimization_level == OptimizationLevel::Full // full optimizations && args.iter().all(|expr| expr.is_constant()) // all arguments are constants => { - let engine = state.engine.expect("engine should be Some"); let mut arg_values: Vec<_> = args.iter().map(Expr::get_constant_value).collect(); let call_args: FnCallArgs = arg_values.iter_mut().map(Dynamic::as_mut).collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure let arg_for_type_of = if id == KEYWORD_TYPE_OF && call_args.len() == 1 { - engine.map_type_name(call_args[0].type_name()) + state.engine.map_type_name(call_args[0].type_name()) } else { "" }; - engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| + state.engine.call_ext_fn_raw(&id, call_args, pos).ok().map(|r| r.or_else(|| { if !arg_for_type_of.is_empty() { // Handle `type_of()` @@ -425,10 +465,10 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { }) ).flatten().unwrap_or_else(|| Expr::FunctionCall(id, args, def_value, pos)) } - // Optimize the function call arguments + // id(args ..) -> optimize function call arguments Expr::FunctionCall(id, args, def_value, pos) => Expr::FunctionCall(id, args.into_iter().map(|a| optimize_expr(a, state)).collect(), def_value, pos), - + // constant-name Expr::Variable(ref name, _) if state.contains_constant(name) => { state.set_dirty(); @@ -438,28 +478,25 @@ fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { .expect("should find constant in scope!") .clone() } - + // All other expressions - skip expr => expr, } } -pub(crate) fn optimize<'a>( - statements: Vec, - engine: Option<&Engine<'a>>, - scope: &Scope, -) -> Vec { +pub(crate) fn optimize<'a>(statements: Vec, engine: &Engine<'a>, scope: &Scope) -> Vec { // If optimization level is None then skip optimizing - if engine - .map(|eng| eng.optimization_level == OptimizationLevel::None) - .unwrap_or(false) - { + if engine.optimization_level == OptimizationLevel::None { return statements; } // Set up the state - let mut state = State::new(); - state.engine = engine; + let mut state = State { + changed: false, + constants: vec![], + engine, + }; + // Add constants from the scope into the state scope .iter() .filter(|ScopeEntry { var_type, expr, .. }| { @@ -476,9 +513,9 @@ pub(crate) fn optimize<'a>( let orig_constants_len = state.constants.len(); - // Optimization loop let mut result = statements; + // Optimization loop loop { state.reset(); state.restore_constants(orig_constants_len); @@ -496,7 +533,7 @@ pub(crate) fn optimize<'a>( } else { // Keep all variable declarations at this level // and always keep the last return value - let keep = stmt.is_var() || i == num_statements - 1; + let keep = matches!(stmt, Stmt::Let(_, _, _)) || i == num_statements - 1; optimize_stmt(stmt, &mut state, keep) } @@ -521,6 +558,7 @@ pub(crate) fn optimize<'a>( result } +/// Optimize an AST. pub fn optimize_ast( engine: &Engine, scope: &Scope, @@ -530,8 +568,8 @@ pub fn optimize_ast( AST( match engine.optimization_level { OptimizationLevel::None => statements, - OptimizationLevel::Simple => optimize(statements, None, &scope), - OptimizationLevel::Full => optimize(statements, Some(engine), &scope), + OptimizationLevel::Simple => optimize(statements, engine, &scope), + OptimizationLevel::Full => optimize(statements, engine, &scope), }, functions .into_iter() @@ -540,14 +578,21 @@ pub fn optimize_ast( OptimizationLevel::None => (), OptimizationLevel::Simple | OptimizationLevel::Full => { let pos = fn_def.body.position(); - let mut body = optimize(vec![fn_def.body], None, &Scope::new()); + + // Optimize the function body + let mut body = optimize(vec![fn_def.body], engine, &Scope::new()); + + // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { + // { return val; } -> val Stmt::ReturnWithVal(Some(val), ReturnType::Return, _) => { Stmt::Expr(val) } + // { return; } -> () Stmt::ReturnWithVal(None, ReturnType::Return, pos) => { Stmt::Expr(Box::new(Expr::Unit(pos))) } + // All others stmt => stmt, }; } diff --git a/src/parser.rs b/src/parser.rs index 702f0fed..b89ef1b4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -29,13 +29,13 @@ use crate::stdlib::{ #[cfg(not(feature = "only_i32"))] pub type INT = i64; -/// The system integer type +/// The system integer type. /// /// If the `only_i32` feature is not enabled, this will be `i64` instead. #[cfg(feature = "only_i32")] pub type INT = i32; -/// The system floating-point type +/// The system floating-point type. #[cfg(not(feature = "no_float"))] pub type FLOAT = f64; @@ -45,7 +45,9 @@ type PERR = ParseErrorType; /// A location (line number + character position) in the input script. #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] pub struct Position { + /// Line number - 0 = none, MAX = EOF line: usize, + /// Character position - 0 = BOL, MAX = EOF pos: usize, } @@ -162,45 +164,69 @@ impl fmt::Debug for Position { #[derive(Debug, Clone)] pub struct AST(pub(crate) Vec, pub(crate) Vec>); +/// A script-function definition. #[derive(Debug, Clone)] pub struct FnDef { + /// Function name. pub name: String, + /// Names of function parameters. pub params: Vec, + /// Function body. pub body: Stmt, + /// Position of the function definition. pub pos: Position, } impl FnDef { + /// Function to order two FnDef records, for binary search. pub fn compare(&self, name: &str, params_len: usize) -> Ordering { + // First order by name match self.name.as_str().cmp(name) { + // Then by number of parameters Ordering::Equal => self.params.len().cmp(¶ms_len), order => order, } } } +/// `return`/`throw` statement. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub enum ReturnType { + /// `return` statement. Return, + /// `throw` statement. Exception, } +/// A statement. #[derive(Debug, Clone)] pub enum Stmt { + /// No-op. Noop(Position), + /// if expr { stmt } else { stmt } IfElse(Box, Box, Option>), + /// while expr { stmt } While(Box, Box), + /// loop { stmt } Loop(Box), + /// for id in expr { stmt } For(String, Box, Box), + /// let id = expr Let(String, Option>, Position), + /// const id = expr Const(String, Box, Position), + /// { stmt; ... } Block(Vec, Position), + /// { stmt } Expr(Box), + /// break Break(Position), + /// `return`/`throw` ReturnWithVal(Option>, ReturnType, Position), } impl Stmt { + /// Get the `Position` of this statement. pub fn position(&self) -> Position { match self { Stmt::Noop(pos) @@ -214,18 +240,7 @@ impl Stmt { } } - pub fn is_noop(&self) -> bool { - matches!(self, Stmt::Noop(_)) - } - - pub fn is_op(&self) -> bool { - !matches!(self, Stmt::Noop(_)) - } - - pub fn is_var(&self) -> bool { - matches!(self, Stmt::Let(_, _, _)) - } - + /// Is this statement self-terminated (i.e. no need for a semicolon terminator)? pub fn is_self_terminated(&self) -> bool { match self { Stmt::Noop(_) @@ -243,6 +258,7 @@ impl Stmt { } } + /// Is this statement _pure_? pub fn is_pure(&self) -> bool { match self { Stmt::Noop(_) => true, @@ -262,31 +278,54 @@ impl Stmt { } } +/// An expression. #[derive(Debug, Clone)] pub enum Expr { + /// Integer constant. IntegerConstant(INT, Position), + /// Floating-point constant. #[cfg(not(feature = "no_float"))] FloatConstant(FLOAT, Position), - Variable(String, Position), - Property(String, Position), + /// Character constant. CharConstant(char, Position), + /// String constant. StringConstant(String, Position), + /// Variable access. + Variable(String, Position), + /// Property access. + Property(String, Position), + /// { stmt } Stmt(Box, Position), + /// func(expr, ... ) FunctionCall(String, Vec, Option, Position), + /// expr = expr Assignment(Box, Box, Position), + /// lhs.rhs Dot(Box, Box, Position), + /// expr[expr] #[cfg(not(feature = "no_index"))] Index(Box, Box, Position), #[cfg(not(feature = "no_index"))] + /// [ expr, ... ] Array(Vec, Position), + /// lhs && rhs And(Box, Box), + /// lhs || rhs Or(Box, Box), + /// true True(Position), + /// false False(Position), + /// () Unit(Position), } impl Expr { + /// Get the `Dynamic` value of a constant expression. + /// + /// # Panics + /// + /// Panics when the expression is not constant. pub fn get_constant_value(&self) -> Dynamic { match self { Expr::IntegerConstant(i, _) => i.into_dynamic(), @@ -310,6 +349,11 @@ impl Expr { } } + /// Get the display value of a constant expression. + /// + /// # Panics + /// + /// Panics when the expression is not constant. pub fn get_constant_str(&self) -> String { match self { Expr::IntegerConstant(i, _) => i.to_string(), @@ -329,6 +373,7 @@ impl Expr { } } + /// Get the `Position` of the expression. pub fn position(&self) -> Position { match self { Expr::IntegerConstant(_, pos) @@ -358,7 +403,7 @@ impl Expr { } } - /// Is this expression pure? + /// Is the expression pure? /// /// A pure expression has no side effects. pub fn is_pure(&self) -> bool { @@ -377,6 +422,7 @@ impl Expr { } } + /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { Expr::IntegerConstant(_, _) @@ -389,6 +435,7 @@ impl Expr { #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, _) => true, + // An array literal is constant if all items are constant #[cfg(not(feature = "no_index"))] Expr::Array(expressions, _) => expressions.iter().all(Expr::is_constant), @@ -397,6 +444,7 @@ impl Expr { } } +/// Tokens. #[derive(Debug, PartialEq, Clone)] pub enum Token { IntegerConstant(INT), @@ -470,6 +518,7 @@ pub enum Token { } impl Token { + /// Get the syntax of the token. pub fn syntax<'a>(&'a self) -> Cow<'a, str> { use self::Token::*; @@ -550,8 +599,8 @@ impl Token { } } - // if another operator is after these, it's probably an unary operator - // not sure about fn's name + // If another operator is after these, it's probably an unary operator + // (not sure about fn name). pub fn is_next_unary(&self) -> bool { use self::Token::*; @@ -612,6 +661,7 @@ impl Token { } } + /// Get the precedence number of the token. pub fn precedence(&self) -> u8 { match self { Self::Equals @@ -652,8 +702,10 @@ impl Token { } } + /// Does an expression bind to the right (instead of left)? pub fn is_bind_right(&self) -> bool { match self { + // Assignments bind to the right Self::Equals | Self::PlusAssign | Self::MinusAssign @@ -667,6 +719,7 @@ impl Token { | Self::ModuloAssign | Self::PowerOfAssign => true, + // Property access binds to the right Self::Period => true, _ => false, @@ -674,24 +727,36 @@ impl Token { } } +/// An iterator on a `Token` stream. pub struct TokenIterator<'a> { + /// The last token seen. last: Token, + /// Current position. pos: Position, + /// The input characters stream. char_stream: Peekable>, } impl<'a> TokenIterator<'a> { + /// Move the current position one character ahead. fn advance(&mut self) { self.pos.advance(); } + /// Move the current position back one character. + /// + /// # Panics + /// + /// Panics if already at the beginning of a line - cannot rewind to the previous line. fn rewind(&mut self) { self.pos.rewind(); } + /// Move the current position to the next line. fn new_line(&mut self) { self.pos.new_line() } - pub fn parse_string_const( + /// Parse a string literal wrapped by `enclosing_char`. + pub fn parse_string_literal( &mut self, enclosing_char: char, ) -> Result { @@ -703,25 +768,31 @@ impl<'a> TokenIterator<'a> { self.advance(); match next_char.ok_or((LERR::UnterminatedString, Position::eof()))? { + // \... '\\' if escape.is_empty() => { escape.push('\\'); } + // \\ '\\' if !escape.is_empty() => { escape.clear(); result.push('\\'); } + // \t 't' if !escape.is_empty() => { escape.clear(); result.push('\t'); } + // \n 'n' if !escape.is_empty() => { escape.clear(); result.push('\n'); } + // \r 'r' if !escape.is_empty() => { escape.clear(); result.push('\r'); } + // \x??, \u????, \U???????? ch @ 'x' | ch @ 'u' | ch @ 'U' if !escape.is_empty() => { let mut seq = escape.clone(); seq.push(ch); @@ -754,15 +825,25 @@ impl<'a> TokenIterator<'a> { .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), self.pos))?, ); } + + // \{enclosing_char} - escaped ch if enclosing_char == ch && !escape.is_empty() => result.push(ch), + + // Close wrapper ch if enclosing_char == ch && escape.is_empty() => break, + + // Unknown escape sequence _ if !escape.is_empty() => { return Err((LERR::MalformedEscapeSequence(escape), self.pos)) } + + // Cannot have new-lines inside string literals '\n' => { self.rewind(); return Err((LERR::UnterminatedString, self.pos)); } + + // All other characters ch => { escape.clear(); result.push(ch); @@ -773,6 +854,7 @@ impl<'a> TokenIterator<'a> { Ok(result.iter().collect()) } + /// Get the next token. fn inner_next(&mut self) -> Option<(Token, Position)> { let mut negated = false; @@ -782,7 +864,9 @@ impl<'a> TokenIterator<'a> { let pos = self.pos; match c { + // \n '\n' => self.new_line(), + // digit ... '0'..='9' => { let mut result = Vec::new(); let mut radix_base: Option = None; @@ -811,6 +895,7 @@ impl<'a> TokenIterator<'a> { } } } + // 0x????, 0o????, 0b???? ch @ 'x' | ch @ 'X' | ch @ 'o' | ch @ 'O' | ch @ 'b' | ch @ 'B' if c == '0' => { @@ -860,6 +945,7 @@ impl<'a> TokenIterator<'a> { result.insert(0, '-'); } + // Parse number if let Some(radix) = radix_base { let out: String = result.iter().skip(2).filter(|&&c| c != '_').collect(); @@ -875,6 +961,7 @@ impl<'a> TokenIterator<'a> { let out: String = result.iter().filter(|&&c| c != '_').collect(); let num = INT::from_str(&out).map(Token::IntegerConstant); + // If integer parsing is unnecessary, try float instead #[cfg(not(feature = "no_float"))] let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); @@ -886,6 +973,7 @@ impl<'a> TokenIterator<'a> { )); } } + // letter ... 'A'..='Z' | 'a'..='z' | '_' => { let mut result = Vec::new(); result.push(c); @@ -930,13 +1018,15 @@ impl<'a> TokenIterator<'a> { pos, )); } + // " - string literal '"' => { - return match self.parse_string_const('"') { + return match self.parse_string_literal('"') { Ok(out) => Some((Token::StringConst(out), pos)), Err(e) => Some((Token::LexError(e.0), e.1)), } } - '\'' => match self.parse_string_const('\'') { + // ' - character literal + '\'' => match self.parse_string_literal('\'') { Ok(result) => { let mut chars = result.chars(); @@ -955,16 +1045,22 @@ impl<'a> TokenIterator<'a> { } Err(e) => return Some((Token::LexError(e.0), e.1)), }, + + // Braces '{' => return Some((Token::LeftBrace, pos)), '}' => return Some((Token::RightBrace, pos)), + + // Parentheses '(' => return Some((Token::LeftParen, pos)), ')' => return Some((Token::RightParen, pos)), + // Indexing #[cfg(not(feature = "no_index"))] '[' => return Some((Token::LeftBracket, pos)), #[cfg(not(feature = "no_index"))] ']' => return Some((Token::RightBracket, pos)), + // Operators '+' => { return Some(( match self.char_stream.peek() { @@ -1228,6 +1324,7 @@ impl<'a> Iterator for TokenIterator<'a> { } } +/// Tokenize an input text stream. pub fn lex(input: &str) -> TokenIterator<'_> { TokenIterator { last: Token::LexError(LERR::InputError("".into())), @@ -1236,6 +1333,7 @@ pub fn lex(input: &str) -> TokenIterator<'_> { } } +/// Parse ( expr ) fn parse_paren_expr<'a>( input: &mut Peekable>, begin: Position, @@ -1252,13 +1350,16 @@ fn parse_paren_expr<'a>( let expr = parse_expr(input)?; match input.next() { + // ( xxx ) Some((Token::RightParen, _)) => Ok(expr), + // ( xxx ??? Some((_, pos)) => { return Err(ParseError::new( PERR::MissingRightParen("a matching ( in the expression".into()), pos, )) } + // ( xxx None => Err(ParseError::new( PERR::MissingRightParen("a matching ( in the expression".into()), Position::eof(), @@ -1266,6 +1367,7 @@ fn parse_paren_expr<'a>( } } +/// Parse a function call. fn parse_call_expr<'a>( id: String, input: &mut Peekable>, @@ -1273,6 +1375,7 @@ fn parse_call_expr<'a>( ) -> Result { let mut args_expr_list = Vec::new(); + // id() if let (Token::RightParen, _) = input.peek().ok_or_else(|| { ParseError::new( PERR::MissingRightParen(format!( @@ -1318,6 +1421,7 @@ fn parse_call_expr<'a>( } } +/// Parse an indexing expression.s #[cfg(not(feature = "no_index"))] fn parse_index_expr<'a>( lhs: Box, @@ -1328,6 +1432,7 @@ fn parse_index_expr<'a>( // Check type of indexing - must be integer match &idx_expr { + // lhs[int] Expr::IntegerConstant(i, pos) if *i < 0 => { return Err(ParseError::new( PERR::MalformedIndexExpr(format!( @@ -1337,6 +1442,7 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[float] #[cfg(not(feature = "no_float"))] Expr::FloatConstant(_, pos) => { return Err(ParseError::new( @@ -1344,6 +1450,7 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[char] Expr::CharConstant(_, pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1352,18 +1459,21 @@ fn parse_index_expr<'a>( *pos, )) } + // lhs[string] Expr::StringConstant(_, pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr("Array access expects integer index, not a string".into()), *pos, )) } + // lhs[??? = ??? ], lhs[()] Expr::Assignment(_, _, pos) | Expr::Unit(pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr("Array access expects integer index, not ()".into()), *pos, )) } + // lhs[??? && ???], lhs[??? || ???] Expr::And(lhs, _) | Expr::Or(lhs, _) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1372,6 +1482,7 @@ fn parse_index_expr<'a>( lhs.position(), )) } + // lhs[true], lhs[false] Expr::True(pos) | Expr::False(pos) => { return Err(ParseError::new( PERR::MalformedIndexExpr( @@ -1380,6 +1491,7 @@ fn parse_index_expr<'a>( *pos, )) } + // All other expressions _ => (), } @@ -1403,29 +1515,35 @@ fn parse_index_expr<'a>( } } +/// Parse an expression that begins with an identifier. fn parse_ident_expr<'a>( id: String, input: &mut Peekable>, begin: Position, ) -> Result { match input.peek() { + // id(...) - function call Some((Token::LeftParen, _)) => { input.next(); parse_call_expr(id, input, begin) } + // id[...] - indexing #[cfg(not(feature = "no_index"))] Some((Token::LeftBracket, pos)) => { let pos = *pos; input.next(); parse_index_expr(Box::new(Expr::Variable(id, begin)), input, pos) } + // id - variable Some(_) => Ok(Expr::Variable(id, begin)), + // EOF None => Ok(Expr::Variable(id, Position::eof())), } } +/// Parse an array literal. #[cfg(not(feature = "no_index"))] -fn parse_array_expr<'a>( +fn parse_array_literal<'a>( input: &mut Peekable>, begin: Position, ) -> Result { @@ -1472,8 +1590,9 @@ fn parse_array_expr<'a>( } } +/// Parse a primary expression. fn parse_primary<'a>(input: &mut Peekable>) -> Result { - // Block statement as expression + // { - block statement as expression match input.peek() { Some((Token::LeftBrace, pos)) => { let pos = *pos; @@ -1510,7 +1629,7 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result { can_be_indexed = true; - parse_array_expr(input, pos) + parse_array_literal(input, pos) } (Token::True, pos) => Ok(Expr::True(pos)), (Token::False, pos) => Ok(Expr::False(pos)), @@ -1534,11 +1653,13 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { match input .peek() .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? { + // -expr (Token::UnaryMinus, pos) => { let pos = *pos; @@ -1571,10 +1692,12 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Ok(Expr::FunctionCall("-".into(), vec![expr], None, pos)), } } + // +expr (Token::UnaryPlus, _) => { input.next(); parse_unary(input) } + // !expr (Token::Bang, pos) => { let pos = *pos; @@ -1587,34 +1710,41 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result parse_primary(input), } } +/// Parse an assignment. fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result { + // Is the LHS in a valid format? fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option { match expr { + // var Expr::Variable(_, _) => { assert!(is_top, "property expected but gets variable"); None } + // property Expr::Property(_, _) => { assert!(!is_top, "variable expected but gets property"); None } + // var[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) => { assert!(is_top, "property expected but gets variable"); None } - + // property[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) => { assert!(!is_top, "variable expected but gets property"); None } + // idx_lhs[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, pos) => Some(ParseError::new( match idx_lhs.as_ref() { @@ -1624,22 +1754,27 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result match dot_lhs.as_ref() { + // var.dot_rhs Expr::Variable(_, _) if is_top => valid_assignment_chain(dot_rhs, false), + // property.dot_rhs Expr::Property(_, _) if !is_top => valid_assignment_chain(dot_rhs, false), - + // var[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Variable(_, _)) && is_top => { valid_assignment_chain(dot_rhs, false) } + // property[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) if matches!(idx_lhs.as_ref(), &Expr::Property(_, _)) && !is_top => { valid_assignment_chain(dot_rhs, false) } + // idx_lhs[...] #[cfg(not(feature = "no_index"))] Expr::Index(idx_lhs, _, _) => Some(ParseError::new( ParseErrorType::AssignmentToCopy, @@ -1662,6 +1797,7 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result( input: &mut Peekable>, parent_precedence: u8, @@ -1830,11 +1967,13 @@ fn parse_binary_op<'a>( } } +/// Parse an expression. fn parse_expr<'a>(input: &mut Peekable>) -> Result { let lhs = parse_unary(input)?; parse_binary_op(input, 1, lhs) } +/// Parse an if statement. fn parse_if<'a>( input: &mut Peekable>, breakable: bool, @@ -1859,6 +1998,7 @@ fn parse_if<'a>( Ok(Stmt::IfElse(Box::new(guard), Box::new(if_body), else_body)) } +/// Parse a while loop. fn parse_while<'a>(input: &mut Peekable>) -> Result { input.next(); @@ -1868,6 +2008,7 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); @@ -1876,6 +2017,7 @@ fn parse_loop<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { input.next(); @@ -1904,7 +2046,8 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result( +/// Parse a variable definition statement. +fn parse_let<'a>( input: &mut Peekable>, var_type: VariableType, ) -> Result { @@ -1945,6 +2088,7 @@ fn parse_var<'a>( } } +/// Parse a statement block. fn parse_block<'a>( input: &mut Peekable>, breakable: bool, @@ -2005,10 +2149,12 @@ fn parse_block<'a>( } } +/// Parse an expression as a statement. fn parse_expr_stmt<'a>(input: &mut Peekable>) -> Result { Ok(Stmt::Expr(Box::new(parse_expr(input)?))) } +/// Parse a single statement. fn parse_stmt<'a>( input: &mut Peekable>, breakable: bool, @@ -2040,14 +2186,14 @@ fn parse_stmt<'a>( input.next(); match input.peek() { - // return/throw at EOF + // `return`/`throw` at EOF None => Ok(Stmt::ReturnWithVal(None, return_type, Position::eof())), - // return; or throw; + // `return;` or `throw;` Some((Token::SemiColon, pos)) => { let pos = *pos; Ok(Stmt::ReturnWithVal(None, return_type, pos)) } - // return or throw with expression + // `return` or `throw` with expression Some((_, pos)) => { let pos = *pos; Ok(Stmt::ReturnWithVal( @@ -2059,12 +2205,13 @@ fn parse_stmt<'a>( } } (Token::LeftBrace, _) => parse_block(input, breakable), - (Token::Let, _) => parse_var(input, VariableType::Normal), - (Token::Const, _) => parse_var(input, VariableType::Constant), + (Token::Let, _) => parse_let(input, VariableType::Normal), + (Token::Const, _) => parse_let(input, VariableType::Constant), _ => parse_expr_stmt(input), } } +/// Parse a function definition. #[cfg(not(feature = "no_function"))] fn parse_fn<'a>(input: &mut Peekable>) -> Result { let pos = input @@ -2166,6 +2313,7 @@ fn parse_fn<'a>(input: &mut Peekable>) -> Result( input: &mut Peekable>, ) -> Result<(Vec, Vec), ParseError> { @@ -2214,6 +2362,7 @@ fn parse_global_level<'a, 'e>( Ok((statements, functions)) } +/// Run the parser on an input stream, returning an AST. pub fn parse<'a, 'e>( input: &mut Peekable>, engine: &Engine<'e>, @@ -2229,6 +2378,9 @@ pub fn parse<'a, 'e>( ) } +/// Map a `Dynamic` value to an expression. +/// +/// Returns Some(expression) if conversion is successful. Otherwise None. pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> (Option, Dynamic) { if value.is::() { let value2 = value.clone(); From ecded729ad935d0812d020d17f9ac19a7bdef60c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 10:50:51 +0800 Subject: [PATCH 17/24] Finalize `no_std` support. --- Cargo.toml | 40 +++++++++++++++++++++------------------- README.md | 5 ++++- src/stdlib.rs | 2 ++ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11015a39..060df2f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,27 @@ keywords = [ "scripting" ] [dependencies] num-traits = "0.2.11" +[features] +#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] +default = [ "optimize_full" ] +debug_msgs = [] # print debug messages on function registrations and calls +unchecked = [] # unchecked arithmetic +no_index = [] # no arrays and indexing +no_float = [] # no floating-point +no_function = [] # no script-defined functions +no_optimize = [] # no script optimizer +optimize_full = [] # set optimization level to Full (default is Simple) +only_i32 = [] # set INT=i32 (useful for 32-bit systems) +only_i64 = [] # set INT=i64 (default) and disable support for all other integer types + +# no standard library of utility functions +no_stdlib = [ "num-traits/libm", "hashbrown", "core-error", "libm" ] + +[profile.release] +lto = "fat" +codegen-units = 1 +#opt-level = "z" # optimize for size + [dependencies.libm] version = "0.2.1" optional = true @@ -37,22 +58,3 @@ optional = true version = "0.3.2" default-features = false optional = true - -[features] -#default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] -default = [ "optimize_full" ] -debug_msgs = [] # print debug messages on function registrations and calls -unchecked = [] # unchecked arithmetic -no_stdlib = ["num-traits/libm", "hashbrown", "core-error", "libm"] # no standard library of utility functions -no_index = [] # no arrays and indexing -no_float = [] # no floating-point -no_function = [] # no script-defined functions -no_optimize = [] # no script optimizer -optimize_full = [] # set optimization level to Full (default is Simple) -only_i32 = [] # set INT=i32 (useful for 32-bit systems) -only_i64 = [] # set INT=i64 (default) and disable support for all other integer types - -[profile.release] -lto = "fat" -codegen-units = 1 -#opt-level = "z" # optimize for size diff --git a/README.md b/README.md index 5032e531..7ed77c0f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ Rhai - Embedded Scripting for Rust Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application. -Rhai's current feature set: +Rhai's current features set: +* `no-std` support * Easy integration with Rust functions and data types, supporting getter/setter methods * Easily call a script-defined function from Rust * Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop) @@ -52,6 +53,7 @@ Optional features | `no_optimize` | Disable the script optimizer. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | +| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. @@ -1477,6 +1479,7 @@ engine.set_optimization_level(rhai::OptimizationLevel::None); [`no_optimize`]: #optional-features [`only_i32`]: #optional-features [`only_i64`]: #optional-features +[`no_std`]: #optional-features [`Engine`]: #hello-world [`Scope`]: #initializing-and-maintaining-state diff --git a/src/stdlib.rs b/src/stdlib.rs index fa147c7d..2bffb4c2 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -1,3 +1,5 @@ +//! Helper module which defines most of the needed features from `std` for `no-std` builds. + #[cfg(feature = "no_stdlib")] mod inner { pub use core::{ From e7c669343eb9858d3c68ee8100d2db8d16315e64 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 12:09:34 +0800 Subject: [PATCH 18/24] Add no_std example. --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7ed77c0f..39d8afcf 100644 --- a/README.md +++ b/README.md @@ -71,15 +71,16 @@ Examples A number of examples can be found in the `examples` folder: -| Example | Description | -| -------------------------- | --------------------------------------------------------------------------- | -| `arrays_and_structs` | demonstrates registering a new type to Rhai and the usage of arrays on it | -| `custom_types_and_methods` | shows how to register a type and methods for it | -| `hello` | simple example that evaluates an expression and prints the result | -| `reuse_scope` | evaluates two pieces of code in separate runs, but using a common [`Scope`] | -| `rhai_runner` | runs each filename passed to it as a Rhai script | -| `simple_fn` | shows how to register a Rust function to a Rhai [`Engine`] | -| `repl` | a simple REPL, interactively evaluate statements from stdin | +| Example | Description | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of arrays on it | +| [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a type and methods for it | +| [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | +| [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | +| [`reuse_scope`](examples/reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common [`Scope`] | +| [`rhai_runner`](examples/rhai_runner.rs) | runs each filename passed to it as a Rhai script | +| [`simple_fn`](examples/simple_fn.rs) | shows how to register a Rust function to a Rhai [`Engine`] | +| [`repl`](examples/repl.rs) | a simple REPL, interactively evaluate statements from stdin | Examples can be run with the following command: From 3518c5a630aaede2920a5686152aa277ada1ce4a Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 12:13:44 +0800 Subject: [PATCH 19/24] Bump version to 0.11.0. --- Cargo.toml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ae3e5a2..8de522a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.10.2" +version = "0.11.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/README.md b/README.md index 39d8afcf..98bca82a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Rhai's current features set: * Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations); For [`no_std`] builds, a number of additional dependencies are pulled in to provide for basic library functionalities. -**Note:** Currently, the version is 0.10.2, so the language and API's may change before they stabilize. +**Note:** Currently, the version is 0.11.0, so the language and API's may change before they stabilize. Installation ------------ @@ -25,7 +25,7 @@ Install the Rhai crate by adding this line to `dependencies`: ```toml [dependencies] -rhai = "0.10.2" +rhai = "0.11.0" ``` or simply: From 03b2e9ad69eb1e1dfd7e6763e8577f6fb91ff9ba Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 13:09:38 +0800 Subject: [PATCH 20/24] Fix default release profile and change optimization default back to Simple. --- Cargo.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8de522a1..a8faf087 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2.11" [features] #default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] -default = [ "optimize_full" ] +default = [] debug_msgs = [] # print debug messages on function registrations and calls unchecked = [] # unchecked arithmetic no_stdlib = [] # no standard library of utility functions @@ -28,7 +28,7 @@ no_index = [] # no arrays and indexing no_float = [] # no floating-point no_function = [] # no script-defined functions no_optimize = [] # no script optimizer -optimize_full = [] # set optimization level to Full (default is Simple) +optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simply testing only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types @@ -38,7 +38,8 @@ no_std = [ "num-traits/libm", "hashbrown", "core-error", "libm" ] [profile.release] lto = "fat" codegen-units = 1 -opt-level = "z" # optimize for size +#opt-level = "z" # optimize for size +#panic = 'abort' # remove stack backtrace for no-std [dependencies.libm] version = "0.2.1" From 019e73bc7e5f0e34dcfe3e95da56f09295f47198 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 18:41:18 +0800 Subject: [PATCH 21/24] Allow empty statements. --- src/api.rs | 12 +++---- src/optimize.rs | 15 ++++++--- src/parser.rs | 89 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/api.rs b/src/api.rs index d087c9c9..1bdf34e0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,7 +10,7 @@ use crate::result::EvalAltResult; use crate::scope::Scope; #[cfg(not(feature = "no_optimize"))] -use crate::optimize::optimize_ast; +use crate::optimize::optimize_into_ast; use crate::stdlib::{ any::{type_name, TypeId}, @@ -415,12 +415,10 @@ impl<'e> Engine<'e> { /// (i.e. with `scope.push_constant(...)`). Then, the AST is cloned and the copy re-optimized before running. #[cfg(not(feature = "no_optimize"))] pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST { - optimize_ast( - self, - scope, - ast.0.clone(), - ast.1.iter().map(|f| (**f).clone()).collect(), - ) + let statements = ast.0.clone(); + let functions = ast.1.iter().map(|f| (**f).clone()).collect(); + + optimize_into_ast(self, scope, statements, functions) } /// Override default action of `print` (print to stdout using `println!`) diff --git a/src/optimize.rs b/src/optimize.rs index 79c922f6..7ac461ec 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -8,9 +8,11 @@ use crate::parser::{map_dynamic_to_expr, Expr, FnDef, ReturnType, Stmt, AST}; use crate::scope::{Scope, ScopeEntry, VariableType}; use crate::stdlib::{ + boxed::Box, + string::{String, ToString}, sync::Arc, - vec::Vec, string::{String, ToString}, - boxed::Box, vec, + vec, + vec::Vec, }; /// Level of optimization performed. @@ -549,17 +551,20 @@ pub(crate) fn optimize<'a>(statements: Vec, engine: &Engine<'a>, scope: &S let last_stmt = result.pop(); // Remove all pure statements at global level - result.retain(|stmt| !matches!(stmt, Stmt::Expr(expr) if expr.is_pure())); + result.retain(|stmt| !stmt.is_pure()); + // Add back the last statement unless it is a lone No-op if let Some(stmt) = last_stmt { - result.push(stmt); // Add back the last statement + if result.len() > 0 || !matches!(stmt, Stmt::Noop(_)) { + result.push(stmt); + } } result } /// Optimize an AST. -pub fn optimize_ast( +pub fn optimize_into_ast( engine: &Engine, scope: &Scope, statements: Vec, diff --git a/src/parser.rs b/src/parser.rs index b89ef1b4..c23c65a3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6,7 +6,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::scope::{Scope, VariableType}; #[cfg(not(feature = "no_optimize"))] -use crate::optimize::optimize_ast; +use crate::optimize::optimize_into_ast; use crate::stdlib::{ borrow::Cow, @@ -243,13 +243,15 @@ impl Stmt { /// Is this statement self-terminated (i.e. no need for a semicolon terminator)? pub fn is_self_terminated(&self) -> bool { match self { - Stmt::Noop(_) - | Stmt::IfElse(_, _, _) + Stmt::IfElse(_, _, _) | Stmt::While(_, _) | Stmt::Loop(_) | Stmt::For(_, _, _) | Stmt::Block(_, _) => true, + // A No-op requires a semicolon in order to know it is an empty statement! + Stmt::Noop(_) => false, + Stmt::Let(_, _, _) | Stmt::Const(_, _, _) | Stmt::Expr(_) @@ -1717,7 +1719,7 @@ fn parse_unary<'a>(input: &mut Peekable>) -> Result Result { - // Is the LHS in a valid format? + // Is the LHS in a valid format for an assignment target? fn valid_assignment_chain(expr: &Expr, is_top: bool) -> Option { match expr { // var @@ -1798,17 +1800,13 @@ fn parse_assignment(lhs: Expr, rhs: Expr, pos: Position) -> Result Result { +fn parse_op_assignment(op: &str, lhs: Expr, rhs: Expr, pos: Position) -> Result { let lhs_copy = lhs.clone(); + // lhs op= rhs -> lhs = op(lhs, rhs) parse_assignment( lhs, - Expr::FunctionCall(function.into(), vec![lhs_copy, rhs], None, pos), + Expr::FunctionCall(op.into(), vec![lhs_copy, rhs], None, pos), pos, ) } @@ -1978,17 +1976,22 @@ fn parse_if<'a>( input: &mut Peekable>, breakable: bool, ) -> Result { + // if ... input.next(); + // if guard { body } let guard = parse_expr(input)?; let if_body = parse_block(input, breakable)?; + // if guard { body } else ... let else_body = if matches!(input.peek(), Some((Token::Else, _))) { input.next(); Some(Box::new(if matches!(input.peek(), Some((Token::If, _))) { + // if guard { body } else if ... parse_if(input, breakable)? } else { + // if guard { body } else { else-body } parse_block(input, breakable)? })) } else { @@ -2000,8 +2003,10 @@ fn parse_if<'a>( /// Parse a while loop. fn parse_while<'a>(input: &mut Peekable>) -> Result { + // while ... input.next(); + // while guard { body } let guard = parse_expr(input)?; let body = parse_block(input, true)?; @@ -2010,8 +2015,10 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { + // loop ... input.next(); + // loop { body } let body = parse_block(input, true)?; Ok(Stmt::Loop(Box::new(body))) @@ -2019,19 +2026,25 @@ fn parse_loop<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { + // for ... input.next(); + // for name ... let name = match input .next() .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? { + // Variable name (Token::Identifier(s), _) => s, + // Bad identifier (Token::LexError(err), pos) => { return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } + // Not a variable name (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; + // for name in ... match input .next() .ok_or_else(|| ParseError::new(PERR::MissingIn, Position::eof()))? @@ -2040,6 +2053,7 @@ fn parse_for<'a>(input: &mut Peekable>) -> Result return Err(ParseError::new(PERR::MissingIn, pos)), } + // for name in expr { body } let expr = parse_expr(input)?; let body = parse_block(input, true)?; @@ -2051,39 +2065,43 @@ fn parse_let<'a>( input: &mut Peekable>, var_type: VariableType, ) -> Result { - let pos = input - .next() - .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? - .1; + // let/const... (specified in `var_type`) + input.next(); - let name = match input + // let name ... + let (name, pos) = match input .next() .ok_or_else(|| ParseError::new(PERR::VariableExpected, Position::eof()))? { - (Token::Identifier(s), _) => s, + (Token::Identifier(s), pos) => (s, pos), (Token::LexError(err), pos) => { return Err(ParseError::new(PERR::BadInput(err.to_string()), pos)) } (_, pos) => return Err(ParseError::new(PERR::VariableExpected, pos)), }; + // let name = ... if matches!(input.peek(), Some((Token::Equals, _))) { input.next(); + + // let name = expr let init_value = parse_expr(input)?; match var_type { + // let name = expr VariableType::Normal => Ok(Stmt::Let(name, Some(Box::new(init_value)), pos)), - + // const name = { expr:constant } VariableType::Constant if init_value.is_constant() => { Ok(Stmt::Const(name, Box::new(init_value), pos)) } - // Constants require a constant expression + // const name = expr - error VariableType::Constant => Err(ParseError( PERR::ForbiddenConstantExpr(name.to_string()), init_value.position(), )), } } else { + // let name Ok(Stmt::Let(name, None, pos)) } } @@ -2093,6 +2111,7 @@ fn parse_block<'a>( input: &mut Peekable>, breakable: bool, ) -> Result { + // Must start with { let pos = match input .next() .ok_or_else(|| ParseError::new(PERR::MissingLeftBrace, Position::eof()))? @@ -2113,15 +2132,19 @@ fn parse_block<'a>( statements.push(stmt); match input.peek() { + // EOF None => break, - + // { ... stmt } Some((Token::RightBrace, _)) => break, - - Some((Token::SemiColon, _)) => { + // { ... stmt; + Some((Token::SemiColon, _)) if need_semicolon => { input.next(); } + // { ... { stmt } ; + Some((Token::SemiColon, _)) if !need_semicolon => (), + // { ... { stmt } ??? Some((_, _)) if !need_semicolon => (), - + // { ... stmt ??? - error Some((_, pos)) => { // Semicolons are not optional between statements return Err(ParseError::new( @@ -2163,6 +2186,10 @@ fn parse_stmt<'a>( .peek() .ok_or_else(|| ParseError::new(PERR::InputPastEndOfFile, Position::eof()))? { + // Semicolon - empty statement + (Token::SemiColon, pos) => Ok(Stmt::Noop(*pos)), + + // fn ... #[cfg(not(feature = "no_function"))] (Token::Fn, pos) => return Err(ParseError::new(PERR::WrongFnDefinition, *pos)), @@ -2323,6 +2350,7 @@ fn parse_global_level<'a, 'e>( while input.peek().is_some() { #[cfg(not(feature = "no_function"))] { + // Collect all the function definitions if matches!(input.peek().expect("should not be None"), (Token::Fn, _)) { let f = parse_fn(input)?; @@ -2336,6 +2364,7 @@ fn parse_global_level<'a, 'e>( } } + // Actual statement let stmt = parse_stmt(input, false)?; let need_semicolon = !stmt.is_self_terminated(); @@ -2343,12 +2372,17 @@ fn parse_global_level<'a, 'e>( statements.push(stmt); match input.peek() { + // EOF None => break, - Some((Token::SemiColon, _)) => { + // stmt ; + Some((Token::SemiColon, _)) if need_semicolon => { input.next(); } + // stmt ; + Some((Token::SemiColon, _)) if !need_semicolon => (), + // { stmt } ??? Some((_, _)) if !need_semicolon => (), - + // stmt ??? - error Some((_, pos)) => { // Semicolons are not optional between statements return Err(ParseError::new( @@ -2371,8 +2405,11 @@ pub fn parse<'a, 'e>( let (statements, functions) = parse_global_level(input)?; Ok( + // Optimize AST #[cfg(not(feature = "no_optimize"))] - optimize_ast(engine, scope, statements, functions), + optimize_into_ast(engine, scope, statements, functions), + // + // Do not optimize AST if `no_optimize` #[cfg(feature = "no_optimize")] AST(statements, functions.into_iter().map(Arc::new).collect()), ) From 0dc51f8e59a6d474cc9db292a45d968a9431ee01 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 22:03:50 +0800 Subject: [PATCH 22/24] Removee `debug_msgs` feature. --- Cargo.toml | 3 +-- README.md | 1 - src/api.rs | 14 -------------- src/call.rs | 3 ++- src/engine.rs | 10 ---------- src/error.rs | 1 + src/fn_register.rs | 1 + src/lib.rs | 25 +------------------------ 8 files changed, 6 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8faf087..593091cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,14 +21,13 @@ num-traits = "0.2.11" [features] #default = ["no_function", "no_index", "no_float", "only_i32", "no_stdlib", "unchecked", "no_optimize"] default = [] -debug_msgs = [] # print debug messages on function registrations and calls unchecked = [] # unchecked arithmetic no_stdlib = [] # no standard library of utility functions no_index = [] # no arrays and indexing no_float = [] # no floating-point no_function = [] # no script-defined functions no_optimize = [] # no script optimizer -optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simply testing +optimize_full = [] # set optimization level to Full (default is Simple) - this is a feature used only to simplify testing only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types diff --git a/README.md b/README.md index 98bca82a..9b50bf3f 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ Optional features | Feature | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `debug_msgs` | Print debug messages to stdout related to function registrations and calls. | | `no_stdlib` | Exclude the standard library of utility functions in the build, and only include the minimum necessary functionalities. Standard types are not affected. | | `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! | | `no_function` | Disable script-defined functions if not needed. | diff --git a/src/api.rs b/src/api.rs index 1bdf34e0..ad12218f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -29,20 +29,6 @@ impl<'e> Engine<'e> { args: Option>, f: Box, ) { - debug_println!( - "Register function: {} with {}", - fn_name, - if let Some(a) = &args { - format!( - "{} parameter{}", - a.len(), - if a.len() > 1 { "s" } else { "" } - ) - } else { - "no parameter".to_string() - } - ); - let spec = FnSpec { name: fn_name.to_string().into(), args, diff --git a/src/call.rs b/src/call.rs index 5c06e8a9..cf7356fe 100644 --- a/src/call.rs +++ b/src/call.rs @@ -3,11 +3,12 @@ #![allow(non_snake_case)] use crate::any::{Any, Dynamic}; -use crate::stdlib::{string::String, vec, vec::Vec}; #[cfg(not(feature = "no_index"))] use crate::engine::Array; +use crate::stdlib::{string::String, vec, vec::Vec}; + /// Trait that represent arguments to a function call. pub trait FuncArgs { /// Convert to a `Vec` of `Dynamic` arguments. diff --git a/src/engine.rs b/src/engine.rs index 5985108f..0689dbb5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -165,16 +165,6 @@ impl Engine<'_> { def_val: Option<&Dynamic>, pos: Position, ) -> Result { - debug_println!( - "Calling function: {} ({})", - fn_name, - args.iter() - .map(|x| (*x).type_name()) - .map(|name| self.map_type_name(name)) - .collect::>() - .join(", ") - ); - // First search in script-defined functions (can override built-in) if let Ok(n) = self .script_functions diff --git a/src/error.rs b/src/error.rs index 59816e96..87e2e2d5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ //! Module containing error definitions for the parsing process. use crate::parser::Position; + use crate::stdlib::{char, error::Error, fmt, string::String}; /// Error when tokenizing the script text. diff --git a/src/fn_register.rs b/src/fn_register.rs index 065dac0f..53c646a0 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -6,6 +6,7 @@ use crate::any::{Any, Dynamic}; use crate::engine::{Engine, FnCallArgs}; use crate::parser::Position; use crate::result::EvalAltResult; + use crate::stdlib::{any::TypeId, boxed::Box, string::ToString, vec}; /// A trait to register custom functions with the `Engine`. diff --git a/src/lib.rs b/src/lib.rs index 67190112..5293982c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,30 +43,6 @@ #[cfg(feature = "no_std")] extern crate alloc; -// needs to be here, because order matters for macros -macro_rules! debug_println { - () => ( - #[cfg(feature = "debug_msgs")] - { - print!("\n"); - } - ); - ($fmt:expr) => ( - #[cfg(feature = "debug_msgs")] - { - print!(concat!($fmt, "\n")); - } - ); - ($fmt:expr, $($arg:tt)*) => ( - #[cfg(feature = "debug_msgs")] - { - print!(concat!($fmt, "\n"), $($arg)*); - } - ); -} - -#[macro_use] -mod stdlib; mod any; mod api; mod builtin; @@ -78,6 +54,7 @@ mod optimize; mod parser; mod result; mod scope; +mod stdlib; pub use any::{Any, AnyExt, Dynamic, Variant}; pub use call::FuncArgs; From b3efb8b2649bc420d37c871fcdb554e3bf531f06 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Wed, 18 Mar 2020 23:09:53 +0800 Subject: [PATCH 23/24] Disallow variable names starting with _ + digit. --- src/parser.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index c23c65a3..0f62ee1e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -991,9 +991,18 @@ impl<'a> TokenIterator<'a> { } } - let has_letter = result.iter().any(char::is_ascii_alphabetic); + let is_valid_identifier = result + .iter() + .find(|&ch| char::is_ascii_alphanumeric(ch)) // first alpha-numeric character + .map(char::is_ascii_alphabetic) // is a letter + .unwrap_or(false); // if no alpha-numeric at all - syntax error + let identifier: String = result.iter().collect(); + if !is_valid_identifier { + return Some((Token::LexError(LERR::MalformedIdentifier(identifier)), pos)); + } + return Some(( match identifier.as_str() { "true" => Token::True, @@ -1013,9 +1022,7 @@ impl<'a> TokenIterator<'a> { #[cfg(not(feature = "no_function"))] "fn" => Token::Fn, - _ if has_letter => Token::Identifier(identifier), - - _ => Token::LexError(LERR::MalformedIdentifier(identifier)), + _ => Token::Identifier(identifier), }, pos, )); From cc8ec12691d970ac777ab452127de41ac44c13d6 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 19 Mar 2020 13:52:10 +0800 Subject: [PATCH 24/24] Add more comments and examples. --- README.md | 275 +++++++++-------- examples/arrays_and_structs.rs | 2 +- examples/custom_types_and_methods.rs | 2 +- src/any.rs | 2 +- src/api.rs | 438 ++++++++++++++++++++++++--- src/engine.rs | 2 +- src/fn_register.rs | 141 ++++----- src/lib.rs | 4 +- src/scope.rs | 2 +- tests/arrays.rs | 2 +- tests/float.rs | 2 +- tests/get_set.rs | 2 +- tests/method_call.rs | 2 +- tests/mismatched_op.rs | 2 +- 14 files changed, 633 insertions(+), 245 deletions(-) diff --git a/README.md b/README.md index 9b50bf3f..a53651d9 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ Rhai's current features set: * Easy-to-use language similar to JS+Rust * Support for overloaded functions * Compiled script is optimized for repeat evaluations -* Very few additional dependencies (right now only [`num-traits`] to do checked arithmetic operations); - For [`no_std`] builds, a number of additional dependencies are pulled in to provide for basic library functionalities. +* Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) + to do checked arithmetic operations); for [`no_std`] builds, a number of additional dependencies are + pulled in to provide for functionalities that used to be in `std`. **Note:** Currently, the version is 0.11.0, so the language and API's may change before they stabilize. @@ -57,6 +58,16 @@ Optional features By default, Rhai includes all the standard functionalities in a small, tight package. Most features are here to opt-**out** of certain functionalities that are not needed. Excluding unneeded functionalities can result in smaller, faster builds as well as less bugs due to a more restricted language. +[`unchecked`]: #optional-features +[`no_stdlib`]: #optional-features +[`no_index`]: #optional-features +[`no_float`]: #optional-features +[`no_function`]: #optional-features +[`no_optimize`]: #optional-features +[`only_i32`]: #optional-features +[`only_i64`]: #optional-features +[`no_std`]: #optional-features + Related ------- @@ -126,6 +137,8 @@ cargo run --example rhai_runner scripts/any_script.rhai Hello world ----------- +[`Engine`]: #hello-world + To get going with Rhai, create an instance of the scripting engine and then call `eval`: ```rust @@ -224,35 +237,62 @@ let result: i64 = engine.call_fn("hello", 123_i64)? Values and types ---------------- +[`type_of`]: #values-and-types + The following primitive types are supported natively: -| Category | Types | -| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| **Integer** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | -| **Floating-point** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | -| **Character** | `char` | -| **Boolean** | `bool` | -| **Array** (disabled with [`no_index`]) | `rhai::Array` | -| **Dynamic** (i.e. can be anything) | `rhai::Dynamic` | -| **System** (current configuration) | `rhai::INT` (`i32` or `i64`),
`rhai::FLOAT` (`f32` or `f64`) | +| Category | Equivalent Rust types | `type_of()` name | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------- | +| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | _same as type_ | +| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | _same as type_ | +| **Boolean value** | `bool` | `"bool"` | +| **Unicode character** | `char` | `"char"` | +| **Unicode string** | `String` (_not_ `&str`) | `"string"` | +| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | +| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | +| **System number** (current configuration) | `rhai::INT` (`i32` or `i64`),
`rhai::FLOAT` (`f32` or `f64`) | _same as type_ | +| **Nothing/void/nil/null** (or whatever you want to call it) | `()` | `"()"` | + +[`Dynamic`]: #values-and-types +[`()`]: #values-and-types All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. The default integer type is `i64`. If other integer types are not needed, it is possible to exclude them and make a smaller build with the [`only_i64`] feature. If only 32-bit integers are needed, enabling the [`only_i32`] feature will remove support for all integer types other than `i32`, including `i64`. -This is useful on 32-bit systems where using 64-bit integers incur a performance penalty. +This is useful on some 32-bit systems where using 64-bit integers incurs a performance penalty. -If no floating-point is needed, use the [`no_float`] feature to remove support. +If no floating-point is needed or supported, use the [`no_float`] feature to remove it. + +There is a `type_of` function to detect the actual type of a value. This is useful because all variables are `Dynamic`. + +```rust +// Use 'type_of()' to get the actual types of values +type_of('c') == "char"; +type_of(42) == "i64"; + +let x = 123; +x.type_of() == "i64"; + +x = 99.999; +x.type_of() == "f64"; + +x = "hello"; +if type_of(x) == "string" { + do_something_with_string(x); +} +``` Value conversions ----------------- +[`to_int`]: #value-conversions +[`to_float`]: #value-conversions + There is a `to_float` function to convert a supported number to an `f64`, and a `to_int` function to convert a supported number to `i64` and that's about it. For other conversions, register custom conversion functions. -There is also a `type_of` function to detect the type of a value. - ```rust let x = 42; let y = x * 100.0; // error: cannot multiply i64 with f64 @@ -261,22 +301,13 @@ let z = y.to_int() + x; // works let c = 'X'; // character print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88" - -// Use 'type_of' to get the type of variables -type_of(c) == "char"; -type_of(x) == "i64"; -y.type_of() == "f64"; - -if z.type_of() == "string" { - do_something_with_strong(z); -} ``` Working with functions ---------------------- Rhai's scripting engine is very lightweight. It gets most of its abilities from functions. -To call these functions, they need to be registered with the engine. +To call these functions, they need to be registered with the [`Engine`]. ```rust use rhai::{Engine, EvalAltResult}; @@ -314,7 +345,7 @@ fn main() -> Result<(), EvalAltResult> } ``` -To return a [`Dynamic`] value, simply `Box` it and return it. +To return a [`Dynamic`] value from a Rust function, simply `Box` it and return it. ```rust fn decide(yes_no: bool) -> Dynamic { @@ -412,16 +443,16 @@ use rhai::RegisterFn; #[derive(Clone)] struct TestStruct { - x: i64 + field: i64 } impl TestStruct { fn update(&mut self) { - self.x += 1000; + self.field += 41; } - fn new() -> TestStruct { - TestStruct { x: 1 } + fn new() -> Self { + TestStruct { field: 1 } } } @@ -436,7 +467,7 @@ fn main() -> Result<(), EvalAltResult> let result = engine.eval::("let x = new_ts(); x.update(); x")?; - println!("result: {}", result.x); // prints 1001 + println!("result: {}", result.field); // prints 42 Ok(()) } @@ -447,7 +478,7 @@ All custom types must implement `Clone`. This allows the [`Engine`] to pass by ```rust #[derive(Clone)] struct TestStruct { - x: i64 + field: i64 } ``` @@ -456,11 +487,11 @@ Next, we create a few methods that we'll later use in our scripts. Notice that ```rust impl TestStruct { fn update(&mut self) { - self.x += 1000; + self.field += 41; } - fn new() -> TestStruct { - TestStruct { x: 1 } + fn new() -> Self { + TestStruct { field: 1 } } } @@ -474,8 +505,8 @@ To use methods and functions with the [`Engine`], we need to register them. The *Note: [`Engine`] follows the convention that methods use a `&mut` first parameter so that invoking methods can update the value in memory.* ```rust -engine.register_fn("update", TestStruct::update); -engine.register_fn("new_ts", TestStruct::new); +engine.register_fn("update", TestStruct::update); // registers 'update(&mut ts)' +engine.register_fn("new_ts", TestStruct::new); // registers 'new' ``` Finally, we call our script. The script can see the function and method we registered earlier. We need to get the result back out from script land just as before, this time casting to our custom struct type. @@ -483,31 +514,36 @@ Finally, we call our script. The script can see the function and method we regi ```rust let result = engine.eval::("let x = new_ts(); x.update(); x")?; -println!("result: {}", result.x); // prints 1001 +println!("result: {}", result.field); // prints 42 ``` In fact, any function with a first argument (either by copy or via a `&mut` reference) can be used as a method-call on that type because internally they are the same thing: methods on a type is implemented as a functions taking an first argument. ```rust fn foo(ts: &mut TestStruct) -> i64 { - ts.x + ts.field } engine.register_fn("foo", foo); let result = engine.eval::("let x = new_ts(); x.foo()")?; -println!("result: {}", result); // prints 1 +println!("result: {}", result); // prints 1 ``` -`type_of` works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type -with a special "pretty-print" name, `type_of` will return that name instead. +[`type_of`] works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type +with a special "pretty-print" name, [`type_of`] will return that name instead. ```rust +engine.register_type::(); +engine.register_fn("new_ts", TestStruct::new); let x = new_ts(); -print(x.type_of()); // prints "foo::bar::TestStruct" - // prints "Hello" if TestStruct is registered with - // engine.register_type_with_name::("Hello")?; +print(x.type_of()); // prints "path::to::module::TestStruct" + +engine.register_type_with_name::("Hello"); +engine.register_fn("new_ts", TestStruct::new); +let x = new_ts(); +print(x.type_of()); // prints "Hello" ``` Getters and setters @@ -518,20 +554,20 @@ Similarly, custom types can expose members by registering a `get` and/or `set` f ```rust #[derive(Clone)] struct TestStruct { - x: i64 + field: i64 } impl TestStruct { - fn get_x(&mut self) -> i64 { - self.x + fn get_field(&mut self) -> i64 { + self.field } - fn set_x(&mut self, new_x: i64) { - self.x = new_x; + fn set_field(&mut self, new_val: i64) { + self.field = new_val; } - fn new() -> TestStruct { - TestStruct { x: 1 } + fn new() -> Self { + TestStruct { field: 1 } } } @@ -539,17 +575,19 @@ let mut engine = Engine::new(); engine.register_type::(); -engine.register_get_set("x", TestStruct::get_x, TestStruct::set_x); +engine.register_get_set("xyz", TestStruct::get_field, TestStruct::set_field); engine.register_fn("new_ts", TestStruct::new); -let result = engine.eval::("let a = new_ts(); a.x = 500; a.x")?; +let result = engine.eval::("let a = new_ts(); a.xyz = 42; a.xyz")?; -println!("result: {}", result); +println!("Answer: {}", result); // prints 42 ``` Initializing and maintaining state --------------------------------- +[`Scope`]: #initializing-and-maintaining-state + By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions that have been defined but no global state. This gives each evaluation a clean starting slate. In order to continue using the same global state from one invocation to the next, such a state must be manually created and passed in. @@ -571,20 +609,21 @@ fn main() -> Result<(), EvalAltResult> // Better stick to them or it gets hard working with the script. scope.push("y", 42_i64); scope.push("z", 999_i64); + scope.push("s", "hello, world!".to_string()); // remember to use 'String', not '&str' // First invocation engine.eval_with_scope::<()>(&mut scope, r" - let x = 4 + 5 - y + z; + let x = 4 + 5 - y + z + s.len(); y = 1; ")?; // Second invocation using the same state let result = engine.eval_with_scope::(&mut scope, "x")?; - println!("result: {}", result); // should print 966 + println!("result: {}", result); // prints 979 // Variable y is changed in the script - assert_eq!(scope.get_value::("y")?, 1); + assert_eq!(scope.get_value::("y").expect("variable x should exist"), 1); Ok(()) } @@ -620,15 +659,16 @@ Statements are terminated by semicolons '`;`' - they are mandatory, except for t A statement can be used anywhere where an expression is expected. The _last_ statement of a statement block (enclosed by '`{`' .. '`}`' pairs) is always the return value of the statement. If a statement has no return value -(e.g. variable definitions, assignments) then the value will be `()`. +(e.g. variable definitions, assignments) then the value will be [`()`]. ```rust let a = 42; // normal assignment statement let a = foo(42); // normal function call statement foo < 42; // normal expression as statement -let a = { 40 + 2 }; // the value of 'a' is the value of the statement block, which is the value of the last statement -// ^ notice that the last statement does not require an ending semicolon +let a = { 40 + 2 }; // 'a' is set to the value of the statement block, which is the value of the last statement +// ^ notice that the last statement does not require a terminating semicolon (although it also works with it) +// ^ notice that a semicolon is required here to terminate the assignment statement; it is syntax error without it 4 * 10 + 2 // this is also a statement, which is an expression, with no ending semicolon because // it is the last statement of the whole block @@ -639,8 +679,9 @@ Variables Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). -Variable names must start with an ASCII letter or an underscore '`_`', and must contain at least one ASCII letter within. -Therefore, names like '`_`', '`_42`' etc. are not legal variable names. Variable names are also case _sensitive_. +Variable names must start with an ASCII letter or an underscore '`_`', must contain at least one ASCII letter, and must start with an ASCII letter before a digit. +Therefore, names like '`_`', '`_42`', '`3a`' etc. are not legal variable names, but '`_c3po`' and '`r2d2`' are. +Variable names are also case _sensitive_. Variables are defined using the `let` keyword. A variable defined within a statement block is _local_ to that block. @@ -676,7 +717,7 @@ print(x * 2); // prints 84 x = 123; // syntax error - cannot assign to constant ``` -Constants must be assigned a _value_ not an expression. +Constants must be assigned a _value_, not an expression. ```rust const x = 40 + 2; // syntax error - cannot assign expression to constant @@ -706,19 +747,19 @@ Numeric operators Numeric operators generally follow C styles. -| Operator | Description | Integers only | -| -------- | ----------------------------------------------------------- | :-----------: | -| `+` | Plus | | -| `-` | Minus | | -| `*` | Multiply | | -| `/` | Divide (C-style integer division if acted on integer types) | | -| `%` | Modulo (remainder) | | -| `~` | Power | | -| `&` | Binary _And_ bit-mask | Yes | -| `\|` | Binary _Or_ bit-mask | Yes | -| `^` | Binary _Xor_ bit-mask | Yes | -| `<<` | Left bit-shift | Yes | -| `>>` | Right bit-shift | Yes | +| Operator | Description | Integers only | +| -------- | ---------------------------------------------------- | :-----------: | +| `+` | Plus | | +| `-` | Minus | | +| `*` | Multiply | | +| `/` | Divide (integer division if acting on integer types) | | +| `%` | Modulo (remainder) | | +| `~` | Power | | +| `&` | Binary _And_ bit-mask | Yes | +| `\|` | Binary _Or_ bit-mask | Yes | +| `^` | Binary _Xor_ bit-mask | Yes | +| `<<` | Left bit-shift | Yes | +| `>>` | Right bit-shift | Yes | ```rust let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses @@ -747,10 +788,10 @@ Numeric functions The following standard functions (defined in the standard library but excluded if [`no_stdlib`]) operate on `i8`, `i16`, `i32`, `i64`, `f32` and `f64` only: -| Function | Description | -| ---------- | --------------------------------- | -| `abs` | absolute value | -| `to_float` | converts an integer type to `f64` | +| Function | Description | +| ------------ | --------------------------------- | +| `abs` | absolute value | +| [`to_float`] | converts an integer type to `f64` | Floating-point functions ------------------------ @@ -765,7 +806,7 @@ The following standard functions (defined in the standard library but excluded i | Exponential | `exp` (base _e_) | | Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | | Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` | -| Conversion | `to_int` | +| Conversion | [`to_int`] | | Testing | `is_nan`, `is_finite`, `is_infinite` | Strings and Chars @@ -782,7 +823,7 @@ Individual characters within a Rhai string can be replaced. In Rhai, there is no Strings can be built up from other strings and types via the `+` operator (provided by the standard library but excluded if [`no_stdlib`]). This is particularly useful when printing output. -`type_of()` a string returns `"string"`. +[`type_of()`] a string returns `"string"`. ```rust let name = "Bob"; @@ -871,7 +912,7 @@ Arrays Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices. Array literals are built within square brackets '`[`' ,, '`]`' and separated by commas '`,`'. -The type of a Rhai array is `rhai::Array`. `type_of()` returns `"array"`. +The type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns `"array"`. Arrays are disabled via the [`no_index`] feature. @@ -880,8 +921,8 @@ The following functions (defined in the standard library but excluded if [`no_st | Function | Description | | ---------- | ------------------------------------------------------------------------------------- | | `push` | inserts an element at the end | -| `pop` | removes the last element and returns it (`()` if empty) | -| `shift` | removes the first element and returns it (`()` if empty) | +| `pop` | removes the last element and returns it ([`()`] if empty) | +| `shift` | removes the first element and returns it ([`()`] if empty) | | `len` | returns the number of elements | | `pad` | pads the array with an element until a specified length | | `clear` | empties the array | @@ -941,9 +982,7 @@ print(y.len()); // prints 0 `push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered: ```rust -engine.register_fn("push", - |list: &mut Array, item: MyType| list.push(Box::new(item)) -); +engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(item)) ); ``` Comparison operators @@ -1103,7 +1142,7 @@ return 123 + 456; // returns 579 Errors and exceptions --------------------- -All of `Engine`'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. +All of [`Engine`]'s evaluation/consuming methods return `Result` with `EvalAltResult` holding error information. To deliberately return an error during an evaluation, use the `throw` keyword. ```rust @@ -1215,11 +1254,11 @@ abc(); // prints "None." Members and methods ------------------- -Properties and methods in a Rust custom type registered with the engine can be called just like in Rust: +Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like in Rust: ```rust let a = new_ts(); // constructor function -a.x = 500; // property access +a.field = 500; // property access a.update(); // method call ``` @@ -1307,8 +1346,8 @@ print("done!"); // <- the line above is further simp These are quite effective for template-based machine-generated scripts where certain constant values are spliced into the script text in order to turn on/off certain sections. -For fixed script texts, the constant values can be provided in a user-defined `Scope` object -to the `Engine` for use in compilation and evaluation. +For fixed script texts, the constant values can be provided in a user-defined [`Scope`] object +to the [`Engine`] for use in compilation and evaluation. Beware, however, that most operators are actually function calls, and those functions can be overridden, so they are not optimized away: @@ -1353,9 +1392,14 @@ large `if`-`else` branches because they do not depend on operators. Alternatively, turn the optimizer to [`OptimizationLevel::Full`] Here be dragons! ----------------- +================ -### Optimization levels +Optimization levels +------------------- + +[`OptimizationLevel::Full`]: #optimization-levels +[`OptimizationLevel::Simple`]: #optimization-levels +[`OptimizationLevel::None`]: #optimization-levels There are actually three levels of optimizations: `None`, `Simple` and `Full`. @@ -1367,14 +1411,14 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`. * `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. -An engine's optimization level is set via a call to `set_optimization_level`: +An [`Engine`]'s optimization level is set via a call to `set_optimization_level`: ```rust // Turn on aggressive optimizations engine.set_optimization_level(rhai::OptimizationLevel::Full); ``` -When the optimization level is [`OptimizationLevel::Full`], the engine assumes all functions to be _pure_ and will _eagerly_ +When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_ evaluated all function calls with constant arguments, using the result to replace the call. This also applies to all operators (which are implemented as functions). For instance, the same example above: @@ -1393,7 +1437,7 @@ print("hello!"); // <- the above is equivalent to this ('print' and 'debu ``` Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result. -This does not happen with `OptimizationLevel::Simple` which doesn't assume all functions to be _pure_. +This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all functions to be _pure_. ```rust // When compiling the following with OptimizationLevel::Full... @@ -1402,7 +1446,8 @@ let x = (1 + 2) * 3 - 4 / 5 % 6; // <- will be replaced by 'let x = 9' let y = (1 > 2) || (3 <= 4); // <- will be replaced by 'let y = true' ``` -### Function side effect considerations +Function side effect considerations +---------------------------------- All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state nor cause side any effects, with the exception of `print` and `debug` which are handled specially) so using [`OptimizationLevel::Full`] @@ -1412,7 +1457,8 @@ If custom functions are registered, they _may_ be called (or maybe not, if the c If custom functions are registered to replace built-in operators, they will also be called when the operators are used (in an `if` statement, for example) and cause side-effects. -### Function volatility considerations +Function volatility considerations +--------------------------------- Even if a custom function does not mutate state nor cause side effects, it may still be _volatile_, i.e. it _depends_ on the external environment and not _pure_. A perfect example is a function that gets the current time - obviously each run will return a different value! @@ -1422,7 +1468,8 @@ always be the same value. Therefore, **avoid using [`OptimizationLevel::Full`]** if you intend to register non-_pure_ custom types and/or functions. -### Subtle semantic changes +Subtle semantic changes +----------------------- Some optimizations can alter subtle semantics of the script. For example: @@ -1456,10 +1503,11 @@ In the script above, if `my_decision` holds anything other than a boolean value, However, after optimization, the entire `if` statement is removed (because an access to `my_decision` produces no side effects), thus the script silently runs to completion without errors. -### Turning off optimizations +Turning off optimizations +------------------------- It is usually a bad idea to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess), -turn it off by setting the optimization level to `OptimizationLevel::None`. +turn it off by setting the optimization level to [`OptimizationLevel::None`]. ```rust let engine = rhai::Engine::new(); @@ -1467,22 +1515,3 @@ let engine = rhai::Engine::new(); // Turn off the optimizer engine.set_optimization_level(rhai::OptimizationLevel::None); ``` - - -[`num-traits`]: https://crates.io/crates/num-traits/ -[`debug_msgs`]: #optional-features -[`unchecked`]: #optional-features -[`no_stdlib`]: #optional-features -[`no_index`]: #optional-features -[`no_float`]: #optional-features -[`no_function`]: #optional-features -[`no_optimize`]: #optional-features -[`only_i32`]: #optional-features -[`only_i64`]: #optional-features -[`no_std`]: #optional-features - -[`Engine`]: #hello-world -[`Scope`]: #initializing-and-maintaining-state -[`Dynamic`]: #values-and-types - -[`OptimizationLevel::Full`]: #optimization-levels diff --git a/examples/arrays_and_structs.rs b/examples/arrays_and_structs.rs index 215abc8d..cfdd7dc1 100644 --- a/examples/arrays_and_structs.rs +++ b/examples/arrays_and_structs.rs @@ -10,7 +10,7 @@ impl TestStruct { self.x += 1000; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } } diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index 4471cadf..3ac644f5 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -10,7 +10,7 @@ impl TestStruct { self.x += 1000; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } } diff --git a/src/any.rs b/src/any.rs index f804cb24..e92b3e9d 100644 --- a/src/any.rs +++ b/src/any.rs @@ -101,7 +101,7 @@ impl AnyExt for Dynamic { /// /// # Example /// - /// ```rust + /// ``` /// use rhai::{Dynamic, Any, AnyExt}; /// /// let x: Dynamic = 42_u32.into_dynamic(); diff --git a/src/api.rs b/src/api.rs index ad12218f..34be8687 100644 --- a/src/api.rs +++ b/src/api.rs @@ -23,6 +23,7 @@ use crate::stdlib::{ use crate::stdlib::{fs::File, io::prelude::*, path::PathBuf}; impl<'e> Engine<'e> { + /// Register a custom function. pub(crate) fn register_fn_raw( &mut self, fn_name: &str, @@ -38,13 +39,88 @@ impl<'e> Engine<'e> { } /// Register a custom type for use with the `Engine`. - /// The type must be `Clone`. + /// The type must implement `Clone`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// field: i64 + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { field: 1 } } + /// fn update(&mut self) { self.field += 41; } + /// } + /// + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register method on the type. + /// engine.register_fn("update", TestStruct::update); + /// + /// assert_eq!( + /// engine.eval::("let x = new_ts(); x.update(); x")?.field, + /// 42 + /// ); + /// # Ok(()) + /// # } + /// ``` pub fn register_type(&mut self) { self.register_type_with_name::(type_name::()); } - /// Register a custom type for use with the `Engine` with a name for the `type_of` function. - /// The type must be `Clone`. + /// Register a custom type for use with the `Engine`, with a pretty-print name + /// for the `type_of` function. The type must implement `Clone`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// field: i64 + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { field: 1 } } + /// } + /// + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// assert_eq!( + /// engine.eval::("let x = new_ts(); type_of(x)")?, + /// "rust_out::TestStruct" + /// ); + /// + /// // Register the custom type with a name. + /// engine.register_type_with_name::("Hello"); + /// + /// // Register methods on the type. + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// assert_eq!( + /// engine.eval::("let x = new_ts(); type_of(x)")?, + /// "Hello" + /// ); + /// # Ok(()) + /// # } + /// ``` pub fn register_type_with_name(&mut self, name: &str) { // Add the pretty-print type name into the map self.type_names @@ -52,6 +128,7 @@ impl<'e> Engine<'e> { } /// Register an iterator adapter for a type with the `Engine`. + /// This is an advanced feature. pub fn register_iterator(&mut self, f: F) where F: Fn(&Dynamic) -> Box> + 'static, @@ -60,6 +137,37 @@ impl<'e> Engine<'e> { } /// Register a getter function for a member of a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// field: i64 + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { field: 1 } } + /// fn get_field(&mut self) -> i64 { self.field } + /// } + /// + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register a getter on a property (notice it doesn't have to be the same name). + /// engine.register_get("xyz", TestStruct::get_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a.xyz")?, 1); + /// # Ok(()) + /// # } + /// ``` pub fn register_get( &mut self, name: &str, @@ -70,6 +178,38 @@ impl<'e> Engine<'e> { } /// Register a setter function for a member of a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// field: i64 + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { field: 1 } } + /// fn set_field(&mut self, new_val: i64) { self.field = new_val; } + /// } + /// + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register a setter on a property (notice it doesn't have to be the same name) + /// engine.register_set("xyz", TestStruct::set_field); + /// + /// // Notice that, with a getter, there is no way to get the property value + /// engine.eval("let a = new_ts(); a.xyz = 42;")?; + /// # Ok(()) + /// # } + /// ``` pub fn register_set( &mut self, name: &str, @@ -81,6 +221,39 @@ impl<'e> Engine<'e> { /// Shorthand for registering both getter and setter functions /// of a registered type with the `Engine`. + /// + /// # Example + /// + /// ``` + /// #[derive(Clone)] + /// struct TestStruct { + /// field: i64 + /// } + /// + /// impl TestStruct { + /// fn new() -> Self { TestStruct { field: 1 } } + /// fn get_field(&mut self) -> i64 { self.field } + /// fn set_field(&mut self, new_val: i64) { self.field = new_val; } + /// } + /// + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register the custom type. + /// engine.register_type::(); + /// + /// engine.register_fn("new_ts", TestStruct::new); + /// + /// // Register a getter and a setter on a property + /// // (notice it doesn't have to be the same name) + /// engine.register_get_set("xyz", TestStruct::get_field, TestStruct::set_field); + /// + /// assert_eq!(engine.eval::("let a = new_ts(); a.xyz = 42; a.xyz")?, 42); + /// # Ok(()) + /// # } + /// ``` pub fn register_get_set( &mut self, name: &str, @@ -91,18 +264,59 @@ impl<'e> Engine<'e> { self.register_set(name, set_fn); } - /// Compile a string into an AST. + /// Compile a string into an `AST`, which can be used later for evaluations. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Compile a script to an AST and store it for later evaluations + /// let ast = engine.compile("40 + 2")?; + /// + /// for _ in 0..42 { + /// assert_eq!(engine.eval_ast::(&ast)?, 42); + /// } + /// # Ok(()) + /// # } + /// ``` pub fn compile(&self, input: &str) -> Result { self.compile_with_scope(&Scope::new(), input) } - /// Compile a string into an AST using own scope. + /// Compile a string into an `AST` using own scope, which can be used later for evaluations. /// The scope is useful for passing constants into the script for optimization. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Scope}; + /// + /// let mut engine = Engine::new(); + /// + /// // Create initialized scope + /// let mut scope = Scope::new(); + /// scope.push_constant("x", 42_i64); // 'x' is a constant + /// + /// // Compile a script to an AST and store it for later evaluations + /// let ast = engine.compile_with_scope(&mut scope, + /// "if x > 40 { x } else { 0 }" + /// )?; + /// + /// assert_eq!(engine.eval_ast::(&ast)?, 42); + /// # Ok(()) + /// # } + /// ``` pub fn compile_with_scope(&self, scope: &Scope, input: &str) -> Result { let tokens_stream = lex(input); parse(&mut tokens_stream.peekable(), self, scope) } + /// Read the contents of a file into a string. #[cfg(not(feature = "no_std"))] fn read_file(path: PathBuf) -> Result { let mut f = File::open(path.clone()) @@ -115,14 +329,54 @@ impl<'e> Engine<'e> { .map(|_| contents) } - /// Compile a file into an AST. + /// Compile a script file into an `AST`, which can be used later for evaluations. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Compile a script file to an AST and store it for later evaluations + /// // Notice that a PathBuf is required which can easily be constructed from a string. + /// let ast = engine.compile_file("script.rhai".into())?; + /// + /// for _ in 0..42 { + /// engine.eval_ast::(&ast)?; + /// } + /// # Ok(()) + /// # } + /// ``` #[cfg(not(feature = "no_std"))] pub fn compile_file(&self, path: PathBuf) -> Result { self.compile_file_with_scope(&Scope::new(), path) } - /// Compile a file into an AST using own scope. + /// Compile a script file into an `AST` using own scope, which can be used later for evaluations. /// The scope is useful for passing constants into the script for optimization. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Scope}; + /// + /// let mut engine = Engine::new(); + /// + /// // Create initialized scope + /// let mut scope = Scope::new(); + /// scope.push_constant("x", 42_i64); // 'x' is a constant + /// + /// // Compile a script to an AST and store it for later evaluations + /// // Notice that a PathBuf is required which can easily be constructed from a string. + /// let ast = engine.compile_file_with_scope(&mut scope, "script.rhai".into())?; + /// + /// let result = engine.eval_ast::(&ast)?; + /// # Ok(()) + /// # } + /// ``` #[cfg(not(feature = "no_std"))] pub fn compile_file_with_scope( &self, @@ -135,13 +389,45 @@ impl<'e> Engine<'e> { }) } - /// Evaluate a file. + /// Evaluate a script file. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Notice that a PathBuf is required which can easily be constructed from a string. + /// let result = engine.eval_file::("script.rhai".into())?; + /// # Ok(()) + /// # } + /// ``` #[cfg(not(feature = "no_std"))] pub fn eval_file(&mut self, path: PathBuf) -> Result { Self::read_file(path).and_then(|contents| self.eval::(&contents)) } - /// Evaluate a file with own scope. + /// Evaluate a script file with own scope. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Scope}; + /// + /// let mut engine = Engine::new(); + /// + /// // Create initialized scope + /// let mut scope = Scope::new(); + /// scope.push("x", 42_i64); + /// + /// // Notice that a PathBuf is required which can easily be constructed from a string. + /// let result = engine.eval_file_with_scope::(&mut scope, "script.rhai".into())?; + /// # Ok(()) + /// # } + /// ``` #[cfg(not(feature = "no_std"))] pub fn eval_file_with_scope( &mut self, @@ -152,12 +438,46 @@ impl<'e> Engine<'e> { } /// Evaluate a string. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// assert_eq!(engine.eval::("40 + 2")?, 42); + /// # Ok(()) + /// # } + /// ``` pub fn eval(&mut self, input: &str) -> Result { let mut scope = Scope::new(); self.eval_with_scope(&mut scope, input) } /// Evaluate a string with own scope. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Scope}; + /// + /// let mut engine = Engine::new(); + /// + /// // Create initialized scope + /// let mut scope = Scope::new(); + /// scope.push("x", 40_i64); + /// + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 42); + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 44); + /// + /// // The variable in the scope is modified + /// assert_eq!(scope.get_value::("x").expect("variable x should exist"), 44); + /// # Ok(()) + /// # } + /// ``` pub fn eval_with_scope( &mut self, scope: &mut Scope, @@ -167,13 +487,55 @@ impl<'e> Engine<'e> { self.eval_ast_with_scope(scope, &ast) } - /// Evaluate an AST. + /// Evaluate an `AST`. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Compile a script to an AST and store it for later evaluations + /// let ast = engine.compile("40 + 2")?; + /// + /// // Evaluate it + /// assert_eq!(engine.eval_ast::(&ast)?, 42); + /// # Ok(()) + /// # } + /// ``` pub fn eval_ast(&mut self, ast: &AST) -> Result { let mut scope = Scope::new(); self.eval_ast_with_scope(&mut scope, ast) } - /// Evaluate an AST with own scope. + /// Evaluate an `AST` with own scope. + /// + /// # Example + /// + /// ```no_run + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Scope}; + /// + /// let mut engine = Engine::new(); + /// + /// // Compile a script to an AST and store it for later evaluations + /// let ast = engine.compile("x + 2")?; + /// + /// // Create initialized scope + /// let mut scope = Scope::new(); + /// scope.push("x", 40_i64); + /// + /// // Evaluate it + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 42); + /// assert_eq!(engine.eval_with_scope::(&mut scope, "x = x + 2; x")?, 44); + /// + /// // The variable in the scope is modified + /// assert_eq!(scope.get_value::("x").expect("variable x should exist"), 44); + /// # Ok(()) + /// # } + /// ``` pub fn eval_ast_with_scope( &mut self, scope: &mut Scope, @@ -221,8 +583,7 @@ impl<'e> Engine<'e> { /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file( &mut self, @@ -235,8 +596,7 @@ impl<'e> Engine<'e> { /// Evaluate a file with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ and not cleared from run to run. #[cfg(not(feature = "no_std"))] pub fn consume_file_with_scope( &mut self, @@ -251,8 +611,7 @@ impl<'e> Engine<'e> { /// Evaluate a string, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume(&mut self, retain_functions: bool, input: &str) -> Result<(), EvalAltResult> { self.consume_with_scope(&mut Scope::new(), retain_functions, input) } @@ -260,8 +619,7 @@ impl<'e> Engine<'e> { /// Evaluate a string with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_with_scope( &mut self, scope: &mut Scope, @@ -279,17 +637,15 @@ impl<'e> Engine<'e> { /// Evaluate an AST, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_ast(&mut self, retain_functions: bool, ast: &AST) -> Result<(), EvalAltResult> { self.consume_ast_with_scope(&mut Scope::new(), retain_functions, ast) } - /// Evaluate an AST with own scope, but throw away the result and only return error (if any). + /// Evaluate an `AST` with own scope, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. /// - /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_ - /// and not cleared from run to run. + /// Note - if `retain_functions` is set to `true`, functions defined by previous scripts are _retained_and not cleared from run to run. pub fn consume_ast_with_scope( &mut self, scope: &mut Scope, @@ -341,7 +697,7 @@ impl<'e> Engine<'e> { /// /// # Example /// - /// ```rust + /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { /// # #[cfg(not(feature = "no_stdlib"))] /// # #[cfg(not(feature = "no_function"))] @@ -350,8 +706,10 @@ impl<'e> Engine<'e> { /// /// let mut engine = Engine::new(); /// + /// // Set 'retain_functions' in 'consume' to keep the function definitions /// engine.consume(true, "fn add(x, y) { x.len() + y }")?; /// + /// // Call the script-defined function /// let result: i64 = engine.call_fn("add", (String::from("abc"), 123_i64))?; /// /// assert_eq!(result, 126); @@ -388,17 +746,17 @@ impl<'e> Engine<'e> { }) } - /// Optimize the AST with constants defined in an external Scope. - /// An optimized copy of the AST is returned while the original AST is untouched. + /// Optimize the `AST` with constants defined in an external Scope. + /// An optimized copy of the `AST` is returned while the original `AST` is untouched. /// /// Although optimization is performed by default during compilation, sometimes it is necessary to /// _re_-optimize an AST. For example, when working with constants that are passed in via an - /// external scope, it will be more efficient to optimize the AST once again to take advantage + /// external scope, it will be more efficient to optimize the `AST` once again to take advantage /// of the new constants. /// - /// With this method, it is no longer necessary to recompile a large script. The script AST can be + /// With this method, it is no longer necessary to recompile a large script. The script `AST` can be /// compiled just once. Before evaluation, constants are passed into the `Engine` via an external scope - /// (i.e. with `scope.push_constant(...)`). Then, the AST is cloned and the copy re-optimized before running. + /// (i.e. with `scope.push_constant(...)`). Then, the `AST is cloned and the copy re-optimized before running. #[cfg(not(feature = "no_optimize"))] pub fn optimize_ast(&self, scope: &Scope, ast: &AST) -> AST { let statements = ast.0.clone(); @@ -411,17 +769,17 @@ impl<'e> Engine<'e> { /// /// # Example /// - /// ```rust + /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// /// let mut result = String::from(""); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'print' function - /// engine.on_print(|s| result.push_str(s)); - /// engine.consume(false, "print(40 + 2);")?; + /// // Override action of 'print' function + /// engine.on_print(|s| result.push_str(s)); + /// engine.consume(false, "print(40 + 2);")?; /// } /// assert_eq!(result, "42"); /// # Ok(()) @@ -435,17 +793,17 @@ impl<'e> Engine<'e> { /// /// # Example /// - /// ```rust + /// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// /// let mut result = String::from(""); /// { - /// let mut engine = Engine::new(); + /// let mut engine = Engine::new(); /// - /// // Override action of 'debug' function - /// engine.on_debug(|s| result.push_str(s)); - /// engine.consume(false, r#"debug("hello");"#)?; + /// // Override action of 'debug' function + /// engine.on_debug(|s| result.push_str(s)); + /// engine.consume(false, r#"debug("hello");"#)?; /// } /// assert_eq!(result, "\"hello\""); /// # Ok(()) diff --git a/src/engine.rs b/src/engine.rs index 0689dbb5..9ea647f8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -57,7 +57,7 @@ pub struct FnSpec<'a> { /// Rhai main scripting engine. /// -/// ```rust +/// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::Engine; /// diff --git a/src/fn_register.rs b/src/fn_register.rs index 53c646a0..fd67757a 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -10,89 +10,90 @@ use crate::result::EvalAltResult; use crate::stdlib::{any::TypeId, boxed::Box, string::ToString, vec}; /// A trait to register custom functions with the `Engine`. -/// -/// # Example -/// -/// ```rust -/// # fn main() -> Result<(), rhai::EvalAltResult> { -/// use rhai::{Engine, RegisterFn}; -/// -/// // Normal function -/// fn add(x: i64, y: i64) -> i64 { -/// x + y -/// } -/// -/// let mut engine = Engine::new(); -/// -/// // You must use the trait rhai::RegisterFn to get this method. -/// engine.register_fn("add", add); -/// -/// let result = engine.eval::("add(40, 2)")?; -/// -/// println!("Answer: {}", result); // prints 42 -/// # Ok(()) -/// # } -/// ``` pub trait RegisterFn { /// Register a custom function with the `Engine`. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, RegisterFn}; + /// + /// // Normal function + /// fn add(x: i64, y: i64) -> i64 { + /// x + y + /// } + /// + /// let mut engine = Engine::new(); + /// + /// // You must use the trait rhai::RegisterFn to get this method. + /// engine.register_fn("add", add); + /// + /// assert_eq!(engine.eval::("add(40, 2)")?, 42); + /// + /// // You can also register a closure. + /// engine.register_fn("sub", |x: i64, y: i64| x - y ); + /// + /// assert_eq!(engine.eval::("sub(44, 2)")?, 42); + /// # Ok(()) + /// # } + /// ``` fn register_fn(&mut self, name: &str, f: FN); } /// A trait to register custom functions that return `Dynamic` values with the `Engine`. -/// -/// # Example -/// -/// ```rust -/// # fn main() -> Result<(), rhai::EvalAltResult> { -/// use rhai::{Engine, Dynamic, RegisterDynamicFn}; -/// -/// // Function that returns a Dynamic value -/// fn get_an_any(x: i64) -> Dynamic { -/// Box::new(x) -/// } -/// -/// let mut engine = Engine::new(); -/// -/// // You must use the trait rhai::RegisterDynamicFn to get this method. -/// engine.register_dynamic_fn("get_an_any", get_an_any); -/// -/// let result = engine.eval::("get_an_any(42)")?; -/// -/// println!("Answer: {}", result); // prints 42 -/// # Ok(()) -/// # } -/// ``` pub trait RegisterDynamicFn { /// Register a custom function returning `Dynamic` values with the `Engine`. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), rhai::EvalAltResult> { + /// use rhai::{Engine, Dynamic, RegisterDynamicFn}; + /// + /// // Function that returns a Dynamic value + /// fn return_the_same_as_dynamic(x: i64) -> Dynamic { + /// Box::new(x) + /// } + /// + /// let mut engine = Engine::new(); + /// + /// // You must use the trait rhai::RegisterDynamicFn to get this method. + /// engine.register_dynamic_fn("get_any_number", return_the_same_as_dynamic); + /// + /// assert_eq!(engine.eval::("get_any_number(42)")?, 42); + /// # Ok(()) + /// # } + /// ``` fn register_dynamic_fn(&mut self, name: &str, f: FN); } /// A trait to register fallible custom functions returning Result<_, EvalAltResult> with the `Engine`. -/// -/// # Example -/// -/// ```rust -/// # fn main() -> Result<(), rhai::EvalAltResult> { -/// use rhai::{Engine, RegisterFn}; -/// -/// // Normal function -/// fn add(x: i64, y: i64) -> i64 { -/// x + y -/// } -/// -/// let mut engine = Engine::new(); -/// -/// // You must use the trait rhai::RegisterFn to get this method. -/// engine.register_fn("add", add); -/// -/// let result = engine.eval::("add(40, 2)")?; -/// -/// println!("Answer: {}", result); // prints 42 -/// # Ok(()) -/// # } -/// ``` pub trait RegisterResultFn { - /// Register a custom function with the `Engine`. + /// Register a custom fallible function with the `Engine`. + /// + /// # Example + /// + /// ``` + /// use rhai::{Engine, RegisterResultFn, EvalAltResult}; + /// + /// // Normal function + /// fn div(x: i64, y: i64) -> Result { + /// if y == 0 { + /// Err("division by zero!".into()) // '.into()' automatically converts to 'EvalAltResult::ErrorRuntime' + /// } else { + /// Ok(x / y) + /// } + /// } + /// + /// let mut engine = Engine::new(); + /// + /// // You must use the trait rhai::RegisterResultFn to get this method. + /// engine.register_result_fn("div", div); + /// + /// engine.eval::("div(42, 0)") + /// .expect_err("expecting division by zero error!"); + /// ``` fn register_result_fn(&mut self, name: &str, f: FN); } diff --git a/src/lib.rs b/src/lib.rs index 5293982c..0344a1c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! It provides a familiar syntax based on JS and Rust and a simple Rust interface. //! Here is a quick example. First, the contents of `my_script.rhai`: //! -//! ```rust,ignore +//! ```,ignore //! fn factorial(x) { //! if x == 1 { return 1; } //! x * factorial(x - 1) @@ -16,7 +16,7 @@ //! //! And the Rust part: //! -//! ```rust,no_run +//! ```,no_run //! use rhai::{Engine, EvalAltResult, RegisterFn}; //! //! fn main() -> Result<(), EvalAltResult> diff --git a/src/scope.rs b/src/scope.rs index e1bedde6..6cb1c4af 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -36,7 +36,7 @@ pub struct ScopeEntry<'a> { /// /// # Example /// -/// ```rust +/// ``` /// # fn main() -> Result<(), rhai::EvalAltResult> { /// use rhai::{Engine, Scope}; /// diff --git a/tests/arrays.rs b/tests/arrays.rs index d8c6a38d..710d0cf7 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -35,7 +35,7 @@ fn test_array_with_structs() -> Result<(), EvalAltResult> { self.x = new_x; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } } diff --git a/tests/float.rs b/tests/float.rs index af33ee34..8c57339c 100644 --- a/tests/float.rs +++ b/tests/float.rs @@ -38,7 +38,7 @@ fn struct_with_float() -> Result<(), EvalAltResult> { self.x = new_x; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1.0 } } } diff --git a/tests/get_set.rs b/tests/get_set.rs index 71886d2d..7ea064b1 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -16,7 +16,7 @@ fn test_get_set() -> Result<(), EvalAltResult> { self.x = new_x; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } } diff --git a/tests/method_call.rs b/tests/method_call.rs index e1825094..89e33138 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -12,7 +12,7 @@ fn test_method_call() -> Result<(), EvalAltResult> { self.x += 1000; } - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } } diff --git a/tests/mismatched_op.rs b/tests/mismatched_op.rs index a3779705..3fcc02c6 100644 --- a/tests/mismatched_op.rs +++ b/tests/mismatched_op.rs @@ -19,7 +19,7 @@ fn test_mismatched_op_custom_type() { } impl TestStruct { - fn new() -> TestStruct { + fn new() -> Self { TestStruct { x: 1 } } }