diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebb3d60..c81c5682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,19 @@ Bug fixes New features ------------ -* A new feature, `no_custom_syntax`, is added to remove custom syntax support from Rhai for applications that do not require it (which should be most). +### New feature flag + +* A new feature flag, `no_custom_syntax`, is added to remove custom syntax support from Rhai for applications that do not require it (which should be most). + +### Module documentation + * Comment lines beginning with `//!` (requires the `metadata` feature) are now collected as the script file's _module documentation_. * `AST` and `Module` have methods to access and manipulate documentation. +### Output definition files + +* An API is added to automatically generate definition files from a fully-configured `Engine`, for use with the Rhai Language Server. + Enhancements ------------ diff --git a/examples/README.md b/examples/README.md index 033be2af..17c379fe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,18 +4,18 @@ Sample Applications Standard Examples ----------------- -| Example | Description | -| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`arrays_and_structs`](arrays_and_structs.rs) | shows how to register a Rust type and using it with arrays | -| [`callback`](callback.rs) | shows how to store a Rhai closure and call it later within Rust | -| [`custom_types_and_methods`](custom_types_and_methods.rs) | shows how to register a Rust type and methods/getters/setters for it | -| [`hello`](hello.rs) | simple example that evaluates an expression and prints the result | -| [`reuse_scope`](reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common `Scope` | -| [`serde`](serde.rs) | example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde) (requires the `serde` feature) | -| [`simple_fn`](simple_fn.rs) | shows how to register a simple Rust function | -| [`strings`](strings.rs) | shows different ways to register Rust functions taking string arguments | -| [`threading`](threading.rs) | shows how to communicate with an `Engine` running in a separate thread via an MPSC channel | -| [`definitions`](./definitions) | shows how to generate definition files for manual inspection or for use with [Rhai LSP](https://github.com/rhaiscript/lsp), requires the `metadata` feature | +| Example | Description | +| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`arrays_and_structs`](arrays_and_structs.rs) | shows how to register a Rust type and using it with arrays | +| [`callback`](callback.rs) | shows how to store a Rhai closure and call it later within Rust | +| [`custom_types_and_methods`](custom_types_and_methods.rs) | shows how to register a Rust type and methods/getters/setters for it | +| [`definitions`](./definitions) | shows how to generate definition files for use with the [Rhai Language Server](https://github.com/rhaiscript/lsp) (requires the `metadata` feature) | +| [`hello`](hello.rs) | simple example that evaluates an expression and prints the result | +| [`reuse_scope`](reuse_scope.rs) | evaluates two pieces of code in separate runs, but using a common `Scope` | +| [`serde`](serde.rs) | example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde) (requires the `serde` feature) | +| [`simple_fn`](simple_fn.rs) | shows how to register a simple Rust function | +| [`strings`](strings.rs) | shows different ways to register Rust functions taking string arguments | +| [`threading`](threading.rs) | shows how to communicate with an `Engine` running in a separate thread via an MPSC channel | Scriptable Event Handler With State Examples diff --git a/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai b/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai index 4a5bdfd6..b5ba3344 100644 --- a/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai +++ b/examples/definitions/.rhai/definitions/__builtin-operators__.d.rhai @@ -249,3 +249,11 @@ op +=(Blob, Blob); op +=(Blob, i64); op +=(Blob, char); op +=(Blob, String); + +op in(?, Array) -> bool; +op in(String, String) -> bool; +op in(char, String) -> bool; +op in(int, Range) -> bool; +op in(int, RangeInclusive) -> bool; +op in(String, Map) -> bool; +op in(int, Blob) -> bool; diff --git a/examples/definitions/.rhai/definitions/__builtin__.d.rhai b/examples/definitions/.rhai/definitions/__builtin__.d.rhai index 2e08fa44..881c1582 100644 --- a/examples/definitions/.rhai/definitions/__builtin__.d.rhai +++ b/examples/definitions/.rhai/definitions/__builtin__.d.rhai @@ -112,7 +112,7 @@ fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr; /// print(is_def_fn("foo", 0)); // prints false /// print(is_def_fn("bar", 1)); // prints false /// ``` -fn is_def_fn(fn_name: String, num_params: i64) -> bool; +fn is_def_fn(fn_name: String, num_params: int) -> bool; /// Return `true` if a variable matching a specified name is defined. /// @@ -162,9 +162,100 @@ fn is_shared(variable: ?) -> bool; /// ``` fn eval(script: String) -> ?; +/// Return `true` if the string contains another string. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let x = "hello world!"; +/// +/// // The 'in' operator calls 'contains' in the background +/// if "world" in x { +/// print("found!"); +/// } +/// ``` fn contains(string: String, find: String) -> bool; -fn contains(range: Range, value: i64) -> bool; -fn contains(range: RangeInclusive, value: i64) -> bool; -fn contains(map: Map, string: String) -> bool; -fn contains(blob: Blob, value: i64) -> bool; + +/// Return `true` if the string contains a character. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let x = "hello world!"; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 'w' in x { +/// print("found!"); +/// } +/// ``` fn contains(string: String, ch: char) -> bool; + +/// Return `true` if a value falls within the exclusive range. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let r = 1..100; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 42 in r { +/// print("found!"); +/// } +/// ``` +fn contains(range: Range, value: int) -> bool; + +/// Return `true` if a value falls within the inclusive range. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let r = 1..=100; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 42 in r { +/// print("found!"); +/// } +/// ``` +fn contains(range: RangeInclusive, value: int) -> bool; + +/// Return `true` if a key exists within the object map. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let m = #{a:1, b:2, c:3}; +/// +/// // The 'in' operator calls 'contains' in the background +/// if "c" in m { +/// print("found!"); +/// } +/// ``` +fn contains(map: Map, string: String) -> bool; + +/// Return `true` if a value is found within the BLOB. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let b = blob(); +/// +/// b += 1; b += 2; b += 3; b += 4; b += 5; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 3 in b { +/// print("found!"); +/// } +/// ``` +fn contains(blob: Blob, value: int) -> bool; diff --git a/examples/definitions/.rhai/definitions/general_kenobi.d.rhai b/examples/definitions/.rhai/definitions/general_kenobi.d.rhai index c6485585..dfa07701 100644 --- a/examples/definitions/.rhai/definitions/general_kenobi.d.rhai +++ b/examples/definitions/.rhai/definitions/general_kenobi.d.rhai @@ -1,5 +1,4 @@ module general_kenobi; -/// Returns a string where `hello there ` -/// is repeated `n` times. +/// Returns a string where "hello there" is repeated `n` times. fn hello_there(n: i64) -> String; \ No newline at end of file diff --git a/examples/definitions/main.rs b/examples/definitions/main.rs index f83ac4fd..96abc522 100644 --- a/examples/definitions/main.rs +++ b/examples/definitions/main.rs @@ -1,21 +1,20 @@ -use rhai::{plugin::*, Engine, Scope}; +use rhai::plugin::*; +use rhai::{Engine, EvalAltResult, Scope}; #[export_module] pub mod general_kenobi { - /// Returns a string where `hello there ` - /// is repeated `n` times. + /// Returns a string where "hello there" is repeated `n` times. pub fn hello_there(n: i64) -> String { use std::convert::TryInto; "hello there ".repeat(n.try_into().unwrap()) } } -fn main() { +fn main() -> Result<(), Box> { let mut engine = Engine::new(); let mut scope = Scope::new(); - // This variable will also show up in the definitions, - // since it will be part of the scope. + // This variable will also show up in the definitions, since it will be part of the scope. scope.push("hello_there", "hello there"); #[cfg(not(feature = "no_module"))] @@ -28,17 +27,15 @@ fn main() { engine.register_fn("minus", |a: i64, b: i64| a - b); } - engine - .eval_with_scope::<()>( - &mut scope, - r#" -hello_there = general_kenobi::hello_there(4 minus 2); -"#, - ) - .unwrap(); + engine.run_with_scope( + &mut scope, + "hello_there = general_kenobi::hello_there(4 minus 2);", + )?; engine .definitions_with_scope(&scope) .write_to_dir("examples/definitions/.rhai/definitions") .unwrap(); + + Ok(()) } diff --git a/src/api/definitions/builtin-operators.d.rhai b/src/api/definitions/builtin-operators.d.rhai index 4a5bdfd6..b5ba3344 100644 --- a/src/api/definitions/builtin-operators.d.rhai +++ b/src/api/definitions/builtin-operators.d.rhai @@ -249,3 +249,11 @@ op +=(Blob, Blob); op +=(Blob, i64); op +=(Blob, char); op +=(Blob, String); + +op in(?, Array) -> bool; +op in(String, String) -> bool; +op in(char, String) -> bool; +op in(int, Range) -> bool; +op in(int, RangeInclusive) -> bool; +op in(String, Map) -> bool; +op in(int, Blob) -> bool; diff --git a/src/api/definitions/builtin.d.rhai b/src/api/definitions/builtin.d.rhai index 2e08fa44..881c1582 100644 --- a/src/api/definitions/builtin.d.rhai +++ b/src/api/definitions/builtin.d.rhai @@ -112,7 +112,7 @@ fn curry(fn_ptr: FnPtr, ...args: ?) -> FnPtr; /// print(is_def_fn("foo", 0)); // prints false /// print(is_def_fn("bar", 1)); // prints false /// ``` -fn is_def_fn(fn_name: String, num_params: i64) -> bool; +fn is_def_fn(fn_name: String, num_params: int) -> bool; /// Return `true` if a variable matching a specified name is defined. /// @@ -162,9 +162,100 @@ fn is_shared(variable: ?) -> bool; /// ``` fn eval(script: String) -> ?; +/// Return `true` if the string contains another string. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let x = "hello world!"; +/// +/// // The 'in' operator calls 'contains' in the background +/// if "world" in x { +/// print("found!"); +/// } +/// ``` fn contains(string: String, find: String) -> bool; -fn contains(range: Range, value: i64) -> bool; -fn contains(range: RangeInclusive, value: i64) -> bool; -fn contains(map: Map, string: String) -> bool; -fn contains(blob: Blob, value: i64) -> bool; + +/// Return `true` if the string contains a character. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let x = "hello world!"; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 'w' in x { +/// print("found!"); +/// } +/// ``` fn contains(string: String, ch: char) -> bool; + +/// Return `true` if a value falls within the exclusive range. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let r = 1..100; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 42 in r { +/// print("found!"); +/// } +/// ``` +fn contains(range: Range, value: int) -> bool; + +/// Return `true` if a value falls within the inclusive range. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let r = 1..=100; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 42 in r { +/// print("found!"); +/// } +/// ``` +fn contains(range: RangeInclusive, value: int) -> bool; + +/// Return `true` if a key exists within the object map. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let m = #{a:1, b:2, c:3}; +/// +/// // The 'in' operator calls 'contains' in the background +/// if "c" in m { +/// print("found!"); +/// } +/// ``` +fn contains(map: Map, string: String) -> bool; + +/// Return `true` if a value is found within the BLOB. +/// +/// This function also drives the `in` operator. +/// +/// # Example +/// +/// ```rhai +/// let b = blob(); +/// +/// b += 1; b += 2; b += 3; b += 4; b += 5; +/// +/// // The 'in' operator calls 'contains' in the background +/// if 3 in b { +/// print("found!"); +/// } +/// ``` +fn contains(blob: Blob, value: int) -> bool; diff --git a/src/api/definitions/mod.rs b/src/api/definitions/mod.rs index 53ffe6f3..7d195243 100644 --- a/src/api/definitions/mod.rs +++ b/src/api/definitions/mod.rs @@ -1,21 +1,18 @@ -use crate::{ - module::FuncInfo, plugin::*, tokenizer::is_valid_function_name, Engine, Module, Scope, -}; -use core::{cmp::Ordering, fmt, iter}; +//! Module that defines functions to output definition files for [`Engine`]. +#![cfg(feature = "metadata")] + +use crate::module::FuncInfo; +use crate::plugin::*; +use crate::tokenizer::is_valid_function_name; +use crate::{Engine, Module, Scope}; #[cfg(feature = "no_std")] use std::prelude::v1::*; - -#[cfg(feature = "no_std")] -use alloc::borrow::Cow; - -#[cfg(not(feature = "no_std"))] -use std::borrow::Cow; +use std::{borrow::Cow, cmp::Ordering, fmt}; impl Engine { - /// Return [`Definitions`] that can be used to - /// generate definition files that contain all - /// the visible items in the engine. + /// Return [`Definitions`] that can be used to generate definition files for the [`Engine`]. + /// Exported under the `metadata` feature only. /// /// # Example /// @@ -23,12 +20,15 @@ impl Engine { /// # use rhai::Engine; /// # fn main() -> std::io::Result<()> { /// let engine = Engine::new(); + /// /// engine /// .definitions() /// .write_to_dir(".rhai/definitions")?; /// # Ok(()) /// # } /// ``` + #[inline(always)] + #[must_use] pub fn definitions(&self) -> Definitions { Definitions { engine: self, @@ -36,10 +36,9 @@ impl Engine { } } - /// Return [`Definitions`] that can be used to - /// generate definition files that contain all - /// the visible items in the engine and the - /// given scope. + /// Return [`Definitions`] that can be used to generate definition files for the [`Engine`] and + /// the given [`Scope`]. + /// Exported under the `metadata` feature only. /// /// # Example /// @@ -54,6 +53,8 @@ impl Engine { /// # Ok(()) /// # } /// ``` + #[inline(always)] + #[must_use] pub fn definitions_with_scope<'e>(&'e self, scope: &'e Scope<'e>) -> Definitions<'e> { Definitions { engine: self, @@ -62,19 +63,20 @@ impl Engine { } } -/// Definitions helper that is used to generate -/// definition files based on the contents of an [`Engine`]. +/// Definitions helper type to generate definition files based on the contents of an [`Engine`]. #[must_use] pub struct Definitions<'e> { + /// The [`Engine`]. engine: &'e Engine, + /// Optional [`Scope`] to include. scope: Option<&'e Scope<'e>>, } impl<'e> Definitions<'e> { - /// Write all the definition files returned from [`iter_files`] to a directory. + /// Output all definition files returned from [`iter_files`][Definitions::iter_files] to a + /// specified directory. /// - /// This function will create the directory path if it does not yet exist, - /// it will also override any existing files as needed. + /// This function creates the directory if it does not exist, and overrides any existing files. #[cfg(all(not(feature = "no_std"), not(target_family = "wasm")))] pub fn write_to_dir(&self, path: impl AsRef) -> std::io::Result<()> { use std::fs; @@ -98,6 +100,7 @@ impl<'e> Definitions<'e> { fs::write(path.join("__scope__.d.rhai"), self.scope())?; } + #[cfg(not(feature = "no_module"))] for (name, decl) in self.modules() { fs::write(path.join(format!("{name}.d.rhai")), decl)?; } @@ -105,9 +108,11 @@ impl<'e> Definitions<'e> { Ok(()) } - /// Iterate over the generated definition files. + /// Iterate over generated definition files. /// - /// The returned iterator yields all the definition files as (filename, content) pairs. + /// The returned iterator yields all definition files as (filename, content) pairs. + #[inline] + #[must_use] pub fn iter_files(&self) -> impl Iterator + '_ { IntoIterator::into_iter([ ( @@ -120,23 +125,27 @@ impl<'e> Definitions<'e> { ), ("__static__.d.rhai".to_string(), self.static_module()), ]) - .chain(iter::from_fn(move || { - if self.scope.is_some() { - Some(("__scope__.d.rhai".to_string(), self.scope())) - } else { - None - } - })) .chain( - self.modules() - .map(|(name, def)| (format!("{name}.d.rhai"), def)), + self.scope + .iter() + .map(move |_| ("__scope__.d.rhai".to_string(), self.scope())), + ) + .chain( + #[cfg(not(feature = "no_module"))] + { + self.modules() + .map(|(name, def)| (format!("{name}.d.rhai"), def)) + }, + #[cfg(feature = "no_module")] + { + std::iter::empty() + }, ) } - /// Return the definitions for the globally available - /// items of the engine. + /// Return definitions for all globally available functions. /// - /// The definitions will always start with `module static;`. + /// Always starts with `module static;`. #[must_use] pub fn static_module(&self) -> String { let mut s = String::from("module static;\n\n"); @@ -153,11 +162,9 @@ impl<'e> Definitions<'e> { s } - /// Return the definitions for the available - /// items of the scope. + /// Return definitions for all items inside the [`Scope`], if any. /// - /// The definitions will always start with `module static;`, - /// even if the scope is empty or none was provided. + /// Always starts with `module static;` even if the [`Scope`] is empty or none was provided. #[must_use] pub fn scope(&self) -> String { let mut s = String::from("module static;\n\n"); @@ -169,44 +176,41 @@ impl<'e> Definitions<'e> { s } - /// Return module name and definition pairs for each registered module. + /// Return a (module name, definitions) pair for each registered static [module][Module]. /// - /// The definitions will always start with `module ;`. + /// Not available under `no_module`. /// - /// If the feature `no_module` is enabled, this will yield no elements. + /// Always starts with `module ;`. + #[cfg(not(feature = "no_module"))] + #[must_use] pub fn modules(&self) -> impl Iterator + '_ { - #[cfg(not(feature = "no_module"))] - let m = { - let mut m = self - .engine - .global_sub_modules - .iter() - .map(move |(name, module)| { - ( - name.to_string(), - format!("module {name};\n\n{}", module.definition(self)), - ) - }) - .collect::>(); + let mut m = self + .engine + .global_sub_modules + .iter() + .map(move |(name, module)| { + ( + name.to_string(), + format!("module {name};\n\n{}", module.definition(self)), + ) + }) + .collect::>(); - m.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); - m - }; - - #[cfg(feature = "no_module")] - let m = Vec::new(); + m.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); m.into_iter() } } impl Module { + /// Return definitions for all items inside the [`Module`]. fn definition(&self, def: &Definitions) -> String { let mut s = String::new(); self.write_definition(&mut s, def).unwrap(); s } + /// Output definitions for all items inside the [`Module`]. fn write_definition(&self, writer: &mut dyn fmt::Write, def: &Definitions) -> fmt::Result { let mut first = true; @@ -259,6 +263,7 @@ impl Module { } impl FuncInfo { + /// Output definitions for a function. fn write_definition( &self, writer: &mut dyn fmt::Write, @@ -324,14 +329,11 @@ impl FuncInfo { /// We have to transform some of the types. /// -/// This is highly inefficient and is currently based on -/// trial and error with the core packages. +/// This is highly inefficient and is currently based on trial and error with the core packages. /// -/// It tries to flatten types, removing `&` and `&mut`, -/// and paths, while keeping generics. +/// It tries to flatten types, removing `&` and `&mut`, and paths, while keeping generics. /// -/// Associated generic types are also rewritten into regular -/// generic type parameters. +/// Associated generic types are also rewritten into regular generic type parameters. fn def_type_name<'a>(ty: &'a str, engine: &'a Engine) -> Cow<'a, str> { let ty = engine.format_type_name(ty).replace("crate::", ""); let ty = ty.strip_prefix("&mut").unwrap_or(&*ty).trim(); @@ -353,6 +355,7 @@ fn def_type_name<'a>(ty: &'a str, engine: &'a Engine) -> Cow<'a, str> { } impl Scope<'_> { + /// Return definitions for all items inside the [`Scope`]. fn write_definition(&self, writer: &mut dyn fmt::Write) -> fmt::Result { let mut first = true; for (name, constant, _) in self.iter_raw() { diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 5cccd741..682e4285 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -568,7 +568,7 @@ pub enum Token { Reserved(SmartString), /// A custom keyword. /// - /// Not available under the `no_custom_syntax` feature. + /// Not available under `no_custom_syntax`. #[cfg(not(feature = "no_custom_syntax"))] Custom(SmartString), /// End of the input stream.