Merge pull request #41 from luciusmagn/master

Loops, comments, refactor, docs, unary operators
This commit is contained in:
Lukáš Hozda [magnusi] 2017-10-30 08:54:06 +01:00 committed by GitHub
commit 39c9460bbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 410 additions and 26 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "rhai"
version = "0.5.1"
version = "0.6.2"
authors = ["Jonathan Turner", "Lukáš Hozda"]
description = "Embedded scripting for Rust"
homepage = "https://github.com/jonathandturner/rhai"

View File

@ -37,12 +37,36 @@ The repository contains several examples in the `examples` folder:
- `reuse_scope` evaluates two pieces of code in separate runs, but using a common scope
- `rhai_runner` runs each filename passed to it as a Rhai script
- `simple_fn` shows how to register a Rust function to a Rhai engine
- `repl` a simple REPL, see source code for what it can do at the moment
Examples can be run with the following command:
```bash
cargo run --example name
```
## Example Scripts
We also have a few examples scripts that showcase Rhai's features, all stored in the `scripts` folder:
- `array.rhai` - arrays in Rhai
- `assignment.rhai` - variable declarations
- `comments.rhai` - just comments
- `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
- `speed_test.rhai` - a simple program to measure the speed of Rhai's interpreter
- `string.rhai`- string operations
- `while.rhai` - while loop
To run the scripts, you can either make your own tiny program, or make use of the `rhai_runner`
example program:
```bash
cargo run --example rhai_runner scripts/any_script.rhai
```
# Hello world
To get going with Rhai, you create an instance of the scripting engine and then run eval.
@ -289,6 +313,17 @@ while x > 0 {
}
```
## Loop
```rust
let x = 10;
loop {
print(x);
x = x - 1;
if x == 0 { break; }
}
```
## Functions
Rhai supports defining functions in script:
@ -336,3 +371,18 @@ let name = "Bob";
let middle_initial = 'C';
```
## Comments
```rust
let /* intruder comment */ name = "Bob";
// This is a very important comment
/* This comment spans
multiple lines, so it
only makes sense that
it is even more important */
/* Fear not, Rhai satisfies all your nesting
needs with nested comments:
/*/*/*/*/**/*/*/*/*/
*/
```

4
TODO
View File

@ -1,11 +1,7 @@
pre 1.0:
- loop
- binary ops
- unary minus and negation
- bool ops
- basic threads
- stdlib
- comments
- floats
- REPL (consume functions)
1.0:

10
scripts/comments.rhai Normal file
View File

@ -0,0 +1,10 @@
// I am a single line comment!
let /* I am a spy in a variable declaration! */ x = 5;
/* I am a simple
multiline comment */
/* look /* at /* that, /* multiline */ comments */ can be */ nested */
/* sorrounded by */ x // comments

8
scripts/loop.rhai Normal file
View File

@ -0,0 +1,8 @@
let x = 10;
// simulate do..while using loop
loop {
print(x);
x = x - 1;
if x <= 0 { break; }
}

View File

@ -7,7 +7,7 @@ use std::fmt;
use parser::{lex, parse, Expr, Stmt, FnDef};
use fn_register::FnRegister;
use std::ops::{Add, Sub, Mul, Div};
use std::ops::{Add, Sub, Mul, Div, Neg};
use std::cmp::{Ord, Eq};
#[derive(Debug)]
@ -91,14 +91,44 @@ pub enum FnType {
InternalFn(FnDef),
}
/// Rhai's engine type. This is what you use to run Rhai scripts
///
/// ```rust
/// extern crate rhai;
/// use rhai::Engine;
///
/// fn main() {
/// let mut engine = Engine::new();
///
/// if let Ok(result) = engine.eval::<i64>("40 + 2") {
/// println!("Answer: {}", result); // prints 42
/// }
/// }
/// ```
pub struct Engine {
pub fns: HashMap<String, Vec<FnType>>,
}
/// A type containing information about current scope.
/// Useful for keeping state between `Engine` runs
///
/// ```rust
/// use rhai::{Engine, Scope};
///
/// let mut engine = Engine::new();
/// let mut my_scope = Scope::new();
///
/// assert!(engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;").is_ok());
/// assert_eq!(engine.eval_with_scope::<i64>(&mut my_scope, "x + 1").unwrap(), 6);
/// ```
///
/// Between runs, `Engine` only remembers functions when not using own `Scope`.
pub type Scope = Vec<(String, Box<Any>)>;
impl Engine {
fn call_fn(&self,
/// Universal method for calling functions, that are either
/// registered with the `Engine` or written in Rhai
pub fn call_fn(&self,
name: &str,
arg1: Option<&mut Box<Any>>,
arg2: Option<&mut Box<Any>>,
@ -500,6 +530,8 @@ impl Engine {
}
}
/// Register a type for use with Engine. Keep in mind that
/// your type must implement Clone.
pub fn register_type<T: Clone + Any>(&mut self) {
fn clone_helper<T: Clone>(t: T) -> T {
t.clone()
@ -508,6 +540,7 @@ impl Engine {
self.register_fn("clone", clone_helper as fn(T) -> T);
}
/// Register a get function for a member of a registered type
pub fn register_get<T: Clone + Any, U: Clone + Any, F>(&mut self, name: &str, get_fn: F)
where F: 'static + Fn(&mut T) -> U
{
@ -516,6 +549,7 @@ impl Engine {
self.register_fn(&get_name, get_fn);
}
/// Register a set function for a member of a registered type
pub fn register_set<T: Clone + Any, U: Clone + Any, F>(&mut self, name: &str, set_fn: F)
where F: 'static + Fn(&mut T, U) -> ()
{
@ -524,6 +558,7 @@ impl Engine {
self.register_fn(&set_name, set_fn);
}
/// Shorthand for registering both getters and setters
pub fn register_get_set<T: Clone + Any, U: Clone + Any, F, G>(&mut self,
name: &str,
get_fn: F,
@ -1153,6 +1188,19 @@ impl Engine {
}
}
}
Stmt::Loop(ref body) => {
loop {
match self.eval_stmt(scope, body) {
Err(EvalAltResult::LoopBreak) => {
return Ok(Box::new(()));
}
Err(x) => {
return Err(x);
}
_ => (),
}
}
}
Stmt::Break => Err(EvalAltResult::LoopBreak),
Stmt::Return => Err(EvalAltResult::Return(Box::new(()))),
Stmt::ReturnWithVal(ref a) => {
@ -1174,6 +1222,7 @@ impl Engine {
}
}
/// Evaluate a file
pub fn eval_file<T: Any + Clone>(&mut self, fname: &str) -> Result<T, EvalAltResult> {
use std::fs::File;
use std::io::prelude::*;
@ -1191,12 +1240,14 @@ impl Engine {
}
}
/// Evaluate a string
pub fn eval<T: Any + Clone>(&mut self, input: &str) -> Result<T, EvalAltResult> {
let mut scope: Scope = Vec::new();
self.eval_with_scope(&mut scope, input)
}
/// Evaluate with own scope
pub fn eval_with_scope<T: Any + Clone>(&mut self,
scope: &mut Scope,
input: &str)
@ -1241,6 +1292,9 @@ impl Engine {
}
}
/// Evaluate a file, but only return errors, if there are any.
/// Useful for when you don't need the result, but still need
/// to keep track of possible errors
pub fn consume_file(&mut self, fname: &str) -> Result<(), EvalAltResult> {
use std::fs::File;
use std::io::prelude::*;
@ -1260,6 +1314,9 @@ impl Engine {
}
}
/// Evaluate a string, but only return errors, if there are any.
/// Useful for when you don't need the result, but still need
/// to keep track of possible errors
pub fn consume(&mut self, input: &str) -> Result<(), EvalAltResult> {
let mut scope: Scope = Scope::new();
@ -1268,6 +1325,9 @@ impl Engine {
res
}
/// Evaluate a string with own scoppe, but only return errors, if there are any.
/// Useful for when you don't need the result, but still need
/// to keep track of possible errors
pub fn consume_with_scope(&mut self, scope: &mut Scope, input: &str) -> Result<(), EvalAltResult> {
let tokens = lex(input);
@ -1298,6 +1358,8 @@ impl Engine {
}
}
/// Register the default library. That means, numberic types, char, bool
/// String, arithmetics and string concatenations.
pub fn register_default_lib(engine: &mut Engine) {
engine.register_type::<i32>();
engine.register_type::<u32>();
@ -1317,6 +1379,14 @@ impl Engine {
)
}
macro_rules! reg_un {
($engine:expr, $x:expr, $op:expr, $( $y:ty ),*) => (
$(
$engine.register_fn($x, ($op as fn(x: $y)->$y));
)*
)
}
macro_rules! reg_cmp {
($engine:expr, $x:expr, $op:expr, $( $y:ty ),*) => (
$(
@ -1329,6 +1399,7 @@ impl Engine {
fn sub<T: Sub>(x: T, y: T) -> <T as Sub>::Output { x - y }
fn mul<T: Mul>(x: T, y: T) -> <T as Mul>::Output { x * y }
fn div<T: Div>(x: T, y: T) -> <T as Div>::Output { x / y }
fn neg<T: Neg>(x: T) -> <T as Neg>::Output { -x }
fn lt<T: Ord>(x: T, y: T) -> bool { x < y }
fn lte<T: Ord>(x: T, y: T) -> bool { x <= y }
fn gt<T: Ord>(x: T, y: T) -> bool { x > y }
@ -1337,6 +1408,7 @@ impl Engine {
fn ne<T: Eq>(x: T, y: T) -> bool { x != y }
fn and(x: bool, y: bool) -> bool { x && y }
fn or(x: bool, y: bool) -> bool { x || y }
fn not(x: bool) -> bool { !x }
fn concat(x: String, y: String) -> String { x + &y }
reg_op!(engine, "+", add, i32, i64, u32, u64, f32, f64);
@ -1354,6 +1426,9 @@ impl Engine {
reg_op!(engine, "||", or, bool);
reg_op!(engine, "&&", and, bool);
reg_un!(engine, "-", neg, i32, i64, f32, f64);
reg_un!(engine, "!", not, bool);
engine.register_fn("+", concat);
// engine.register_fn("[]", idx);
@ -1362,6 +1437,7 @@ impl Engine {
// (*ent).push(FnType::ExternalFn2(Box::new(idx)));
}
/// Make a new engine
pub fn new() -> Engine {
let mut engine = Engine { fns: HashMap::new() };

View File

@ -1,4 +1,39 @@
//! Rhai - embedded scripting for Rust
//! # Rhai - embedded scripting for Rust
//! Rhai is a tiny, simple and very fast embedded scripting language for Rust
//! that gives you a safe and easy way to add scripting to your applications.
//! It provides a familiar syntax based on JS and Rust and a simple Rust interface.
//! Here is a quick example. First, the contents of `my_script.rhai`:
//!
//! ```rust_todo_disable_testing_enable_highlighting
//! fn factorial(x) {
//! if x == 1 { return 1; }
//! x * factorial(x - 1)
//! }
//!
//! compute_something(factorial(10))
//! ```
//!
//! And the Rust part:
//!
//! ```rust
//! use rhai::{FnRegister, Engine};
//!
//! fn compute_something(x: i64) -> bool {
//! (x % 40) == 0
//! }
//!
//! let mut engine = Engine::new();
//! engine.register_fn("compute_something", compute_something);
//! # // Very ugly hack incoming, TODO
//! # use std::fs::{File, remove_file};
//! # use std::io::Write;
//! # let mut f = File::create("my_script.rhai").unwrap();
//! # let _ = write!(f, "{}", "fn f(x) { if x == 1 { return 1; } x * f(x-1) } compute_something(f(10))");
//! assert!(engine.eval_file::<bool>("my_script.rhai").unwrap());
//! # let _ = remove_file("my_script.rhai");
//! ```
//!
//! [Check out the README on github for more information!](https://github.com/jonathandturner/rhai)
// lints required by Rhai
#![allow(unknown_lints,
@ -16,3 +51,4 @@ mod tests;
pub use engine::{Engine, Scope};
pub use fn_register::FnRegister;

View File

@ -4,12 +4,13 @@ use std::iter::Peekable;
use std::str::Chars;
use std::char;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum LexError {
UnexpectedChar,
MalformedEscapeSequence,
MalformedNumber,
MalformedChar,
Nothing
}
impl Error for LexError {
@ -19,12 +20,9 @@ impl Error for LexError {
LexError::MalformedEscapeSequence => "Unexpected values in escape sequence",
LexError::MalformedNumber => "Unexpected characters in number",
LexError::MalformedChar => "Char constant not a single character",
LexError::Nothing => "This error is for internal use only"
}
}
fn cause(&self) -> Option<&Error> {
None
}
}
impl fmt::Display for LexError {
@ -88,6 +86,7 @@ pub enum Stmt {
If(Box<Expr>, Box<Stmt>),
IfElse(Box<Expr>, Box<Stmt>, Box<Stmt>),
While(Box<Expr>, Box<Stmt>),
Loop(Box<Stmt>),
Var(String, Option<Box<Expr>>),
Block(Vec<Stmt>),
Expr(Box<Expr>),
@ -111,7 +110,7 @@ pub enum Expr {
False,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Token {
IntConst(i64),
Identifier(String),
@ -124,7 +123,9 @@ pub enum Token {
LSquare,
RSquare,
Plus,
UnaryPlus,
Minus,
UnaryMinus,
Multiply,
Divide,
Semicolon,
@ -138,6 +139,7 @@ pub enum Token {
If,
Else,
While,
Loop,
LessThan,
GreaterThan,
Bang,
@ -157,7 +159,93 @@ pub enum Token {
LexErr(LexError),
}
impl Token {
// if another operator is after these, it's probably an unary operator
// not sure about fn's name
pub fn is_next_unary(&self) -> bool {
use self::Token::*;
match *self {
LCurly | // (+expr) - is unary
// RCurly | {expr} - expr not unary & is closing
LParen | // {-expr} - is unary
// RParen | (expr) - expr not unary & is closing
LSquare | // [-expr] - is unary
// RSquare | [expr] - expr not unary & is closing
Plus |
UnaryPlus |
Minus |
UnaryMinus |
Multiply |
Divide |
Colon |
Comma |
Period |
Equals |
LessThan |
GreaterThan |
Bang |
LessThanEqual |
GreaterThanEqual |
EqualTo |
NotEqualTo |
Pipe |
Or |
Ampersand |
And |
If |
While |
Return => true,
_ => false,
}
}
#[allow(dead_code)]
pub fn is_bin_op(&self) -> bool {
use self::Token::*;
match *self {
RCurly |
RParen |
RSquare |
Plus |
Minus |
Multiply |
Divide |
Comma |
// Period | <- does period count?
Equals |
LessThan |
GreaterThan |
LessThanEqual |
GreaterThanEqual |
EqualTo |
NotEqualTo |
Pipe |
Or |
Ampersand |
And => true,
_ => false,
}
}
#[allow(dead_code)]
pub fn is_un_op(&self) -> bool {
use self::Token::*;
match *self {
UnaryPlus |
UnaryMinus |
Equals |
Bang |
Return => true,
_ => false,
}
}
}
pub struct TokenIterator<'a> {
last: Token,
char_stream: Peekable<Chars<'a>>,
}
@ -264,12 +352,8 @@ impl<'a> TokenIterator<'a> {
let out: String = result.iter().cloned().collect();
Ok(out)
}
}
impl<'a> Iterator for TokenIterator<'a> {
type Item = Token;
fn next(&mut self) -> Option<Self::Item> {
fn inner_next(&mut self) -> Option<Token> {
while let Some(c) = self.char_stream.next() {
match c {
'0'...'9' => {
@ -316,6 +400,7 @@ impl<'a> Iterator for TokenIterator<'a> {
"if" => return Some(Token::If),
"else" => return Some(Token::Else),
"while" => return Some(Token::While),
"loop" => return Some(Token::Loop),
"break" => return Some(Token::Break),
"return" => return Some(Token::Return),
"fn" => return Some(Token::Fn),
@ -358,7 +443,8 @@ impl<'a> Iterator for TokenIterator<'a> {
self.char_stream.next();
Some(Token::PlusEquals)
},
_ => Some(Token::Plus)
_ if self.last.is_next_unary() => Some(Token::UnaryPlus),
_ => Some(Token::Plus),
}
},
'-' => {
@ -367,11 +453,41 @@ impl<'a> Iterator for TokenIterator<'a> {
self.char_stream.next();
Some(Token::MinusEquals)
},
_ => Some(Token::Minus)
_ if self.last.is_next_unary() => Some(Token::UnaryMinus),
_ => Some(Token::Minus),
}
},
'*' => return Some(Token::Multiply),
'/' => return Some(Token::Divide),
'/' => {
match self.char_stream.peek() {
Some(&'/') => {
self.char_stream.next();
while let Some(c) = self.char_stream.next() {
if c == '\n' { break; }
}
}
Some(&'*') => {
let mut level = 1;
self.char_stream.next();
while let Some(c) = self.char_stream.next() {
match c {
'/' => if let Some('*') = self.char_stream.next() {
level+=1;
}
'*' => if let Some('/') = self.char_stream.next() {
level-=1;
}
_ => (),
}
if level == 0 {
break;
}
}
}
_ => return Some(Token::Divide),
}
}
';' => return Some(Token::Semicolon),
':' => return Some(Token::Colon),
',' => return Some(Token::Comma),
@ -439,8 +555,21 @@ impl<'a> Iterator for TokenIterator<'a> {
}
}
impl<'a> Iterator for TokenIterator<'a> {
type Item = Token;
// TODO - perhaps this could be optimized?
fn next(&mut self) -> Option<Self::Item> {
self.last = match self.inner_next() {
Some(c) => c,
None => return None,
};
Some(self.last.clone())
}
}
pub fn lex(input: &str) -> TokenIterator {
TokenIterator { char_stream: input.chars().peekable() }
TokenIterator { last: Token::LexErr(LexError::Nothing), char_stream: input.chars().peekable() }
}
fn get_precedence(token: &Token) -> i32 {
@ -590,6 +719,20 @@ fn parse_primary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, Pa
}
}
fn parse_unary<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
let tok = match input.peek() {
Some(tok) => tok.clone(),
None => return Err(ParseError::InputPastEndOfFile),
};
match tok {
Token::UnaryMinus => { input.next(); Ok(Expr::FnCall("-".to_string(), vec![parse_primary(input)?])) }
Token::UnaryPlus => { input.next(); parse_primary(input) }
Token::Bang => { input.next(); Ok(Expr::FnCall("!".to_string(), vec![parse_primary(input)?])) }
_ => parse_primary(input)
}
}
fn parse_binop<'a>(input: &mut Peekable<TokenIterator<'a>>,
prec: i32,
lhs: Expr)
@ -608,7 +751,7 @@ fn parse_binop<'a>(input: &mut Peekable<TokenIterator<'a>>,
}
if let Some(op_token) = input.next() {
let mut rhs = try!(parse_primary(input));
let mut rhs = try!(parse_unary(input));
let mut next_prec = -1;
@ -663,7 +806,7 @@ fn parse_binop<'a>(input: &mut Peekable<TokenIterator<'a>>,
}
fn parse_expr<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Expr, ParseError> {
let lhs = try!(parse_primary(input));
let lhs = try!(parse_unary(input));
parse_binop(input, 0, lhs)
}
@ -693,6 +836,14 @@ fn parse_while<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Pars
Ok(Stmt::While(Box::new(guard), Box::new(body)))
}
fn parse_loop<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
input.next();
let body = try!(parse_block(input));
Ok(Stmt::Loop(Box::new(body)))
}
fn parse_var<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, ParseError> {
input.next();
@ -756,6 +907,7 @@ fn parse_stmt<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Parse
match input.peek() {
Some(&Token::If) => parse_if(input),
Some(&Token::While) => parse_while(input),
Some(&Token::Loop) => parse_loop(input),
Some(&Token::Break) => {
input.next();
Ok(Stmt::Break)

View File

@ -402,6 +402,62 @@ fn test_array_with_structs() {
}
}
#[test]
fn test_comments() {
let mut engine = Engine::new();
assert!(engine.eval::<i64>("let x = 5; x // I am a single line comment, yay!").is_ok());
assert!(engine.eval::<i64>("let /* I am a multiline comment, yay! */ x = 5; x").is_ok());
}
#[test]
fn test_unary_minus() {
let mut engine = Engine::new();
assert_eq!(engine.eval::<i64>("let x = -5; x").unwrap(), -5);
assert_eq!(engine.eval::<i64>("fn n(x) { -x } n(5)").unwrap(), -5);
assert_eq!(engine.eval::<i64>("5 - -(-5)").unwrap(), 0);
}
#[test]
fn test_not() {
let mut engine = Engine::new();
assert_eq!(engine.eval::<bool>("let not_true = !true; not_true").unwrap(), false);
assert_eq!(engine.eval::<bool>("fn not(x) { !x } not(false)").unwrap(), true);
// TODO - do we allow stacking unary operators directly? e.g '!!!!!!!true'
assert_eq!(engine.eval::<bool>("!(!(!(!(true))))").unwrap(), true)
}
#[test]
fn test_loop() {
let mut engine = Engine::new();
assert!(
engine.eval::<bool>("
let x = 0;
let i = 0;
loop {
if i < 10 {
x = x + i;
i = i + 1;
}
else {
break;
}
}
x == 45
").unwrap()
)
}
#[test]
fn test_increment() {
let mut engine = Engine::new();
@ -434,4 +490,4 @@ fn test_decrement() {
} else {
assert!(true);
}
}
}