diff --git a/src/grain/vm/mod.rs b/src/grain/vm/mod.rs index c7cc90421..cc42796c6 100644 --- a/src/grain/vm/mod.rs +++ b/src/grain/vm/mod.rs @@ -10,26 +10,25 @@ use std::prelude::v1::*; #[cfg(not(feature = "unchecked"))] #[cfg(not(all(feature = "no_index", feature = "no_object")))] use crate::eval::calc_data_sizes; +use crate::eval::{Caches, GlobalRuntimeState}; use crate::func::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn}; use crate::packages::string_basic::print_with_func; use crate::types::dynamic::DynamicWriteLock; use crate::types::fn_ptr::FnPtrType; +use crate::types::StringsInterner; // `Variant` is only re-exported from the crate root under `internals`, so it // comes from where it is defined. use crate::ast::Expr; -#[cfg(not(feature = "no_function"))] -use crate::types::dynamic::Variant; #[cfg(not(feature = "no_index"))] use crate::Array; -#[cfg(not(feature = "no_function"))] -use crate::CallFnOptions; #[cfg(not(feature = "no_object"))] use crate::Map; +#[cfg(not(feature = "no_function"))] +use crate::{types::dynamic::Variant, CallFnOptions}; use crate::{ - eval::Caches, eval::GlobalRuntimeState, Dynamic, Engine, EvalAltResult, EvalContext, FnArgsVec, - Scope, + Dynamic, Engine, EvalAltResult, EvalContext, FnArgsVec, FnPtr, ImmutableString, + NativeCallContext, Position, Scope, FUNC_TO_STRING, INT, }; -use crate::{FnPtr, ImmutableString, NativeCallContext, Position, ThinVec, FUNC_TO_STRING, INT}; mod callback; @@ -348,6 +347,8 @@ pub struct Vm<'e> { global: GlobalRuntimeState, caches: Caches, stack: Vec, + #[cfg_attr(any(feature = "no_index", feature = "no_object"), allow(unused))] + strings_interner: StringsInterner, /// One entry per `for` loop currently running. /// /// Not on the operand stack, because an iterator is not a `Dynamic`. A @@ -478,6 +479,7 @@ impl<'e> Vm<'e> { engine, global, caches: Caches::new(), + strings_interner: StringsInterner::new(256), stack: Vec::new(), iterators: Vec::new(), handlers: Vec::new(), @@ -518,6 +520,7 @@ impl<'e> Vm<'e> { engine: context.engine(), global: context.global_runtime_state().clone(), caches: Caches::new(), + strings_interner: StringsInterner::new(256), stack: Vec::new(), iterators: Vec::new(), handlers: Vec::new(), @@ -1618,6 +1621,87 @@ impl<'e> Vm<'e> { } } + // Try to get a property through an indexer. + // + // This requires `no_index` and `no_object` to be off, + // otherwise it just passes the error through. + fn try_index_get( + &mut self, + target: &mut Dynamic, + key: &str, + err: Box, + pos: Position, + ) -> VmResult { + #[cfg(not(any(feature = "no_index", feature = "no_object")))] + return match *err { + EvalAltResult::ErrorDotExpr(..) => { + let mut index = self.strings_interner.get(key).into(); + self.engine + .call_indexer_get(&mut self.global, &mut self.caches, target, &mut index, pos) + .map_err(|err2| match *err2 { + EvalAltResult::ErrorIndexingType(..) => err, + _ => positioned(err2, pos), + }) + } + _ => Err(err), + }; + #[cfg(any(feature = "no_index", feature = "no_object"))] + { + let _ = (target, key, pos); + return Err(err); + } + } + + // Try to set a property through an index setter. + // + // This requires `no_index` and `no_object` to be off, + // otherwise it just passes the error through. + fn try_index_set( + &mut self, + target: &mut Dynamic, + key: &str, + value: &mut Dynamic, + fail_silently: bool, + err: Box, + pos: Position, + ) -> VmResult { + #[cfg(not(any(feature = "no_index", feature = "no_object")))] + return match *err { + EvalAltResult::ErrorDotExpr(..) => { + let mut index = self.strings_interner.get(key).into(); + match self + .engine + .call_indexer_set( + &mut self.global, + &mut self.caches, + target, + &mut index, + value, + true, + pos, + ) + .map(|_| ()) + { + Ok(()) => Ok(Dynamic::UNIT), + Err(err2) if matches!(*err2, EvalAltResult::ErrorIndexingType(..)) => { + if fail_silently { + Ok(Dynamic::UNIT) + } else { + Err(err) + } + } + Err(err2) => Err(positioned(err2, pos)), + } + } + _ => Err(err), + }; + #[cfg(any(feature = "no_index", feature = "no_object"))] + { + let _ = (target, key, value, fail_silently, pos); + return Err(err); + } + } + /// `.name`, which is a key on a map and a getter call on anything else. /// /// The distinction is Rhai's and it is made at runtime, not at parse time @@ -1643,14 +1727,12 @@ impl<'e> Vm<'e> { setter: u32, ) -> Result<(Dynamic, bool), Box> { let last = rest.is_empty(); - // The key names a map entry; a host type is reached through the getter - // and setter names instead, which are looked up below. - #[cfg(not(feature = "no_object"))] + + // The name is a map key for maps, and the same string is what a host + // type's fallback string indexer is addressed with. let key = program .name(name) .ok_or_else(|| malformed(format!("no name {name}")))?; - #[cfg(feature = "no_object")] - let _ = name; // A map is the one property holder that is not a host type, and // `no_object` removes both it and the syntax that would reach one. @@ -1712,32 +1794,34 @@ impl<'e> Vm<'e> { // `x.p += 1` has to read `p` back through the getter before it // can add to it — the setter takes a finished value. let mut new_val = if matches!(chain.tail, Tail::Assign { op: Some(_) }) { - let mut current = call(self, getter, &mut [target])?; + let mut current = call(self, getter, &mut [target]) + .or_else(|err| self.try_index_get(target, key, err, step_pos))?; self.store(program, chain_op(program, chain)?, &mut current, value, pos)?; current } else { value }; // A setter's return value is thrown away, as in Rhai. - let _ = call(self, setter, &mut [target, &mut new_val])?; + let _ = call(self, setter, &mut [target, &mut new_val]).or_else(|err| { + self.try_index_set(target, key, &mut new_val, false, err, step_pos) + })?; return Ok((Dynamic::UNIT, true)); } - let out = call(self, getter, &mut [target])?; + let out = call(self, getter, &mut [target]) + .or_else(|err| self.try_index_get(target, key, err, step_pos))?; return Ok((out, false)); } // A getter returns a value, so the rest of the chain works on a // temporary. Rhai puts it back through the setter when the sub-chain // was a method call, and skips the setter otherwise. - let mut temp = call(self, getter, &mut [target])?; + let mut temp = call(self, getter, &mut [target]) + .or_else(|err| self.try_index_get(target, key, err, step_pos))?; let (out, changed) = self.walk_chain(program, chain, rest, &mut temp, operands, value, pos)?; if changed { - let _ = call(self, setter, &mut [target, &mut temp]).or_else(|err| match *err { - // Fail silently if the property is read-only, as Rhai does (`eval/chaining.rs:1039`). - EvalAltResult::ErrorDotExpr(..) => Ok(Dynamic::UNIT), - _ => Err(err), - })?; + let _ = call(self, setter, &mut [target, &mut temp]) + .or_else(|err| self.try_index_set(target, key, &mut temp, true, err, step_pos))?; } Ok((out, changed)) } @@ -2430,7 +2514,7 @@ impl<'e> Vm<'e> { /// /// First check whether the call is a syntactic one (e.g. `is_def_fn`) /// which are self-implemented or directly called into the - /// corresponding Rhai functinon. + /// corresponding Rhai function. /// /// If the call is not to a syntactic one, it calls the function /// normally, with arguments pushed onto the stack. @@ -3971,7 +4055,7 @@ impl<'e> Vm<'e> { self.stack.push( FnPtr { name: name.into(), - curry: ThinVec::new(), + curry: Default::default(), #[cfg(not(feature = "no_function"))] env: None, typ: FnPtrType::Normal, diff --git a/tests/get_set.rs b/tests/get_set.rs index 99a2b2b8e..af3b19e7b 100644 --- a/tests/get_set.rs +++ b/tests/get_set.rs @@ -51,6 +51,7 @@ fn test_get_set() { assert_eq!(engine.eval::(r"let a = new_ts(); a.abc").unwrap(), 4); assert_eq!(engine.eval::(r"let a = new_ts(); a.abc = 42; a.abc").unwrap(), 42); + assert_eq!(engine.eval::(r"let a = new_ts(); a.abc += 10; a.abc").unwrap(), 14); } #[test] diff --git a/tests/grain/corpus/mod.rs b/tests/grain/corpus/mod.rs index 82728b7c0..7628cde62 100644 --- a/tests/grain/corpus/mod.rs +++ b/tests/grain/corpus/mod.rs @@ -101,6 +101,19 @@ pub fn engine() -> rhai::Engine { Ok(()) }, ); + #[cfg(not(all(feature = "no_index", feature = "no_object")))] + engine.register_indexer_get_set( + |w: &mut Widget, name: &str| -> Result> { + let index = name.len() as INT - 1; + w.cells.get(index as usize).copied().ok_or_else(|| out_of_range(index, w.cells.len())) + }, + |w: &mut Widget, name: &str, v: INT| -> Result<(), Box> { + let index = name.len() as INT - 1; + let len = w.cells.len(); + *w.cells.get_mut(index as usize).ok_or_else(|| out_of_range(index, len))? = v; + Ok(()) + }, + ); #[cfg(not(feature = "no_object"))] { @@ -247,7 +260,11 @@ pub fn applies_to_this_build(name: &str) -> bool { | "host_index_set" | "host_mutation_before_a_failure_survives_in_an_array" | "host_index_temp_set" + | "host_string_index_property_get_fallback" + | "host_string_index_property_set_fallback" + | "host_string_index_property_op_assign_fallback" | "host_temp_index_set" + | "host_temp_string_index_property_set_fallback" | "index_assign_array" | "index_assign_nested" | "index_expression_reads_the_root" @@ -328,6 +345,10 @@ pub fn applies_to_this_build(name: &str) -> bool { | "fn_ptr_curried" | "fn_ptr_from_dynamic_name" | "fn_ptr_to_native" + | "host_string_index_property_get_fallback" + | "host_string_index_property_set_fallback" + | "host_string_index_property_op_assign_fallback" + | "host_temp_string_index_property_set_fallback" | "index_assign_nested" | "index_expression_reads_the_root" | "interpolation_of_containers" @@ -750,9 +771,13 @@ pub const CASES: &[Case] = &[ case("host_index_set", "let w = widget(1); w[1] = 99; w[1]"), case("host_method_mutates", "let w = widget(4); w.bump(); w.level"), case("host_method_pure", "let w = widget(4); w.doubled()"), + case("host_string_index_property_get_fallback", "let w = widget(1); w.a"), + case("host_string_index_property_set_fallback", "let w = widget(1); w.ab = 77; w.ab"), + case("host_string_index_property_op_assign_fallback", "let w = widget(1); w.a += 5; w.a"), // Two levels, so the middle one is a temporary. case("host_temp_set", "let h = holder(3); h.inner.level = 8; h.inner.level"), case("host_temp_index_set", "let h = holder(3); h.inner[0] = 7; h.inner[0]"), + case("host_temp_string_index_property_set_fallback", "let h = holder(3); h.inner.ab = 7; h.inner.ab"), // The mirror of it: an *index* step handing back the temporary, with the // property below. The index has to survive the getter to address the setter // with afterwards.