diff --git a/Cargo.toml b/Cargo.toml index 8b7ccaca..3db83960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 113e178c..a02a84fa 100644 --- a/README.md +++ b/README.md @@ -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: + /*/*/*/*/**/*/*/*/*/ +*/ +``` diff --git a/TODO b/TODO index 513f5173..b5b4fdf5 100644 --- a/TODO +++ b/TODO @@ -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: diff --git a/scripts/comments.rhai b/scripts/comments.rhai new file mode 100644 index 00000000..50f15278 --- /dev/null +++ b/scripts/comments.rhai @@ -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 diff --git a/scripts/loop.rhai b/scripts/loop.rhai new file mode 100644 index 00000000..91b7a5d8 --- /dev/null +++ b/scripts/loop.rhai @@ -0,0 +1,8 @@ +let x = 10; + +// simulate do..while using loop +loop { + print(x); + x = x - 1; + if x <= 0 { break; } +} diff --git a/src/engine.rs b/src/engine.rs index 1ca8fffa..51789068 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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::{PartialOrd, PartialEq}; #[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::("40 + 2") { +/// println!("Answer: {}", result); // prints 42 +/// } +/// } +/// ``` pub struct Engine { pub fns: HashMap>, } +/// 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::(&mut my_scope, "x + 1").unwrap(), 6); +/// ``` +/// +/// Between runs, `Engine` only remembers functions when not using own `Scope`. pub type Scope = Vec<(String, Box)>; 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>, arg2: Option<&mut Box>, @@ -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(&mut self) { fn clone_helper(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(&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(&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(&mut self, name: &str, get_fn: F, @@ -1154,6 +1189,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) => { @@ -1175,6 +1223,7 @@ impl Engine { } } + /// Evaluate a file pub fn eval_file(&mut self, fname: &str) -> Result { use std::fs::File; use std::io::prelude::*; @@ -1192,12 +1241,14 @@ impl Engine { } } + /// Evaluate a string pub fn eval(&mut self, input: &str) -> Result { let mut scope: Scope = Vec::new(); self.eval_with_scope(&mut scope, input) } + /// Evaluate with own scope pub fn eval_with_scope(&mut self, scope: &mut Scope, input: &str) @@ -1242,6 +1293,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::*; @@ -1261,6 +1315,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(); @@ -1269,6 +1326,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); @@ -1299,6 +1359,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::(); engine.register_type::(); @@ -1318,6 +1380,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 ),*) => ( $( @@ -1330,6 +1400,7 @@ impl Engine { fn sub(x: T, y: T) -> ::Output { x - y } fn mul(x: T, y: T) -> ::Output { x * y } fn div(x: T, y: T) -> ::Output { x / y } + fn neg(x: T) -> ::Output { -x } fn lt(x: T, y: T) -> bool { x < y } fn lte(x: T, y: T) -> bool { x <= y } fn gt(x: T, y: T) -> bool { x > y } @@ -1338,6 +1409,7 @@ impl Engine { fn ne(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); @@ -1355,6 +1427,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); @@ -1363,6 +1438,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() }; diff --git a/src/lib.rs b/src/lib.rs index 0b9cc5ca..d97fea0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::("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; + diff --git a/src/parser.rs b/src/parser.rs index 07eb1145..a713ddc3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, Box), IfElse(Box, Box, Box), While(Box, Box), + Loop(Box), Var(String, Option>), Block(Vec), Expr(Box), @@ -112,7 +111,7 @@ pub enum Expr { False, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Token { IntConst(i64), FloatConst(f64), @@ -126,7 +125,9 @@ pub enum Token { LSquare, RSquare, Plus, + UnaryPlus, Minus, + UnaryMinus, Multiply, Divide, Semicolon, @@ -140,6 +141,7 @@ pub enum Token { If, Else, While, + Loop, LessThan, GreaterThan, Bang, @@ -154,10 +156,98 @@ pub enum Token { Fn, Break, Return, + PlusEquals, + MinusEquals, 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>, } @@ -264,12 +354,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 { + fn inner_next(&mut self) -> Option { while let Some(c) = self.char_stream.next() { match c { '0'...'9' => { @@ -330,6 +416,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), @@ -366,10 +453,57 @@ impl<'a> Iterator for TokenIterator<'a> { ')' => return Some(Token::RParen), '[' => return Some(Token::LSquare), ']' => return Some(Token::RSquare), - '+' => return Some(Token::Plus), - '-' => return Some(Token::Minus), + '+' => { + return match self.char_stream.peek() { + Some(&'=') => { + self.char_stream.next(); + Some(Token::PlusEquals) + }, + _ if self.last.is_next_unary() => Some(Token::UnaryPlus), + _ => Some(Token::Plus), + } + }, + '-' => { + return match self.char_stream.peek() { + Some(&'=') => { + self.char_stream.next(); + Some(Token::MinusEquals) + }, + _ 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), @@ -437,13 +571,28 @@ 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.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 { match *token { - Token::Equals => 10, + Token::Equals + | Token::PlusEquals + | Token::MinusEquals => 10, Token::Or => 11, Token::And => 12, Token::LessThan @@ -587,6 +736,20 @@ fn parse_primary<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { + 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>, prec: i32, lhs: Expr) @@ -605,7 +768,7 @@ fn parse_binop<'a>(input: &mut Peekable>, } 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; @@ -626,6 +789,20 @@ fn parse_binop<'a>(input: &mut Peekable>, Token::Multiply => Expr::FnCall("*".to_string(), vec![lhs_curr, rhs]), Token::Divide => Expr::FnCall("/".to_string(), vec![lhs_curr, rhs]), Token::Equals => Expr::Assignment(Box::new(lhs_curr), Box::new(rhs)), + Token::PlusEquals => { + let lhs_copy = lhs_curr.clone(); + Expr::Assignment( + Box::new(lhs_curr), + Box::new(Expr::FnCall("+".to_string(), vec![lhs_copy, rhs])) + ) + }, + Token::MinusEquals => { + let lhs_copy = lhs_curr.clone(); + Expr::Assignment( + Box::new(lhs_curr), + Box::new(Expr::FnCall("-".to_string(), vec![lhs_copy, rhs])) + ) + }, Token::Period => Expr::Dot(Box::new(lhs_curr), Box::new(rhs)), Token::EqualTo => Expr::FnCall("==".to_string(), vec![lhs_curr, rhs]), Token::NotEqualTo => Expr::FnCall("!=".to_string(), vec![lhs_curr, rhs]), @@ -646,7 +823,7 @@ fn parse_binop<'a>(input: &mut Peekable>, } fn parse_expr<'a>(input: &mut Peekable>) -> Result { - let lhs = try!(parse_primary(input)); + let lhs = try!(parse_unary(input)); parse_binop(input, 0, lhs) } @@ -676,6 +853,14 @@ fn parse_while<'a>(input: &mut Peekable>) -> Result(input: &mut Peekable>) -> Result { + input.next(); + + let body = try!(parse_block(input)); + + Ok(Stmt::Loop(Box::new(body))) +} + fn parse_var<'a>(input: &mut Peekable>) -> Result { input.next(); @@ -739,6 +924,7 @@ fn parse_stmt<'a>(input: &mut Peekable>) -> Result parse_if(input), Some(&Token::While) => parse_while(input), + Some(&Token::Loop) => parse_loop(input), Some(&Token::Break) => { input.next(); Ok(Stmt::Break) diff --git a/src/tests.rs b/src/tests.rs index 5f920537..a812d663 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -469,4 +469,94 @@ fn struct_with_float() { } else { assert!(false); } -} \ No newline at end of file +} + +#[test] +fn test_comments() { + let mut engine = Engine::new(); + + assert!(engine.eval::("let x = 5; x // I am a single line comment, yay!").is_ok()); + + assert!(engine.eval::("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::("let x = -5; x").unwrap(), -5); + + assert_eq!(engine.eval::("fn n(x) { -x } n(5)").unwrap(), -5); + + assert_eq!(engine.eval::("5 - -(-5)").unwrap(), 0); +} + +#[test] +fn test_not() { + let mut engine = Engine::new(); + + assert_eq!(engine.eval::("let not_true = !true; not_true").unwrap(), false); + + assert_eq!(engine.eval::("fn not(x) { !x } not(false)").unwrap(), true); + + // TODO - do we allow stacking unary operators directly? e.g '!!!!!!!true' + assert_eq!(engine.eval::("!(!(!(!(true))))").unwrap(), true) +} + +#[test] +fn test_loop() { + let mut engine = Engine::new(); + + assert!( + engine.eval::(" + 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(); + + if let Ok(result) = engine.eval::("let x = 1; x += 2; x") { + assert_eq!(result, 3); + } else { + assert!(false); + } + + if let Ok(result) = engine.eval::("let s = \"test\"; s += \"ing\"; s") { + assert_eq!(result, "testing".to_owned()); + } else { + assert!(false); + } +} + +#[test] +fn test_decrement() { + let mut engine = Engine::new(); + + if let Ok(result) = engine.eval::("let x = 10; x -= 7; x") { + assert_eq!(result, 3); + } else { + assert!(false); + } + + if let Ok(_) = engine.eval::("let s = \"test\"; s -= \"ing\"; s") { + assert!(false); + } else { + assert!(true); + } +}