From 5ad2d24251ab178eb6ac58a934abf805a22860d5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 16 Mar 2020 14:50:12 +0800 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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();