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 @@ -215,6 +215,7 @@ fn fixture() -> mir::Module {
}],
result: value(10),
result_type: ValueType::I32,
frame_weight: 0,
span,
}],
entry: Some(symbol(20)),
Expand Down
67 changes: 67 additions & 0 deletions crates/psrs-backend/src/mir/budget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! The stack-exhaustion budget's weight model (EH-12): the per-function frame
//! weight `W_f` and the profile-fixed constants the emitted budget check
//! compares against. These are the design's provisional calibration targets
//! (`docs/design/backend/fp/exception-handling-and-stack-exhaustion.md`,
//! "The stack-exhaustion budget"); the coordination fixture of EH-15 is the
//! protocol that validates them under the pinned engine, and this file's
//! documented values record the measured result of that protocol.

/// The fixed part of every frame's estimated native stack weight, in weight
/// units.
pub const FRAME_WEIGHT_FIXED: i32 = 32;

/// The per-local part of every frame's estimated native stack weight, in
/// weight units. `locals(f)` is the lowered function's parameter and local
/// value count; P10's list-copy and dispatcher locals are not known at P9 and
/// stay inside the headroom the budget reserves against the engine limit.
pub const FRAME_WEIGHT_PER_LOCAL: i32 = 16;

/// The budget the emitted prologue check compares the depth global against,
/// in the same weight units. The counter `depth` accumulates the current
/// call stack's weights, so a frame whose prologue pushes `depth` past this
/// value throws the `psrs:stack-exhaustion` tag instead of consuming the
/// native stack the engine's `max_wasm_stack` limit would trap on.
///
/// Calibration contract (EH-15): on the pinned wasmtime baseline with the
/// engine stack limit pinned for budget-dependent evidence, deep non-tail
/// recursion must produce the compiler's stack-exhaustion exception and never
/// the engine's `call stack exhausted` trap, and the corpus's deepest
/// *returning* recursion (TCOMutRec's depth-100 calls) must still return. The
/// value below is the measured outcome of that protocol for wasmtime 48.0.1
/// (7bac2c277 2026-08-24) with `-W max-wasm-stack=524288`; see the checklist
/// record for the measurement.
pub const STACK_BUDGET: i32 = 262_144;

/// The estimated native stack weight of one frame of a lowered function with
/// `locals` parameters and local values:
/// `W_f = FRAME_WEIGHT_FIXED + FRAME_WEIGHT_PER_LOCAL × locals`.
///
/// Recorded on each MIR function as `Function::frame_weight` at P9; the
/// structuring emission (P10) reads it back for the prologue increment and
/// the epilogue/tail decrements.
pub fn frame_weight(locals: usize) -> u32 {
(FRAME_WEIGHT_FIXED + FRAME_WEIGHT_PER_LOCAL * (locals as i32)) as u32
}

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

#[test]
fn frame_weight_adds_a_fixed_weight_per_local() {
// The design's formula: fixed 32, 16 per local.
assert_eq!(frame_weight(0), FRAME_WEIGHT_FIXED as u32);
assert_eq!(frame_weight(3), 32 + 16 * 3);
assert_eq!(
frame_weight(12),
(FRAME_WEIGHT_FIXED + FRAME_WEIGHT_PER_LOCAL * 12) as u32
);
// The constants are the design's provisional targets.
assert_eq!(FRAME_WEIGHT_FIXED, 32);
assert_eq!(FRAME_WEIGHT_PER_LOCAL, 16);
// The budget leaves the depth-100 returning recursion of the corpus
// (TCOMutRec) inside the budget: its frames weigh well below
// `STACK_BUDGET` at depth 100.
assert!(frame_weight(16) as i64 * 100 < STACK_BUDGET as i64);
}
}
55 changes: 55 additions & 0 deletions crates/psrs-backend/src/mir/imports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! The import symbols a lowered MIR module references: the one place P9 and
//! the optimizer consult shared call/list-copy reachability.
use crate::mir::{Function, Instruction, Terminator};
use psrs_hir::SymbolId;
use std::collections::HashSet;

/// Whether a list element carries byte-list payloads that need the string
/// boundary helpers.
pub fn element_has_bytes(element: &crate::abi::canonical::CanonicalType) -> bool {
use crate::abi::canonical::CanonicalType;
match element {
CanonicalType::String => true,
CanonicalType::List(inner) | CanonicalType::FixedList { element: inner, .. } => {
element_has_bytes(inner)
}
CanonicalType::Record(fields) => fields
.iter()
.any(|field| field.ty.is_byte_list() || element_has_bytes(&field.ty)),
CanonicalType::Option(inner) => element_has_bytes(inner),
CanonicalType::Result { ok, err } => {
ok.as_deref().is_some_and(element_has_bytes)
|| err.as_deref().is_some_and(element_has_bytes)
}
CanonicalType::Variant(cases) => cases
.iter()
.any(|case| case.payload.as_deref().is_some_and(element_has_bytes)),
_ => false,
}
}

/// The import symbols referenced by any call in the module.
pub fn referenced_imports(functions: &[Function]) -> HashSet<SymbolId> {
let mut used = HashSet::new();
for function in functions {
for block in &function.blocks {
for instruction in &block.instructions {
match instruction {
Instruction::Call { function, .. } | Instruction::CallVoid { function, .. } => {
used.insert(*function);
}
Instruction::ListCopy { element, .. } if element_has_bytes(element) => {
used.insert(crate::abi::STRING_TO_BYTES_SYMBOL);
used.insert(crate::abi::BYTES_TO_STRING_SYMBOL);
used.insert(crate::abi::REALLOC_SYMBOL);
}
_ => {}
}
}
if let Some(Terminator::ReturnCall { function, .. }) = &block.terminator {
used.insert(*function);
}
}
}
used
}
5 changes: 5 additions & 0 deletions crates/psrs-backend/src/mir/lower/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::BoundWasiImport;
use super::budget;
use super::layout::{LayoutError, PlannedLayout};
use super::literals::StringLiterals;
use super::wit;
Expand Down Expand Up @@ -112,8 +113,12 @@ pub(super) fn lower_function(
result_type: layout
.value_type(&source.result_type)
.map_err(|error| layout_error(source.span, error))?,
frame_weight: 0,
span: source.span,
};
// EH-12: the weight formula reads the *lowered* function's local count —
// P9's lowering may add fresh values beyond the CC source's.
function.frame_weight = budget::frame_weight(function.parameters.len() + function.values.len());
for plan in &lowerer.wit_calls {
plan.verify(&function)?;
}
Expand Down
59 changes: 8 additions & 51 deletions crates/psrs-backend/src/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use crate::types::{FunctionId, RecGroup, ValueDecl, ValueId, ValueType};
use crate::{BackendError, annotate_errors, cc};
use psrs_hir::SymbolId;
use psrs_span::TextRange;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;

pub mod budget;
pub(crate) mod cfg;
mod control;
mod effects;
mod imports;
mod instruction;
pub(crate) mod layout;
mod literals;
Expand All @@ -28,6 +30,7 @@ use literals::StringLiterals;
use lower::lower_function;
use planner::{GcPlanner, RepresentationPlanner};

pub use imports::{element_has_bytes, referenced_imports};
pub use instruction::{Instruction, ListDirection};
pub use numeric::{NumericOp, UnaryOp};
pub use runtime::RuntimeImport;
Expand Down Expand Up @@ -109,6 +112,10 @@ pub struct Function {
pub blocks: Vec<BasicBlock>,
pub result: ValueId,
pub result_type: ValueType,
/// The estimated native stack weight of one frame of this function
/// (`mir/budget.rs`), recorded at P9 and consumed by the structuring
/// emission of the stack-exhaustion budget.
pub frame_weight: u32,
pub span: TextRange,
}

Expand Down Expand Up @@ -446,53 +453,3 @@ fn lower_module_after_binding_validation(
verify_module_with_capabilities(&mir, target)?;
Ok((mir, wasi))
}

/// Whether a list element carries byte-list payloads that need the string
/// boundary helpers.
fn element_has_bytes(element: &crate::abi::canonical::CanonicalType) -> bool {
use crate::abi::canonical::CanonicalType;
match element {
CanonicalType::String => true,
CanonicalType::List(inner) | CanonicalType::FixedList { element: inner, .. } => {
element_has_bytes(inner)
}
CanonicalType::Record(fields) => fields
.iter()
.any(|field| field.ty.is_byte_list() || element_has_bytes(&field.ty)),
CanonicalType::Option(inner) => element_has_bytes(inner),
CanonicalType::Result { ok, err } => {
ok.as_deref().is_some_and(element_has_bytes)
|| err.as_deref().is_some_and(element_has_bytes)
}
CanonicalType::Variant(cases) => cases
.iter()
.any(|case| case.payload.as_deref().is_some_and(element_has_bytes)),
_ => false,
}
}

/// The import symbols referenced by any call in the module.
fn referenced_imports(functions: &[Function]) -> HashSet<SymbolId> {
let mut used = HashSet::new();
for function in functions {
for block in &function.blocks {
for instruction in &block.instructions {
match instruction {
Instruction::Call { function, .. } | Instruction::CallVoid { function, .. } => {
used.insert(*function);
}
Instruction::ListCopy { element, .. } if element_has_bytes(element) => {
used.insert(crate::abi::STRING_TO_BYTES_SYMBOL);
used.insert(crate::abi::BYTES_TO_STRING_SYMBOL);
used.insert(crate::abi::REALLOC_SYMBOL);
}
_ => {}
}
}
if let Some(Terminator::ReturnCall { function, .. }) = &block.terminator {
used.insert(*function);
}
}
}
used
}
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 @@ -115,6 +115,7 @@ fn number_format_module() -> Module {
}],
result: ValueId(0),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
}],
span: span(),
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 @@ -135,6 +135,7 @@ mod tests {
nullable: false,
heap: HeapType::Index(DefinedTypeId(0)),
}),
frame_weight: 0,
span,
}],
entry: Some(entry),
Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/opt/tests/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fn folds_total_arithmetic_but_keeps_an_unused_trapping_operation() {
}],
result: ValueId(2),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};

Expand Down
2 changes: 2 additions & 0 deletions crates/psrs-backend/src/mir/opt/tests/control_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ fn folds_branch_merges_prunes_blocks_and_projects_imports() {
],
result: ValueId(3),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};
let imports = vec![Import {
Expand Down Expand Up @@ -200,6 +201,7 @@ fn preserves_switch_selectors_and_all_successors_through_p10() {
],
result: ValueId(4),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};

Expand Down
1 change: 1 addition & 0 deletions crates/psrs-backend/src/mir/opt/tests/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn copy_forwarding_updates_the_structurer_result_value() {
}],
result: ValueId(1),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};

Expand Down
4 changes: 4 additions & 0 deletions crates/psrs-backend/src/mir/opt/tests/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn inlines_small_direct_functions_and_folds_the_result() {
}],
result: ValueId(1),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};
let callee = Function {
Expand Down Expand Up @@ -72,6 +73,7 @@ fn inlines_small_direct_functions_and_folds_the_result() {
}],
result: ValueId(2),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};

Expand Down Expand Up @@ -130,6 +132,7 @@ fn inlining_uses_the_callee_function_result_value() {
}],
result: ValueId(0),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};
let callee = Function {
Expand Down Expand Up @@ -161,6 +164,7 @@ fn inlining_uses_the_callee_function_result_value() {
}],
result: ValueId(1),
result_type: ValueType::I32,
frame_weight: 0,
span: span(),
};

Expand Down
Loading
Loading