Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "waffle"
version = "0.3.0"
version = "0.3.1"
description = "Wasm Analysis Framework For Lightweight Experiments"
authors = ["Chris Fallin <chris@cfallin.org>"]
license = "Apache-2.0 WITH LLVM-exception"
Expand Down
38 changes: 38 additions & 0 deletions src/backend/bitset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::convert::TryFrom;

/// A lazily-grown bitset.
#[derive(Clone, Debug, Default)]
pub struct Bitset {
words: Vec<u64>,
}

impl Bitset {
/// Set bit `i`; returns whether it was newly set.
pub fn insert(&mut self, i: u32) -> bool {
let word = usize::try_from(i / 64).unwrap();
let bit = 1u64 << (i % 64);
if word >= self.words.len() {
self.words.resize(word + 1, 0);
}
let w = &mut self.words[word];
let new = *w & bit == 0;
*w |= bit;
new
}

/// Iterate over all set bit-indices.
pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
self.words.iter().enumerate().flat_map(|(wi, &w)| {
let mut w = w;
std::iter::from_fn(move || {
if w == 0 {
return None;
}
let bit = w.trailing_zeros();
w &= w - 1;
let limb_start = u32::try_from(wi).unwrap() * 64;
Some(limb_start + bit)
})
})
}
}
204 changes: 204 additions & 0 deletions src/backend/cross_block_ids.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! Cross-block value id management for localification.
//!
//! Only SSA values that have a use in a different block from where they
//! are defined can appear in live-in/live-out sets. Those "cross-block"
//! values are assigned compact dense indices (`CrossBlockId`) so that
//! inter-block liveness bitsets and dense sets operate over a much
//! smaller space than the full `Value` space.

use std::num::NonZeroU32;
use std::ops::Index;

use crate::backend::treeify::Trees;
use crate::entity::EntityRef;
use crate::ir::{FunctionBody, Value, ValueDef};

/// A dense index into the compact set of cross-block SSA values.
///
/// Only values with a use outside their defining block get a
/// `CrossBlockId`; purely block-local values do not participate in
/// inter-block liveness and are represented by the absence of a value
/// in the two-way map.
///
/// The inner `NonZeroU32` stores the external 0-based ID plus one,
/// so that `Option<CrossBlockId>` benefits from Rust's niche
/// optimization and packs into a single `u32`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CrossBlockId(NonZeroU32);

impl CrossBlockId {
/// Construct a `CrossBlockId` from a raw u32. Used when the id
/// comes from iterating over a bitset or dense array.
pub fn new(n: u32) -> Self {
// SAFETY: n+1 is always >= 1, so never zero.
Self(NonZeroU32::new(n + 1).unwrap())
}

/// The inner numeric value, used for indexing into bitsets and
/// dense arrays.
pub fn as_u32(self) -> u32 {
self.0.get() - 1
}
}

/// Two-way map between `Value` (SSA value) and `CrossBlockId` (compact index).
///
/// Tracks which values are "cross-block" (used in a different block from
/// where they are defined). Only cross-block values can appear in
/// live-in/live-out sets.
#[derive(Clone, Debug, Default)]
pub struct CrossBlockValues {
/// Compact id per value (`None` = block-local). Indexed by `Value` index.
ids: Vec<Option<CrossBlockId>>,
/// Compact id -> Value. Indexed by `CrossBlockId.0`.
values: Vec<Value>,
}

impl CrossBlockValues {
/// The number of cross-block values registered.
pub fn len(&self) -> usize {
self.values.len()
}

/// Whether no values have been registered yet.
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}

/// Pre-allocate the `ids` array for the given number of total SSA values.
pub fn init(&mut self, total_values: usize) {
self.ids = vec![None; total_values];
}

/// Look up the cross-block id for a value, if it is cross-block.
pub fn get(&self, value: Value) -> Option<CrossBlockId> {
self.ids[value.index()]
}

/// Look up the value for a cross-block id.
pub fn value(&self, id: CrossBlockId) -> Value {
self.values[id.0.get() as usize - 1]
}

/// Register a value as cross-block, assigning it a new compact id.
/// Returns the assigned id. If already registered, returns the existing id.
pub fn insert(&mut self, value: Value) -> CrossBlockId {
if let Some(id) = self.ids[value.index()] {
return id;
}
let id = CrossBlockId::new(self.values.len() as u32);
self.ids[value.index()] = Some(id);
self.values.push(value);
id
}

/// Check whether a value has been registered as cross-block.
pub fn contains(&self, value: Value) -> bool {
self.ids[value.index()].is_some()
}

pub fn ids_mut(&mut self) -> &mut Vec<Option<CrossBlockId>> {
&mut self.ids
}

pub fn ids(&self) -> &Vec<Option<CrossBlockId>> {
&self.ids
}

pub fn iter_values(&self) -> impl Iterator<Item = Value> + '_ {
self.values.iter().copied()
}

/// Build the cross-block value map by scanning all uses in the
/// function body.
///
/// Only values that have a (proper) use outside the block where
/// their def is visited are recorded. A def the block walk skips
/// (treeified or remat insts) never cancels its uses, so such
/// values stay tracked everywhere, matching the walk's behavior.
pub fn build(body: &FunctionBody, trees: &Trees) -> Self {
// 1. Compute def_block for every value.
let mut def_block = vec![u32::MAX; body.values.len()];
for (block, block_def) in body.blocks.entries() {
for &(_, param) in &block_def.params {
def_block[param.index()] = block.index() as u32;
}
for &inst in &block_def.insts {
if is_tree_or_remat(inst, trees) {
continue;
}
def_block[inst.index()] = block.index() as u32;
}
}

// 2. Walk every block and record uses that cross block boundaries.
let mut cross_blocks = CrossBlockValues::default();
cross_blocks.init(body.values.len());

for (block, block_def) in body.blocks.entries() {
let cur = block.index() as u32;

// Visit terminator uses.
block_def.terminator.visit_uses(|u| {
visit_use(u, body, trees, cur, &def_block, &mut cross_blocks);
});

// Visit instruction args (in reverse order per the body walk).
for &inst in block_def.insts.iter().rev() {
if is_tree_or_remat(inst, trees) {
continue;
}
if let ValueDef::Operator(_, args, _) = &body.values[inst] {
for &arg in &body.arg_pool[*args] {
visit_use(arg, body, trees, cur, &def_block, &mut cross_blocks);
}
}
}
}

cross_blocks
}
}

/// Check whether a value is treeified (owned by another) or rematerialized.
fn is_tree_or_remat(value: Value, trees: &Trees) -> bool {
trees.owner.contains_key(&value) || trees.remat.contains(&value)
}

/// Recursively resolve a use through aliases and treeified/remat values,
/// recording cross-block values into the map.
fn visit_use(
value: Value,
body: &FunctionBody,
trees: &Trees,
cur_block: u32,
def_block: &[u32],
cross_blocks: &mut CrossBlockValues,
) {
let value = body.resolve_alias(value);
if let ValueDef::PickOutput(value, _, _) = body.values[value] {
visit_use(value, body, trees, cur_block, def_block, cross_blocks);
return;
}
if is_tree_or_remat(value, trees) {
// Treeified or remat: visit its args without recording the value itself.
if let ValueDef::Operator(_, args, _) = body.values[value] {
for &arg in &body.arg_pool[args] {
visit_use(arg, body, trees, cur_block, def_block, cross_blocks);
}
}
return;
}
// Proper use: record if it crosses block boundaries.
if def_block[value.index()] != cur_block {
cross_blocks.insert(value);
}
}

impl Index<CrossBlockId> for CrossBlockValues {
type Output = Value;

fn index(&self, id: CrossBlockId) -> &Self::Output {
&self.values[id.0.get() as usize - 1]
}
}
104 changes: 104 additions & 0 deletions src/backend/dense_live_set.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! A dense, epoch-based live-set used during block walks.
//!
//! # Epoch-based O(1) clearing
//!
//! The `DenseLiveSet` is reused across multiple block visits without
//! reallocating or clearing its backing storage.
//!
//! We want a dense set because accesses are much faster than a HashSet.
//! We would not want to allocate a dense set separately for every block
//! we process, however (as we get further down in the function, there
//! would be a larger and larger "prefix" of unused IDs). Instead we
//! want to use one allocation for a whole scan through a function body.
//!
//! But we cannot clear a dense set in O(1) if its per-index entries are
//! just booleans; that entails scanning through the whole thing with a
//! bulk memset (or at least, a walk through the dirty-index list and a
//! store at each index). So instead, this is achieved through an epoch
//! counter:
//!
//! * Each slot in the `mark` array stores `epoch * 2 + state`, where
//! `state` is `0` (live) or `1` (dead).
//! * When a new block walk begins, `next_epoch()` increments the epoch
//! counter and clears the `touched` list — both O(1) operations.
//! * Because the old epoch values in `mark` no longer match the current
//! epoch, stale entries from previous block visits are automatically
//! treated as absent without needing to zero out the array.

/// A dense live-set with O(1) clearing via epoch tagging.
#[derive(Debug)]
pub struct DenseLiveSet {
/// Per-slot mark: `epoch * 2 + state` where state is 0 (live) or 1
/// (dead). Slots with a mark from a previous epoch are treated as
/// absent.
mark: Vec<u64>,
/// The current epoch counter. Incremented on `next_epoch()` to
/// logically clear the set.
epoch: u64,
/// Indices touched (set_live or set_dead) during the current epoch.
/// Cleared on each `next_epoch()` call.
touched: Vec<u32>,
}

impl DenseLiveSet {
/// Create a new set with space for `n` indices.
pub fn new(n: usize) -> Self {
Self {
mark: vec![0; n],
epoch: 0,
touched: Vec::new(),
}
}

/// Start a new epoch, logically clearing the set in O(1).
///
/// Increments the epoch counter and clears the touched-index list.
pub fn next_epoch(&mut self) {
self.epoch += 1;
self.touched.clear();
}

/// Mark index `i` as live.
///
/// Returns `true` if it was already live in this epoch (i.e., this
/// is not a first-use).
pub fn set_live(&mut self, i: u32) -> bool {
let m = &mut self.mark[i as usize];
let newly_touched = *m / 2 != self.epoch;
if newly_touched {
self.touched.push(i);
}
let was_live = !newly_touched && *m % 2 == 0;
*m = self.epoch * 2;
!was_live
}

/// Mark index `i` as dead (retired by a def).
///
/// Returns `true` if it was live in this epoch.
pub fn set_dead(&mut self, i: u32) -> bool {
let m = &mut self.mark[i as usize];
if *m / 2 != self.epoch {
self.touched.push(i);
*m = self.epoch * 2 + 1;
return false;
}
let was_live = *m % 2 == 0;
*m = self.epoch * 2 + 1;
was_live
}

/// Check if index `i` is currently live in this epoch.
pub fn is_live(&self, i: u32) -> bool {
let m = self.mark[i as usize];
m / 2 == self.epoch && m % 2 == 0
}

/// Returns the list of indices touched during the current epoch.
///
/// Useful for iterating only over entries that were modified in the
/// current epoch, avoiding a full scan of all possible indices.
pub fn touched(&self) -> &[u32] {
&self.touched
}
}
Loading
Loading