Indexer as fallback to property.

This commit is contained in:
Stephen Chung 2021-05-18 20:12:30 +08:00
parent e64dad4e9f
commit dc9b4d7f4d
9 changed files with 155 additions and 79 deletions

View File

@ -15,6 +15,8 @@ Breaking changes
* `Engine::disable_doc_comments` is removed because doc-comments are now placed under the `metadata` feature flag.
* Registering a custom syntax now only requires specifying whether the `Scope` is adjusted (i.e. whether variables are added or removed). There is no need to specify the number of variables added/removed.
* Assigning to a property of a constant is now allowed and no longer raise an `EvalAltResult::ErrorAssignmentToConstant` error. This is to facilitate the Singleton pattern. Registered setter functions are automatically guarded against setters calling on constants and will continue to raise errors unless the `pure` attribute is present (for plugins).
* If a property getter/setter is not found, an indexer with string index, if any, is tried.
* The indexers API (`Engine::register_indexer_XXX` and `Module::set_indexer_XXX`) are now also exposed under `no_index`.
New features
------------
@ -23,6 +25,7 @@ New features
* A new internal feature `no_smartstring` to turn off `SmartString` for those rare cases that it is needed.
* `DynamicReadLock` and `DynamicWriteLoc` are exposed under `internals`.
* `From<Shared<Locked<Dynamic>>>` is added for `Dynamic` mapping directly to a shared value, together with support for `Dynamic::from`.
* An indexer with string index acts as a _fallback_ to a property getter/setter.
Enhancements
------------

View File

@ -1715,7 +1715,13 @@ pub enum Expr {
)>,
),
/// Property access - ((getter, hash), (setter, hash), prop)
Property(Box<((Identifier, u64), (Identifier, u64), Ident)>),
Property(
Box<(
(Identifier, u64),
(Identifier, u64),
(ImmutableString, Position),
)>,
),
/// { [statement][Stmt] ... }
Stmt(Box<StmtBlock>),
/// func `(` expr `,` ... `)`
@ -1780,7 +1786,7 @@ impl fmt::Debug for Expr {
}
f.write_str(")")
}
Self::Property(x) => write!(f, "Property({})", x.2.name),
Self::Property(x) => write!(f, "Property({})", (x.2).0),
Self::Stmt(x) => {
f.write_str("Stmt")?;
f.debug_list().entries(x.0.iter()).finish()
@ -1896,7 +1902,7 @@ impl Expr {
Self::InterpolatedString(x) => x.first().unwrap().position(),
Self::Property(x) => (x.2).pos,
Self::Property(x) => (x.2).1,
Self::Stmt(x) => x.1,
Self::And(x, _) | Self::Or(x, _) | Self::Dot(x, _) | Self::Index(x, _) => {
@ -1931,7 +1937,7 @@ impl Expr {
x.first_mut().unwrap().set_position(new_pos);
}
Self::Property(x) => (x.2).pos = new_pos,
Self::Property(x) => (x.2).1 = new_pos,
Self::Stmt(x) => x.1 = new_pos,
}

View File

@ -224,9 +224,9 @@ pub const KEYWORD_GLOBAL: &str = "global";
pub const FN_GET: &str = "get$";
#[cfg(not(feature = "no_object"))]
pub const FN_SET: &str = "set$";
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
pub const FN_IDX_GET: &str = "index$get$";
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
pub const FN_IDX_SET: &str = "index$set$";
#[cfg(not(feature = "no_function"))]
pub const FN_ANONYMOUS: &str = "anon$";
@ -1256,7 +1256,7 @@ impl Engine {
}
// {xxx:map}.id op= ???
Expr::Property(x) if target.is::<Map>() && new_val.is_some() => {
let Ident { name, pos, .. } = &x.2;
let (name, pos) = &x.2;
let index = name.into();
let val = self.get_indexed_mut(
mods, state, lib, target, index, *pos, true, is_ref, false, level,
@ -1269,7 +1269,7 @@ impl Engine {
}
// {xxx:map}.id
Expr::Property(x) if target.is::<Map>() => {
let Ident { name, pos, .. } = &x.2;
let (name, pos) = &x.2;
let index = name.into();
let val = self.get_indexed_mut(
mods, state, lib, target, index, *pos, false, is_ref, false, level,
@ -1279,8 +1279,7 @@ impl Engine {
}
// xxx.id op= ???
Expr::Property(x) if new_val.is_some() => {
let ((getter, hash_get), (setter, hash_set), Ident { pos, .. }) =
x.as_ref();
let ((getter, hash_get), (setter, hash_set), (name, pos)) = x.as_ref();
let ((mut new_val, new_pos), (op_info, op_pos)) = new_val.unwrap();
if op_info.is_some() {
@ -1303,24 +1302,66 @@ impl Engine {
mods, state, lib, setter, hash, &mut args, is_ref, true, *pos, None,
level,
)
.map(|(v, _)| (v, true))
.or_else(|err| match *err {
// Try an indexer if property does not exist
EvalAltResult::ErrorDotExpr(_, _) => {
let mut prop = name.into();
let args = &mut [target, &mut prop, &mut new_val];
let hash_set = FnCallHashes::from_native(crate::calc_fn_hash(
std::iter::empty(),
FN_IDX_SET,
3,
));
self.exec_fn_call(
mods, state, lib, FN_IDX_SET, hash_set, args, is_ref, true,
*pos, None, level,
)
.map_err(
|idx_err| match *idx_err {
EvalAltResult::ErrorIndexingType(_, _) => err,
_ => idx_err,
},
)
}
_ => Err(err),
})
}
// xxx.id
Expr::Property(x) => {
let ((getter, hash_get), _, Ident { pos, .. }) = x.as_ref();
let ((getter, hash_get), _, (name, pos)) = x.as_ref();
let hash = FnCallHashes::from_native(*hash_get);
let mut args = [target.as_mut()];
self.exec_fn_call(
mods, state, lib, getter, hash, &mut args, is_ref, true, *pos, None,
level,
)
.map(|(v, _)| (v, false))
.map_or_else(
|err| match *err {
// Try an indexer if property does not exist
EvalAltResult::ErrorDotExpr(_, _) => {
let prop = name.into();
self.get_indexed_mut(
mods, state, lib, target, prop, *pos, false, is_ref, true,
level,
)
.map(|v| (v.take_or_clone(), false))
.map_err(|idx_err| {
match *idx_err {
EvalAltResult::ErrorIndexingType(_, _) => err,
_ => idx_err,
}
})
}
_ => Err(err),
},
|(v, _)| Ok((v, false)),
)
}
// {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr
Expr::Index(x, x_pos) | Expr::Dot(x, x_pos) if target.is::<Map>() => {
let mut val = match &x.lhs {
Expr::Property(p) => {
let Ident { name, pos, .. } = &p.2;
let (name, pos) = &p.2;
let index = name.into();
self.get_indexed_mut(
mods, state, lib, target, index, *pos, false, is_ref, true,
@ -1356,17 +1397,36 @@ impl Engine {
match &x.lhs {
// xxx.prop[expr] | xxx.prop.expr
Expr::Property(p) => {
let ((getter, hash_get), (setter, hash_set), Ident { pos, .. }) =
let ((getter, hash_get), (setter, hash_set), (name, pos)) =
p.as_ref();
let rhs_chain = rhs_chain.unwrap();
let hash_get = FnCallHashes::from_native(*hash_get);
let hash_set = FnCallHashes::from_native(*hash_set);
let arg_values = &mut [target.as_mut(), &mut Default::default()];
let mut arg_values = [target.as_mut(), &mut Default::default()];
let args = &mut arg_values[..1];
let (mut val, updated) = self.exec_fn_call(
mods, state, lib, getter, hash_get, args, is_ref, true, *pos,
None, level,
)?;
let (mut val, updated) = self
.exec_fn_call(
mods, state, lib, getter, hash_get, args, is_ref, true,
*pos, None, level,
)
.or_else(|err| match *err {
// Try an indexer if property does not exist
EvalAltResult::ErrorDotExpr(_, _) => {
let prop = name.into();
self.get_indexed_mut(
mods, state, lib, target, prop, *pos, false,
is_ref, true, level,
)
.map(|v| (v.take_or_clone(), false))
.map_err(
|idx_err| match *idx_err {
EvalAltResult::ErrorIndexingType(_, _) => err,
_ => idx_err,
},
)
}
_ => Err(err),
})?;
let val = &mut val;
@ -1389,17 +1449,36 @@ impl Engine {
// Feed the value back via a setter just in case it has been updated
if updated || may_be_changed {
// Re-use args because the first &mut parameter will not be consumed
arg_values[1] = val;
let mut arg_values = [target.as_mut(), val];
let args = &mut arg_values;
self.exec_fn_call(
mods, state, lib, setter, hash_set, arg_values, is_ref,
true, *pos, None, level,
mods, state, lib, setter, hash_set, args, is_ref, true,
*pos, None, level,
)
.or_else(
|err| match *err {
// If there is no setter, no need to feed it back because
// the property is read-only
// Try an indexer if property does not exist
EvalAltResult::ErrorDotExpr(_, _) => {
Ok((Dynamic::UNIT, false))
let mut prop = name.into();
let args = &mut [target.as_mut(), &mut prop, val];
let hash_set =
FnCallHashes::from_native(crate::calc_fn_hash(
std::iter::empty(),
FN_IDX_SET,
3,
));
self.exec_fn_call(
mods, state, lib, FN_IDX_SET, hash_set, args,
is_ref, true, *pos, None, level,
)
.or_else(|idx_err| match *idx_err {
EvalAltResult::ErrorIndexingType(_, _) => {
// If there is no setter, no need to feed it back because
// the property is read-only
Ok((Dynamic::UNIT, false))
}
_ => Err(idx_err),
})
}
_ => Err(err),
},
@ -1550,7 +1629,7 @@ impl Engine {
#[cfg(not(feature = "no_object"))]
Expr::Property(x) if _parent_chain_type == ChainType::Dot => {
idx_values.push(ChainArgument::Property(x.2.pos))
idx_values.push(ChainArgument::Property((x.2).1))
}
Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"),
@ -1561,7 +1640,7 @@ impl Engine {
let lhs_val = match lhs {
#[cfg(not(feature = "no_object"))]
Expr::Property(x) if _parent_chain_type == ChainType::Dot => {
ChainArgument::Property(x.2.pos)
ChainArgument::Property((x.2).1)
}
Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"),
@ -1748,7 +1827,6 @@ impl Engine {
#[cfg(not(feature = "no_index"))]
_ if _indexers => {
let type_name = target.type_name();
let args = &mut [target, &mut _idx];
let hash_get =
FnCallHashes::from_native(calc_fn_hash(std::iter::empty(), FN_IDX_GET, 2));
@ -1758,15 +1836,6 @@ impl Engine {
_level,
)
.map(|(v, _)| v.into())
.map_err(|err| match *err {
EvalAltResult::ErrorFunctionNotFound(fn_sig, _) if fn_sig.ends_with(']') => {
Box::new(EvalAltResult::ErrorIndexingType(
type_name.into(),
Position::NONE,
))
}
_ => err,
})
}
_ => EvalAltResult::ErrorIndexingType(

View File

@ -534,7 +534,7 @@ impl Engine {
///
/// The function signature must start with `&mut self` and not `&self`.
///
/// Not available under `no_index`.
/// Not available under both `no_index` and `no_object`.
///
/// # Panics
///
@ -574,12 +574,13 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn register_indexer_get<T: Variant + Clone, X: Variant + Clone, V: Variant + Clone>(
&mut self,
get_fn: impl Fn(&mut T, X) -> V + SendSync + 'static,
) -> &mut Self {
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -600,7 +601,7 @@ impl Engine {
///
/// The function signature must start with `&mut self` and not `&self`.
///
/// Not available under `no_index`.
/// Not available under both `no_index` and `no_object`.
///
/// # Panics
///
@ -642,7 +643,7 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn register_indexer_get_result<
T: Variant + Clone,
@ -652,6 +653,7 @@ impl Engine {
&mut self,
get_fn: impl Fn(&mut T, X) -> Result<V, Box<EvalAltResult>> + SendSync + 'static,
) -> &mut Self {
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -670,7 +672,7 @@ impl Engine {
}
/// Register an index setter for a custom type with the [`Engine`].
///
/// Not available under `no_index`.
/// Not available under both `no_index` and `no_object`.
///
/// # Panics
///
@ -712,12 +714,13 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn register_indexer_set<T: Variant + Clone, X: Variant + Clone, V: Variant + Clone>(
&mut self,
set_fn: impl Fn(&mut T, X, V) + SendSync + 'static,
) -> &mut Self {
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -736,7 +739,7 @@ impl Engine {
}
/// Register an index setter for a custom type with the [`Engine`].
///
/// Not available under `no_index`.
/// Not available under both `no_index` and `no_object`.
///
/// # Panics
///
@ -781,7 +784,7 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn register_indexer_set_result<
T: Variant + Clone,
@ -791,6 +794,7 @@ impl Engine {
&mut self,
set_fn: impl Fn(&mut T, X, V) -> Result<(), Box<EvalAltResult>> + SendSync + 'static,
) -> &mut Self {
#[cfg(not(feature = "no_index"))]
if TypeId::of::<T>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -809,7 +813,7 @@ impl Engine {
}
/// Short-hand for registering both index getter and setter functions for a custom type with the [`Engine`].
///
/// Not available under `no_index`.
/// Not available under both `no_index` and `no_object`.
///
/// # Panics
///
@ -851,7 +855,7 @@ impl Engine {
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn register_indexer_get_set<T: Variant + Clone, X: Variant + Clone, V: Variant + Clone>(
&mut self,

View File

@ -375,12 +375,8 @@ impl Engine {
crate::engine::FN_IDX_GET => {
assert!(args.len() == 2);
EvalAltResult::ErrorFunctionNotFound(
format!(
"{} [{}]",
self.map_type_name(args[0].type_name()),
self.map_type_name(args[1].type_name()),
),
EvalAltResult::ErrorIndexingType(
self.map_type_name(args[0].type_name()).to_string(),
pos,
)
.into()
@ -391,12 +387,8 @@ impl Engine {
crate::engine::FN_IDX_SET => {
assert!(args.len() == 3);
EvalAltResult::ErrorFunctionNotFound(
format!(
"{} [{}]=",
self.map_type_name(args[0].type_name()),
self.map_type_name(args[1].type_name()),
),
EvalAltResult::ErrorIndexingType(
self.map_type_name(args[0].type_name()).to_string(),
pos,
)
.into()

View File

@ -24,7 +24,6 @@ use std::{
#[cfg(not(feature = "no_index"))]
use crate::Array;
#[cfg(not(feature = "no_index"))]
#[cfg(not(feature = "no_object"))]
use crate::Map;
@ -956,7 +955,7 @@ impl Module {
/// });
/// assert!(module.contains_fn(hash));
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn set_indexer_get_fn<ARGS, A, B, T, F>(&mut self, func: F) -> u64
where
@ -966,6 +965,7 @@ impl Module {
F: RegisterNativeFunction<ARGS, Result<T, Box<EvalAltResult>>>,
F: Fn(&mut A, B) -> Result<T, Box<EvalAltResult>> + SendSync + 'static,
{
#[cfg(not(feature = "no_index"))]
if TypeId::of::<A>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -1016,7 +1016,7 @@ impl Module {
/// });
/// assert!(module.contains_fn(hash));
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn set_indexer_set_fn<ARGS, A, B, C, F>(&mut self, func: F) -> u64
where
@ -1026,6 +1026,7 @@ impl Module {
F: RegisterNativeFunction<ARGS, Result<(), Box<EvalAltResult>>>,
F: Fn(&mut A, B, C) -> Result<(), Box<EvalAltResult>> + SendSync + 'static,
{
#[cfg(not(feature = "no_index"))]
if TypeId::of::<A>() == TypeId::of::<Array>() {
panic!("Cannot register indexer for arrays.");
}
@ -1082,7 +1083,7 @@ impl Module {
/// assert!(module.contains_fn(hash_get));
/// assert!(module.contains_fn(hash_set));
/// ```
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
#[inline(always)]
pub fn set_indexer_get_set_fn<A, B, T>(
&mut self,

View File

@ -696,7 +696,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) {
Expr::Dot(x, _) => match (&mut x.lhs, &mut x.rhs) {
// map.string
(Expr::Map(m, pos), Expr::Property(p)) if m.0.iter().all(|(_, x)| x.is_pure()) => {
let prop = &p.2.name;
let prop = p.2.0.as_str();
// Map literal where everything is pure - promote the indexed item.
// All other items can be thrown away.
state.set_dirty();

View File

@ -232,10 +232,7 @@ impl Expr {
Self::Property(Box::new((
(getter, hash_get),
(setter, hash_set),
Ident {
name: state.get_identifier(ident),
pos,
},
(state.get_identifier(ident).into(), pos),
)))
}
_ => self,
@ -1541,10 +1538,7 @@ fn make_dot_expr(
let rhs = Expr::Property(Box::new((
(getter, hash_get),
(setter, hash_set),
Ident {
name: state.get_identifier(ident),
pos: var_pos,
},
(state.get_identifier(ident).into(), var_pos),
)));
Expr::Dot(Box::new(BinaryExpr { lhs, rhs }), op_pos)

View File

@ -1,6 +1,6 @@
#![cfg(not(feature = "no_object"))]
use rhai::{Engine, EvalAltResult, ImmutableString, INT};
use rhai::{Engine, EvalAltResult, INT};
#[test]
fn test_get_set() -> Result<(), Box<EvalAltResult>> {
@ -46,20 +46,27 @@ fn test_get_set() -> Result<(), Box<EvalAltResult>> {
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.x.add(); a.x")?, 42);
assert_eq!(engine.eval::<INT>("let a = new_ts(); a.y.add(); a.y")?, 0);
#[cfg(not(feature = "no_index"))]
#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))]
{
engine.register_indexer_get_set(
|value: &mut TestStruct, index: ImmutableString| value.array[index.len()],
|value: &mut TestStruct, index: ImmutableString, new_val: INT| {
value.array[index.len()] = new_val
},
|value: &mut TestStruct, index: &str| value.array[index.len()],
|value: &mut TestStruct, index: &str, new_val: INT| value.array[index.len()] = new_val,
);
#[cfg(not(feature = "no_index"))]
assert_eq!(engine.eval::<INT>(r#"let a = new_ts(); a["abc"]"#)?, 4);
#[cfg(not(feature = "no_index"))]
assert_eq!(
engine.eval::<INT>(r#"let a = new_ts(); a["abc"] = 42; a["abc"]"#)?,
42
);
assert_eq!(engine.eval::<INT>(r"let a = new_ts(); a.abc")?, 4);
assert_eq!(
engine.eval::<INT>(r"let a = new_ts(); a.abc = 42; a.abc")?,
42
);
}
Ok(())
}