Add support for anonymous functions in Rust.

This commit is contained in:
Stephen Chung 2020-04-08 23:01:48 +08:00
parent 660ce6cc79
commit 518725e119
7 changed files with 181 additions and 11 deletions

View File

@ -175,6 +175,10 @@ fn main() -> Result<(), EvalAltResult>
} }
``` ```
`EvalAltResult` is a Rust `enum` containing all errors encountered during the parsing or evaluation process.
### Script evaluation
The type parameter is used to specify the type of the return value, which _must_ match the actual type or an error is returned. The type parameter is used to specify the type of the return value, which _must_ match the actual type or an error is returned.
Rhai is very strict here. There are two ways to specify the return type - _turbofish_ notation, or type inference. Rhai is very strict here. There are two ways to specify the return type - _turbofish_ notation, or type inference.
@ -192,6 +196,8 @@ Evaluate a script file directly:
let result = engine.eval_file::<i64>("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf' let result = engine.eval_file::<i64>("hello_world.rhai".into())?; // 'eval_file' takes a 'PathBuf'
``` ```
### Compiling scripts (to AST)
To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form: To repeatedly evaluate a script, _compile_ it first into an AST (abstract syntax tree) form:
```rust ```rust
@ -211,6 +217,8 @@ Compiling a script file is also supported:
let ast = engine.compile_file("hello_world.rhai".into())?; let ast = engine.compile_file("hello_world.rhai".into())?;
``` ```
### Calling Rhai functions from Rust
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`. Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`.
```rust ```rust
@ -251,6 +259,37 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?
// ^^ unit = tuple of zero // ^^ unit = tuple of zero
``` ```
### Creating Rust anonymous functions from Rhai script
[`AnonymousFn`]: #creating-rust-anonymous-functions-from-rhai-script
It is possible to further encapsulate a script in Rust such that it essentially becomes a normal Rust function.
This is accomplished via the `AnonymousFn` trait which contains `create_from_script` (as well as its associate
method `create_from_ast`):
```rust
use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script'
let engine = Engine::new(); // create a new 'Engine' just for this
let script = "fn calc(x, y) { x + y.len() < 42 }";
// AnonymousFn takes two type parameters:
// 1) a tuple made up of the types of the script function's parameters
// 2) the return type of the script function
//
// 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, EvalAltResult>> and is callable!
let func = AnonymousFn::<(i64, String), bool>::create_from_script(
// ^^^^^^^^^^^^^ function parameter types in tuple
engine, // the 'Engine' is consumed into the closure
script, // the script, notice number of parameters must match
"calc" // the entry-point function name
)?;
func(123, "hello".to_string())? == false; // call the anonymous function
```
Raw `Engine` Raw `Engine`
------------ ------------
@ -448,8 +487,8 @@ To call these functions, they need to be registered with the [`Engine`].
```rust ```rust
use rhai::{Engine, EvalAltResult}; use rhai::{Engine, EvalAltResult};
use rhai::RegisterFn; // use `RegisterFn` trait for `register_fn` use rhai::RegisterFn; // use 'RegisterFn' trait for 'register_fn'
use rhai::{Any, Dynamic, RegisterDynamicFn}; // use `RegisterDynamicFn` trait for `register_dynamic_fn` use rhai::{Any, Dynamic, RegisterDynamicFn}; // use 'RegisterDynamicFn' trait for 'register_dynamic_fn'
// Normal function // Normal function
fn add(x: i64, y: i64) -> i64 { fn add(x: i64, y: i64) -> i64 {
@ -536,7 +575,7 @@ and the error text gets converted into `EvalAltResult::ErrorRuntime`.
```rust ```rust
use rhai::{Engine, EvalAltResult, Position}; use rhai::{Engine, EvalAltResult, Position};
use rhai::RegisterResultFn; // use `RegisterResultFn` trait for `register_result_fn` use rhai::RegisterResultFn; // use 'RegisterResultFn' trait for 'register_result_fn'
// Function that may fail // Function that may fail
fn safe_divide(x: i64, y: i64) -> Result<i64, EvalAltResult> { fn safe_divide(x: i64, y: i64) -> Result<i64, EvalAltResult> {

View File

@ -1,9 +1,9 @@
//! Module that defines the extern API of `Engine`. //! Module that defines the extern API of `Engine`.
use crate::any::{Any, AnyExt, Dynamic}; use crate::any::{Any, AnyExt, Dynamic};
use crate::call::FuncArgs;
use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec};
use crate::error::ParseError; use crate::error::ParseError;
use crate::fn_call::FuncArgs;
use crate::fn_register::RegisterFn; use crate::fn_register::RegisterFn;
use crate::parser::{lex, parse, parse_global_expr, Position, AST}; use crate::parser::{lex, parse, parse_global_expr, Position, AST};
use crate::result::EvalAltResult; use crate::result::EvalAltResult;

118
src/fn_anonymous.rs Normal file
View File

@ -0,0 +1,118 @@
//! Module which defines the function registration mechanism.
#![cfg(not(feature = "no_function"))]
#![allow(non_snake_case)]
use crate::any::Any;
use crate::engine::Engine;
use crate::error::ParseError;
use crate::parser::AST;
use crate::result::EvalAltResult;
use crate::scope::Scope;
/// A trait to create a Rust anonymous function from a script.
pub trait AnonymousFn<ARGS, RET> {
type Output;
/// Create a Rust anonymous function from an `AST`.
/// The `Engine` and `AST` are consumed and basically embedded into the closure.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_ast'
///
/// let engine = Engine::new(); // create a new 'Engine' just for this
///
/// let ast = engine.compile("fn calc(x, y) { x + y.len() < 42 }")?;
///
/// // AnonymousFn takes two type parameters:
/// // 1) a tuple made up of the types of the script function's parameters
/// // 2) the return type of the script function
/// //
/// // 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, EvalAltResult>> and is callable!
/// let func = AnonymousFn::<(i64, String), bool>::create_from_ast(
/// // ^^^^^^^^^^^^^ function parameter types in tuple
///
/// engine, // the 'Engine' is consumed into the closure
/// ast, // the 'AST'
/// "calc" // the entry-point function name
/// );
///
/// func(123, "hello".to_string())? == false; // call the anonymous function
/// # Ok(())
/// # }
fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output;
/// Create a Rust anonymous function from a script.
/// The `Engine` is consumed and basically embedded into the closure.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, AnonymousFn}; // use 'AnonymousFn' for 'create_from_script'
///
/// let engine = Engine::new(); // create a new 'Engine' just for this
///
/// let script = "fn calc(x, y) { x + y.len() < 42 }";
///
/// // AnonymousFn takes two type parameters:
/// // 1) a tuple made up of the types of the script function's parameters
/// // 2) the return type of the script function
/// //
/// // 'func' will have type Box<dyn Fn(i64, String) -> Result<bool, EvalAltResult>> and is callable!
/// let func = AnonymousFn::<(i64, String), bool>::create_from_script(
/// // ^^^^^^^^^^^^^ function parameter types in tuple
///
/// engine, // the 'Engine' is consumed into the closure
/// script, // the script, notice number of parameters must match
/// "calc" // the entry-point function name
/// )?;
///
/// func(123, "hello".to_string())? == false; // call the anonymous function
/// # Ok(())
/// # }
/// ```
fn create_from_script(
self,
script: &str,
entry_point: &str,
) -> Result<Self::Output, ParseError>;
}
macro_rules! def_anonymous_fn {
() => {
def_anonymous_fn!(imp);
};
(imp $($par:ident),*) => {
impl<'e, $($par: Any + Clone,)* RET: Any + Clone> AnonymousFn<($($par,)*), RET> for Engine<'e>
{
#[cfg(feature = "sync")]
type Output = Box<dyn Fn($($par),*) -> Result<RET, EvalAltResult> + Send + Sync + 'e>;
#[cfg(not(feature = "sync"))]
type Output = Box<dyn Fn($($par),*) -> Result<RET, EvalAltResult> + 'e>;
fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output {
let name = entry_point.to_string();
Box::new(move |$($par: $par),*| {
self.call_fn::<_, RET>(&mut Scope::new(), &ast, &name, ($($par,)*))
})
}
fn create_from_script(self, script: &str, entry_point: &str) -> Result<Self::Output, ParseError> {
let ast = self.compile(script)?;
Ok(AnonymousFn::<($($par,)*), RET>::create_from_ast(self, ast, entry_point))
}
}
};
($p0:ident $(, $p:ident)*) => {
def_anonymous_fn!(imp $p0 $(, $p)*);
def_anonymous_fn!($($p),*);
};
}
#[rustfmt::skip]
def_anonymous_fn!(A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V);

View File

@ -262,10 +262,6 @@ macro_rules! def_register {
def_register!($($p),*); def_register!($($p),*);
}; };
// (imp_pop) => {};
// (imp_pop $head:ident => $head_mark:ty => $head_param:ty $(,$tail:ident => $tail_mark:ty => $tp:ty)*) => {
// def_register!(imp $($tail => $tail_mark => $tp),*);
// };
} }
#[rustfmt::skip] #[rustfmt::skip]

View File

@ -61,9 +61,10 @@ extern crate alloc;
mod any; mod any;
mod api; mod api;
mod builtin; mod builtin;
mod call;
mod engine; mod engine;
mod error; mod error;
mod fn_anonymous;
mod fn_call;
mod fn_register; mod fn_register;
mod optimize; mod optimize;
mod parser; mod parser;
@ -72,14 +73,17 @@ mod scope;
mod stdlib; mod stdlib;
pub use any::{Any, AnyExt, Dynamic, Variant}; pub use any::{Any, AnyExt, Dynamic, Variant};
pub use call::FuncArgs;
pub use engine::Engine; pub use engine::Engine;
pub use error::{ParseError, ParseErrorType}; pub use error::{ParseError, ParseErrorType};
pub use fn_call::FuncArgs;
pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn}; pub use fn_register::{RegisterDynamicFn, RegisterFn, RegisterResultFn};
pub use parser::{Position, AST, INT}; pub use parser::{Position, AST, INT};
pub use result::EvalAltResult; pub use result::EvalAltResult;
pub use scope::Scope; pub use scope::Scope;
#[cfg(not(feature = "no_function"))]
pub use fn_anonymous::AnonymousFn;
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
pub use engine::Array; pub use engine::Array;

View File

@ -1,5 +1,5 @@
#![cfg(not(feature = "no_function"))] #![cfg(not(feature = "no_function"))]
use rhai::{Engine, EvalAltResult, ParseErrorType, Scope, INT}; use rhai::{AnonymousFn, Engine, EvalAltResult, ParseErrorType, Scope, INT};
#[test] #[test]
fn test_fn() -> Result<(), EvalAltResult> { fn test_fn() -> Result<(), EvalAltResult> {
@ -59,3 +59,16 @@ fn test_call_fn() -> Result<(), EvalAltResult> {
Ok(()) Ok(())
} }
#[test]
fn test_anonymous_fn() -> Result<(), EvalAltResult> {
let calc_func = AnonymousFn::<(INT, INT, INT), INT>::create_from_script(
Engine::new(),
"fn calc(x, y, z) { (x + y) * z }",
"calc",
)?;
assert_eq!(calc_func(42, 123, 9)?, 1485);
Ok(())
}