diff --git a/README.md b/README.md index 136ec458..e6279422 100644 --- a/README.md +++ b/README.md @@ -322,8 +322,6 @@ Use `Engine::new_raw` to create a _raw_ `Engine`, in which: let mut engine = Engine::new_raw(); // create a 'raw' Engine engine.register_stdlib(); // register the standard library manually - -engine. ``` Evaluate expressions only @@ -1149,16 +1147,19 @@ record == "Bob X. Davis: age 42 ❤\n"; The following standard methods (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: -| Function | Parameter(s) | Description | -| ---------- | ------------------------------------- | -------------------------------------------------------------------- | -| `len` | _none_ | returns the number of characters (not number of bytes) in the string | -| `pad` | character to pad, target length | pads the string with an character to a specified length | -| `append` | character/string to append | Adds a character or a string to the end of another string | -| `clear` | _none_ | empties the string | -| `truncate` | target length | cuts off the string at exactly a specified number of characters | -| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | -| `replace` | target sub-string, replacement string | replaces a substring with another | -| `trim` | _none_ | trims the string of whitespace at the beginning and end | +| Function | Parameter(s) | Description | +| ------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `len` | _none_ | returns the number of characters (not number of bytes) in the string | +| `pad` | character to pad, target length | pads the string with an character to at least a specified length | +| `append` | character/string to append | Adds a character or a string to the end of another string | +| `clear` | _none_ | empties the string | +| `truncate` | target length | cuts off the string at exactly a specified number of characters | +| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | +| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | +| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | +| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | +| `replace` | target sub-string, replacement string | replaces a sub-string with another | +| `trim` | _none_ | trims the string of whitespace at the beginning and end | ### Examples @@ -1174,17 +1175,30 @@ full_name.pad(15, '$'); full_name.len() == 15; full_name == "Bob C. Davis$$$"; +let n = full_name.index_of('$'); +n == 12; + +full_name.index_of("$$", n + 1) == 13; + +full_name.sub_string(n, 3) == "$$$"; + full_name.truncate(6); full_name.len() == 6; full_name == "Bob C."; full_name.replace("Bob", "John"); full_name.len() == 7; -full_name = "John C."; +full_name == "John C."; full_name.contains('C') == true; full_name.contains("John") == true; +full_name.crop(5); +full_name == "C."; + +full_name.crop(0, 1); +full_name == "C"; + full_name.clear(); full_name.len() == 0; ``` @@ -1218,7 +1232,7 @@ The following methods (defined in the standard library but excluded if using a [ | `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | | `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | | `len` | _none_ | returns the number of elements | -| `pad` | element to pad, target length | pads the array with an element until a specified length | +| `pad` | element to pad, target length | pads the array with an element to at least a specified length | | `clear` | _none_ | empties the array | | `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | diff --git a/benches/eval_expression.rs b/benches/eval_expression.rs new file mode 100644 index 00000000..131d3bb5 --- /dev/null +++ b/benches/eval_expression.rs @@ -0,0 +1,43 @@ +#![feature(test)] + +///! Test evaluating expressions +extern crate test; + +use rhai::{Engine, OptimizationLevel}; +use test::Bencher; + +#[bench] +fn bench_eval_expression_single(bench: &mut Bencher) { + let script = "1"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast(&ast).unwrap()); +} + +#[bench] +fn bench_eval_expression_number_literal(bench: &mut Bencher) { + let script = "2 > 1"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast(&ast).unwrap()); +} + +#[bench] +fn bench_eval_expression_number_operators(bench: &mut Bencher) { + let script = "2 + 2 == 4"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast(&ast).unwrap()); +} diff --git a/benches/eval_scope.rs b/benches/eval_scope.rs new file mode 100644 index 00000000..5d38f7d7 --- /dev/null +++ b/benches/eval_scope.rs @@ -0,0 +1,77 @@ +#![feature(test)] + +///! Test evaluating with scope +extern crate test; + +use rhai::{Engine, OptimizationLevel, Scope, INT}; +use test::Bencher; + +#[bench] +fn bench_eval_scope_single(bench: &mut Bencher) { + let script = "requests_made == requests_made"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let mut scope = Scope::new(); + scope.push("requests_made", 99 as INT); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_eval_scope_multiple(bench: &mut Bencher) { + let script = "requests_made > requests_succeeded"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let mut scope = Scope::new(); + scope.push("requests_made", 99 as INT); + scope.push("requests_succeeded", 90 as INT); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_eval_scope_longer(bench: &mut Bencher) { + let script = "(requests_made * requests_succeeded / 100) >= 90"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let mut scope = Scope::new(); + scope.push("requests_made", 99 as INT); + scope.push("requests_succeeded", 90 as INT); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_eval_scope_complex(bench: &mut Bencher) { + let script = r#" + 2 > 1 && + "something" != "nothing" || + "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && + Variable_name_with_spaces <= variableName && + modifierTest + 1000 / 2 > (80 * 100 % 2) + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let mut scope = Scope::new(); + scope.push("Variable_name_with_spaces", 99 as INT); + scope.push("variableName", 90 as INT); + scope.push("modifierTest", 5 as INT); + + let ast = engine.compile_expression(script).unwrap(); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} diff --git a/benches/eval_type.rs b/benches/eval_type.rs new file mode 100644 index 00000000..1e5a70c8 --- /dev/null +++ b/benches/eval_type.rs @@ -0,0 +1,100 @@ +#![feature(test)] + +///! Test evaluating expressions +extern crate test; + +use rhai::{Engine, OptimizationLevel, RegisterFn, Scope, INT}; +use test::Bencher; + +#[derive(Debug, Clone)] +struct Test { + x: INT, +} + +impl Test { + pub fn get_x(&mut self) -> INT { + self.x + } + pub fn action(&mut self) { + self.x = 0; + } + pub fn update(&mut self, val: INT) { + self.x = val; + } + pub fn get_nest(&mut self) -> Test { + Test { x: 9 } + } +} + +#[bench] +fn bench_type_field(bench: &mut Bencher) { + let script = "foo.field"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + engine.register_type_with_name::("Test"); + engine.register_get("field", Test::get_x); + + let ast = engine.compile_expression(script).unwrap(); + + let mut scope = Scope::new(); + scope.push("foo", Test { x: 42 }); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_type_method(bench: &mut Bencher) { + let script = "foo.action()"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + engine.register_type_with_name::("Test"); + engine.register_fn("action", Test::action); + + let ast = engine.compile_expression(script).unwrap(); + + let mut scope = Scope::new(); + scope.push("foo", Test { x: 42 }); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_type_method_with_params(bench: &mut Bencher) { + let script = "foo.update(1)"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + engine.register_type_with_name::("Test"); + engine.register_fn("update", Test::update); + + let ast = engine.compile_expression(script).unwrap(); + + let mut scope = Scope::new(); + scope.push("foo", Test { x: 42 }); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} + +#[bench] +fn bench_type_method_nested(bench: &mut Bencher) { + let script = "foo.nest.field"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + engine.register_type_with_name::("Test"); + engine.register_get("field", Test::get_x); + engine.register_get("nest", Test::get_nest); + + let ast = engine.compile_expression(script).unwrap(); + + let mut scope = Scope::new(); + scope.push("foo", Test { x: 42 }); + + bench.iter(|| engine.consume_ast_with_scope(&mut scope, &ast).unwrap()); +} diff --git a/benches/iterations.rs b/benches/iterations.rs new file mode 100644 index 00000000..31da75f3 --- /dev/null +++ b/benches/iterations.rs @@ -0,0 +1,25 @@ +#![feature(test)] + +///! Test 1,000 iterations +extern crate test; + +use rhai::{Engine, OptimizationLevel}; +use test::Bencher; + +#[bench] +fn bench_iterations_1000(bench: &mut Bencher) { + let script = r#" + let x = 1_000; + + while x > 0 { + x = x - 1; + } + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let ast = engine.compile(script).unwrap(); + + bench.iter(|| engine.consume_ast(&ast).unwrap()); +} diff --git a/benches/parsing.rs b/benches/parsing.rs new file mode 100644 index 00000000..280b9f45 --- /dev/null +++ b/benches/parsing.rs @@ -0,0 +1,83 @@ +#![feature(test)] + +///! Test parsing expressions +extern crate test; + +use rhai::{Engine, OptimizationLevel}; +use test::Bencher; + +#[bench] +fn bench_parse_single(bench: &mut Bencher) { + let script = "1"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + bench.iter(|| engine.compile_expression(script).unwrap()); +} + +#[bench] +fn bench_parse_simple(bench: &mut Bencher) { + let script = "(requests_made * requests_succeeded / 100) >= 90"; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + bench.iter(|| engine.compile_expression(script).unwrap()); +} + +#[bench] +fn bench_parse_full(bench: &mut Bencher) { + let script = r#" + 2 > 1 && + "something" != "nothing" || + "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && + [array, with, spaces].len() <= #{prop:name}.len() && + modifierTest + 1000 / 2 > (80 * 100 % 2) + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + bench.iter(|| engine.compile_expression(script).unwrap()); +} + +#[bench] +fn bench_parse_primes(bench: &mut Bencher) { + let script = r#" + // This script uses the Sieve of Eratosthenes to calculate prime numbers. + + let now = timestamp(); + + const MAX_NUMBER_TO_CHECK = 10_000; // 1229 primes <= 10000 + + let prime_mask = []; + prime_mask.pad(MAX_NUMBER_TO_CHECK, true); + + prime_mask[0] = prime_mask[1] = false; + + let total_primes_found = 0; + + for p in range(2, MAX_NUMBER_TO_CHECK) { + if prime_mask[p] { + print(p); + + total_primes_found += 1; + let i = 2 * p; + + while i < MAX_NUMBER_TO_CHECK { + prime_mask[i] = false; + i += p; + } + } + } + + print("Total " + total_primes_found + " primes."); + print("Run time = " + now.elapsed() + " seconds."); + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + bench.iter(|| engine.compile(script).unwrap()); +} diff --git a/src/builtin.rs b/src/builtin.rs index 256361a0..77abf74c 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -1024,17 +1024,113 @@ impl Engine<'_> { } // Register string utility functions + fn sub_string(s: &mut String, start: INT, len: INT) -> String { + let offset = if s.is_empty() || len <= 0 { + return "".to_string(); + } else if start < 0 { + 0 + } else if (start as usize) >= s.chars().count() { + return "".to_string(); + } else { + start as usize + }; + + let chars: Vec<_> = s.chars().collect(); + + let len = if offset + (len as usize) > chars.len() { + chars.len() - offset + } else { + len as usize + }; + + chars[offset..][..len].into_iter().collect::() + } + + fn crop_string(s: &mut String, start: INT, len: INT) { + let offset = if s.is_empty() || len <= 0 { + s.clear(); + return; + } else if start < 0 { + 0 + } else if (start as usize) >= s.chars().count() { + s.clear(); + return; + } else { + start as usize + }; + + let chars: Vec<_> = s.chars().collect(); + + let len = if offset + (len as usize) > chars.len() { + chars.len() - offset + } else { + len as usize + }; + + s.clear(); + + chars[offset..][..len] + .into_iter() + .for_each(|&ch| s.push(ch)); + } + self.register_fn("len", |s: &mut String| s.chars().count() as INT); self.register_fn("contains", |s: &mut String, ch: char| s.contains(ch)); self.register_fn("contains", |s: &mut String, find: String| s.contains(&find)); + self.register_fn("index_of", |s: &mut String, ch: char, start: INT| { + let start = if start < 0 { + 0 + } else if (start as usize) >= s.chars().count() { + return -1 as INT; + } else { + s.chars().take(start as usize).collect::().len() + }; + + s[start..] + .find(ch) + .map(|index| s[0..start + index].chars().count() as INT) + .unwrap_or(-1 as INT) + }); + self.register_fn("index_of", |s: &mut String, ch: char| { + s.find(ch) + .map(|index| s[0..index].chars().count() as INT) + .unwrap_or(-1 as INT) + }); + self.register_fn("index_of", |s: &mut String, find: String, start: INT| { + let start = if start < 0 { + 0 + } else if (start as usize) >= s.chars().count() { + return -1 as INT; + } else { + s.chars().take(start as usize).collect::().len() + }; + + s[start..] + .find(&find) + .map(|index| s[0..start + index].chars().count() as INT) + .unwrap_or(-1 as INT) + }); + self.register_fn("index_of", |s: &mut String, find: String| { + s.find(&find) + .map(|index| s[0..index].chars().count() as INT) + .unwrap_or(-1 as INT) + }); self.register_fn("clear", |s: &mut String| s.clear()); self.register_fn("append", |s: &mut String, ch: char| s.push(ch)); self.register_fn("append", |s: &mut String, add: String| s.push_str(&add)); + self.register_fn("sub_string", sub_string); + self.register_fn("sub_string", |s: &mut String, start: INT| { + sub_string(s, start, s.len() as INT) + }); + self.register_fn("crop", crop_string); + self.register_fn("crop", |s: &mut String, start: INT| { + crop_string(s, start, s.len() as INT) + }); self.register_fn("truncate", |s: &mut String, len: INT| { if len >= 0 { let chars: Vec<_> = s.chars().take(len as usize).collect(); s.clear(); - chars.iter().for_each(|&ch| s.push(ch)); + chars.into_iter().for_each(|ch| s.push(ch)); } else { s.clear(); } diff --git a/tests/string.rs b/tests/string.rs index 7454dcf2..1ec77e2e 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, INT}; #[test] fn test_string() -> Result<(), EvalAltResult> { @@ -33,3 +33,110 @@ fn test_string() -> Result<(), EvalAltResult> { Ok(()) } + +#[cfg(not(feature = "no_stdlib"))] +#[cfg(not(feature = "no_object"))] +#[test] +fn test_string_substring() -> Result<(), EvalAltResult> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(-1, 2)"# + )?, + "❤❤" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, 5)"# + )?, + "❤❤ he" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1)"# + )?, + "❤❤ hello! ❤❤❤" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(99)"# + )?, + "" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, -1)"# + )?, + "" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.sub_string(1, 999)"# + )?, + "❤❤ hello! ❤❤❤" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(1, -1); x"# + )?, + "" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(4, 6); x"# + )?, + "hello!" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.crop(1, 999); x"# + )?, + "❤❤ hello! ❤❤❤" + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764')"# + )?, + 0 + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', 5)"# + )?, + 11 + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', -1)"# + )?, + 0 + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('\u2764', 999)"# + )?, + -1 + ); + + assert_eq!( + engine.eval::( + r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.index_of('x')"# + )?, + -1 + ); + + Ok(()) +}