Merge pull request #456 from schungx/master

Bug fixes.
This commit is contained in:
Stephen Chung 2021-09-28 17:22:03 +08:00 committed by GitHub
commit c977140ecb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 81 deletions

View File

@ -19,6 +19,7 @@ Enhancements
* `Engine::consume_XXX` methods are renamed to `Engine::run_XXX` to make meanings clearer. The `consume_XXX` API is deprecated.
* `Engine::register_type_XXX` are now available even under `no_object`.
* Added `Engine::on_parse_token` to allow remapping certain tokens during parsing.
* Added `Engine::const_empty_string` to merge empty strings into a single instance.
### Custom Syntax
@ -42,6 +43,7 @@ Enhancements
* `SmartString` now uses `LazyCompact` instead of `Compact` to minimize allocations.
* Added `pop` for strings.
* Added `ImmutableString::ptr_eq` to test if two strings point to the same allocation.
### `Scope` API
@ -57,9 +59,22 @@ Enhancements
* `StaticVec` is changed to keep three items inline instead of four.
Version 1.0.7
=============
Version 1.0.6
=============
Bug fixes
---------
* Eliminate unnecessary property write-back when accessed via a getter since property getters are assumed to be _pure_.
* Writing to a property of an indexed valued obtained via an indexer now works properly by writing back the changed value via an index setter.
Enhancements
------------
* `MultiInputsStream`, `ParseState`, `TokenIterator`, `IdentifierBuilder` and `AccessMode` are exported under the `internals` feature.

View File

@ -84,6 +84,7 @@ Below is the standard _Fibonacci_ example for scripting languages:
const TARGET = 28;
const REPEAT = 5;
const ANSWER = 317_811;
fn fib(n) {
if n < 2 {
@ -107,8 +108,8 @@ print(`Finished. Run time = ${now.elapsed} seconds.`);
print(`Fibonacci number #${TARGET} = ${result}`);
if result != 317_811 {
print("The answer is WRONG! Should be 317,811!");
if result != ANSWER {
print(`The answer is WRONG! Should be ${ANSWER}!`);
}
```

View File

@ -13,9 +13,9 @@ help: a struct with a similar name exists
| ~~~~~
help: consider importing one of these items
|
11 | use core::fmt::Pointer;
|
11 | use std::fmt::Pointer;
|
11 | use syn::__private::fmt::Pointer;
|
11 | use core::fmt::Pointer;
|

View File

@ -3,6 +3,7 @@
const TARGET = 28;
const REPEAT = 5;
const ANSWER = 317_811;
fn fib(n) {
if n < 2 {
@ -26,6 +27,6 @@ print(`Finished. Run time = ${now.elapsed} seconds.`);
print(`Fibonacci number #${TARGET} = ${result}`);
if result != 317_811 {
print("The answer is WRONG! Should be 317,811!");
if result != ANSWER {
print(`The answer is WRONG! Should be ${ANSWER}!`);
}

View File

@ -316,6 +316,24 @@ impl Engine {
///
/// All custom keywords used as symbols must be manually registered via [`Engine::register_custom_operator`].
/// Otherwise, they won't be recognized.
///
/// # Implementation Function Signature
///
/// The implementation function has the following signature:
///
/// > `Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>`
///
/// where:
/// * `symbols`: a slice of symbols that have been parsed so far, possibly containing `$expr$` and/or `$block$`;
/// `$ident$` and other literal markers are replaced by the actual text
/// * `look_ahead`: a string slice containing the next symbol that is about to be read
///
/// ## Return value
///
/// * `Ok(None)`: parsing complete and there are no more symbols to match.
/// * `Ok(Some(symbol))`: the next symbol to match, which can also be `$expr$`, `$ident$` or `$block$`.
/// * `Err(ParseError)`: error that is reflected back to the [`Engine`], normally `ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE)` to indicate a syntax error, but it can be any [`ParseError`].
///
pub fn register_custom_syntax_raw(
&mut self,
key: impl Into<Identifier>,

View File

@ -1877,7 +1877,7 @@ impl Dynamic {
_ => Err(self.type_name()),
}
}
/// Cast the [`Dynamic`] as an [`ImmutableString`] and return it.
/// Cast the [`Dynamic`] as an [`ImmutableString`] and return it as a string slice.
/// Returns the name of the actual type if the cast fails.
///
/// # Panics
@ -1888,7 +1888,7 @@ impl Dynamic {
match self.0 {
Union::Str(ref s, _, _) => Ok(s),
#[cfg(not(feature = "no_closure"))]
Union::Shared(_, _, _) => panic!("as_str() cannot be called on shared values"),
Union::Shared(_, _, _) => panic!("as_str_ref() cannot be called on shared values"),
_ => Err(self.type_name()),
}
}

View File

@ -3,7 +3,7 @@
use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, Stmt, AST_OPTION_FLAGS::*};
use crate::custom_syntax::CustomSyntax;
use crate::dynamic::{map_std_type_name, AccessMode, Union, Variant};
use crate::fn_hash::get_hasher;
use crate::fn_hash::{calc_fn_hash, get_hasher};
use crate::fn_native::{
CallableFunction, IteratorFn, OnDebugCallback, OnParseTokenCallback, OnPrintCallback,
OnVarCallback,
@ -336,6 +336,20 @@ impl ChainArgument {
_ => None,
}
}
/// Return the [position][Position].
#[inline(always)]
#[must_use]
#[allow(dead_code)]
pub const fn position(&self) -> Position {
match self {
#[cfg(not(feature = "no_object"))]
ChainArgument::Property(pos) => *pos,
#[cfg(not(feature = "no_object"))]
ChainArgument::MethodCallArgs(_, pos) => *pos,
#[cfg(not(feature = "no_index"))]
ChainArgument::IndexValue(_, pos) => *pos,
}
}
}
#[cfg(not(feature = "no_object"))]
@ -790,6 +804,31 @@ impl Default for Limits {
}
}
/// A type containing useful constants for the [`Engine`].
#[derive(Debug)]
pub struct GlobalConstants {
/// An empty [`ImmutableString`] for cloning purposes.
pub(crate) empty_string: ImmutableString,
/// Function call hash to FN_IDX_GET
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
pub(crate) fn_hash_idx_get: u64,
/// Function call hash to FN_IDX_SET
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
pub(crate) fn_hash_idx_set: u64,
}
impl Default for GlobalConstants {
fn default() -> Self {
Self {
empty_string: Default::default(),
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
fn_hash_idx_get: calc_fn_hash(FN_IDX_GET, 2),
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
fn_hash_idx_set: calc_fn_hash(FN_IDX_SET, 3),
}
}
}
/// Context of a script evaluation process.
#[derive(Debug)]
pub struct EvalContext<'a, 'x, 'px, 'm, 's, 'b, 't, 'pt> {
@ -911,8 +950,8 @@ pub struct Engine {
/// A map mapping type names to pretty-print names.
pub(crate) type_names: BTreeMap<Identifier, Box<Identifier>>,
/// An empty [`ImmutableString`] for cloning purposes.
pub(crate) empty_string: ImmutableString,
/// Useful constants
pub(crate) constants: GlobalConstants,
/// A set of symbols to disable.
pub(crate) disabled_symbols: BTreeSet<Identifier>,
@ -1042,7 +1081,7 @@ impl Engine {
module_resolver: None,
type_names: Default::default(),
empty_string: Default::default(),
constants: Default::default(),
disabled_symbols: Default::default(),
custom_keywords: Default::default(),
custom_syntax: Default::default(),
@ -1070,6 +1109,16 @@ impl Engine {
engine
}
/// Get an empty [`ImmutableString`].
///
/// [`Engine`] keeps a single instance of an empty [`ImmutableString`] and uses this to create
/// shared instances for subsequent uses. This minimizes unnecessary allocations for empty strings.
#[inline(always)]
#[must_use]
pub fn const_empty_string(&self) -> ImmutableString {
self.constants.empty_string.clone()
}
/// Search for a module within an imports stack.
#[inline]
#[must_use]
@ -1244,6 +1293,7 @@ impl Engine {
#[cfg(not(feature = "no_index"))]
ChainType::Indexing => {
let pos = rhs.position();
let root_pos = idx_val.position();
let idx_val = idx_val
.into_index_value()
.expect("`chain_type` is `ChainType::Index`");
@ -1253,17 +1303,50 @@ impl Engine {
Expr::Dot(x, term, x_pos) | Expr::Index(x, term, x_pos)
if !_terminate_chaining =>
{
let mut idx_val_for_setter = idx_val.clone();
let idx_pos = x.lhs.position();
let obj_ptr = &mut self.get_indexed_mut(
mods, state, lib, target, idx_val, idx_pos, false, true, level,
)?;
let rhs_chain = match_chaining_type(rhs);
self.eval_dot_index_chain_helper(
mods, state, lib, this_ptr, obj_ptr, root, &x.rhs, *term, idx_values,
rhs_chain, level, new_val,
)
.map_err(|err| err.fill_position(*x_pos))
let (try_setter, result) = {
let mut obj = self.get_indexed_mut(
mods, state, lib, target, idx_val, idx_pos, false, true, level,
)?;
let is_obj_temp_val = obj.is_temp_value();
let obj_ptr = &mut obj;
match self.eval_dot_index_chain_helper(
mods, state, lib, this_ptr, obj_ptr, root, &x.rhs, *term,
idx_values, rhs_chain, level, new_val,
) {
Ok((result, true)) if is_obj_temp_val => {
(Some(obj.take_or_clone()), (result, true))
}
Ok(result) => (None, result),
Err(err) => return Err(err.fill_position(*x_pos)),
}
};
if let Some(mut new_val) = try_setter {
// Try to call index setter if value is changed
let hash_set =
FnCallHashes::from_native(self.constants.fn_hash_idx_set);
let args = &mut [target, &mut idx_val_for_setter, &mut new_val];
if let Err(err) = self.exec_fn_call(
mods, state, lib, FN_IDX_SET, hash_set, args, is_ref_mut, true,
root_pos, None, level,
) {
// Just ignore if there is no index setter
if !matches!(*err, EvalAltResult::ErrorFunctionNotFound(_, _)) {
return Err(err);
}
}
}
self.check_data_size(target.as_ref())
.map_err(|err| err.fill_position(root.1))?;
Ok(result)
}
// xxx[rhs] op= new_val
_ if new_val.is_some() => {
@ -1294,13 +1377,12 @@ impl Engine {
if let Some(mut new_val) = try_setter {
// Try to call index setter
let hash_set =
FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_SET, 3));
FnCallHashes::from_native(self.constants.fn_hash_idx_set);
let args = &mut [target, &mut idx_val_for_setter, &mut new_val];
let pos = Position::NONE;
self.exec_fn_call(
mods, state, lib, FN_IDX_SET, hash_set, args, is_ref_mut, true,
pos, None, level,
root_pos, None, level,
)?;
}
@ -1427,7 +1509,7 @@ impl Engine {
EvalAltResult::ErrorDotExpr(_, _) => {
let args = &mut [target, &mut name.into(), &mut new_val];
let hash_set =
FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_SET, 3));
FnCallHashes::from_native(self.constants.fn_hash_idx_set);
let pos = Position::NONE;
self.exec_fn_call(
@ -1471,6 +1553,7 @@ impl Engine {
}
_ => Err(err),
},
// Assume getters are always pure
|(v, _)| Ok((v, false)),
)
}
@ -1524,7 +1607,9 @@ impl Engine {
let hash_set = FnCallHashes::from_native(*hash_set);
let mut arg_values = [target.as_mut(), &mut Default::default()];
let args = &mut arg_values[..1];
let (mut val, updated) = self
// Assume getters are always pure
let (mut val, _) = self
.exec_fn_call(
mods, state, lib, getter, hash_get, args, is_ref_mut, true,
*pos, None, level,
@ -1568,7 +1653,7 @@ impl Engine {
.map_err(|err| err.fill_position(*x_pos))?;
// Feed the value back via a setter just in case it has been updated
if updated || may_be_changed {
if may_be_changed {
// Re-use args because the first &mut parameter will not be consumed
let mut arg_values = [target.as_mut(), val];
let args = &mut arg_values;
@ -1583,7 +1668,7 @@ impl Engine {
let args =
&mut [target.as_mut(), &mut name.into(), val];
let hash_set = FnCallHashes::from_native(
crate::calc_fn_hash(FN_IDX_SET, 3),
self.constants.fn_hash_idx_set,
);
self.exec_fn_call(
mods, state, lib, FN_IDX_SET, hash_set, args,
@ -1987,7 +2072,7 @@ impl Engine {
_ if use_indexers => {
let args = &mut [target, &mut idx];
let hash_get = FnCallHashes::from_native(crate::calc_fn_hash(FN_IDX_GET, 2));
let hash_get = FnCallHashes::from_native(self.constants.fn_hash_idx_get);
let idx_pos = Position::NONE;
self.exec_fn_call(
@ -2059,7 +2144,7 @@ impl Engine {
// `... ${...} ...`
Expr::InterpolatedString(x, pos) => {
let mut pos = *pos;
let mut result: Dynamic = self.empty_string.clone().into();
let mut result: Dynamic = self.const_empty_string().into();
for expr in x.iter() {
let item = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?;
@ -2330,7 +2415,7 @@ impl Engine {
let args = &mut [lhs_ptr_inner, &mut new_val];
match self.call_native_fn(mods, state, lib, op, hash, args, true, true, op_pos) {
Err(err) if matches!(err.as_ref(), EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with(op)) =>
Err(err) if matches!(*err, EvalAltResult::ErrorFunctionNotFound(ref f, _) if f.starts_with(op)) =>
{
// Expand to `var = var op rhs`
let op = &op[..op.len() - 1]; // extract operator without =
@ -3006,17 +3091,23 @@ impl Engine {
}
/// Check a result to ensure that the data size is within allowable limit.
#[cfg(feature = "unchecked")]
#[inline(always)]
fn check_return_value(&self, result: RhaiResult) -> RhaiResult {
result
}
fn check_return_value(&self, mut result: RhaiResult) -> RhaiResult {
if let Ok(ref mut r) = result {
// Concentrate all empty strings into one instance to save memory
if let Dynamic(crate::dynamic::Union::Str(s, _, _)) = r {
if s.is_empty() {
if !s.ptr_eq(&self.constants.empty_string) {
*s = self.const_empty_string();
}
return result;
}
}
/// Check a result to ensure that the data size is within allowable limit.
#[cfg(not(feature = "unchecked"))]
#[inline(always)]
fn check_return_value(&self, result: RhaiResult) -> RhaiResult {
result.and_then(|r| self.check_data_size(&r).map(|_| r))
#[cfg(not(feature = "unchecked"))]
self.check_data_size(&r)?;
}
result
}
#[cfg(feature = "unchecked")]

View File

@ -1368,15 +1368,17 @@ impl Engine {
};
let (stream, tokenizer_control) = engine.lex_raw(
&scripts,
Some(if has_null {
&|token| match token {
// If `null` is present, make sure `null` is treated as a variable
Token::Reserved(s) if s == "null" => Token::Identifier(s),
_ => token,
}
if has_null {
Some(&|token, _, _| {
match token {
// If `null` is present, make sure `null` is treated as a variable
Token::Reserved(s) if s == "null" => Token::Identifier(s),
_ => token,
}
})
} else {
&|t| t
}),
None
},
);
let mut state = ParseState::new(engine, tokenizer_control);
let ast = engine.parse_global_expr(
@ -2078,12 +2080,28 @@ impl Engine {
}
/// Provide a callback that will be invoked before each variable access.
///
/// # Return Value of Callback
/// # Callback Function Signature
///
/// Return `Ok(None)` to continue with normal variable access.
/// Return `Ok(Some(Dynamic))` as the variable's value.
/// The callback function signature takes the following form:
///
/// # Errors in Callback
/// > `Fn(name: &str, index: usize, context: &EvalContext)`
/// > ` -> Result<Option<Dynamic>, Box<EvalAltResult>> + 'static`
///
/// where:
/// * `index`: an offset from the bottom of the current [`Scope`] that the variable is supposed
/// to reside. Offsets start from 1, with 1 meaning the last variable in the current
/// [`Scope`]. Essentially the correct variable is at position `scope.len() - index`.
/// If `index` is zero, then there is no pre-calculated offset position and a search through the
/// current [`Scope`] must be performed.
///
/// * `context`: the current [evaluation context][`EvalContext`].
///
/// ## Return value
///
/// * `Ok(None)`: continue with normal variable access.
/// * `Ok(Some(Dynamic))`: the variable's value.
///
/// ## Raising errors
///
/// Return `Err(...)` if there is an error.
///
@ -2121,6 +2139,22 @@ impl Engine {
/// _(internals)_ Provide a callback that will be invoked during parsing to remap certain tokens.
/// Exported under the `internals` feature only.
///
/// # Callback Function Signature
///
/// The callback function signature takes the following form:
///
/// > `Fn(token: Token, pos: Position, state: &TokenizeState) -> Token`
///
/// where:
/// * [`token`][crate::token::Token]: current token parsed
/// * [`pos`][`Position`]: location of the token
/// * [`state`][crate::token::TokenizeState]: current state of the tokenizer
///
/// ## Raising errors
///
/// It is possible to raise a parsing error by returning
/// [`Token::LexError`][crate::token::Token::LexError] as the mapped token.
///
/// # Example
///
/// ```
@ -2130,7 +2164,7 @@ impl Engine {
/// let mut engine = Engine::new();
///
/// // Register a token mapper.
/// engine.on_parse_token(|token| {
/// engine.on_parse_token(|token, _, _| {
/// match token {
/// // Convert all integer literals to strings
/// Token::IntegerConstant(n) => Token::StringConstant(n.to_string()),
@ -2153,8 +2187,12 @@ impl Engine {
#[inline(always)]
pub fn on_parse_token(
&mut self,
callback: impl Fn(crate::token::Token) -> crate::token::Token + SendSync + 'static,
callback: impl Fn(crate::token::Token, Position, &crate::token::TokenizeState) -> crate::token::Token
+ SendSync
+ 'static,
) -> &mut Self {
use std::string::ParseError;
self.token_mapper = Some(Box::new(callback));
self
}
@ -2162,6 +2200,17 @@ impl Engine {
///
/// Not available under `unchecked`.
///
/// # Callback Function Signature
///
/// The callback function signature takes the following form:
///
/// > `Fn(counter: u64) -> Option<Dynamic>`
///
/// ## Return value
///
/// * `None`: continue running the script.
/// * `Some(Dynamic)`: terminate the script with the specified exception value.
///
/// # Example
///
/// ```
@ -2234,6 +2283,17 @@ impl Engine {
}
/// Override default action of `debug` (print to stdout using [`println!`])
///
/// # Callback Function Signature
///
/// The callback function signature passed takes the following form:
///
/// > `Fn(text: &str, source: Option<&str>, pos: Position)`
///
/// where:
/// * `text`: the text to display
/// * `source`: current source, if any
/// * [`pos`][`Position`]: location of the `debug` call
///
/// # Example
///
/// ```

View File

@ -4,7 +4,7 @@ use crate::ast::{FnAccess, FnCallHashes};
use crate::engine::Imports;
use crate::fn_call::FnCallArgs;
use crate::plugin::PluginFunction;
use crate::token::Token;
use crate::token::{Token, TokenizeState};
use crate::{
calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, Module, Position, RhaiResult,
};
@ -310,10 +310,11 @@ pub type OnDebugCallback = Box<dyn Fn(&str, Option<&str>, Position) + Send + Syn
/// A standard callback function for mapping tokens during parsing.
#[cfg(not(feature = "sync"))]
pub type OnParseTokenCallback = dyn Fn(Token) -> Token;
pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token;
/// A standard callback function for mapping tokens during parsing.
#[cfg(feature = "sync")]
pub type OnParseTokenCallback = dyn Fn(Token) -> Token + Send + Sync + 'static;
pub type OnParseTokenCallback =
dyn Fn(Token, Position, &TokenizeState) -> Token + Send + Sync + 'static;
/// A standard callback function for variable access.
#[cfg(not(feature = "sync"))]

View File

@ -44,9 +44,7 @@ pub fn by_value<T: Variant + Clone>(data: &mut Dynamic) -> T {
if TypeId::of::<T>() == TypeId::of::<&str>() {
// If T is `&str`, data must be `ImmutableString`, so map directly to it
data.flatten_in_place();
let ref_str = data
.as_str_ref()
.expect("argument passed by value is not shared");
let ref_str = data.as_str_ref().expect("argument type is &str");
let ref_t = unsafe { mem::transmute::<_, &T>(&ref_str) };
ref_t.clone()
} else if TypeId::of::<T>() == TypeId::of::<String>() {

View File

@ -538,4 +538,27 @@ impl ImmutableString {
pub(crate) fn make_mut(&mut self) -> &mut SmartString {
shared_make_mut(&mut self.0)
}
/// Returns `true` if the two [`ImmutableString`]'s point to the same allocation.
///
/// # Example
///
/// ```
/// use rhai::ImmutableString;
///
/// let s1: ImmutableString = "hello".into();
/// let s2 = s1.clone();
/// let s3: ImmutableString = "hello".into();
///
/// assert_eq!(s1, s2);
/// assert_eq!(s1, s3);
/// assert_eq!(s2, s3);
///
/// assert!(s1.ptr_eq(&s2));
/// assert!(!s1.ptr_eq(&s3));
/// assert!(!s2.ptr_eq(&s3));
/// ```
#[inline(always)]
pub fn ptr_eq(&self, other: &Self) -> bool {
Shared::ptr_eq(&self.0, &other.0)
}
}

View File

@ -822,7 +822,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) {
// ``
Expr::InterpolatedString(x, pos) if x.is_empty() => {
state.set_dirty();
*expr = Expr::StringConstant(state.engine.empty_string.clone(), *pos);
*expr = Expr::StringConstant(state.engine.const_empty_string(), *pos);
}
// `...`
Expr::InterpolatedString(x, _) if x.len() == 1 && matches!(x[0], Expr::StringConstant(_, _)) => {

View File

@ -126,7 +126,7 @@ mod string_functions {
len: INT,
) -> ImmutableString {
if string.is_empty() || len <= 0 {
return ctx.engine().empty_string.clone();
return ctx.engine().const_empty_string();
}
let mut chars = StaticVec::<char>::with_capacity(len as usize);
@ -305,13 +305,13 @@ mod string_functions {
len: INT,
) -> ImmutableString {
if string.is_empty() {
return ctx.engine().empty_string.clone();
return ctx.engine().const_empty_string();
}
let mut chars = StaticVec::with_capacity(string.len());
let offset = if string.is_empty() || len <= 0 {
return ctx.engine().empty_string.clone();
return ctx.engine().const_empty_string();
} else if start < 0 {
if let Some(n) = start.checked_abs() {
chars.extend(string.chars());
@ -324,7 +324,7 @@ mod string_functions {
0
}
} else if start as usize >= string.chars().count() {
return ctx.engine().empty_string.clone();
return ctx.engine().const_empty_string();
} else {
start as usize
};
@ -354,7 +354,7 @@ mod string_functions {
start: INT,
) -> ImmutableString {
if string.is_empty() {
ctx.engine().empty_string.clone()
ctx.engine().const_empty_string()
} else {
let len = string.len() as INT;
sub_string(ctx, string, start, len)
@ -571,14 +571,14 @@ mod string_functions {
if let Some(n) = start.checked_abs() {
let num_chars = string.chars().count();
if n as usize > num_chars {
vec![ctx.engine().empty_string.clone().into(), string.into()]
vec![ctx.engine().const_empty_string().into(), string.into()]
} else {
let prefix: String = string.chars().take(num_chars - n as usize).collect();
let prefix_len = prefix.len();
vec![prefix.into(), string[prefix_len..].into()]
}
} else {
vec![ctx.engine().empty_string.clone().into(), string.into()]
vec![ctx.engine().const_empty_string().into(), string.into()]
}
} else {
let prefix: String = string.chars().take(start as usize).collect();

View File

@ -29,6 +29,10 @@ use rust_decimal::Decimal;
use crate::engine::KEYWORD_IS_DEF_FN;
/// _(internals)_ A type containing commands to control the tokenizer.
///
/// # Volatile Data Structure
///
/// This type is volatile and may change.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy, Default)]
pub struct TokenizerControlBlock {
/// Is the current tokenizer position within an interpolated text string?
@ -992,7 +996,7 @@ pub struct TokenizeState {
/// Maximum length of a string.
pub max_string_size: Option<NonZeroUsize>,
/// Can the next token be a unary operator?
pub non_unary: bool,
pub next_token_cannot_be_unary: bool,
/// Is the tokenizer currently inside a block comment?
pub comment_level: usize,
/// Include comments?
@ -1327,7 +1331,7 @@ pub fn get_next_token(
// Save the last token's state
if let Some((ref token, _)) = result {
state.non_unary = !token.is_next_unary();
state.next_token_cannot_be_unary = !token.is_next_unary();
}
result
@ -1678,10 +1682,12 @@ fn get_next_token_inner(
eat_next(stream, pos);
return Some((Token::Reserved("++".into()), start_pos));
}
('+', _) if !state.non_unary => return Some((Token::UnaryPlus, start_pos)),
('+', _) if !state.next_token_cannot_be_unary => {
return Some((Token::UnaryPlus, start_pos))
}
('+', _) => return Some((Token::Plus, start_pos)),
('-', '0'..='9') if !state.non_unary => negated = Some(start_pos),
('-', '0'..='9') if !state.next_token_cannot_be_unary => negated = Some(start_pos),
('-', '0'..='9') => return Some((Token::Minus, start_pos)),
('-', '=') => {
eat_next(stream, pos);
@ -1695,7 +1701,9 @@ fn get_next_token_inner(
eat_next(stream, pos);
return Some((Token::Reserved("--".into()), start_pos));
}
('-', _) if !state.non_unary => return Some((Token::UnaryMinus, start_pos)),
('-', _) if !state.next_token_cannot_be_unary => {
return Some((Token::UnaryMinus, start_pos))
}
('-', _) => return Some((Token::Minus, start_pos)),
('*', ')') => {
@ -2117,6 +2125,10 @@ impl InputStream for MultiInputsStream<'_> {
/// _(internals)_ An iterator on a [`Token`] stream.
/// Exported under the `internals` feature only.
///
/// # Volatile Data Structure
///
/// This type is volatile and may change.
pub struct TokenIterator<'a> {
/// Reference to the scripting `Engine`.
pub engine: &'a Engine,
@ -2224,7 +2236,7 @@ impl<'a> Iterator for TokenIterator<'a> {
// Run the mapper, if any
let token = if let Some(map_func) = self.token_mapper {
map_func(token)
map_func(token, pos, &self.state)
} else {
token
};
@ -2278,7 +2290,7 @@ impl Engine {
max_string_size: self.limits.max_string_size,
#[cfg(feature = "unchecked")]
max_string_size: None,
non_unary: false,
next_token_cannot_be_unary: false,
comment_level: 0,
include_comments: false,
is_within_text_terminated_by: None,

View File

@ -1,6 +1,6 @@
#![cfg(not(feature = "no_object"))]
use rhai::{Engine, EvalAltResult, INT};
use rhai::{Engine, EvalAltResult, Scope, INT};
#[test]
fn test_get_set() -> Result<(), Box<EvalAltResult>> {
@ -70,7 +70,7 @@ fn test_get_set() -> Result<(), Box<EvalAltResult>> {
}
#[test]
fn test_get_set_chain() -> Result<(), Box<EvalAltResult>> {
fn test_get_set_chain_with_write_back() -> Result<(), Box<EvalAltResult>> {
#[derive(Clone)]
struct TestChild {
x: INT,
@ -119,11 +119,22 @@ fn test_get_set_chain() -> Result<(), Box<EvalAltResult>> {
engine.register_get_set("x", TestChild::get_x, TestChild::set_x);
engine.register_get_set("child", TestParent::get_child, TestParent::set_child);
engine.register_fn("new_tp", TestParent::new);
#[cfg(not(feature = "no_index"))]
engine.register_indexer_get_set(
|parent: &mut TestParent, _: INT| parent.child.clone(),
|parent: &mut TestParent, n: INT, mut new_child: TestChild| {
new_child.x *= n;
parent.child = new_child;
},
);
engine.register_fn("new_tp", TestParent::new);
engine.register_fn("new_tc", TestChild::new);
assert_eq!(engine.eval::<INT>("let a = new_tp(); a.child.x")?, 1);
assert_eq!(
engine.eval::<INT>("let a = new_tp(); a.child.x = 500; a.child.x")?,
500
engine.eval::<INT>("let a = new_tp(); a.child.x = 42; a.child.x")?,
42
);
assert_eq!(
@ -131,6 +142,18 @@ fn test_get_set_chain() -> Result<(), Box<EvalAltResult>> {
"TestParent"
);
#[cfg(not(feature = "no_index"))]
assert_eq!(
engine.eval::<INT>("let a = new_tp(); let c = new_tc(); c.x = 123; a[2] = c; a.child.x")?,
246
);
#[cfg(not(feature = "no_index"))]
assert_eq!(
engine.eval::<INT>("let a = new_tp(); a[2].x = 42; a.child.x")?,
84
);
Ok(())
}
@ -166,3 +189,68 @@ fn test_get_set_op_assignment() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_get_set_chain_without_write_back() -> Result<(), Box<EvalAltResult>> {
#[derive(Debug, Clone)]
struct Outer {
pub inner: Inner,
}
#[derive(Debug, Clone)]
struct Inner {
pub value: INT,
}
let mut engine = Engine::new();
let mut scope = Scope::new();
scope.push(
"outer",
Outer {
inner: Inner { value: 42 },
},
);
engine
.register_type::<Inner>()
.register_get_set(
"value",
|t: &mut Inner| t.value,
|_: &mut Inner, new: INT| panic!("Inner::value setter called with {}", new),
)
.register_type::<Outer>()
.register_get_set(
"inner",
|t: &mut Outer| t.inner.clone(),
|_: &mut Outer, new: Inner| panic!("Outer::inner setter called with {:?}", new),
);
#[cfg(not(feature = "no_index"))]
engine.register_indexer_get_set(
|t: &mut Outer, n: INT| Inner {
value: t.inner.value * n,
},
|_: &mut Outer, n: INT, new: Inner| {
panic!("Outer::inner index setter called with {} and {:?}", n, new)
},
);
assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "outer.inner.value")?,
42
);
#[cfg(not(feature = "no_index"))]
assert_eq!(
engine.eval_with_scope::<INT>(&mut scope, "outer[2].value")?,
84
);
engine.run_with_scope(&mut scope, "print(outer.inner.value)")?;
#[cfg(not(feature = "no_index"))]
engine.run_with_scope(&mut scope, "print(outer[0].value)")?;
Ok(())
}