Skip to content

Commit

Permalink
Add support for new exception handling proposal
Browse files Browse the repository at this point in the history
The exception handling proposal switched its design from
an exnref-based approach with first-class exception reference
values to a version with tagged catch blocks and implicit
exception references.

This commit adds support for this new semantics and revises
tests to match.

It does not yet remove exnref, as that can be done in a separate
patch.

Note: it does not add the `delegate` instruction in the new
proposal yet, as it does not yet have an opcode assigned.
  • Loading branch information
takikawa committed Dec 1, 2020
1 parent 74be3f6 commit a608502
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 74 deletions.
9 changes: 5 additions & 4 deletions crates/wasmparser/src/binary_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,15 +1101,16 @@ impl<'a> BinaryReader<'a> {
0x06 => Operator::Try {
ty: self.read_blocktype()?,
},
0x07 => Operator::Catch,
0x07 => Operator::Catch {
index: self.read_var_u32()?,
},
0x08 => Operator::Throw {
index: self.read_var_u32()?,
},
0x09 => Operator::Rethrow,
0x0a => Operator::BrOnExn {
0x09 => Operator::Rethrow {
relative_depth: self.read_var_u32()?,
index: self.read_var_u32()?,
},
0x0a => Operator::Unwind,
0x0b => Operator::End,
0x0c => Operator::Br {
relative_depth: self.read_var_u32()?,
Expand Down
63 changes: 44 additions & 19 deletions crates/wasmparser/src/operators_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ enum FrameKind {
Loop,
Try,
Catch,
CatchAll,
Unwind,
}

impl OperatorValidator {
Expand Down Expand Up @@ -567,10 +569,23 @@ impl OperatorValidator {
}
Operator::Else => {
let frame = self.pop_ctrl(resources)?;
if frame.kind != FrameKind::If {
bail_op_err!("else found outside of an `if` block");
// The `catch_all` instruction shares an opcode with `else`,
// so we check the frame to see how it's interpreted.
if self.features.exceptions
&& (frame.kind == FrameKind::Try || frame.kind == FrameKind::Catch)
{
self.control.push(Frame {
kind: FrameKind::CatchAll,
block_type: frame.block_type,
height: self.operands.len(),
unreachable: false,
});
} else {
if frame.kind != FrameKind::If {
bail_op_err!("else found outside of an `if` block");
}
self.push_ctrl(FrameKind::Else, frame.block_type, resources)?;
}
self.push_ctrl(FrameKind::Else, frame.block_type, resources)?;
}
Operator::Try { ty } => {
self.check_exceptions_enabled()?;
Expand All @@ -580,10 +595,10 @@ impl OperatorValidator {
}
self.push_ctrl(FrameKind::Try, ty, resources)?;
}
Operator::Catch => {
Operator::Catch { index } => {
self.check_exceptions_enabled()?;
let frame = self.pop_ctrl(resources)?;
if frame.kind != FrameKind::Try {
if frame.kind != FrameKind::Try && frame.kind != FrameKind::Catch {
bail_op_err!("catch found outside of an `try` block");
}
// Start a new frame and push `exnref` value.
Expand All @@ -593,7 +608,11 @@ impl OperatorValidator {
height: self.operands.len(),
unreachable: false,
});
self.push_operand(Type::ExnRef)?;
// Push exception argument types.
let ty = func_type_at(&resources, index)?;
for ty in ty.inputs() {
self.push_operand(ty)?;
}
}
Operator::Throw { index } => {
self.check_exceptions_enabled()?;
Expand All @@ -607,24 +626,30 @@ impl OperatorValidator {
}
self.unreachable();
}
Operator::Rethrow => {
Operator::Rethrow { relative_depth } => {
self.check_exceptions_enabled()?;
self.pop_operand(Some(Type::ExnRef))?;
// This is not a jump, but we need to check that the `rethrow`
// targets an actual `catch` to get the exception.
let (_, kind) = self.jump(relative_depth)?;
if kind != FrameKind::Catch && kind != FrameKind::CatchAll {
bail_op_err!("rethrow target was not a `catch` block");
}
self.unreachable();
}
Operator::BrOnExn {
relative_depth,
index,
} => {
Operator::Unwind => {
self.check_exceptions_enabled()?;
let (ty, kind) = self.jump(relative_depth)?;
self.pop_operand(Some(Type::ExnRef))?;
// Check the exception's argument values with target block's.
let exn_args = func_type_at(&resources, index)?;
if Iterator::ne(exn_args.inputs(), label_types(ty, resources, kind)?) {
bail_op_err!("target block types do not match");
// Switch from `try` to an `unwind` frame, so we can check that
// the result type is empty.
let frame = self.pop_ctrl(resources)?;
if frame.kind != FrameKind::Try {
bail_op_err!("unwind found outside of an `try` block");
}
self.push_operand(Type::ExnRef)?;
self.control.push(Frame {
kind: FrameKind::Unwind,
block_type: TypeOrFuncType::Type(Type::EmptyBlockType),
height: self.operands.len(),
unreachable: false,
});
}
Operator::End => {
let mut frame = self.pop_ctrl(resources)?;
Expand Down
6 changes: 3 additions & 3 deletions crates/wasmparser/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ pub enum Operator<'a> {
If { ty: TypeOrFuncType },
Else,
Try { ty: TypeOrFuncType },
Catch,
Catch { index: u32 },
Throw { index: u32 },
Rethrow,
BrOnExn { relative_depth: u32, index: u32 },
Rethrow { relative_depth: u32 },
Unwind,
End,
Br { relative_depth: u32 },
BrIf { relative_depth: u32 },
Expand Down
19 changes: 8 additions & 11 deletions crates/wasmprinter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ impl Printer {
// `else`/`catch` are special in that it's printed at
// the previous indentation, but it doesn't actually change
// our nesting level.
Operator::Else | Operator::Catch => {
Operator::Else | Operator::Catch { .. } | Operator::Unwind => {
self.nesting -= 1;
self.newline();
self.nesting += 1;
Expand Down Expand Up @@ -729,24 +729,21 @@ impl Printer {
self.print_blockty(ty)?;
write!(self.result, " ;; label = @{}", cur_label)?;
}
Catch => self.result.push_str("catch"),
Catch { index } => {
write!(self.result, "catch {}", index)?;
}
Throw { index } => {
write!(self.result, "throw {}", index)?;
}
Rethrow => self.result.push_str("rethrow"),
BrOnExn {
relative_depth,
index,
} => {
Rethrow { relative_depth } => {
write!(
self.result,
"br_on_exn {} (;{};)",
"rethrow {} (;{};)",
relative_depth,
label(*relative_depth),
label(*relative_depth)
)?;
write!(self.result, " {}", index)?;
}

Unwind => self.result.push_str("unwind"),
End => self.result.push_str("end"),
Br { relative_depth } => {
write!(
Expand Down
58 changes: 47 additions & 11 deletions crates/wast/src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ enum If<'a> {
enum Try<'a> {
/// Next thing to parse is the `do` block.
Do(Instruction<'a>),
/// Next thing to parse is the `catch` block.
/// Next thing to parse is `catch`/`catch_all`, or `unwind`.
CatchOrUnwind,
/// Next thing to parse is a `catch` block or `catch_all`.
Catch,
/// This `try` statement has finished parsing and if anything remains it's a
/// syntax error.
Expand Down Expand Up @@ -195,8 +197,8 @@ impl<'a> ExpressionParser<'a> {
Level::Try(Try::Do(_)) => {
return Err(parser.error("previous `try` had no `do`"));
}
Level::Try(Try::Catch) => {
return Err(parser.error("previous `try` had no `catch`"));
Level::Try(Try::CatchOrUnwind) => {
return Err(parser.error("previous `try` had no `catch`, `catch_all`, or `unwind`"));
}
Level::Try(_) => {
self.instrs.push(Instruction::End(None));
Expand Down Expand Up @@ -305,7 +307,7 @@ impl<'a> ExpressionParser<'a> {
/// than an `if` as the syntactic form is:
///
/// ```wat
/// (try (do $do) (catch $catch))
/// (try (do $do) (catch $event $catch))
/// ```
///
/// where the `do` and `catch` keywords are mandatory, even for an empty
Expand All @@ -328,7 +330,7 @@ impl<'a> ExpressionParser<'a> {
if parser.parse::<Option<kw::r#do>>()?.is_some() {
// The state is advanced here only if the parse succeeds in
// order to strictly require the keyword.
*i = Try::Catch;
*i = Try::CatchOrUnwind;
self.stack.push(Level::TryArm);
return Ok(true);
}
Expand All @@ -338,17 +340,50 @@ impl<'a> ExpressionParser<'a> {
return Ok(false);
}

// `catch` handled similar to `do`, including requiring the keyword.
if let Try::Catch = i {
self.instrs.push(Instruction::Catch);
// After a try's `do`, there are several possible kinds of handlers.
if let Try::CatchOrUnwind = i {
// `catch` may be followed by more `catch`s or `catch_all`.
if parser.parse::<Option<kw::catch>>()?.is_some() {
let evt = parser.parse::<ast::Index<'a>>()?;
self.instrs.push(Instruction::Catch(evt));
*i = Try::Catch;
self.stack.push(Level::TryArm);
return Ok(true);
}
// `catch_all` can only come at the end and has no argument.
if parser.parse::<Option<kw::catch_all>>()?.is_some() {
self.instrs.push(Instruction::CatchAll);
*i = Try::End;
self.stack.push(Level::TryArm);
return Ok(true);
}
// `unwind` is similar to `catch_all`.
if parser.parse::<Option<kw::unwind>>()?.is_some() {
self.instrs.push(Instruction::Unwind);
*i = Try::End;
self.stack.push(Level::TryArm);
return Ok(true);
}
return Ok(false);
}

if let Try::Catch = i {
if parser.parse::<Option<kw::catch>>()?.is_some() {
let evt = parser.parse::<ast::Index<'a>>()?;
self.instrs.push(Instruction::Catch(evt));
*i = Try::Catch;
self.stack.push(Level::TryArm);
return Ok(true);
}
if parser.parse::<Option<kw::catch_all>>()?.is_some() {
self.instrs.push(Instruction::CatchAll);
*i = Try::End;
self.stack.push(Level::TryArm);
return Ok(true);
}
return Err(parser.error("unexpected items after `catch`"));
}

Err(parser.error("too many payloads inside of `(try)`"))
}
}
Expand Down Expand Up @@ -997,11 +1032,12 @@ instructions! {
V128Load64Zero(MemArg<8>) : [0xfd, 0xfd] : "v128.load64_zero",

// Exception handling proposal
CatchAll : [0x05] : "catch_all", // Reuses the else opcode.
Try(BlockType<'a>) : [0x06] : "try",
Catch : [0x07] : "catch",
Catch(ast::Index<'a>) : [0x07] : "catch",
Throw(ast::Index<'a>) : [0x08] : "throw",
Rethrow : [0x09] : "rethrow",
BrOnExn(BrOnExn<'a>) : [0x0a] : "br_on_exn",
Rethrow(ast::Index<'a>) : [0x09] : "rethrow",
Unwind : [0x0a] : "unwind",
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/wast/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ pub mod kw {
custom_keyword!(binary);
custom_keyword!(block);
custom_keyword!(catch);
custom_keyword!(catch_all);
custom_keyword!(code);
custom_keyword!(data);
custom_keyword!(declare);
Expand Down Expand Up @@ -416,6 +417,7 @@ pub mod kw {
custom_keyword!(table);
custom_keyword!(then);
custom_keyword!(r#try = "try");
custom_keyword!(unwind);
custom_keyword!(v128);
}

Expand Down
9 changes: 6 additions & 3 deletions crates/wast/src/resolve/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1950,10 +1950,13 @@ impl<'a, 'b> ExprResolver<'a, 'b> {
Throw(i) => {
self.module.resolve(i, Ns::Event)?;
}
BrOnExn(b) => {
self.resolve_label(&mut b.label)?;
self.module.resolve(&mut b.exn, Ns::Event)?;
Rethrow(i) => {
self.resolve_label(i)?;
}
Catch(i) => {
self.module.resolve(i, Ns::Event)?;
}

BrOnCast(b) => {
self.resolve_label(&mut b.label)?;
self.module.resolve_heaptype(&mut b.val)?;
Expand Down
58 changes: 43 additions & 15 deletions tests/local/exception-handling.wast
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
;; --enable-exceptions --enable-multi-value
(module
(type (func (param i32 i64)))
(type (func (param i32)))
(event (type 0))
(func $check-br_on_exn-rethrow (param exnref)
block $l (result i32 i64)
local.get 0
;; exnref $e is on the stack at this point
br_on_exn $l 0 ;; branch to $l with $e's arguments
rethrow
end
drop
drop
)
(event (type 1))
(func $check-throw
i32.const 1
i64.const 2
throw 0
)
(func $check-try-w-calls (result i32)
try (result i32)
(func $check-try-catch-rethrow
try (result i32 i64)
call $check-throw
i32.const 0
catch
call $check-br_on_exn-rethrow
unreachable
catch 0
;; the exception arguments are on the stack at this point
catch 1
i64.const 2
catch_all
rethrow 0
end
drop
drop
)
(func $check-unwind (local i32)
try
i32.const 1
local.set 0
call $check-throw
unwind
i32.const 0
local.set 0
end
)
)

(assert_invalid
(module
(func try catch_all catch_all end))
;; we can't distinguish between `catch_all` and `else` in error cases
"else found outside of an `if` block")

(assert_invalid
(module
(func try catch_all catch 0 end))
"catch found outside of an `try` block")

(assert_invalid
(module
(func try unwind i32.const 1 end))
"type mismatch: values remaining on stack at end of block")

(assert_invalid
(module
(func block try catch_all rethrow 1 end end))
"rethrow target was not a `catch` block")
Loading

0 comments on commit a608502

Please sign in to comment.