Skip to content

Commit

Permalink
Implement reference-types Wasm proposal (#635)
Browse files Browse the repository at this point in the history
* add reference_types flag to Config

* enable all Wasm spec tests again

Many are failing at the moment since reference-types is not yet implemented in wasmi. The goal of this PR is to make all tests pass again.

* make table.copy and table.init instructions take table parameters

* make call.indirect take table parameter

* make FuncIdx use NonZeroU32

This allows to have size_of<Func> == size_of<Option<Func>>

* remove some From impls for Value

* create FuncRef type in wasmi

* add more Func and FuncRef size_of tests

* add test comment

* add FuncRef <-> UntypedValue conversions

* add ValueType::FuncRef

* make Value no longer Copy

This is in preparation for the introduction of the ExternRef type that is going to be a non-Copy type since internally it will be reference counted.

* add ExternRef type to wasmi

* fix no_std compilation

* fix another silly no_std compile error

* remove From<&Value> impl for UntypedValue

* re-design ExternRef

It no longer owns its data itself but uses to the store to do so just like any other entity in wasmi.

* add accessor methods to Value

Thesse methods were taken over from the Wasmtime API.

* apply clippy suggestion

* make Value no longer impl PartialEq

Just like in the Wasmtime API. Better mirroring.

* improve docs

* add ExternRef to Value

This also removes the Display impl for Value.
The wasmi CLI tool now instead implements its own way to display Value which makes more sense anyways.

* remove Display impl for FuncType

The Display implementation now resides in wasmi_cli which was its only user and makes more sense since Display impls usually are biased.

* add Default derive for ExternRef

* remove PartialEq impl of FuncRef

* remove TryInto API from Value

This better mirrors the Wasmtime API.

* remove Value::try_into

* add From<FuncRef> and From<ExternRef> for Value

* fix UntypedValue <-> [Func|Extern]Ref bugs with null values

* apply rustfmt

* fix internal doc link

* add additional conversion checks for safety

* apply rustfmt

* add element type to TableType

Note that the Table is still working as before.

* make Table use ValueType and Value

Internally TableEntity uses UntypedValue for efficiency reasons.

* fix bug in Arena::get_pair_mut

* make Table::get return Option instead of Result

* implement Table::{get, set} instructions

* add docs

* implement table.size Wasm instruction

* implement table.grow Wasm instruction

* add docs to TableEntity::grow_untyped

* implement Table[Entity]::fill method

* implement table.fill Wasm instruction

* add missing spectest global_i64 definition

* implement more Wast decodings for reference-types

* implement typed_select, ref.null and ref.is_null Wasm instructions

* allow reftypes in Element parsing

* cleanup assert

* add ValueType::{is_num, is_ref} methods

These are copied over from the Wasmtime API.

* properly adjust stack height for new table instructions

* allow RefNull and FuncRef in InitExpr

* dispatch on ValueType for evaluating InitExpr

* change error message to be closer to what Wasm spec testsuite expects

* improve wast testsuite

* fix bug in ExternObjectEntity::data and add test

* extend wast testsuite

* adjust TableOutOfBounds trap message and relax assertions

* apply rustfmt

* apply clippy suggestions

* adjust trap message

* improve trap message

* handle Wasm declared element segments

* account for element type when matching table types

* cleanup instantiation code a bit

* perform some renames and further cleanups

* more cleanups

* more linker cleanups

* improve LinkerError

* more cleanups

* apply rustfmt

* update docs for satisfies methods

* apply clippy suggestion

* fix internal doc link

* improve docs

* rename methods

* refactor check_subtype

* apply clippy suggestions

* implement ref.func Wasm instruction

* fix ModuleResources::global_get method

* improve global.get optimization

* minor cleanup

* update Wasm spec testsuite

* remove debug println

* cleanup testsuite runner

* remove unnecessary match arm

even Wasmtime does not know how to compare funcrefs according to the Wast specification. Therefore I think it has not been designed, yet.

* add print_i64 to Wasm spec testsuite module

* use Self were possible

* add temporary fix

* fix TableEntity::copy_within

* improve Table docs

* fix bug in Arena::get_pair_mut

* cleanup code after bug fix

* add element type check to table.copy

* move comment

* add missing #[repr(transparent)]

* cleanup error handling

* move more into chained iter in TableEntity::init

* fix bug in table.init procedures

* improve Wasm spec testsuite runner

* simplify MemoryType::is_subtype_of check

* simplify TableType::is_subtype_of procesures

* cleanup

* rename method

* improve MemoryError and TableError for subtyping

* cleanup linker errors for invalid subtypes

* improve Wasm spec testsuite runner error reporting

* apply rustfmt

* add Memory::import_ty method

This fixes a bug with import subtyping.
Apparently subtyping is concerned with dynamic types and not with static types.

* add Table::import_ty method for same reason as Memory::import_ty

* make Table::import_ty pub(crate)

* rename import_ty -> dynamic_ty

better name for the same thing

* remove clippy allowance

* table.init bail out earlier for len == 0

* apply rustfmt

* canonicalize FuncRef::null instances

Now all FuncRef::null() instances are always guaranteed to be encoded as 0.

* fi spelling in doc

* move safety comment to unsafe block

* canonicalize ExternRef::null() values properly

* fix bug in Executor::visit_table_copy
  • Loading branch information
Robbepop authored Jan 31, 2023
1 parent 90019d1 commit c15d525
Show file tree
Hide file tree
Showing 40 changed files with 2,192 additions and 972 deletions.
12 changes: 9 additions & 3 deletions crates/arena/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ mod tests;
pub use self::{component_vec::ComponentVec, dedup::DedupArena, guarded::GuardedEntity};
use alloc::vec::Vec;
use core::{
cmp::max,
iter::{DoubleEndedIterator, Enumerate, ExactSizeIterator},
marker::PhantomData,
ops::{Index, IndexMut},
Expand Down Expand Up @@ -159,8 +158,15 @@ where
pub fn get_pair_mut(&mut self, fst: Idx, snd: Idx) -> Option<(&mut T, &mut T)> {
let fst_index = fst.into_usize();
let snd_index = snd.into_usize();
let max_index = max(fst_index, snd_index);
let (fst_set, snd_set) = self.entities.split_at_mut(max_index);
if fst_index == snd_index {
return None;
}
if fst_index > snd_index {
let (fst, snd) = self.get_pair_mut(snd, fst)?;
return Some((snd, fst));
}
// At this point we know that fst_index < snd_index.
let (fst_set, snd_set) = self.entities.split_at_mut(snd_index);
let fst = fst_set.get_mut(fst_index)?;
let snd = snd_set.get_mut(0)?;
Some((fst, snd))
Expand Down
169 changes: 164 additions & 5 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use anyhow::{anyhow, bail, Result};
use clap::Parser;
use core::fmt::Write;
use std::{
ffi::OsStr,
fmt::{self, Write},
fs,
path::{Path, PathBuf},
};
Expand Down Expand Up @@ -221,8 +221,9 @@ fn type_check_arguments(
) -> Result<Vec<Value>> {
if func_type.params().len() != func_args.len() {
bail!(
"invalid number of arguments given for {func_name} of type {func_type}. \
"invalid number of arguments given for {func_name} of type {}. \
expected {} argument but got {}",
DisplayFuncType(func_type),
func_type.params().len(),
func_args.len()
);
Expand Down Expand Up @@ -256,6 +257,12 @@ fn type_check_arguments(
.map(F64::from)
.map(Value::from)
.map_err(make_err!()),
ValueType::FuncRef => {
bail!("the wasmi CLI cannot take arguments of type funcref")
}
ValueType::ExternRef => {
bail!("the wasmi CLI cannot take arguments of type externref")
}
}
})
.collect::<Result<Vec<_>, _>>()?;
Expand All @@ -276,17 +283,39 @@ fn prepare_results_buffer(func_type: &FuncType) -> Vec<Value> {
fn print_execution_start(wasm_file: &Path, func_name: &str, func_args: &[Value]) {
print!("executing {wasm_file:?}::{func_name}(");
if let Some((first_arg, rest_args)) = func_args.split_first() {
print!("{first_arg}");
for arg in rest_args {
print!("{}", DisplayValue(first_arg));
for arg in rest_args.iter().map(DisplayValue) {
print!(", {arg}");
}
}
println!(") ...");
}

/// Wrapper type that implements `Display` for [`Value`].
struct DisplayValue<'a>(&'a Value);

impl<'a> fmt::Display for DisplayValue<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
Value::I32(value) => write!(f, "{value}"),
Value::I64(value) => write!(f, "{value}"),
Value::F32(value) => write!(f, "{value}"),
Value::F64(value) => write!(f, "{value}"),
Value::FuncRef(value) => panic!("cannot display funcref values but found {value:?}"),
Value::ExternRef(value) => {
panic!("cannot display externref values but found {value:?}")
}
}
}
}

/// Prints the results of the Wasm computation in a human readable form.
fn print_pretty_results(results: &[Value]) {
let pretty_results = results.iter().map(Value::to_string).collect::<Vec<_>>();
let pretty_results = results
.iter()
.map(DisplayValue)
.map(|v| v.to_string())
.collect::<Vec<_>>();
match pretty_results.len() {
1 => {
println!("{}", pretty_results[0]);
Expand All @@ -303,3 +332,133 @@ fn print_pretty_results(results: &[Value]) {
}
}
}

/// Wrapper type around [`FuncType`] that implements `Display` for it.
struct DisplayFuncType<'a>(&'a FuncType);

impl fmt::Display for DisplayFuncType<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "fn(")?;
let params = self.0.params();
let results = self.0.results();
write_slice(f, params, ",")?;
write!(f, ")")?;
if let Some((first, rest)) = results.split_first() {
write!(f, " -> ")?;
if !rest.is_empty() {
write!(f, "(")?;
}
write!(f, "{first}")?;
for result in rest {
write!(f, ", {result}")?;
}
if !rest.is_empty() {
write!(f, ")")?;
}
}
Ok(())
}
}

/// Writes the elements of a `slice` separated by the `separator`.
fn write_slice<T>(f: &mut fmt::Formatter, slice: &[T], separator: &str) -> fmt::Result
where
T: fmt::Display,
{
if let Some((first, rest)) = slice.split_first() {
write!(f, "{first}")?;
for param in rest {
write!(f, "{separator} {param}")?;
}
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use core::borrow::Borrow;

fn assert_display(func_type: impl Borrow<FuncType>, expected: &str) {
assert_eq!(
format!("{}", DisplayFuncType(func_type.borrow())),
String::from(expected),
);
}

#[test]
fn display_0in_0out() {
assert_display(FuncType::new([], []), "fn()");
}

#[test]
fn display_1in_0out() {
assert_display(FuncType::new([ValueType::I32], []), "fn(i32)");
}

#[test]
fn display_0in_1out() {
assert_display(FuncType::new([], [ValueType::I32]), "fn() -> i32");
}

#[test]
fn display_1in_1out() {
assert_display(
FuncType::new([ValueType::I32], [ValueType::I32]),
"fn(i32) -> i32",
);
}

#[test]
fn display_4in_0out() {
assert_display(
FuncType::new(
[
ValueType::I32,
ValueType::I64,
ValueType::F32,
ValueType::F64,
],
[],
),
"fn(i32, i64, f32, f64)",
);
}

#[test]
fn display_0in_4out() {
assert_display(
FuncType::new(
[],
[
ValueType::I32,
ValueType::I64,
ValueType::F32,
ValueType::F64,
],
),
"fn() -> (i32, i64, f32, f64)",
);
}

#[test]
fn display_4in_4out() {
assert_display(
FuncType::new(
[
ValueType::I32,
ValueType::I64,
ValueType::F32,
ValueType::F64,
],
[
ValueType::I32,
ValueType::I64,
ValueType::F32,
ValueType::F64,
],
),
"fn(i32, i64, f32, f64) -> (i32, i64, f32, f64)",
);
}
}
6 changes: 3 additions & 3 deletions crates/core/src/trap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,10 @@ impl TrapCode {
/// other uses since it avoid heap memory allocation in certain cases.
pub fn trap_message(&self) -> &'static str {
match self {
Self::UnreachableCodeReached => "unreachable",
Self::UnreachableCodeReached => "wasm `unreachable` instruction executed",
Self::MemoryOutOfBounds => "out of bounds memory access",
Self::TableOutOfBounds => "undefined element",
Self::IndirectCallToNull => "uninitialized element",
Self::TableOutOfBounds => "undefined element: out of bounds table access",
Self::IndirectCallToNull => "uninitialized element 2", // TODO: fixme, remove the trailing " 2" again
Self::IntegerDivisionByZero => "integer divide by zero",
Self::IntegerOverflow => "integer overflow",
Self::BadConversionToInteger => "invalid conversion to integer",
Expand Down
23 changes: 23 additions & 0 deletions crates/core/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ pub enum ValueType {
F32,
/// 64-bit IEEE 754-2008 floating point number.
F64,
/// A nullable function reference.
FuncRef,
/// A nullable external reference.
ExternRef,
}

impl ValueType {
/// Returns `true` if [`ValueType`] is a Wasm numeric type.
///
/// This is `true` for [`ValueType::I32`], [`ValueType::I64`],
/// [`ValueType::F32`] and [`ValueType::F64`].
pub fn is_num(&self) -> bool {
matches!(self, Self::I32 | Self::I64 | Self::F32 | Self::F64)
}

/// Returns `true` if [`ValueType`] is a Wasm reference type.
///
/// This is `true` for [`ValueType::FuncRef`] and [`ValueType::ExternRef`].
pub fn is_ref(&self) -> bool {
matches!(self, Self::ExternRef | Self::FuncRef)
}
}

impl Display for ValueType {
Expand All @@ -28,6 +49,8 @@ impl Display for ValueType {
Self::I64 => write!(f, "i64"),
Self::F32 => write!(f, "f32"),
Self::F64 => write!(f, "f64"),
Self::FuncRef => write!(f, "funcref"),
Self::ExternRef => write!(f, "externref"),
}
}
}
Expand Down
Loading

0 comments on commit c15d525

Please sign in to comment.