From 4878a695030e91e4454e0fb67449c6ba91fb01cf Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 4 Aug 2020 16:27:55 +0800 Subject: [PATCH] Add docs for closures. --- Cargo.toml | 2 +- RELEASES.md | 4 +- doc/src/SUMMARY.md | 2 +- doc/src/about/features.md | 2 + doc/src/language/fn-anon.md | 5 +- doc/src/language/fn-capture.md | 13 ++- doc/src/language/fn-closure.md | 154 ++++++++++++++++++++++++--- doc/src/language/fn-curry.md | 2 +- doc/src/language/values-and-types.md | 32 +++--- doc/src/links.md | 4 +- doc/src/rust/register-raw.md | 18 ++-- doc/src/start/features.md | 2 +- src/any.rs | 24 ++--- 13 files changed, 200 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d20a93c..f6ffdea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ smallvec = { version = "1.4.1", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] default = [] -plugins = [] +plugins = [] # custom plugins support unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer diff --git a/RELEASES.md b/RELEASES.md index 79f298ec..1d2a842b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,7 +9,7 @@ This version adds: * Binding the `this` pointer in a function pointer `call`. * Anonymous functions (in Rust closure syntax). Simplifies creation of single-use ad-hoc functions. * Currying of function pointers. -* Auto-currying of anonymous functions. +* Closures - auto-currying of anonymous functions to capture shared variables from the external scope. * Capturing call scope via `func!(...)` syntax. New features @@ -21,7 +21,7 @@ New features * Anonymous functions are supported in the syntax of a Rust closure, e.g. `|x, y, z| x + y - z`. * Custom syntax now works even without the `internals` feature. * Currying of function pointers is supported via the new `curry` keyword. -* Automatic currying of anonymous functions to capture environment variables. +* Automatic currying of anonymous functions to capture shared variables from the external scope. * Capturing of the calling scope for function call via the `func!(...)` syntax. * `Module::set_indexer_get_set_fn` is added as a shorthand of both `Module::set_indexer_get_fn` and `Module::set_indexer_set_fn`. * New `unicode-xid-ident` feature to allow [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) for identifiers. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 6b966de1..3b23b321 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -79,7 +79,7 @@ The Rhai Scripting Language 4. [Function Pointers](language/fn-ptr.md) 5. [Anonymous Functions](language/fn-anon.md) 6. [Currying](language/fn-curry.md) - 7. [Capturing External Variables](language/fn-closure.md) + 7. [Closures](language/fn-closure.md) 16. [Print and Debug](language/print-debug.md) 17. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 245668b2..6c2e3965 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -37,6 +37,8 @@ Dynamic * Dynamic dispatch via [function pointers] with additional support for [currying]. +* Closures via [automatic currying] with capturing shared variables from the external scope. + * Some support for [object-oriented programming (OOP)][OOP]. Safe diff --git a/doc/src/language/fn-anon.md b/doc/src/language/fn-anon.md index b8a9154d..8b514509 100644 --- a/doc/src/language/fn-anon.md +++ b/doc/src/language/fn-anon.md @@ -55,5 +55,6 @@ WARNING - NOT Real Closures Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves **not** real closures. -In particular, they capture their execution environment via [automatic currying][capture], -unless the [`no_closure`] feature is turned on. + +In particular, they capture their execution environment via [automatic currying] +(disabled via [`no_closure`]). diff --git a/doc/src/language/fn-capture.md b/doc/src/language/fn-capture.md index 042675e9..08052826 100644 --- a/doc/src/language/fn-capture.md +++ b/doc/src/language/fn-capture.md @@ -16,6 +16,8 @@ it raises an evaluation error. It is possible, through a special syntax, to capture the calling scope - i.e. the scope that makes the function call - and access variables defined there. +Capturing can be disabled via the [`no_closure`] feature. + ```rust fn foo(y) { // function accesses 'x' and 'y', but 'x' is not defined x += y; // 'x' is modified in this function @@ -47,7 +49,7 @@ f.call!(41); // <- syntax error: capturing is not allowed in method-c No Mutations ------------ -Variables in the calling scope are accessed as copies. +Variables in the calling scope are captured as copies. Changes to them do not reflect back to the calling scope. Rhai functions remain _pure_ in the sense that they can never mutate their environment. @@ -56,7 +58,10 @@ Rhai functions remain _pure_ in the sense that they can never mutate their envir Caveat Emptor ------------- -Functions relying on the calling scope is a _Very Bad Idea™_ because it makes code almost impossible -to reason and maintain, as their behaviors are volatile and unpredictable. +Functions relying on the calling scope is often a _Very Bad Idea™_ because it makes code +almost impossible to reason and maintain, as their behaviors are volatile and unpredictable. -This usage should be at the last resort. +They behave more like macros that are expanded inline than actual function calls, thus the +syntax is also similar to Rust's macro invocations. + +This usage should be at the last resort. YOU HAVE BEEN WARNED. diff --git a/doc/src/language/fn-closure.md b/doc/src/language/fn-closure.md index 9ba66f2e..2f6ce2c5 100644 --- a/doc/src/language/fn-closure.md +++ b/doc/src/language/fn-closure.md @@ -1,10 +1,10 @@ -Capture External Variables via Automatic Currying -================================================ +Simulating Closures +=================== {{#include ../links.md}} -Poor Man's Closures -------------------- +Capture External Variables via Automatic Currying +------------------------------------------------ Since [anonymous functions] de-sugar to standard function definitions, they retain all the behaviors of Rhai functions, including being _pure_, having no access to external variables. @@ -15,13 +15,23 @@ is created. Variables that are accessible during the time the [anonymous function] is created can be captured, as long as they are not shadowed by local variables defined within the function's scope. -The captured variables are automatically converted into reference-counted shared values. + +The captured variables are automatically converted into **reference-counted shared values** +(`Rc>` in normal builds, `Arc>` in [`sync`] builds). + +Therefore, similar to closures in many languages, these captured shared values persist through +reference counting, and may be read or modified even after the variables that hold them +go out of scope and no longer exist. + +Use the `is_shared` function to check whether a particular value is a shared value. + +Automatic currying can be turned off via the [`no_closure`] feature. -New Parameters For Captured Variables ------------------------------------- +Actual Implementation +--------------------- -In actual implementation, this de-sugars to: +The actual implementation de-sugars to: 1. Keeping track of what variables are accessed inside the anonymous function, @@ -29,32 +39,148 @@ In actual implementation, this de-sugars to: 3. The variable is added to the parameters list of the anonymous function, at the front. -4. The variable is then turned into a reference-counted shared value. +4. The variable is then converted into a **reference-counted shared value**. + + An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai. 5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value and inserting it into future calls of the function. -Automatic currying can be turned off via the [`no_closure`] feature. + This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures. Examples -------- ```rust -let x = 1; +let x = 1; // a normal variable let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f' - // 'x' is converted into a shared value -x = 40; // 'x' can be changed +x.is_shared() == true; // 'x' is now a shared value! + +x = 40; // changing 'x'... f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared // The above de-sugars into this: fn anon$1001(x, y) { x + y } // parameter 'x' is inserted -make_shared(x); // convert 'x' into a shared value +make_shared(x); // convert variable 'x' into a shared value let f = Fn("anon$1001").curry(x); // shared 'x' is curried f.call(2) == 42; ``` + + +Beware: Captured Variables are Truly Shared +------------------------------------------ + +The example below is a typical tutorial sample for many languages to illustrate the traps +that may accompany capturing external scope variables in closures. + +It prints `9`, `9`, `9`, ... `9`, `9`, not `0`, `1`, `2`, ... `8`, `9`, because there is +ever only one captured variable, and all ten closures capture the _same_ variable. + +```rust +let funcs = []; + +for i in range(0, 10) { + funcs.push(|| print(i)); // the for loop variable 'i' is captured +} + +funcs.len() == 10; // 10 closures stored in the array + +funcs[0].type_of() == "Fn"; // make sure these are closures + +for f in funcs { + f.call(); // all the references to 'i' are the same variable! +} +``` + + +Therefore - Be Careful to Prevent Data Races +------------------------------------------- + +Rust does not have data races, but that doesn't mean Rhai doesn't. + +Avoid performing a method call on a captured shared variable (which essentially takes a +mutable reference to the shared object) while using that same variable as a parameter +in the method call - this is a sure-fire way to generate a data race error. + +If a shared value is used as the `this` pointer in a method call to a closure function, +then the same shared value _must not_ be captured inside that function, or a data race +will occur and the script will terminate with an error. + +```rust +let x = 20; + +let f = |a| this += x + a; // 'x' is captured in this closure + +x.is_shared() == true; // now 'x' is shared + +x.call(f, 2); // <- error: data race detected on 'x' +``` + + +Data Races in `sync` Builds Can Become Deadlocks +----------------------------------------------- + +Under the [`sync`] feature, shared values are guarded with a `RwLock`, meaning that data race +conditions no longer raise an error. + +Instead, they wait endlessly for the `RwLock` to be freed, and thus can become deadlocks. + +On the other hand, since the same thread (i.e. the [`Engine`] thread) that is holding the lock +is attempting to read it again, this may also [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1) +depending on the O/S. + +```rust +let x = 20; + +let f = |a| this += x + a; // 'x' is captured in this closure + +// Under `sync`, the following may wait forever, or may panic, +// because 'x' is locked as the `this` pointer but also accessed +// via a captured shared value. +x.call(f, 2); +``` + + +TL;DR +----- + +### Q: Why are closures implemented as automatic currying? + +In concept, a closure _closes_ over captured variables from the outer scope - that's why +they are called _closures_. When this happen, a typical language implementation hoists +those variables that are captured away from the stack frame and into heap-allocated storage. +This is because those variables may be needed after the stack frame goes away. + +These heap-allocated captured variables only go away when all the closures that need them +are finished with them. A garbage collector makes this trivial to implement - they are +automatically collected as soon as all closures needing them are destroyed. + +In Rust, this can be done by reference counting instead, with the potential pitfall of creating +reference loops that will prevent those variables from being deallocated forever. +Rhai avoids this by clone-copying most data values, so reference loops are hard to create. + +Rhai does the hoisting of captured variables into the heap by converting those values +into reference-counted locked values, also allocated on the heap. The process is identical. + +Closures are usually implemented as a data structure containing two items: + +1) A function pointer to the function body of the closure, +2) A data structure containing references to the captured shared variables on the heap. + +Usually a language implementation passes the structure containing references to captured +shared variables into the function pointer, the function body taking this data structure +as an additional parameter. + +This is essentially what Rhai does, except that Rhai passes each variable individually +as separate parameters to the function, instead of creating a structure and passing that +structure as a single parameter. This is the only difference. + +Therefore, in most languages, essentially all closures are implemented as automatic currying of +shared variables hoisted into the heap, automatically passing those variables as parameters into +the function. Rhai just brings this directly up to the front. diff --git a/doc/src/language/fn-curry.md b/doc/src/language/fn-curry.md index 83705175..c223d8cd 100644 --- a/doc/src/language/fn-curry.md +++ b/doc/src/language/fn-curry.md @@ -33,7 +33,7 @@ curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)' Automatic Currying ------------------ -[Anonymous functions] defined via a closure syntax _capture_ the _values_ of external variables +[Anonymous functions] defined via a closure syntax _capture_ external variables that are not shadowed inside the function's scope. This is accomplished via [automatic currying]. diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index 833eead5..f7446913 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -5,22 +5,22 @@ Values and Types The following primitive types are supported natively: -| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | -| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | ----------------------- | -| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | -| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | -| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | -| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | -| **Immutable Unicode [string]** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | -| **[`Array`]** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | -| **[Object map]** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `"#{ "a": 1, "b": 2 }"` | -| **[Timestamp]** (implemented in the [`BasicTimePackage`][packages], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if [WASM] build) | `"timestamp"` | `""` | -| **[Function pointer]** | `rhai::FnPtr` | `Fn` | `"Fn(foo)"` | -| **[`Dynamic`] value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | -| **Shared value** (a reference-counted, shared [`Dynamic`] value) | | _the actual type_ | _actual value_ | -| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | -| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | -| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | +| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | +| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | ----------------------- | +| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | +| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | +| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | +| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | +| **Immutable Unicode [string]** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | +| **[`Array`]** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | +| **[Object map]** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `"#{ "a": 1, "b": 2 }"` | +| **[Timestamp]** (implemented in the [`BasicTimePackage`][packages], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if [WASM] build) | `"timestamp"` | `""` | +| **[Function pointer]** | `rhai::FnPtr` | `Fn` | `"Fn(foo)"` | +| **[`Dynamic`] value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | +| **Shared value** (a reference-counted, shared [`Dynamic`] value, created via [automatic currying], disabled with [`no_closure`]) | | _the actual type_ | _actual value_ | +| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | +| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | +| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. diff --git a/doc/src/links.md b/doc/src/links.md index 8667082c..11b16796 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -79,8 +79,10 @@ [function pointer]: {{rootUrl}}/language/fn-ptr.md [function pointers]: {{rootUrl}}/language/fn-ptr.md [currying]: {{rootUrl}}/language/fn-curry.md -[capture]: {{rootUrl}}/language/fn-closure.md +[capture]: {{rootUrl}}/language/fn-capture.md [automatic currying]: {{rootUrl}}/language/fn-closure.md +[closure]: {{rootUrl}}/language/fn-closure.md +[closures]: {{rootUrl}}/language/fn-closure.md [function namespace]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md [anonymous function]: {{rootUrl}}/language/fn-anon.md diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index cf997286..b666d588 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -35,12 +35,12 @@ engine.register_raw_fn( // Therefore, get a '&mut' reference to the first argument _last_. // Alternatively, use `args.split_at_mut(1)` etc. to split the slice first. - let y: i64 = *args[1].downcast_ref::() // get a reference to the second argument + let y: i64 = *args[1].read_lock::() // get a reference to the second argument .unwrap(); // then copying it because it is a primary type let y: i64 = std::mem::take(args[1]).cast::(); // alternatively, directly 'consume' it - let x: &mut i64 = args[0].downcast_mut::() // get a '&mut' reference to the + let x: &mut i64 = args[0].write_lock::() // get a '&mut' reference to the .unwrap(); // first argument *x += y; // perform the action @@ -84,12 +84,12 @@ Extract Arguments To extract an argument from the `args` parameter (`&mut [&mut Dynamic]`), use the following: -| Argument type | Access (`n` = argument position) | Result | -| ------------------------------ | -------------------------------------- | ---------------------------------------------------------- | -| [Primary type][standard types] | `args[n].clone().cast::()` | Copy of value. | -| Custom type | `args[n].downcast_ref::().unwrap()` | Immutable reference to value. | -| Custom type (consumed) | `std::mem::take(args[n]).cast::()` | The _consumed_ value.
The original value becomes `()`. | -| `this` object | `args[0].downcast_mut::().unwrap()` | Mutable reference to value. | +| Argument type | Access (`n` = argument position) | Result | +| ------------------------------ | ------------------------------------- | ---------------------------------------------------------- | +| [Primary type][standard types] | `args[n].clone().cast::()` | Copy of value. | +| Custom type | `args[n].read_lock::().unwrap()` | Immutable reference to value. | +| Custom type (consumed) | `std::mem::take(args[n]).cast::()` | The _consumed_ value.
The original value becomes `()`. | +| `this` object | `args[0].write_lock::().unwrap()` | Mutable reference to value. | When there is a mutable reference to the `this` object (i.e. the first argument), there can be no other immutable references to `args`, otherwise the Rust borrow checker will complain. @@ -156,5 +156,5 @@ let this_ptr = first[0].downcast_mut::().unwrap(); // Immutable reference to the second value parameter // This can be mutable but there is no point because the parameter is passed by value -let value = rest[0].downcast_ref::().unwrap(); +let value_ref = rest[0].read_lock::().unwrap(); ``` diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 3b8cee04..3201aa07 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -23,7 +23,7 @@ more control over what a script can (or cannot) do. | `no_object` | Disable support for [custom types] and [object maps]. | | `no_function` | Disable script-defined [functions]. | | `no_module` | Disable loading external [modules]. | -| `no_closure` | Disable [capturing][capture] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls. | +| `no_closure` | Disable [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls. | | `no_std` | Build for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features. | | `serde` | Enable serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | | `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | diff --git a/src/any.rs b/src/any.rs index bdde78f3..5efcad16 100644 --- a/src/any.rs +++ b/src/any.rs @@ -283,9 +283,9 @@ impl Dynamic { /// Get the TypeId of the value held by this `Dynamic`. /// - /// # Panics and Deadlocks When Value is Shared + /// # Panics or Deadlocks When Value is Shared /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. pub fn type_id(&self) -> TypeId { match &self.0 { @@ -313,9 +313,9 @@ impl Dynamic { /// Get the name of the type of the value held by this `Dynamic`. /// - /// # Panics and Deadlocks When Value is Shared + /// # Panics or Deadlocks When Value is Shared /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. pub fn type_name(&self) -> &'static str { match &self.0 { @@ -621,9 +621,9 @@ impl Dynamic { /// /// Returns `None` if types mismatched. /// - /// # Panics and Deadlocks + /// # Panics or Deadlocks /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. /// /// These normally shouldn't occur since most operations in Rhai is single-threaded. @@ -744,12 +744,12 @@ impl Dynamic { /// /// Returns `None` if types mismatched. /// - /// # Panics and Deadlocks + /// # Panics or Deadlocks /// /// Panics if the cast fails (e.g. the type of the actual value is not the /// same as the specified type). /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. /// /// These normally shouldn't occur since most operations in Rhai is single-threaded. @@ -817,9 +817,9 @@ impl Dynamic { /// /// Returns `None` if the cast fails. /// - /// # Panics and Deadlocks When Value is Shared + /// # Panics or Deadlocks When Value is Shared /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. #[inline(always)] pub fn read_lock(&self) -> Option> { @@ -852,9 +852,9 @@ impl Dynamic { /// /// Returns `None` if the cast fails. /// - /// # Panics and Deadlocks When Value is Shared + /// # Panics or Deadlocks When Value is Shared /// - /// Under the `sync` feature, this call may deadlock. + /// Under the `sync` feature, this call may deadlock, or [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1). /// Otherwise, this call panics if the data is currently borrowed for write. #[inline(always)] pub fn write_lock(&mut self) -> Option> {