From b43223a94fcebbf7300ca1ecfd116c39a8fc04d0 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 23 Nov 2020 19:11:32 +0800 Subject: [PATCH] Add get_fn_metadata_list. --- RELEASES.md | 1 + doc/src/language/functions.md | 55 ++++++++++++----- src/engine.rs | 4 +- src/module/mod.rs | 10 ++- src/packages/fn_basic.rs | 112 +++++++++++++++++++++++++++++++++- src/packages/pkg_core.rs | 27 +------- 6 files changed, 162 insertions(+), 47 deletions(-) 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);