diff --git a/RELEASES.md b/RELEASES.md
index e0dc9f06..b3e0b141 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -39,6 +39,7 @@ New features
* `Module::set_getter_fn`, `Module::set_setter_fn`, `Module::set_indexer_get_fn`, `Module::set_indexer_set_fn` all expose the function to the global namespace by default. This is convenient when registering an API for a custom type.
* New `Module::update_fn_metadata` to update a module function's parameter names and types.
* New `#[rhai_fn(global)]` and `#[rhai_fn(internal)]` attributes to determine whether a function defined in a plugin module should be exposed to the global namespace. This is convenient when defining an API for a custom type.
+* New `get_fn_metadata_list` to get the metadata of all script-defined functions in scope.
Enhancements
------------
diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md
index acb3e33c..677deebc 100644
--- a/doc/src/language/functions.md
+++ b/doc/src/language/functions.md
@@ -101,21 +101,6 @@ a statement in the script can freely call a function defined afterwards.
This is similar to Rust and many other modern languages, such as JavaScript's `function` keyword.
-`is_def_fn`
------------
-
-Use `is_def_fn` to detect if a Rhai function is defined (and therefore callable), based on its name
-and the number of parameters.
-
-```rust
-fn foo(x) { x + 1 }
-
-is_def_fn("foo", 1) == true;
-
-is_def_fn("bar", 1) == false;
-```
-
-
Arguments are Passed by Value
----------------------------
@@ -159,3 +144,43 @@ x == 42; // 'x' is changed!
change(); // <- error: `this` is unbound
```
+
+
+`is_def_fn`
+-----------
+
+Use `is_def_fn` to detect if a Rhai function is defined (and therefore callable), based on its name
+and the number of parameters.
+
+```rust
+fn foo(x) { x + 1 }
+
+is_def_fn("foo", 1) == true;
+
+is_def_fn("bar", 1) == false;
+```
+
+
+Metadata
+--------
+
+The function `get_fn_metadata_list` is a _reflection_ API that returns an array of the metadata
+of all script-defined functions in scope.
+
+Functions from the following sources are returned, in order:
+
+1) Encapsulated script environment (e.g. when loading a [module] from a script file),
+2) Current script,
+3) [Modules] imported via the [`import`] statement (latest imports first),
+4) [Modules] added via [`Engine::register_module`]({{rootUrl}}/rust/modules/create.md) (latest registrations first)
+
+The return value is an [array] of [object maps] (so `get_fn_metadata_list` is not available under
+[`no_index`] or [`no_object`]), containing the following fields:
+
+| Field | Type | Optional? | Description |
+| -------------- | :------------------: | :-------: | ---------------------------------------------------------------------- |
+| `namespace` | [string] | yes | the module _namespace_ if the function is defined within a module |
+| `access` | [string] | no | `"public"` if the function is public,
`"private"` if it is private |
+| `name` | [string] | no | function name |
+| `params` | [array] of [strings] | no | parameter names |
+| `is_anonymous` | `bool` | no | is this function an anonymous function? |
diff --git a/src/engine.rs b/src/engine.rs
index 1a8afa66..a6ad78d1 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -97,11 +97,11 @@ impl Imports {
}
/// Get an iterator to this stack of imported modules in reverse order.
#[allow(dead_code)]
- pub fn iter(&self) -> impl Iterator- )> {
+ pub fn iter<'a>(&'a self) -> impl Iterator
- )> + 'a {
self.0.iter().flat_map(|lib| {
lib.iter()
.rev()
- .map(|(name, module)| (name.as_str(), module.clone()))
+ .map(|(name, module)| (name.clone(), module.clone()))
})
}
/// Get an iterator to this stack of imported modules in reverse order.
diff --git a/src/module/mod.rs b/src/module/mod.rs
index 4ad40090..78e3628c 100644
--- a/src/module/mod.rs
+++ b/src/module/mod.rs
@@ -1500,10 +1500,16 @@ impl Module {
)
}
+ /// Get an iterator to the sub-modules in the module.
+ #[inline(always)]
+ pub fn iter_sub_modules(&self) -> impl Iterator
- )> {
+ self.modules.iter().map(|(k, m)| (k.as_str(), m.clone()))
+ }
+
/// Get an iterator to the variables in the module.
#[inline(always)]
- pub fn iter_var(&self) -> impl Iterator
- {
- self.variables.iter()
+ pub fn iter_var(&self) -> impl Iterator
- {
+ self.variables.iter().map(|(k, v)| (k.as_str(), v))
}
/// Get an iterator to the functions in the module.
diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs
index 630c58d9..f017e326 100644
--- a/src/packages/fn_basic.rs
+++ b/src/packages/fn_basic.rs
@@ -1,5 +1,12 @@
+use crate::module::SharedScriptFnDef;
use crate::plugin::*;
-use crate::{def_package, FnPtr};
+use crate::stdlib::iter::empty;
+use crate::{calc_script_fn_hash, def_package, FnPtr, ImmutableString, NativeCallContext, INT};
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+use crate::{stdlib::collections::HashMap, Array, Map};
def_package!(crate:BasicFnPackage:"Basic Fn functions.", lib, {
combine_with_exported_module!(lib, "FnPtr", fn_ptr_functions);
@@ -13,10 +20,111 @@ mod fn_ptr_functions {
}
#[cfg(not(feature = "no_function"))]
- pub mod anonymous {
+ pub mod functions {
#[rhai_fn(name = "is_anonymous", get = "is_anonymous")]
pub fn is_anonymous(f: &mut FnPtr) -> bool {
f.is_anonymous()
}
+
+ pub fn is_def_fn(ctx: NativeCallContext, fn_name: &str, num_params: INT) -> bool {
+ if num_params < 0 {
+ false
+ } else {
+ let hash_script = calc_script_fn_hash(empty(), fn_name, num_params as usize);
+ ctx.engine()
+ .has_override(ctx.mods, ctx.lib, 0, hash_script, true)
+ }
+ }
+ }
+
+ #[cfg(not(feature = "no_function"))]
+ #[cfg(not(feature = "no_index"))]
+ #[cfg(not(feature = "no_object"))]
+ pub mod functions_and_maps {
+ pub fn get_fn_metadata_list(ctx: NativeCallContext) -> Array {
+ collect_fn_metadata(ctx)
+ }
}
}
+
+#[cfg(not(feature = "no_function"))]
+#[cfg(not(feature = "no_index"))]
+#[cfg(not(feature = "no_object"))]
+fn collect_fn_metadata(ctx: NativeCallContext) -> Array {
+ // Create a metadata record for a function.
+ fn make_metadata(
+ dict: &HashMap,
+ namespace: Option,
+ f: SharedScriptFnDef,
+ ) -> Map {
+ let mut map = Map::with_capacity(6);
+
+ if let Some(ns) = namespace {
+ map.insert(dict["namespace"].clone(), ns.into());
+ }
+ map.insert(dict["name"].clone(), f.name.clone().into());
+ map.insert(
+ dict["access"].clone(),
+ match f.access {
+ FnAccess::Public => dict["public"].clone(),
+ FnAccess::Private => dict["private"].clone(),
+ }
+ .into(),
+ );
+ map.insert(
+ dict["is_anonymous"].clone(),
+ f.name.starts_with(crate::engine::FN_ANONYMOUS).into(),
+ );
+ map.insert(
+ dict["params"].clone(),
+ f.params
+ .iter()
+ .cloned()
+ .map(Into::::into)
+ .collect::()
+ .into(),
+ );
+
+ map.into()
+ }
+
+ // Recursively scan modules for script-defined functions.
+ fn scan_module(
+ list: &mut Array,
+ dict: &HashMap,
+ namespace: ImmutableString,
+ module: &Module,
+ ) {
+ module.iter_script_fn().for_each(|(_, _, _, _, f)| {
+ list.push(make_metadata(dict, Some(namespace.clone()), f).into())
+ });
+ module.iter_sub_modules().for_each(|(ns, m)| {
+ let ns: ImmutableString = format!("{}::{}", namespace, ns).into();
+ scan_module(list, dict, ns, m.as_ref())
+ });
+ }
+
+ // Intern strings
+ let mut dict = HashMap::::with_capacity(8);
+ dict.insert("namespace".into(), "namespace".into());
+ dict.insert("name".into(), "name".into());
+ dict.insert("access".into(), "access".into());
+ dict.insert("public".into(), "public".into());
+ dict.insert("private".into(), "private".into());
+ dict.insert("is_anonymous".into(), "is_anonymous".into());
+ dict.insert("params".into(), "params".into());
+
+ let mut list: Array = Default::default();
+
+ ctx.lib
+ .iter()
+ .flat_map(|m| m.iter_script_fn())
+ .for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into()));
+
+ if let Some(mods) = ctx.mods {
+ mods.iter()
+ .for_each(|(ns, m)| scan_module(&mut list, &dict, ns, m.as_ref()));
+ }
+
+ list
+}
diff --git a/src/packages/pkg_core.rs b/src/packages/pkg_core.rs
index 7480fdd1..284c5997 100644
--- a/src/packages/pkg_core.rs
+++ b/src/packages/pkg_core.rs
@@ -4,34 +4,9 @@ use super::iter_basic::BasicIteratorPackage;
use super::logic::LogicPackage;
use super::string_basic::BasicStringPackage;
-use crate::fn_native::{CallableFunction, FnCallArgs};
-use crate::stdlib::{any::TypeId, boxed::Box, iter::empty};
-use crate::{
- calc_script_fn_hash, def_package, FnAccess, FnNamespace, ImmutableString, NativeCallContext,
- INT,
-};
+use crate::def_package;
def_package!(crate:CorePackage:"_Core_ package containing basic facilities.", lib, {
- #[cfg(not(feature = "no_function"))]
- {
- let f = |ctx: NativeCallContext, args: &mut FnCallArgs| {
- let num_params = args[1].clone().cast::();
- let fn_name = args[0].as_str().unwrap();
-
- Ok(if num_params < 0 {
- false.into()
- } else {
- let hash_script = calc_script_fn_hash(empty(), fn_name, num_params as usize);
- ctx.engine().has_override(ctx.mods, ctx.lib, 0, hash_script, true).into()
- })
- };
-
- lib.set_fn("is_def_fn", FnNamespace::Global, FnAccess::Public,
- Some(&["fn_name: &str", "num_params: INT"]),
- &[TypeId::of::(), TypeId::of::()],
- CallableFunction::from_method(Box::new(f)));
- }
-
ArithmeticPackage::init(lib);
LogicPackage::init(lib);
BasicStringPackage::init(lib);