Allow if expressions.

This commit is contained in:
Stephen Chung 2020-03-27 23:47:23 +08:00
parent a541a4507f
commit ef6dd9414a
6 changed files with 115 additions and 59 deletions

108
README.md
View File

@ -258,7 +258,7 @@ let result = engine.eval_expression_with_scope::<i64>(&mut scope, "if x { 42 } e
Values and types
----------------
[`type_of`]: #values-and-types
[`type_of()`]: #values-and-types
The following primitive types are supported natively:
@ -553,8 +553,8 @@ let result = engine.eval::<i64>("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. If `register_type_with_name` is used to register the custom type
with a special "pretty-print" name, [`type_of`] will return that name instead.
[`type_of()`] works fine with custom types and returns the name of the type. If `register_type_with_name` is used to register the custom type
with a special "pretty-print" name, [`type_of()`] will return that name instead.
```rust
engine.register_type::<TestStruct>();
@ -850,9 +850,11 @@ String and char literals follow C-style formatting, with support for Unicode ('`
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).
Individual characters within a Rhai string can be replaced. In Rhai, there is no separate concepts of `String` and `&str` as in Rust.
Internally Rhai strings are stored as UTF-8 just like Rust (they _are_ Rust `String`s!), but there are major differences.
In Rhai a string is the same as an array of Unicode characters and can be directly indexed (unlike Rust).
This is similar to most other languages where strings are internally represented not as UTF-8 but as arrays of multi-byte Unicode characters.
Individual characters within a Rhai string can also be replaced just as if the string is an array of Unicode characters.
In Rhai, there is also no separate concepts of `String` and `&str` as in Rust.
Strings can be built up from other strings and types via the `+` operator (provided by the standard library but excluded if [`no_stdlib`]).
This is particularly useful when printing output.
@ -944,9 +946,11 @@ Arrays
------
Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices.
Array literals are built within square brackets '`[`' ,, '`]`' and separated by commas '`,`'.
Array literals are built within square brackets '`[`' ... '`]`' and separated by commas '`,`'.
The type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns `"array"`.
The Rust type of a Rhai array is `rhai::Array`.
[`type_of()`] an array returns `"array"`.
Arrays are disabled via the [`no_index`] feature.
@ -1093,17 +1097,13 @@ my_str += 12345;
my_str == "abcABC12345"
```
If statements
-------------
All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement.
Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to.
`if` statements
---------------
```rust
if true {
if foo(x) {
print("It's true!");
} else if true {
} else if bar == baz {
print("It's true again!");
} else if ... {
:
@ -1112,13 +1112,28 @@ if true {
} else {
print("It's finally false!");
}
```
All branches of an `if` statement must be enclosed within braces '`{`' .. '`}`', even when there is only one statement.
Like Rust, there is no ambiguity regarding which `if` clause a statement belongs to.
```rust
if (decision) print("I've decided!");
// ^ syntax error, expecting '{' in statement block
```
While loops
-----------
Like Rust, `if` statements can also be used as _expressions_, replacing the `? :` conditional operators in other C-like languages.
```rust
let x = 1 + if true { 42 } else { 123 } / 2;
x == 22;
let x = if false { 42 }; // No else branch defaults to '()'
x == ();
```
`while` loops
-------------
```rust
let x = 10;
@ -1130,8 +1145,8 @@ while x > 0 {
}
```
Infinite loops
--------------
Infinite `loop`
---------------
```rust
let x = 10;
@ -1143,8 +1158,8 @@ loop {
}
```
For loops
---------
`for` loops
-----------
Iterating through a range or an array is provided by the `for` ... `in` loop.
@ -1164,8 +1179,8 @@ for x in range(0, 50) {
}
```
Returning values
----------------
`return`-ing values
-------------------
```rust
return; // equivalent to return ();
@ -1173,8 +1188,8 @@ return; // equivalent to return ();
return 123 + 456; // returns 579
```
Errors and exceptions
---------------------
Errors and `throw`-ing exceptions
--------------------------------
All of [`Engine`]'s evaluation/consuming methods return `Result<T, rhai::EvalAltResult>` with `EvalAltResult` holding error information.
To deliberately return an error during an evaluation, use the `throw` keyword.
@ -1215,8 +1230,10 @@ fn add(x, y) {
print(add(2, 3));
```
### Implicit return
Just like in Rust, an implicit return can be used. In fact, the last statement of a block is _always_ the block's return value
regardless of whether it is terminated with a semicolon `;`. This is different from Rust.
regardless of whether it is terminated with a semicolon `';'`. This is different from Rust.
```rust
fn add(x, y) {
@ -1231,6 +1248,16 @@ print(add(2, 3)); // prints 5
print(add2(42)); // prints 44
```
### No access to external scope
Functions can only access their parameters. They cannot access external variables (even _global_ variables).
```rust
let x = 42;
fn foo() { x } // syntax error - variable 'x' doesn't exist
```
### Passing arguments by value
Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type).
@ -1269,31 +1296,34 @@ fn do_addition(x) {
### Functions overloading
Functions can be _overloaded_ based on the _number_ of parameters (but not parameter _types_, since all parameters are the same type - [`Dynamic`]).
New definitions of the same name and number of parameters overwrite previous definitions.
Functions can be _overloaded_ and are resolved purely upon the function's _name_ and the _number_ of parameters
(but not parameter _types_, since all parameters are the same type - [`Dynamic`]).
New definitions _overwrite_ previous definitions of the same name and number of parameters.
```rust
fn abc(x,y,z) { print("Three!!! " + x + "," + y + "," + z) }
fn abc(x) { print("One! " + x) }
fn abc(x,y) { print("Two! " + x + "," + y) }
fn abc() { print("None.") }
fn abc(x) { print("HA! NEW ONE! " + x) } // overwrites previous definition
fn foo(x,y,z) { print("Three!!! " + x + "," + y + "," + z) }
fn foo(x) { print("One! " + x) }
fn foo(x,y) { print("Two! " + x + "," + y) }
fn foo() { print("None.") }
fn foo(x) { print("HA! NEW ONE! " + x) } // overwrites previous definition
abc(1,2,3); // prints "Three!!! 1,2,3"
abc(42); // prints "HA! NEW ONE! 42"
abc(1,2); // prints "Two!! 1,2"
abc(); // prints "None."
foo(1,2,3); // prints "Three!!! 1,2,3"
foo(42); // prints "HA! NEW ONE! 42"
foo(1,2); // prints "Two!! 1,2"
foo(); // prints "None."
```
Members and methods
-------------------
Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like in Rust:
Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like in Rust.
```rust
let a = new_ts(); // constructor function
a.field = 500; // property access
a.update(); // method call
update(a); // this works, but 'a' is unchanged because only a COPY of 'a' is passed to 'update' by VALUE
```
`print` and `debug`

View File

@ -1555,20 +1555,17 @@ fn parse_array_literal<'a>(
arr.push(parse_expr(input, allow_stmt_expr)?);
match input.peek().ok_or_else(|| {
ParseError(
PERR::MissingRightBracket("separating items in array literal".into()),
Position::eof(),
)
PERR::MissingRightBracket("separating items in array literal".into()).into_err_eof()
})? {
(Token::Comma, _) => {
input.next();
}
(Token::RightBracket, _) => break,
(_, pos) => {
return Err(ParseError(
PERR::MissingComma("separating items in array literal".into()),
*pos,
))
return Err(
PERR::MissingComma("separating items in array literal".into())
.into_err(*pos),
)
}
}
}
@ -1660,6 +1657,14 @@ fn parse_unary<'a>(
.peek()
.ok_or_else(|| PERR::UnexpectedEOF.into_err_eof())?
{
// If statement is allowed to act as expressions
(Token::If, pos) => {
let pos = *pos;
Ok(Expr::Stmt(
Box::new(parse_if(input, false, allow_stmt_expr)?),
pos,
))
}
// -expr
(Token::UnaryMinus, pos) => {
let pos = *pos;
@ -1956,6 +1961,7 @@ fn parse_expr<'a>(
input: &mut Peekable<TokenIterator<'a>>,
allow_stmt_expr: bool,
) -> Result<Expr, ParseError> {
// Parse a real expression
let lhs = parse_unary(input, allow_stmt_expr)?;
parse_binary_op(input, 1, lhs, allow_stmt_expr)
}
@ -1967,10 +1973,10 @@ fn ensure_not_statement_expr<'a>(
) -> Result<(), ParseError> {
match input
.peek()
.ok_or_else(|| ParseError(PERR::ExprExpected(type_name.to_string()), Position::eof()))?
.ok_or_else(|| PERR::ExprExpected(type_name.to_string()).into_err_eof())?
{
// Disallow statement expressions
(Token::LeftBrace, pos) => Err(ParseError(PERR::ExprExpected(type_name.to_string()), *pos)),
(Token::LeftBrace, pos) => Err(PERR::ExprExpected(type_name.to_string()).into_err(*pos)),
// No need to check for others at this time - leave it for the expr parser
_ => Ok(()),
}
@ -2111,10 +2117,9 @@ fn parse_let<'a>(
Ok(Stmt::Const(name, Box::new(init_value), pos))
}
// const name = expr - error
ScopeEntryType::Constant => Err(ParseError(
PERR::ForbiddenConstantExpr(name),
init_value.position(),
)),
ScopeEntryType::Constant => {
Err(PERR::ForbiddenConstantExpr(name).into_err(init_value.position()))
}
}
} else {
// let name

View File

@ -1,9 +1,9 @@
#![cfg(not(feature = "no_function"))]
use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType, INT};
use rhai::{Engine, EvalAltResult, ParseErrorType, INT};
#[test]
fn test_fn() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
let engine = Engine::new();
// Expect duplicated parameters error
match engine

View File

@ -12,14 +12,15 @@ fn test_expressions() -> Result<(), EvalAltResult> {
engine.eval_expression_with_scope::<INT>(&mut scope, "2 + (x + 10) * 2")?,
42
);
assert_eq!(
engine.eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else { 123 }")?,
42
);
assert!(engine.eval_expression::<()>("40 + 2;").is_err());
assert!(engine.eval_expression::<()>("40 + { 2 }").is_err());
assert!(engine.eval_expression::<()>("x = 42").is_err());
assert!(engine.compile_expression("let x = 42").is_err());
assert!(engine
.eval_expression_with_scope::<INT>(&mut scope, "if x > 0 { 42 } else { 123 }")
.is_err());
Ok(())
}

View File

@ -27,3 +27,21 @@ fn test_if() -> Result<(), EvalAltResult> {
Ok(())
}
#[test]
fn test_if_expr() -> Result<(), EvalAltResult> {
let mut engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
r"
let x = 42;
let y = 1 + if x > 40 { 100 } else { 0 } / x;
y
"
)?,
3
);
Ok(())
}

View File

@ -25,8 +25,10 @@ fn test_method_call() -> Result<(), EvalAltResult> {
engine.register_fn("new_ts", TestStruct::new);
let ts = engine.eval::<TestStruct>("let x = new_ts(); x.update(); x")?;
assert_eq!(ts.x, 1001);
let ts = engine.eval::<TestStruct>("let x = new_ts(); update(x); x")?;
assert_eq!(ts.x, 1);
Ok(())
}