Skip to content

Commit

Permalink
Add array/map callbacks.
Browse files Browse the repository at this point in the history
  • Loading branch information
schungx committed Feb 23, 2024
1 parent 7e0ac9d commit f1698a3
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ New features
------------

* New options `Engine::set_max_strings_interned` and `Engine::max_strings_interned` are added to limit the maximum number of strings interned in the `Engine`'s string interner.
* A new advanced callback can now be registered (gated under the `internals` feature), `Engine::on_invalid_array_index`, to handle access to missing properties in object maps.
* A new advanced callback can now be registered (gated under the `internals` feature), `Engine::on_missing_map_property`, to handle out-of-bound index into arrays.

Enhancements
------------
Expand Down
72 changes: 72 additions & 0 deletions src/api/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,78 @@ impl Engine {
self.debug = Some(Box::new(callback));
self
}
/// _(internals)_ Register a callback for access to [`Map`][crate::Map] properties that do not exist.
/// Exported under the `internals` feature only.
///
/// Not available under `no_index`.
///
/// # WARNING - Unstable API
///
/// This API is volatile and may change in the future.
///
/// # Callback Function Signature
///
/// `Fn(array: &mut Array, index: INT) -> Result<Target, Box<EvalAltResult>>`
///
/// where:
/// * `array`: mutable reference to the [`Array`][crate::Array] instance.
/// * `index`: numeric index of the array access.
///
/// ## Return value
///
/// * `Ok(Target)`: [`Target`][crate::Target] of the indexing access.
///
/// ## Raising errors
///
/// Return `Err(...)` if there is an error, usually [`EvalAltResult::ErrorPropertyNotFound`][crate::EvalAltResult::ErrorPropertyNotFound].
#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
#[inline(always)]
pub fn on_invalid_array_index(
&mut self,
callback: impl for<'a> Fn(&'a mut crate::Array, crate::INT) -> RhaiResultOf<crate::Target<'a>>
+ SendSync
+ 'static,
) -> &mut Self {
self.invalid_array_index = Some(Box::new(callback));
self
}
/// _(internals)_ Register a callback for access to [`Map`][crate::Map] properties that do not exist.
/// Exported under the `internals` feature only.
///
/// Not available under `no_object`.
///
/// # WARNING - Unstable API
///
/// This API is volatile and may change in the future.
///
/// # Callback Function Signature
///
/// `Fn(map: &mut Map, prop: &str) -> Result<Target, Box<EvalAltResult>>`
///
/// where:
/// * `map`: mutable reference to the [`Map`][crate::Map] instance.
/// * `prop`: name of the property that does not exist.
///
/// ## Return value
///
/// * `Ok(Target)`: [`Target`][crate::Target] of the property access.
///
/// ## Raising errors
///
/// Return `Err(...)` if there is an error, usually [`EvalAltResult::ErrorPropertyNotFound`][crate::EvalAltResult::ErrorPropertyNotFound].
#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
#[inline(always)]
pub fn on_map_missing_property(
&mut self,
callback: impl for<'a> Fn(&'a mut crate::Map, &str) -> RhaiResultOf<crate::Target<'a>>
+ SendSync
+ 'static,
) -> &mut Self {
self.missing_map_property = Some(Box::new(callback));
self
}
/// _(debugging)_ Register a callback for debugging.
/// Exported under the `debugging` feature only.
///
Expand Down
16 changes: 16 additions & 0 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ pub struct Engine {
/// Callback closure to remap tokens during parsing.
pub(crate) token_mapper: Option<Box<OnParseTokenCallback>>,

/// Callback closure when a [`Array`][crate::Array] property accessed does not exist.
#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
pub(crate) invalid_array_index: Option<Box<crate::func::native::OnInvalidArrayIndexCallback>>,
/// Callback closure when a [`Map`][crate::Map] property accessed does not exist.
#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
pub(crate) missing_map_property: Option<Box<crate::func::native::OnMissingMapPropertyCallback>>,

/// Callback closure for implementing the `print` command.
pub(crate) print: Option<Box<OnPrintCallback>>,
/// Callback closure for implementing the `debug` command.
Expand Down Expand Up @@ -244,6 +253,13 @@ impl Engine {
resolve_var: None,
token_mapper: None,

#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
invalid_array_index: None,
#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
missing_map_property: None,

print: None,
debug: None,

Expand Down
40 changes: 28 additions & 12 deletions src/eval/chaining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,20 @@ impl Engine {
.as_int()
.map_err(|typ| self.make_type_mismatch_err::<crate::INT>(typ, idx_pos))?;
let len = arr.len();
let arr_idx = super::calc_index(len, index, true, || {

let arr_idx = match super::calc_index(len, index, true, || {
ERR::ErrorArrayBounds(len, index, idx_pos).into()
})?;
}) {
Ok(idx) => idx,
Err(err) => {
#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
if let Some(ref cb) = self.invalid_array_index {
return cb(arr, index).map_err(|err| err.fill_position(idx_pos));
}
return Err(err);
}
};

Ok(arr.get_mut(arr_idx).map(Target::from).unwrap())
}
Expand Down Expand Up @@ -160,20 +171,25 @@ impl Engine {
self.make_type_mismatch_err::<crate::ImmutableString>(idx.type_name(), idx_pos)
})?;

#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
if let Some(ref cb) = self.missing_map_property {
if !map.contains_key(index.as_str()) {
return cb(map, index.as_str()).map_err(|err| err.fill_position(idx_pos));
}
}

if _add_if_not_found && (map.is_empty() || !map.contains_key(index.as_str())) {
map.insert(index.clone().into(), Dynamic::UNIT);
}

map.get_mut(index.as_str()).map_or_else(
|| {
if self.fail_on_invalid_map_property() {
Err(ERR::ErrorPropertyNotFound(index.to_string(), idx_pos).into())
} else {
Ok(Target::from(Dynamic::UNIT))
}
},
|value| Ok(Target::from(value)),
)
if let Some(value) = map.get_mut(index.as_str()) {
Ok(Target::from(value))
} else if self.fail_on_invalid_map_property() {
Err(ERR::ErrorPropertyNotFound(index.to_string(), idx_pos).into())
} else {
Ok(Target::from(Dynamic::UNIT))
}
}

#[cfg(not(feature = "no_index"))]
Expand Down
10 changes: 7 additions & 3 deletions src/eval/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ pub fn calc_index<E>(
err_func()
}

/// A type that encapsulates a mutation target for an expression with side effects.
/// _(internals)_ A type that encapsulates a mutation target for an expression with side effects.
/// Exported under the `internals` feature only.
///
/// This type is typically used to hold a mutable reference to the target of an indexing or property
/// access operation.
#[derive(Debug)]
#[must_use]
pub enum Target<'a> {
Expand Down Expand Up @@ -121,7 +125,7 @@ pub enum Target<'a> {
shift: u8,
},
/// The target is a byte inside a [`Blob`][crate::Blob].
/// This is necessary because directly pointing to a byte (in [`Dynamic`] form) inside a blob is impossible.
/// This is necessary because directly pointing to a [byte][u8] inside a BLOB is impossible.
#[cfg(not(feature = "no_index"))]
BlobByte {
/// Mutable reference to the source [`Dynamic`].
Expand All @@ -132,7 +136,7 @@ pub enum Target<'a> {
index: usize,
},
/// The target is a character inside a string.
/// This is necessary because directly pointing to a char inside a String is impossible.
/// This is necessary because directly pointing to a [`char`] inside a [`String`] is impossible.
#[cfg(not(feature = "no_index"))]
StringChar {
/// Mutable reference to the source [`Dynamic`].
Expand Down
31 changes: 31 additions & 0 deletions src/func/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,37 @@ pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position);
#[cfg(feature = "sync")]
pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position) + Send + Sync;

/// _(internals)_ Callback function when a property accessed is not found in a [`Map`][crate::Map].
/// Exported under the `internals` feature only.
#[cfg(not(feature = "sync"))]
#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
pub type OnInvalidArrayIndexCallback =
dyn for<'a> Fn(&'a mut crate::Array, crate::INT) -> RhaiResultOf<crate::Target<'a>>;
/// Callback function when a property accessed is not found in a [`Map`][crate::Map].
/// Exported under the `internals` feature only.
#[cfg(feature = "sync")]
#[cfg(not(feature = "no_index"))]
#[cfg(feature = "internals")]
pub type OnInvalidArrayIndexCallback = dyn for<'a> Fn(&'a mut crate::Array, crate::INT) -> RhaiResultOf<crate::Target<'a>>
+ Send
+ Sync;

/// _(internals)_ Callback function when a property accessed is not found in a [`Map`][crate::Map].
/// Exported under the `internals` feature only.
#[cfg(not(feature = "sync"))]
#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
pub type OnMissingMapPropertyCallback =
dyn for<'a> Fn(&'a mut crate::Map, &str) -> RhaiResultOf<crate::eval::Target<'a>>;
/// Callback function when a property accessed is not found in a [`Map`][crate::Map].
/// Exported under the `internals` feature only.
#[cfg(feature = "sync")]
#[cfg(not(feature = "no_object"))]
#[cfg(feature = "internals")]
pub type OnMissingMapPropertyCallback =
dyn for<'a> Fn(&'a mut crate::Map, &str) -> RhaiResultOf<crate::eval::Target<'a>> + Send + Sync;

/// Callback function for mapping tokens during parsing.
#[cfg(not(feature = "sync"))]
pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token;
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ pub use ast::CustomExpr;
pub use ast::Namespace;

#[cfg(feature = "internals")]
pub use eval::{Caches, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeState};
pub use eval::{Caches, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeState, Target};

#[cfg(feature = "internals")]
#[allow(deprecated)]
Expand Down
30 changes: 29 additions & 1 deletion tests/arrays.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![cfg(not(feature = "no_index"))]
use rhai::{Array, Dynamic, Engine, ParseErrorType, INT};
use rhai::{Array, Dynamic, Engine, EvalAltResult, ParseErrorType, Position, INT};

Check warning on line 2 in tests/arrays.rs

View workflow job for this annotation

GitHub Actions / Build (stable, windows-latest, false)

unused imports: `EvalAltResult`, `Position`

Check warning on line 2 in tests/arrays.rs

View workflow job for this annotation

GitHub Actions / Build (stable, macos-latest, false)

unused imports: `EvalAltResult`, `Position`

Check warning on line 2 in tests/arrays.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,serde, stable, false)

unused imports: `EvalAltResult`, `Position`

Check warning on line 2 in tests/arrays.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,decimal, stable, false)

unused imports: `EvalAltResult`, `Position`
use std::iter::FromIterator;

#[test]
Expand Down Expand Up @@ -500,3 +500,31 @@ fn test_arrays_elvis() {

engine.run("let x = (); x?[2] = 42").unwrap();
}

#[test]
#[cfg(feature = "internals")]
fn test_array_invalid_index_callback() {
let mut engine = Engine::new();

engine.on_invalid_array_index(|arr, index| match index {
-100 => {
arr.push((42 as INT).into());
Ok(arr.last_mut().unwrap().into())
}
100 => Ok(Dynamic::from(100 as INT).into()),
_ => Err(EvalAltResult::ErrorArrayBounds(arr.len(), index, Position::NONE).into()),
});

assert_eq!(
engine
.eval::<INT>(
"
let a = [1, 2, 3];
a[-100] += 1;
a[3] + a[100]
"
)
.unwrap(),
143
);
}
30 changes: 29 additions & 1 deletion tests/maps.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![cfg(not(feature = "no_object"))]
use rhai::{Engine, EvalAltResult, Map, ParseErrorType, Scope, INT};
use rhai::{Dynamic, Engine, EvalAltResult, Map, ParseErrorType, Position, Scope, INT};

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (beta, ubuntu-latest, false, --features unstable)

unused imports: `Dynamic`, `Position`

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_index,serde,metadata,internals,debugging, sta...

unused import: `Scope`

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (stable, macos-latest, false)

unused imports: `Dynamic`, `Position`

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,unicode-xid-ident, stable, false)

unused imports: `Dynamic`, `Position`

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,metadata, stable, false)

unused imports: `Dynamic`, `Position`

Check warning on line 2 in tests/maps.rs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, --features testing-environ,no_float,decimal, stable, false)

unused imports: `Dynamic`, `Position`

#[test]
fn test_map_indexing() {
Expand Down Expand Up @@ -255,3 +255,31 @@ fn test_map_oop() {
42
);
}

#[test]
#[cfg(feature = "internals")]
fn test_map_missing_property_callback() {
let mut engine = Engine::new();

engine.on_map_missing_property(|map, prop| match prop {
"x" => {
map.insert("y".into(), (42 as INT).into());
Ok(map.get_mut("y").unwrap().into())
}
"z" => Ok(Dynamic::from(100 as INT).into()),
_ => Err(EvalAltResult::ErrorPropertyNotFound(prop.to_string(), Position::NONE).into()),
});

assert_eq!(
engine
.eval::<INT>(
"
let obj = #{ a:1, b:2 };
obj.x += 1;
obj.y + obj.z
"
)
.unwrap(),
143
);
}

0 comments on commit f1698a3

Please sign in to comment.