Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add sidetable #72

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/core/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::core::indices::GlobalIdx;
use crate::validation_stack::LabelKind;
use crate::validation_stack::LabelInfo;
use core::fmt::{Display, Formatter};
use core::str::Utf8Error;

Expand Down Expand Up @@ -28,6 +28,7 @@ pub enum Error {
InvalidNumType,
InvalidVecType,
InvalidFuncType,
InvalidFuncTypeIdx,
InvalidRefType,
InvalidValType,
InvalidExportDesc(u8),
Expand All @@ -44,7 +45,8 @@ pub enum Error {
InvalidGlobalIdx(GlobalIdx),
GlobalIsConst,
RuntimeError(RuntimeError),
FoundLabel(LabelKind),
FoundLabel(LabelInfo),
InvalidLabelIdx,
}

impl Display for Error {
Expand Down Expand Up @@ -75,6 +77,9 @@ impl Display for Error {
Error::InvalidFuncType => {
f.write_str("An invalid byte was read where a functype was expected")
}
Error::InvalidFuncTypeIdx => {
f.write_str("An invalid index to the fuctypes table was read")
}
Error::InvalidRefType => {
f.write_str("An invalid byte was read where a reftype was expected")
}
Expand Down Expand Up @@ -108,16 +113,17 @@ impl Display for Error {
"An invalid mut/const byte was found: {byte:#x?}"
)),
Error::MoreThanOneMemory => {
f.write_str("As of not only one memory is allowed per module.")
f.write_str("As of now only one memory is allowed per module.")
}
Error::InvalidGlobalIdx(idx) => f.write_fmt(format_args!(
"An invalid global index `{idx}` was specified"
)),
Error::GlobalIsConst => f.write_str("A const global cannot be written to"),
Error::RuntimeError(err) => err.fmt(f),
Error::FoundLabel(lk) => f.write_fmt(format_args!(
"Expecting a ValType, a Label was found: {lk:?}"
Error::FoundLabel(label_info) => f.write_fmt(format_args!(
"Expecting a ValType, a Label was found: {label_info:?}"
)),
Error::InvalidLabelIdx => f.write_str("An invalid index to a label was read"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod error;

pub mod indices;
pub mod reader;
pub mod sidetable;
4 changes: 2 additions & 2 deletions src/core/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ pub mod span {
/// indexing unknown slices, as a [Span] does not validate the length of the indexed slice.
#[derive(Copy, Clone, Debug, Hash)]
pub struct Span {
pub(super) from: usize,
pub(super) len: usize,
pub(crate) from: usize,
pub(crate) len: usize,
}

impl Span {
Expand Down
80 changes: 80 additions & 0 deletions src/core/reader/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,86 @@ impl WasmReadable for FuncType {
}
}

/// <https://webassembly.github.io/spec/core/binary/instructions.html#binary-blocktype>
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockType {
Empty,
Returns(ValType),
Type(u32),
}

impl WasmReadable for BlockType {
fn read(wasm: &mut WasmReader) -> Result<Self> {
// FIXME: Use transactions for ValType::read
if wasm.peek_u8()? as i8 == 0x40 {
// Empty block type
let _ = wasm.read_u8().expect("read to succeed, as we just peeked");
Ok(BlockType::Empty)
} else if let Ok(val_ty) = ValType::read(wasm) {
// No parameters and given valtype as the result
Ok(BlockType::Returns(val_ty))
} else {
// An index to a function type
wasm.read_var_i33()
.and_then(|idx| idx.try_into().map_err(|_| Error::InvalidFuncTypeIdx))
.map(BlockType::Type)
}
}

fn read_unvalidated(wasm: &mut WasmReader) -> Self {
if wasm.peek_u8().unwrap_validated() as i8 == 0x40 {
// Empty block type
let _ = wasm.read_u8();

BlockType::Empty
} else if let Ok(val_ty) = ValType::read(wasm) {
// No parameters and given valtype as the result
BlockType::Returns(val_ty)
} else {
// An index to a function type
BlockType::Type(
wasm.read_var_i33()
.unwrap_validated()
.try_into()
.unwrap_validated(),
)
}
}
}

impl BlockType {
pub fn as_func_type(&self, func_types: &[FuncType]) -> Result<FuncType> {
match self {
BlockType::Empty => Ok(FuncType {
params: ResultType {
valtypes: Vec::new(),
},
returns: ResultType {
valtypes: Vec::new(),
},
}),
BlockType::Returns(val_type) => Ok(FuncType {
params: ResultType {
valtypes: Vec::new(),
},
returns: ResultType {
valtypes: [val_type.clone()].into(),
},
}),
BlockType::Type(type_idx) => {
let type_idx: usize = (*type_idx)
.try_into()
.map_err(|_| Error::InvalidFuncTypeIdx)?;

func_types
.get(type_idx)
.cloned()
.ok_or_else(|| Error::InvalidFuncTypeIdx)
}
}
}
}

#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Limits {
pub min: u32,
Expand Down
11 changes: 10 additions & 1 deletion src/core/reader/types/opcode.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
//! All opcodes, in alphanumerical order by their numeric (hex-)value
pub const UNREACHABLE: u8 = 0x00;
pub const NOP: u8 = 0x01;
pub const BLOCK: u8 = 0x02;
pub const LOOP: u8 = 0x03;
pub const IF: u8 = 0x04;
pub const ELSE: u8 = 0x05;
pub const CALL: u8 = 0x10;
pub const END: u8 = 0x0B;
pub const BR: u8 = 0x0C;
pub const BR_IF: u8 = 0x0D;
pub const BR_TABLE: u8 = 0x0E;
pub const RETURN: u8 = 0x0F;
pub const CALL: u8 = 0x10;
pub const DROP: u8 = 0x1A;
pub const LOCAL_GET: u8 = 0x20;
pub const LOCAL_SET: u8 = 0x21;
pub const LOCAL_TEE: u8 = 0x22;
Expand Down
4 changes: 4 additions & 0 deletions src/core/reader/types/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ impl WasmReader<'_> {
Ok(result)
}

pub fn read_var_i33(&mut self) -> Result<i64> {
todo!("read 33-bit signed integer D:")
}

pub fn read_var_f32(&mut self) -> Result<u32> {
if self.full_wasm_binary.len() - self.pc < 4 {
return Err(Error::Eof);
Expand Down
117 changes: 117 additions & 0 deletions src/core/sidetable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! This module contains a data structure to allow in-place interpretation
//!
//! Control-flow in WASM is denoted in labels. To avoid linear search through the WASM binary or
//! stack for the respective label of a branch, a sidetable is generated during validation, which
//! stores the offset on the current instruction pointer for the branch. A sidetable entry hence
//! allows to translate the implicit control flow information ("jump to the next `else`") to
//! explicit modifications of the instruction pointer (`instruction_pointer += 13`).
//!
//! Branches in WASM can only go outwards, they either `break` out of a block or `continue` to the
//! head of a loob block. Put differently, a label can only be referenced from within its
//! associated structured control instruction.
//!
//! Noteworthy, branching can also have side-effects on the operand stack:
//!
//! - Taking a branch unwinds the operand stack, down to where the targeted structured control flow
//! instruction was entered. [`SidetableEntry::popcnt`] holds information on how many values to
//! pop from the operand stack when a branch is taken.
//! - When a branch is taken, it may consume arguments from the operand stack. These are pushed
//! back on the operand stack after unwinding. This behavior can be emulated by copying the
//! uppermost [`SidetableEntry::valcnt`] operands on the operand stack before taking a branch
//! into a structured control instruction.
//!
//! # Relevant instructions
//! **Sidetable jump origins (and how many ST entries they require)**
//! - br (1)
//! - br_if (1)
//! - br_table (num_labels + 1 for default label)
//! - if (2, maybe 1??)

//! **Sidetable jump targets**
//! - end of block
//! - loop
//! - else
//! - end of else block
//!
//! # Reference
//!
//! - Core / Syntax / Instructions / Control Instructions, WASM Spec,
//! <https://webassembly.github.io/spec/core/syntax/instructions.html#control-instructions>
//! - "A fast in-place interpreter for WebAssembly", Ben L. Titzer,
//! <https://arxiv.org/abs/2205.01183>

use alloc::vec::Vec;

use crate::{Error, Result};

/// A sidetable
pub type Sidetable = Vec<SidetableEntry>;

/// Entry to translate the current branches implicit target into an explicit offset to the instruction pointer, as well as the side table pointer
///
/// Each of the following constructs requires a [`SidetableEntry`]:
///
/// - br
/// - br_if
/// - br_table
/// - else
#[derive(Copy, Clone)]
pub struct SidetableEntry {
/// Δpc: the amount to adjust the instruction pointer by if the branch is taken
pub delta_pc: isize,

/// Δstp: the amount to adjust the side-table index by if the branch is taken
pub delta_stp: isize,

/// valcnt: the number of values that will be copied if the branch is taken
///
/// Branches may additionally consume operands themselves, which they push back on the operand
/// stack after unwinding.
pub val_count: usize,

/// popcnt: the number of values that will be popped if the branch is taken
///
/// Taking a branch unwinds the operand stack down to the height where the targeted structured
/// control instruction was entered.
pub pop_count: usize,
}

impl core::fmt::Debug for SidetableEntry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SidetableEntry")
.field("Δpc", &self.delta_pc)
.field("Δstp", &self.delta_stp)
.field("valcnt", &self.val_count)
.field("popcnt", &self.pop_count)
.finish()
}
}

pub struct IncompleteSidetableEntry {
pub ip: usize,
pub delta_ip: Option<isize>,
pub delta_stp: Option<isize>,
pub val_count: usize,
pub pop_count: usize,
}

pub struct SidetableBuilder(pub Vec<IncompleteSidetableEntry>);

impl SidetableBuilder {
pub fn new() -> Self {
Self(Vec::new())
}

// This panics if some sidetable entries still contain any None fields.
pub fn into_sidetable(self) -> Sidetable {
self.0
.into_iter()
.map(|entry| SidetableEntry {
delta_pc: entry.delta_ip.expect("Failed to generate sidetable"),
delta_stp: entry.delta_stp.expect("Failed to generate sidetable"),
val_count: entry.val_count,
pop_count: entry.pop_count,
})
.collect()
}
}
Loading
Loading