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/linking/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ fn fixture() -> mir::Module {
},
];
mir::Module {
tags: Vec::new(),
name: "RuntimeStorageConsumer".into(),
types,
strings: vec![],
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/linking/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::types::{

fn module(import: mir::Import) -> mir::Module {
mir::Module {
tags: Vec::new(),
name: "ConsumerAbi".into(),
entry: None,
types: vec![],
Expand Down
9 changes: 8 additions & 1 deletion crates/psrs-backend/src/mir/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,15 @@ pub(crate) fn reachable_without(
Some(false)
}

/// The successor blocks named by a terminator.
/// The successor blocks named by a terminator's normal edges. Exception
/// unwind edges are not normal edges: a `Throw` has none (its handler is
/// reached by unwinding), and a handler block must never become a normal
/// successor.
pub(crate) fn successors(terminator: &Terminator) -> Vec<BlockId> {
match terminator {
Terminator::Return { .. } | Terminator::Trap { .. } => Vec::new(),
Terminator::Throw { .. } => Vec::new(),
Terminator::Try { protected, .. } => vec![*protected],
Terminator::Jump { target, .. } => vec![*target],
Terminator::Branch {
then_block,
Expand Down Expand Up @@ -199,6 +204,8 @@ pub(crate) fn join_blocks(function: &Function) -> HashSet<BlockId> {
Terminator::Trap { .. }
| Terminator::Return { .. }
| Terminator::Jump { .. }
| Terminator::Throw { .. }
| Terminator::Try { .. }
| Terminator::ReturnCall { .. }
| Terminator::ReturnCallRef { .. } => continue,
};
Expand Down
8 changes: 7 additions & 1 deletion crates/psrs-backend/src/mir/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ pub(in crate::mir) fn total_functions(
Some(Terminator::ReturnCall { function, .. }) => {
calls.insert(*function);
}
Some(Terminator::ReturnCallRef { .. } | Terminator::Trap { .. }) => safe = false,
Some(
Terminator::ReturnCallRef { .. }
| Terminator::Trap { .. }
| Terminator::Throw { .. },
) => {
safe = false;
}
_ => {}
}
}
Expand Down
12 changes: 11 additions & 1 deletion crates/psrs-backend/src/mir/lower/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ pub(in crate::mir) fn rewrite(
function: &mut Function,
target: TargetCapabilities,
) -> Result<(), Vec<BackendError>> {
// EH-07: no forced tail call may live in a protected try region — Wasm
// pops the caller's frame at a `return_call`, discarding the armed
// `try_table`. Self-call loopification is unaffected: a loop allocates
// no frame.
let protected = crate::mir::try_region::plan(function, "P9 MIR lowering")?.protected_blocks();
let returned = return_forwarded_values(function);
let uses = use_counts(function);
let symbol = function.symbol;
Expand Down Expand Up @@ -60,8 +65,12 @@ pub(in crate::mir) fn rewrite(
span,
..
} if *callee == symbol => {
// Self-call loopification is unaffected by protection: a
// loop allocates no frame.
self_calls.push((block.id, arguments.clone(), *span));
}
// EH-07: keep a protected block's call ordinary plus its return.
_ if protected.contains(&block.id) => continue,
Instruction::Call {
contract,
function: callee,
Expand Down Expand Up @@ -393,12 +402,13 @@ fn use_counts(function: &Function) -> HashMap<ValueId, usize> {

fn terminator_operands(terminator: &Terminator) -> Vec<ValueId> {
match terminator {
Terminator::Trap { .. } => Vec::new(),
Terminator::Trap { .. } | Terminator::Try { .. } => Vec::new(),
Terminator::Return { value, .. } => vec![*value],
Terminator::Jump { arguments, .. } => arguments.clone(),
Terminator::Branch { condition, .. } => vec![*condition],
Terminator::Switch { value, .. } => vec![*value],
Terminator::ReturnCall { arguments, .. } => arguments.clone(),
Terminator::Throw { arguments, .. } => arguments.clone(),
Terminator::ReturnCallRef {
function,
arguments,
Expand Down
22 changes: 22 additions & 0 deletions crates/psrs-backend/src/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub mod opt;
mod planner;
mod reachable;
mod runtime;
mod tag;
mod try_region;
mod value_identity;
mod verify;
mod wit;
Expand All @@ -29,6 +31,7 @@ use planner::{GcPlanner, RepresentationPlanner};
pub use instruction::{Instruction, ListDirection};
pub use numeric::{NumericOp, UnaryOp};
pub use runtime::RuntimeImport;
pub use tag::{Tag, TagId};
pub use verify::{verify_module, verify_module_with_capabilities};

#[cfg(test)]
Expand Down Expand Up @@ -57,6 +60,8 @@ pub struct Module {
/// Checked physical ABI imports, selected from WIT or raw runtime contracts.
pub imports: Vec<Import>,
pub functions: Vec<Function>,
/// Compiler-defined exception tags; a `TagId` indexes this list.
pub tags: Vec<Tag>,
/// The program entry declaration, if selected by the driver.
pub entry: Option<SymbolId>,
/// The concrete target layout the module was planned from. P10 reads the
Expand Down Expand Up @@ -164,6 +169,22 @@ pub enum Terminator {
arguments: Vec<ValueId>,
span: TextRange,
},
/// Raises a tag's exception; the optional `handler` names the block it
/// unwinds to within this function (`None` unwinds to the caller).
Throw {
tag: TagId,
arguments: Vec<ValueId>,
handler: Option<BlockId>,
span: TextRange,
},
/// Arms a protected region: control proceeds to `protected` as a
/// parameterless `Jump` would; catch clauses declare unwind behavior.
Try {
protected: BlockId,
catches: Vec<(TagId, BlockId)>,
catch_all: Option<BlockId>,
span: TextRange,
},
}

/// Lowers a CC module to MIR, returning the module and the ABI registry that
Expand Down Expand Up @@ -419,6 +440,7 @@ fn lower_module_after_binding_validation(
strings,
imports,
functions,
tags: Vec::new(),
entry: module.entry,
layout: Some(layout),
span: module.span,
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/number_format_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fn registry() -> crate::abi::WasiRegistry {
/// formatter artifact's raw export, and returns the initialized byte length.
fn number_format_module() -> Module {
Module {
tags: Vec::new(),
name: "NumberFormat".into(),
entry: Some(SymbolId::new(ModuleId(0), 0)),
types: Vec::new(),
Expand Down
28 changes: 28 additions & 0 deletions crates/psrs-backend/src/mir/opt/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ pub(super) fn prune_unreachable(module: &mut Module) -> bool {
let mut changed = false;
for function in &mut module.functions {
let reachable = reachable_blocks(function.entry, &function.blocks);
// Exception handlers are reached only through unwind edges, which the
// normal-successor walk does not follow; keep every handler and
// everything it reaches normally.
let reachable = match crate::mir::try_region::handler_closure(function) {
handlers if handlers.is_empty() => reachable,
handlers => {
let by_id = function
.blocks
.iter()
.map(|block| (block.id, block))
.collect::<HashMap<_, _>>();
let mut kept = reachable;
let mut pending = VecDeque::from_iter(handlers);
while let Some(id) = pending.pop_back() {
if !kept.insert(id) {
continue;
}
if let Some(terminator) =
by_id.get(&id).and_then(|block| block.terminator.as_ref())
{
pending.extend(crate::mir::cfg::successors(terminator));
}
}
kept
}
};
let original_len = function.blocks.len();
function
.blocks
Expand Down Expand Up @@ -57,6 +83,8 @@ pub(super) fn simplify_terminators(module: &mut Module) -> bool {
Terminator::Trap { .. }
| Terminator::Return { .. }
| Terminator::Jump { .. }
| Terminator::Throw { .. }
| Terminator::Try { .. }
| Terminator::ReturnCall { .. }
| Terminator::ReturnCallRef { .. } => return None,
};
Expand Down
8 changes: 6 additions & 2 deletions crates/psrs-backend/src/mir/opt/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,14 @@ fn incoming_values(function: &Function, reachable: &HashSet<crate::mir::BlockId>
}
// Switch successors cannot carry block parameters, so there are no
// incoming values to add to the block-parameter lattice. A tail
// call passes its arguments to a callee, not to a block.
// call passes its arguments to a callee, not to a block. A try
// region's protected entry cannot carry block parameters, and a
// throw's edges are unwinding, not parameter passing.
Terminator::Switch { .. }
| Terminator::ReturnCall { .. }
| Terminator::ReturnCallRef { .. } => {}
| Terminator::ReturnCallRef { .. }
| Terminator::Throw { .. }
| Terminator::Try { .. } => {}
Terminator::Return { .. } | Terminator::Trap { .. } => {}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/opt/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ mod tests {
let imported = SymbolId::new(ModuleId(1), 0);
let span = TextRange::new(0, 1);
Module {
tags: Vec::new(),
name: "ImportProjectionTest".into(),
types: Vec::new(),
strings: Vec::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/opt/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn value(id: u32, ty: ValueType) -> ValueDecl {

fn module(functions: Vec<Function>, imports: Vec<Import>, entry: SymbolId) -> Module {
Module {
tags: Vec::new(),
name: "OptimizationTest".into(),
types: Vec::new(),
strings: Vec::new(),
Expand Down
6 changes: 4 additions & 2 deletions crates/psrs-backend/src/mir/opt/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ fn eliminate_function(function: &mut Function) {

fn terminator_operands(terminator: &Terminator) -> Vec<ValueId> {
match terminator {
Terminator::Trap { .. } => Vec::new(),
Terminator::Trap { .. } | Terminator::Try { .. } => Vec::new(),
Terminator::Return { value, .. } => vec![*value],
Terminator::Jump { arguments, .. } => arguments.clone(),
Terminator::Branch { condition, .. } => vec![*condition],
Terminator::Switch { value, .. } => vec![*value],
Terminator::ReturnCall { arguments, .. } => arguments.clone(),
Terminator::Throw { arguments, .. } => arguments.clone(),
Terminator::ReturnCallRef {
function,
arguments,
Expand Down Expand Up @@ -373,12 +374,13 @@ pub(crate) fn remap_terminator(terminator: &mut Terminator, mapping: &HashMap<Va
*value = mapping.get(value).copied().unwrap_or(*value);
};
match terminator {
Terminator::Trap { .. } => {}
Terminator::Trap { .. } | Terminator::Try { .. } => {}
Terminator::Return { value, .. } => replace(value),
Terminator::Jump { arguments, .. } => arguments.iter_mut().for_each(replace),
Terminator::Branch { condition, .. } => replace(condition),
Terminator::Switch { value, .. } => replace(value),
Terminator::ReturnCall { arguments, .. } => arguments.iter_mut().for_each(replace),
Terminator::Throw { arguments, .. } => arguments.iter_mut().for_each(replace),
Terminator::ReturnCallRef {
function,
arguments,
Expand Down
22 changes: 22 additions & 0 deletions crates/psrs-backend/src/mir/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! The MIR tag table: module-local exception identities for the Wasm
//! exception channel. See `docs/design/backend/fp/exception-handling-and-stack-exhaustion.md`.

use crate::types::ValueType;
use psrs_span::TextRange;

/// Module-local tag identity. Distinct from `FunctionId`, `DefinedTypeId`, and
/// every other index domain; a `TagId` indexes `Module::tags` by position.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TagId(pub u32);

/// A compiler-defined tag: an exception kind with a payload type
/// `[t*] -> []`. The channel defines only module-local tags, so a tag is
/// neither imported nor exported.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tag {
pub name: String,
/// At most one payload value; a multi-value payload needs the multi-value
/// block work and is rejected by the verifier.
pub payload: Vec<ValueType>,
pub span: TextRange,
}
93 changes: 93 additions & 0 deletions crates/psrs-backend/src/mir/tests/exception/capability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! EH-08 capability inference for the exception channel.

use super::{at, block, module, simple_function, tag, tag_only_module};
use crate::capability::TargetCapabilities;
use crate::mir::{BlockId, Terminator, ValueId, verify_module_with_capabilities};

fn try_module() -> crate::mir::Module {
module(
simple_function(
vec![
block(
0,
vec![],
vec![],
Terminator::Try {
protected: BlockId(1),
catches: vec![],
catch_all: Some(BlockId(2)),
span: at(10),
},
),
block(
1,
vec![],
vec![],
Terminator::Return {
value: ValueId(0),
span: at(11),
},
),
block(
2,
vec![],
vec![],
Terminator::Return {
value: ValueId(0),
span: at(12),
},
),
],
Vec::new(),
),
Vec::new(),
)
}

#[test]
fn exception_terminators_require_the_exceptions_capability() {
let exception_module = try_module();
let disabled = TargetCapabilities {
exceptions: false,
..TargetCapabilities::default()
};
let errors = verify_module_with_capabilities(&exception_module, disabled)
.expect_err("try regions require the exceptions gate");
assert!(errors.iter().any(|error| {
error
.message
.contains("exception-handling proposal required by this module")
&& error.span == exception_module.span
&& error.module == exception_module.entry.map(|entry| entry.module)
}));

let enabled = TargetCapabilities {
exceptions: true,
..TargetCapabilities::default()
};
verify_module_with_capabilities(&exception_module, enabled)
.expect("the enabled profile admits the try region");
}

#[test]
fn a_declared_tag_requires_the_exceptions_capability() {
let module = tag_only_module(vec![tag(0, Vec::new())]);
let disabled = TargetCapabilities {
exceptions: false,
..TargetCapabilities::default()
};
let errors = verify_module_with_capabilities(&module, disabled)
.expect_err("a declared tag requires the exceptions gate");
assert!(
errors
.iter()
.any(|error| error.message.contains("exception-handling proposal")),
"{errors:?}"
);

let enabled = TargetCapabilities {
exceptions: true,
..TargetCapabilities::default()
};
verify_module_with_capabilities(&module, enabled).expect("the enabled profile admits the tag");
}
Loading
Loading