Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/layout/storage_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ fn consumer(merge_groups: bool) -> Vec<u8> {
span,
}],
memories: Vec::new(),
tags: Vec::new(),
data: Vec::new(),
globals: Vec::new(),
helpers: Vec::new(),
Expand Down
44 changes: 41 additions & 3 deletions crates/psrs-backend/src/wasm/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ use super::{Body, DataMode, ExportIndex, ExportKind, GlobalInit, Module, Op};
use crate::BackendError;
use std::borrow::Cow;
use wasm_encoder::{
BlockType, CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection, Elements,
EntityType, ExportKind as WasmExportKind, ExportSection, Function as EncoderFunction,
BlockType, Catch, CodeSection, ConstExpr, DataCountSection, DataSection, ElementSection,
Elements, EntityType, ExportKind as WasmExportKind, ExportSection, Function as EncoderFunction,
FunctionSection, GlobalSection, GlobalType, ImportSection, Instruction, MemorySection,
MemoryType, Module as EncoderModule, TypeSection, ValType,
MemoryType, Module as EncoderModule, TagKind, TagSection, TagType, TypeSection, ValType,
};

/// Encodes the thin Wasm IR into a binary module.
Expand Down Expand Up @@ -68,6 +68,20 @@ pub fn encode_module(module: &Module) -> Result<Vec<u8>, Vec<BackendError>> {
encoder.section(&memories);
}

// The tag section (id 13) sits between the memory and global sections per
// the WebAssembly 3.0 binary format. Tags are emitted only when the module
// carries a `TagDef`; which tags a module defines belongs to the lowering.
if !module.tags.is_empty() {
let mut tags = TagSection::new();
for tag in &module.tags {
tags.tag(TagType {
kind: TagKind::Exception,
func_type_idx: tag.type_index.0,
});
}
encoder.section(&tags);
}

if !module.globals.is_empty() {
let mut globals = GlobalSection::new();
for global in &module.globals {
Expand Down Expand Up @@ -207,6 +221,9 @@ fn collect_references(body: &Body, references: &mut Vec<u32>) {
Op::Block { body, .. } | Op::Loop { body, .. } => {
collect_references(body, references);
}
Op::Try { body, .. } => {
collect_references(body, references);
}
Op::Leaf(_) => {}
}
}
Expand Down Expand Up @@ -259,6 +276,27 @@ fn emit_body(body: &Body, function: &mut EncoderFunction) {
emit_body(body, function);
function.instruction(&Instruction::End);
}
Op::Try { body, clauses, .. } => {
let catches = clauses
.iter()
.map(|clause| match clause {
super::CatchClause::Catch { tag, label } => Catch::One {
tag: tag.0,
label: *label,
},
super::CatchClause::CatchAll { label } => Catch::All { label: *label },
})
.collect::<Vec<_>>();
// The thin encoding's try_table region always has an empty
// block type: the protected region's values flow through
// ordinary branches, never through the region's own label.
function.instruction(&Instruction::TryTable(
BlockType::Empty,
Cow::Owned(catches),
));
emit_body(body, function);
function.instruction(&Instruction::End);
}
}
}
}
1 change: 1 addition & 0 deletions crates/psrs-backend/src/wasm/lower/codec/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fn decode_module(bytes: &[u8]) -> Module {
minimum: 1,
maximum: None,
}],
tags: Vec::new(),
globals: Vec::new(),
data: vec![DataSegment {
id: DataId(0),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/wasm/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ pub(crate) fn lower_module_with_plan(
minimum,
maximum: None,
}],
tags: Vec::new(),
globals,
data,
exports,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pub(super) fn buffer_post_return_module() -> Module {
minimum: 1,
maximum: None,
}],
tags: Vec::new(),
data: Vec::new(),
exports: vec![
Export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ fn post_return_drops_an_owned_export_handle() {
minimum: 1,
maximum: None,
}],
tags: Vec::new(),
data: Vec::new(),
exports: vec![
Export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fn lower_and_validate(source: &Function) -> (WasmFunction, Vec<u8>) {
type_defs: Vec::new(),
functions: vec![lowered.clone()],
memories: Vec::new(),
tags: Vec::new(),
data: Vec::new(),
exports: vec![Export {
name: source.name.clone(),
Expand Down Expand Up @@ -326,6 +327,7 @@ fn has_if_span(body: &[Op], span: TextRange) -> bool {
..
} => *op_span == span || has_if_span(then_body, span) || has_if_span(else_body, span),
Op::Block { body, .. } | Op::Loop { body, .. } => has_if_span(body, span),
Op::Try { body, .. } => has_if_span(body, span),
Op::Leaf(_) => false,
})
}
Expand Down Expand Up @@ -401,6 +403,7 @@ fn dispatcher_uses_a_br_table_and_explicit_trap_for_invalid_state() {
..
} => contains_dispatch(then_body) || contains_dispatch(else_body),
Op::Block { body, .. } | Op::Loop { body, .. } => contains_dispatch(body),
Op::Try { body, .. } => contains_dispatch(body),
Op::Leaf(_) => false,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ fn structures_sparse_negative_switch_tags_and_executes_the_default() {
..
} => contains_br_table(then_body) || contains_br_table(else_body),
wasm::Op::Block { body, .. } | wasm::Op::Loop { body, .. } => contains_br_table(body),
wasm::Op::Try { body, .. } => contains_br_table(body),
wasm::Op::Leaf(_) => false,
})
}
Expand Down
3 changes: 3 additions & 0 deletions crates/psrs-backend/src/wasm/lower/structure/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub(super) fn lower_and_validate(source: &MirFunction) -> (Function, Vec<u8>) {
type_defs: Vec::new(),
functions: vec![lowered.clone()],
memories: Vec::new(),
tags: Vec::new(),
data: Vec::new(),
exports: vec![Export {
name: source.name.clone(),
Expand All @@ -71,6 +72,7 @@ pub(super) fn count_loops(body: &[wasm::Op]) -> usize {
.map(|op| match op {
wasm::Op::Loop { body, .. } => 1 + count_loops(body),
wasm::Op::Block { body, .. } => count_loops(body),
wasm::Op::Try { body, .. } => count_loops(body),
wasm::Op::If {
then_body,
else_body,
Expand Down Expand Up @@ -100,6 +102,7 @@ fn contains_branch_to_depth(body: &[wasm::Op], expected: u32) -> bool {
wasm::Op::Block { body, .. } | wasm::Op::Loop { body, .. } => {
contains_branch_to_depth(body, expected)
}
wasm::Op::Try { body, .. } => contains_branch_to_depth(body, expected),
wasm::Op::Leaf(_) => false,
})
}
Expand Down
34 changes: 34 additions & 0 deletions crates/psrs-backend/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub struct DataIndex(pub u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GlobalIndex(pub u32);

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TagIndex(pub u32);

/// A module-level global variable.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Global {
Expand Down Expand Up @@ -135,6 +138,36 @@ pub enum Op {
result: Option<ValType>,
span: TextRange,
},
/// A `try_table` region: the protected body executes inside the region's
/// own internal label, and the catch clauses arm its unwinding behavior.
/// A clause's `label` is depth-relative to the active-label stack at the
/// `try_table`'s emission point, not counting the region's internal label.
Try {
body: Body,
clauses: Vec<CatchClause>,
span: TextRange,
},
}

/// A `try_table` catch clause.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CatchClause {
/// `catch $tag $label`: an exception of the named tag is delivered to the
/// label as its branch arguments, so the label's result type must equal
/// the tag's payload.
Catch { tag: TagIndex, label: u32 },
/// `catch_all $label`: any exception is delivered to the label, which
/// must therefore take no arguments.
CatchAll { label: u32 },
}

/// A module-local exception tag. The tag attribute is always 0 (exception),
/// and this design neither imports nor exports a tag; a tag's function type
/// carries the payload as its parameters and produces no results.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TagDef {
pub type_index: TypeIndex,
pub span: TextRange,
}

/// A sequence of structured Wasm operations.
Expand Down Expand Up @@ -190,6 +223,7 @@ pub struct Module {
pub type_defs: Vec<crate::types::RecGroup>,
pub functions: Vec<Function>,
pub memories: Vec<Memory>,
pub tags: Vec<TagDef>,
pub globals: Vec<Global>,
pub data: Vec<DataSegment>,
pub exports: Vec<Export>,
Expand Down
112 changes: 112 additions & 0 deletions crates/psrs-backend/src/wasm/tests/exception/encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Tag-section encoding and profile-gate evidence (EH-02, EH-03).

use super::*;

/// EH-02: a tag-using module encodes with the tag section (id 13) in position
/// between the memory and global sections.
#[test]
fn encodes_the_tag_section_between_memory_and_global() {
let mut module = tag_fixture(catches_payload_body(), vec![WasmValType::I32]);
module.memories = vec![Memory {
id: crate::types::MemoryId(0),
index: super::super::MemoryIndex(0),
minimum: 1,
maximum: None,
}];
module.globals = vec![Global {
index: super::super::GlobalIndex(0),
mutable: true,
ty: WasmValType::I32,
init: GlobalInit::I32(0),
}];
verify(&module).expect("the memory and global skeleton should verify");
let binary = encode_module(&module).expect("the tagged module should encode");

let mut order = Vec::new();
for payload in wasmparser::Parser::new(0).parse_all(&binary) {
match payload.unwrap() {
Payload::TypeSection(_) => order.push("type"),
Payload::FunctionSection(_) => order.push("function"),
Payload::MemorySection(_) => order.push("memory"),
Payload::TagSection(_) => order.push("tag"),
Payload::GlobalSection(_) => order.push("global"),
Payload::ExportSection(_) => order.push("export"),
Payload::CodeSectionEntry(_) => order.push("code"),
_ => {}
}
}
let memory = order
.iter()
.position(|section| *section == "memory")
.unwrap();
let tag = order.iter().position(|section| *section == "tag").unwrap();
let global = order
.iter()
.position(|section| *section == "global")
.unwrap();
assert!(memory < tag && tag < global, "section order: {order:?}");

crate::validator_for(exceptions_enabled_target())
.validate_all(&binary)
.expect("wasmparser should accept the tagged module under the enabled profile");
}

/// EH-02: a module without tags emits no tag section.
#[test]
fn encodes_no_tag_section_without_tags() {
let module = module_with_function_body("NoTags", vec![Op::Leaf(Instruction::I32Const(0))]);
assert!(module.tags.is_empty());
let binary = encode_module(&module).expect("the plain module should encode");
for payload in wasmparser::Parser::new(0).parse_all(&binary) {
assert!(
!matches!(payload.unwrap(), Payload::TagSection(_)),
"a tag-less module must not carry a tag section"
);
}
}

/// EH-02: the encoded tagged module prints as WAT carrying the tag, the
/// try_table region, and the throw.
#[test]
fn prints_the_tagged_module_as_wat() {
let module = tag_fixture(catches_payload_body(), vec![WasmValType::I32]);
let binary = encode_module(&module).expect("the tagged module should encode");
let wat = wasmprinter::print_bytes(&binary).expect("the tagged module should print");
assert!(wat.contains("tag"), "{wat}");
assert!(wat.contains("try_table"), "{wat}");
assert!(wat.contains("throw"), "{wat}");
}

/// EH-03: the profile-derived validator rejects a tag-using module while
/// `exceptions` is disabled and accepts it once enabled; enabling `exceptions`
/// turns on exactly that one wasmparser feature.
#[test]
fn exceptions_gate_the_tag_using_module() {
let module = tag_fixture(catches_payload_body(), vec![WasmValType::I32]);
let binary = encode_module(&module).expect("the tagged module should encode");

let disabled = TargetCapabilities {
exceptions: false,
..TargetCapabilities::default()
};
let rejection = crate::validator_for(disabled)
.validate_all(&binary)
.err()
.expect("a tag-using module must not validate without exceptions");
assert!(
rejection
.to_string()
.contains("exceptions proposal not enabled"),
"the rejection should name the disabled proposal: {rejection}"
);

let enabled = exceptions_enabled_target();
crate::validator_for(enabled)
.validate_all(&binary)
.expect("the same module should validate with exceptions enabled");
assert_eq!(
enabled.wasm_features() ^ disabled.wasm_features(),
WasmFeatures::EXCEPTIONS,
"enabling exceptions must touch no other feature"
);
}
Loading
Loading