Skip to content

Commit 802932b

Browse files
author
metrics
committed
Merge branch 'main' of https://github.com/arnoox/herkos into pr-f3/gvn
2 parents bbe5dc5 + 7cbca56 commit 802932b

8 files changed

Lines changed: 74 additions & 92 deletions

File tree

crates/herkos-core/src/optimizer/branch_fold.rs

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,33 @@
1010
//! After substitution, the defining instruction becomes dead (single use was
1111
//! the branch) and is cleaned up by `dead_instrs`.
1212
13-
use super::utils::{build_global_use_count, instr_dest};
13+
use super::utils::{build_global_const_map, build_global_use_count, instr_dest, is_zero};
1414
use crate::ir::{BinOp, IrFunction, IrInstr, IrTerminator, IrValue, UnOp, VarId};
1515
use std::collections::HashMap;
1616

1717
pub fn eliminate(func: &mut IrFunction) {
1818
loop {
1919
let global_uses = build_global_use_count(func);
20-
if !fold_one(func, &global_uses) {
20+
let global_consts = build_global_const_map(func);
21+
if !fold_one(func, &global_uses, &global_consts) {
2122
break;
2223
}
2324
}
2425
}
2526

2627
/// Attempt a single branch fold across the function. Returns `true` if a
2728
/// change was made.
28-
fn fold_one(func: &mut IrFunction, global_uses: &HashMap<VarId, usize>) -> bool {
29+
fn fold_one(
30+
func: &mut IrFunction,
31+
global_uses: &HashMap<VarId, usize>,
32+
global_consts: &HashMap<VarId, IrValue>,
33+
) -> bool {
2934
// Build a map of VarId → defining instruction info.
3035
// We only care about single-use vars defined by Eqz, Ne(x,0), or Eq(x,0).
3136
let mut var_defs: HashMap<VarId, VarDef> = HashMap::new();
3237

33-
// Also build a global constant map for checking if an operand is zero.
34-
let global_consts = build_global_const_map(func);
35-
3638
for block in &func.blocks {
37-
let mut local_consts = global_consts.clone();
3839
for instr in &block.instructions {
39-
if let IrInstr::Const { dest, value } = instr {
40-
local_consts.insert(*dest, *value);
41-
}
42-
4340
if let Some(dest) = instr_dest(instr) {
4441
match instr {
4542
IrInstr::UnOp {
@@ -55,9 +52,9 @@ fn fold_one(func: &mut IrFunction, global_uses: &HashMap<VarId, usize>) -> bool
5552
rhs,
5653
..
5754
} => {
58-
if is_zero(rhs, &local_consts) {
55+
if is_zero(*rhs, global_consts) {
5956
var_defs.insert(dest, VarDef::NeZero(*lhs));
60-
} else if is_zero(lhs, &local_consts) {
57+
} else if is_zero(*lhs, global_consts) {
6158
var_defs.insert(dest, VarDef::NeZero(*rhs));
6259
}
6360
}
@@ -67,9 +64,9 @@ fn fold_one(func: &mut IrFunction, global_uses: &HashMap<VarId, usize>) -> bool
6764
rhs,
6865
..
6966
} => {
70-
if is_zero(rhs, &local_consts) {
67+
if is_zero(*rhs, global_consts) {
7168
var_defs.insert(dest, VarDef::EqZero(*lhs));
72-
} else if is_zero(lhs, &local_consts) {
69+
} else if is_zero(*lhs, global_consts) {
7370
var_defs.insert(dest, VarDef::EqZero(*rhs));
7471
}
7572
}
@@ -133,34 +130,6 @@ enum VarDef {
133130
EqZero(VarId),
134131
}
135132

136-
fn is_zero(var: &VarId, consts: &HashMap<VarId, IrValue>) -> bool {
137-
matches!(
138-
consts.get(var),
139-
Some(IrValue::I32(0)) | Some(IrValue::I64(0))
140-
)
141-
}
142-
143-
fn build_global_const_map(func: &IrFunction) -> HashMap<VarId, IrValue> {
144-
let mut total_defs: HashMap<VarId, usize> = HashMap::new();
145-
let mut const_defs: HashMap<VarId, IrValue> = HashMap::new();
146-
147-
for block in &func.blocks {
148-
for instr in &block.instructions {
149-
if let Some(dest) = instr_dest(instr) {
150-
*total_defs.entry(dest).or_insert(0) += 1;
151-
if let IrInstr::Const { dest, value } = instr {
152-
const_defs.insert(*dest, *value);
153-
}
154-
}
155-
}
156-
}
157-
158-
const_defs
159-
.into_iter()
160-
.filter(|(v, _)| total_defs.get(v).copied().unwrap_or(0) == 1)
161-
.collect()
162-
}
163-
164133
// ── Tests ─────────────────────────────────────────────────────────────────────
165134

166135
#[cfg(test)]

crates/herkos-core/src/optimizer/local_cse.rs

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use crate::ir::{BinOp, IrFunction, IrInstr, IrValue, UnOp, VarId};
99
use std::collections::HashMap;
1010

11-
use super::utils::prune_dead_locals;
11+
use super::utils::{is_commutative, prune_dead_locals};
1212

1313
// ── Value key ────────────────────────────────────────────────────────────────
1414

@@ -45,37 +45,6 @@ impl From<IrValue> for ConstKey {
4545
}
4646
}
4747

48-
// ── Commutative op detection ─────────────────────────────────────────────────
49-
50-
/// Returns true for operations where `op(a, b) == op(b, a)`.
51-
fn is_commutative(op: &BinOp) -> bool {
52-
matches!(
53-
op,
54-
BinOp::I32Add
55-
| BinOp::I32Mul
56-
| BinOp::I32And
57-
| BinOp::I32Or
58-
| BinOp::I32Xor
59-
| BinOp::I32Eq
60-
| BinOp::I32Ne
61-
| BinOp::I64Add
62-
| BinOp::I64Mul
63-
| BinOp::I64And
64-
| BinOp::I64Or
65-
| BinOp::I64Xor
66-
| BinOp::I64Eq
67-
| BinOp::I64Ne
68-
| BinOp::F32Add
69-
| BinOp::F32Mul
70-
| BinOp::F32Eq
71-
| BinOp::F32Ne
72-
| BinOp::F64Add
73-
| BinOp::F64Mul
74-
| BinOp::F64Eq
75-
| BinOp::F64Ne
76-
)
77-
}
78-
7948
/// Build a `ValueKey` for a `BinOp`, normalizing operand order for commutative ops.
8049
fn binop_key(op: BinOp, lhs: VarId, rhs: VarId) -> ValueKey {
8150
let (lhs, rhs) = if is_commutative(&op) && lhs.0 > rhs.0 {
@@ -97,8 +66,11 @@ pub fn eliminate(func: &mut IrFunction) {
9766
let mut value_map: HashMap<ValueKey, VarId> = HashMap::new();
9867

9968
for instr in &mut block.instructions {
100-
// In strict SSA form each variable is defined exactly once, so there
101-
// is no need to invalidate cached CSE entries on redefinition.
69+
// This pass runs on lowered (post-phi) IR. While the function is no
70+
// longer globally in SSA form, within any single block each BinOp,
71+
// UnOp, and Const dest is still defined at most once: phi lowering
72+
// only inserts Assigns into predecessor blocks, never into the block
73+
// being processed. So value_map entries are never stale.
10274
match instr {
10375
IrInstr::Const { dest, value } => {
10476
let key = ValueKey::Const(ConstKey::from(*value));

crates/herkos-core/src/optimizer/utils.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,37 @@ pub fn is_side_effect_free(instr: &IrInstr) -> bool {
466466
}
467467
}
468468

469+
// ── Commutative op detection ─────────────────────────────────────────────────
470+
471+
/// Returns true for operations where `op(a, b) == op(b, a)`.
472+
pub fn is_commutative(op: &BinOp) -> bool {
473+
matches!(
474+
op,
475+
BinOp::I32Add
476+
| BinOp::I32Mul
477+
| BinOp::I32And
478+
| BinOp::I32Or
479+
| BinOp::I32Xor
480+
| BinOp::I32Eq
481+
| BinOp::I32Ne
482+
| BinOp::I64Add
483+
| BinOp::I64Mul
484+
| BinOp::I64And
485+
| BinOp::I64Or
486+
| BinOp::I64Xor
487+
| BinOp::I64Eq
488+
| BinOp::I64Ne
489+
| BinOp::F32Add
490+
| BinOp::F32Mul
491+
| BinOp::F32Eq
492+
| BinOp::F32Ne
493+
| BinOp::F64Add
494+
| BinOp::F64Mul
495+
| BinOp::F64Eq
496+
| BinOp::F64Ne
497+
)
498+
}
499+
469500
// ── Rewrite terminator block targets ─────────────────────────────────────────
470501

471502
/// Rewrite all block-ID references in a terminator from `old` to `new`.
@@ -495,6 +526,14 @@ pub fn rewrite_terminator_target(term: &mut IrTerminator, old: BlockId, new: Blo
495526
}
496527
}
497528

529+
/// Returns `true` if `var` is known to be zero according to `consts`.
530+
pub fn is_zero(var: VarId, consts: &HashMap<VarId, IrValue>) -> bool {
531+
matches!(
532+
consts.get(&var),
533+
Some(IrValue::I32(0)) | Some(IrValue::I64(0))
534+
)
535+
}
536+
498537
/// Variables with exactly one definition across the function that is a `Const`
499538
/// instruction. These can be treated as constants in any block that uses them.
500539
pub fn build_global_const_map(func: &IrFunction) -> HashMap<VarId, IrValue> {

crates/herkos-runtime/src/memory.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ impl<const MAX_PAGES: usize> IsolatedMemory<MAX_PAGES> {
3434
/// # Errors
3535
/// Returns `ConstructionError::MemoryInitialPagesExceedsMax` if `initial_pages > MAX_PAGES`.
3636
#[inline(never)]
37-
pub fn try_new(initial_pages: usize) -> Result<Self, crate::ConstructionError> {
37+
pub const fn try_new(initial_pages: usize) -> Result<Self, crate::ConstructionError> {
3838
if initial_pages > MAX_PAGES {
3939
return Err(crate::ConstructionError::MemoryInitialPagesExceedsMax {
4040
initial: initial_pages,
@@ -81,13 +81,13 @@ impl<const MAX_PAGES: usize> IsolatedMemory<MAX_PAGES> {
8181

8282
/// Current number of active pages.
8383
#[inline(always)]
84-
pub fn page_count(&self) -> usize {
84+
pub const fn page_count(&self) -> usize {
8585
self.active_pages
8686
}
8787

8888
/// Current active size in bytes.
8989
#[inline(always)]
90-
pub fn active_size(&self) -> usize {
90+
pub const fn active_size(&self) -> usize {
9191
self.active_pages * PAGE_SIZE
9292
}
9393

@@ -109,7 +109,7 @@ impl<const MAX_PAGES: usize> IsolatedMemory<MAX_PAGES> {
109109

110110
/// Wasm `memory.size` — returns current page count.
111111
#[inline(always)]
112-
pub fn size(&self) -> i32 {
112+
pub const fn size(&self) -> i32 {
113113
self.active_pages as i32
114114
}
115115

crates/herkos-runtime/src/module.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ pub struct LibraryModule<G, const TABLE_SIZE: usize> {
102102
impl<G, const TABLE_SIZE: usize> LibraryModule<G, TABLE_SIZE> {
103103
/// Create a new library module with the given globals and table.
104104
#[inline]
105-
pub fn new(globals: G, table: Table<TABLE_SIZE>) -> Self {
105+
pub const fn new(globals: G, table: Table<TABLE_SIZE>) -> Self {
106106
Self { globals, table }
107107
}
108108
}

crates/herkos-runtime/src/ops.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub fn i32_div_u(lhs: i32, rhs: i32) -> WasmResult<i32> {
131131
/// for this case (because the underlying division overflows), so we handle it
132132
/// with an explicit branch.
133133
#[inline(never)]
134-
pub fn i32_rem_s(lhs: i32, rhs: i32) -> WasmResult<i32> {
134+
pub const fn i32_rem_s(lhs: i32, rhs: i32) -> WasmResult<i32> {
135135
if rhs == 0 {
136136
return Err(WasmTrap::DivisionByZero);
137137
}
@@ -174,7 +174,7 @@ pub fn i64_div_u(lhs: i64, rhs: i64) -> WasmResult<i64> {
174174
///
175175
/// Same special case as `i32_rem_s`: `i64::MIN rem_s -1 = 0` per Wasm spec.
176176
#[inline(never)]
177-
pub fn i64_rem_s(lhs: i64, rhs: i64) -> WasmResult<i64> {
177+
pub const fn i64_rem_s(lhs: i64, rhs: i64) -> WasmResult<i64> {
178178
if rhs == 0 {
179179
return Err(WasmTrap::DivisionByZero);
180180
}
@@ -198,7 +198,7 @@ pub fn i64_rem_u(lhs: i64, rhs: i64) -> WasmResult<i64> {
198198

199199
/// Wasm `f32.min`: propagates NaN (unlike Rust's `f32::min` which ignores it).
200200
/// Also preserves the Wasm rule `min(-0.0, +0.0) = -0.0`.
201-
pub fn wasm_min_f32(a: f32, b: f32) -> f32 {
201+
pub const fn wasm_min_f32(a: f32, b: f32) -> f32 {
202202
if a.is_nan() || b.is_nan() {
203203
return f32::NAN;
204204
}
@@ -213,7 +213,7 @@ pub fn wasm_min_f32(a: f32, b: f32) -> f32 {
213213
}
214214

215215
/// Wasm `f32.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
216-
pub fn wasm_max_f32(a: f32, b: f32) -> f32 {
216+
pub const fn wasm_max_f32(a: f32, b: f32) -> f32 {
217217
if a.is_nan() || b.is_nan() {
218218
return f32::NAN;
219219
}
@@ -228,7 +228,7 @@ pub fn wasm_max_f32(a: f32, b: f32) -> f32 {
228228
}
229229

230230
/// Wasm `f64.min`: propagates NaN. `min(-0.0, +0.0) = -0.0`.
231-
pub fn wasm_min_f64(a: f64, b: f64) -> f64 {
231+
pub const fn wasm_min_f64(a: f64, b: f64) -> f64 {
232232
if a.is_nan() || b.is_nan() {
233233
return f64::NAN;
234234
}
@@ -243,7 +243,7 @@ pub fn wasm_min_f64(a: f64, b: f64) -> f64 {
243243
}
244244

245245
/// Wasm `f64.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
246-
pub fn wasm_max_f64(a: f64, b: f64) -> f64 {
246+
pub const fn wasm_max_f64(a: f64, b: f64) -> f64 {
247247
if a.is_nan() || b.is_nan() {
248248
return f64::NAN;
249249
}

crates/herkos-runtime/src/table.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl<const MAX_SIZE: usize> Table<MAX_SIZE> {
4545
///
4646
/// # Errors
4747
/// Returns `ConstructionError::TableInitialSizeExceedsMax` if `initial_size > MAX_SIZE`.
48-
pub fn try_new(initial_size: usize) -> Result<Self, crate::ConstructionError> {
48+
pub const fn try_new(initial_size: usize) -> Result<Self, crate::ConstructionError> {
4949
if initial_size > MAX_SIZE {
5050
return Err(crate::ConstructionError::TableInitialSizeExceedsMax {
5151
initial: initial_size,
@@ -60,7 +60,7 @@ impl<const MAX_SIZE: usize> Table<MAX_SIZE> {
6060

6161
/// Current number of active table slots.
6262
#[inline(always)]
63-
pub fn size(&self) -> usize {
63+
pub const fn size(&self) -> usize {
6464
self.active_size
6565
}
6666

scripts/compare_metrics.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,16 @@ def display_html(groups: Dict[str, List[Dict]], output_path: str = "metrics_repo
238238

239239
timestamps = [parse_timestamp(e["timestamp"]).isoformat() for e in entries]
240240
values = [e["value"] for e in entries]
241+
indices = list(range(len(entries)))
241242

242243
fig.add_trace(
243244
go.Scatter(
244-
x=timestamps,
245+
x=indices,
245246
y=values,
246247
mode="lines+markers",
247248
name=name,
248-
hovertemplate=f"<b>{name}</b><br>Timestamp: %{{x}}<br>Value: %{{y:.2f}}<extra></extra>",
249+
customdata=timestamps,
250+
hovertemplate=f"<b>{name}</b><br>Timestamp: %{{customdata}}<br>Value: %{{y:.2f}}<extra></extra>",
249251
),
250252
row=row,
251253
col=col,

0 commit comments

Comments
 (0)