Skip to content

Commit

Permalink
rune: Improve test case macros and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed Nov 6, 2024
1 parent 982f9a8 commit 496577c
Show file tree
Hide file tree
Showing 53 changed files with 671 additions and 805 deletions.
15 changes: 14 additions & 1 deletion crates/rune/src/compile/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub struct Options {

impl Options {
/// The default options.
const DEFAULT: Options = Options {
pub(crate) const DEFAULT: Options = Options {
link_checks: true,
memoize_instance_fn: true,
debug_info: true,
Expand Down Expand Up @@ -442,4 +442,17 @@ impl Options {
pub fn memoize_instance_fn(&mut self, enabled: bool) {
self.memoize_instance_fn = enabled;
}

/// Whether to build sources as scripts where the source is executed like a
/// function body.
pub fn script(&mut self, enabled: bool) {
self.function_body = enabled;
}
}

impl Default for Options {
#[inline]
fn default() -> Self {
Options::DEFAULT
}
}
72 changes: 72 additions & 0 deletions crates/rune/src/modules/collections/hash_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,78 @@ pub fn module() -> Result<Module, ContextError> {
Ok(m)
}

/// A [hash map] implemented with quadratic probing and SIMD lookup.
///
/// By default, `HashMap` uses a hashing algorithm selected to provide
/// resistance against HashDoS attacks. The algorithm is randomly seeded, and a
/// reasonable best-effort is made to generate this seed from a high quality,
/// secure source of randomness provided by the host without blocking the
/// program. Because of this, the randomness of the seed depends on the output
/// quality of the system's random number coroutine when the seed is created. In
/// particular, seeds generated when the system's entropy pool is abnormally low
/// such as during system boot may be of a lower quality.
///
/// The default hashing algorithm is currently SipHash 1-3, though this is
/// subject to change at any point in the future. While its performance is very
/// competitive for medium sized keys, other hashing algorithms will outperform
/// it for small keys such as integers as well as large keys such as long
/// strings, though those algorithms will typically *not* protect against
/// attacks such as HashDoS.
///
/// The hashing algorithm can be replaced on a per-`HashMap` basis using the
/// [`default`], [`with_hasher`], and [`with_capacity_and_hasher`] methods.
/// There are many alternative [hashing algorithms available on crates.io].
///
/// It is required that the keys implement the [`EQ`] and [`HASH`] protocols. If
/// you implement these yourself, it is important that the following property
/// holds:
///
/// ```text
/// k1 == k2 -> hash(k1) == hash(k2)
/// ```
///
/// In other words, if two keys are equal, their hashes must be equal. Violating
/// this property is a logic error.
///
/// It is also a logic error for a key to be modified in such a way that the
/// key's hash, as determined by the [`HASH`] protocol, or its equality, as
/// determined by the [`EQ`] protocol, changes while it is in the map. This is
/// normally only possible through [`Cell`], [`RefCell`], global state, I/O, or
/// unsafe code.
///
/// The behavior resulting from either logic error is not specified, but will be
/// encapsulated to the `HashMap` that observed the logic error and not result
/// in undefined behavior. This could include panics, incorrect results, aborts,
/// memory leaks, and non-termination.
///
/// The hash table implementation is a Rust port of Google's [SwissTable]. The
/// original C++ version of SwissTable can be found [here], and this [CppCon
/// talk] gives an overview of how the algorithm works.
///
/// [hash map]: crate::collections#use-a-hashmap-when
/// [hashing algorithms available on crates.io]: https://crates.io/keywords/hasher
/// [SwissTable]: https://abseil.io/blog/20180927-swisstables
/// [here]: https://github.com/abseil/abseil-cpp/blob/master/absl/container/internal/raw_hash_set.h
/// [CppCon talk]: https://www.youtube.com/watch?v=ncHmEUmJZf4
///
/// # Examples
///
/// ```rune
/// use std::collections::HashMap;
///
/// enum Tile {
/// Wall,
/// }
///
/// let m = HashMap::new();
///
/// m.insert((0, 1), Tile::Wall);
/// m[(0, 3)] = 5;
///
/// assert_eq!(m.get((0, 1)), Some(Tile::Wall));
/// assert_eq!(m.get((0, 2)), None);
/// assert_eq!(m[(0, 3)], 5);
/// ```
#[derive(Any)]
#[rune(item = ::std::collections::hash_map)]
pub(crate) struct HashMap {
Expand Down
43 changes: 43 additions & 0 deletions crates/rune/src/modules/collections/hash_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,49 @@ pub fn module() -> Result<Module, ContextError> {
Ok(m)
}

/// A [hash set] implemented as a `HashMap` where the value is `()`.
///
/// As with the [`HashMap`] type, a `HashSet` requires that the elements
/// implement the [`EQ`] and [`HASH`] protocols. If you implement these
/// yourself, it is important that the following property holds:
///
/// ```text
/// k1 == k2 -> hash(k1) == hash(k2)
/// ```
///
/// In other words, if two keys are equal, their hashes must be equal. Violating
/// this property is a logic error.
///
/// It is also a logic error for a key to be modified in such a way that the
/// key's hash, as determined by the [`HASH`] protocol, or its equality, as
/// determined by the [`EQ`] protocol, changes while it is in the map. This is
/// normally only possible through [`Cell`], [`RefCell`], global state, I/O, or
/// unsafe code.
///
/// The behavior resulting from either logic error is not specified, but will be
/// encapsulated to the `HashSet` that observed the logic error and not result
/// in undefined behavior. This could include panics, incorrect results, aborts,
/// memory leaks, and non-termination.
///
/// [hash set]: crate::collections#use-the-set-variant-of-any-of-these-maps-when
/// [`HashMap`]: crate::collections::HashMap
///
/// # Examples
///
/// ```rune
/// use std::collections::HashSet;
///
/// enum Tile {
/// Wall,
/// }
///
/// let m = HashSet::new();
///
/// m.insert((0, 1));
///
/// assert!(m.contains((0, 1)));
/// assert!(!m.contains((0, 2)));
/// ```
#[derive(Any)]
#[rune(module = crate, item = ::std::collections::hash_set)]
pub(crate) struct HashSet {
Expand Down
20 changes: 10 additions & 10 deletions crates/rune/src/runtime/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,11 @@ impl Vm {
/// println!("output: {}", output);
/// # Ok::<_, rune::support::Error>(())
/// ```
pub fn execute<A, N>(&mut self, name: N, args: A) -> Result<VmExecution<&mut Self>, VmError>
where
N: ToTypeHash,
A: Args,
{
pub fn execute(
&mut self,
name: impl ToTypeHash,
args: impl Args,
) -> Result<VmExecution<&mut Self>, VmError> {
self.set_entrypoint(name, args.count())?;
args.into_stack(&mut self.stack).into_result()?;
Ok(VmExecution::new(self))
Expand All @@ -397,11 +397,11 @@ impl Vm {
/// This is accomplished by preventing values escaping from being
/// non-exclusively sent with the execution or escaping the execution. We
/// only support encoding arguments which themselves are `Send`.
pub fn send_execute<A, N>(mut self, name: N, args: A) -> Result<VmSendExecution, VmError>
where
N: ToTypeHash,
A: Send + Args,
{
pub fn send_execute(
mut self,
name: impl ToTypeHash,
args: impl Args + Send,
) -> Result<VmSendExecution, VmError> {
// Safety: make sure the stack is clear, preventing any values from
// being sent along with the virtual machine.
self.stack.clear();
Expand Down
59 changes: 32 additions & 27 deletions crates/rune/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub(crate) mod prelude {
pub(crate) use crate::tests::{eval, run};
pub(crate) use crate::{
from_value, prepare, sources, span, vm_try, Any, Context, ContextError, Diagnostics,
FromValue, Hash, Item, ItemBuf, Module, Source, Sources, Value, Vm,
FromValue, Hash, Item, ItemBuf, Module, Options, Source, Sources, Value, Vm,
};
pub(crate) use futures_executor::block_on;

Expand All @@ -43,11 +43,10 @@ use ::rust_alloc::sync::Arc;

use anyhow::{Context as _, Error, Result};

use crate::alloc;
use crate::item::IntoComponent;
use crate::runtime::{Args, VmError};
use crate::{
termcolor, BuildError, Context, Diagnostics, FromValue, ItemBuf, Source, Sources, Unit, Vm,
alloc, termcolor, BuildError, Context, Diagnostics, FromValue, Hash, Options, Source, Sources,
Unit, Vm,
};

/// An error that can be raised during testing.
Expand Down Expand Up @@ -97,9 +96,13 @@ pub fn compile_helper(source: &str, diagnostics: &mut Diagnostics) -> Result<Uni
let mut sources = Sources::new();
sources.insert(Source::new("main", source)?)?;

let mut options = Options::default();
options.script(true);

let unit = crate::prepare(&mut sources)
.with_context(&context)
.with_diagnostics(diagnostics)
.with_options(&options)
.build()?;

Ok(unit)
Expand All @@ -111,10 +114,18 @@ pub fn vm(
context: &Context,
sources: &mut Sources,
diagnostics: &mut Diagnostics,
script: bool,
) -> Result<Vm, TestError> {
let mut options = Options::default();

if script {
options.script(true);
}

let result = crate::prepare(sources)
.with_context(context)
.with_diagnostics(diagnostics)
.with_options(&options)
.build();

let Ok(unit) = result else {
Expand All @@ -134,24 +145,23 @@ pub fn vm(

/// Call the specified function in the given script sources.
#[doc(hidden)]
pub fn run_helper<N, A, T>(
pub fn run_helper<T>(
context: &Context,
sources: &mut Sources,
diagnostics: &mut Diagnostics,
function: N,
args: A,
args: impl Args,
script: bool,
) -> Result<T, TestError>
where
N: IntoIterator,
N::Item: IntoComponent,
A: Args,
T: FromValue,
{
let mut vm = vm(context, sources, diagnostics)?;
let mut vm = vm(context, sources, diagnostics, script)?;

let item = ItemBuf::with_item(function)?;

let mut execute = vm.execute(&item, args).map_err(TestError::VmError)?;
let mut execute = if script {
vm.execute(Hash::EMPTY, args).map_err(TestError::VmError)?
} else {
vm.execute(["main"], args).map_err(TestError::VmError)?
};

let output = ::futures_executor::block_on(execute.async_complete())
.into_result()
Expand All @@ -170,19 +180,16 @@ pub fn sources(source: &str) -> Sources {
}

/// Run the given source with diagnostics being printed to stderr.
pub fn run<N, A, T>(context: &Context, source: &str, function: N, args: A) -> Result<T>
pub fn run<T>(context: &Context, source: &str, args: impl Args, script: bool) -> Result<T>
where
N: IntoIterator,
N::Item: IntoComponent,
A: Args,
T: FromValue,
{
let mut sources = Sources::new();
sources.insert(Source::new("main", source)?)?;
sources.insert(Source::memory(source)?)?;

let mut diagnostics = Default::default();

let e = match run_helper(context, &mut sources, &mut diagnostics, function, args) {
let e = match run_helper(context, &mut sources, &mut diagnostics, args, script) {
Ok(value) => return Ok(value),
Err(e) => e,
};
Expand Down Expand Up @@ -229,7 +236,7 @@ where
let source = source.as_ref();
let context = Context::with_default_modules().expect("Failed to build context");

match run(&context, source, ["main"], ()) {
match run(&context, source, (), true) {
Ok(output) => output,
Err(error) => {
panic!("Program failed to run:\n{error}\n{source}");
Expand Down Expand Up @@ -257,10 +264,10 @@ macro_rules! rune_assert {
/// of native Rust data. This also accepts a tuple of arguments in the second
/// position, to pass native objects as arguments to the script.
macro_rules! rune_n {
($module:expr, $args:expr, $ty:ty => $($tt:tt)*) => {{
($(mod $module:expr,)* $args:expr, $($tt:tt)*) => {{
let mut context = $crate::Context::with_default_modules().expect("Failed to build context");
context.install($module).expect("Failed to install native module");
$crate::tests::run::<_, _, $ty>(&context, stringify!($($tt)*), ["main"], $args).expect("Program ran unsuccessfully")
$(context.install(&$module).expect("Failed to install native module");)*
$crate::tests::run(&context, stringify!($($tt)*), $args, false).expect("Program ran unsuccessfully")
}};
}

Expand All @@ -277,7 +284,7 @@ macro_rules! assert_vm_error {
let mut diagnostics = Default::default();

let mut sources = $crate::tests::sources($source);
let e = match $crate::tests::run_helper::<_, _, $ty>(&context, &mut sources, &mut diagnostics, ["main"], ()) {
let e = match $crate::tests::run_helper::<$ty>(&context, &mut sources, &mut diagnostics, (), true) {
Err(e) => e,
actual => {
expected!("program error", Err(e), actual, $source)
Expand Down Expand Up @@ -430,8 +437,6 @@ mod builtin_macros;
#[cfg(not(miri))]
mod capture;
#[cfg(not(miri))]
mod collections;
#[cfg(not(miri))]
mod comments;
#[cfg(not(miri))]
mod compiler_docs;
Expand Down
Loading

0 comments on commit 496577c

Please sign in to comment.