Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
edb09bc
feat: introduce new ir optimizations
Feb 24, 2026
6c6d2e7
chore: Merge branch 'main' of https://github.com/arnoox/herkos into i…
Feb 28, 2026
e84c33a
refactor: fixed clippy warning
Feb 28, 2026
dc37bc3
Merge branch 'main' of https://github.com/arnoox/herkos into ir-optim…
Feb 28, 2026
8b5a3e2
feat: add shared utility functions for IR optimization passes
Mar 1, 2026
05ea264
Implement constant propagation optimization and update tests for cons…
Mar 1, 2026
30e4e48
feat: implement dead instruction elimination optimization
Mar 1, 2026
096af22
feat: add local common subexpression elimination optimization
Mar 1, 2026
aa64936
Implement Loop-Invariant Code Motion (LICM) optimization pass
Mar 1, 2026
5588d9f
feat: enhance IR optimization by adding redundancy elimination and re…
Mar 2, 2026
fdbb78b
feat: add new benchmarks and implementations for control flow and ari…
Mar 2, 2026
0767225
refactor: remove LICM optimization pass from the optimizer module
Mar 2, 2026
3676231
feat: add Wasm float min/max/nearest functions and integrate into opt…
Mar 3, 2026
5cf1f86
refactor: improve comments and code clarity in optimization passes
Mar 4, 2026
5640607
fix: IR is strict SSA form
Mar 5, 2026
599a7e9
refactor: replace VarId with UseVar and DefVar in IR builder for SSA …
Mar 5, 2026
699674e
refactor: remove constant folding assertion from test_constant_arithm…
Mar 5, 2026
fec1b7d
refactor: enhance documentation and add examples for SSA phi-node low…
Mar 5, 2026
407fdc1
refactor: enhance constant propagation algorithm with global constant…
Mar 5, 2026
c1af62c
Implement Loop-Invariant Code Motion (LICM) optimization pass
Mar 5, 2026
d03c787
refactor: add support for ignoring compiled WebAssembly Rust files in…
Mar 6, 2026
c8a12ad
Add algebraic simplifications and branch condition folding optimizations
Mar 6, 2026
3dd4197
refactor: fix formatting
Mar 6, 2026
16904ef
feat: Cross-block copy propagation
Mar 6, 2026
1a97a8a
feat: add Global Value Numbering (GVN) optimization pass
Mar 6, 2026
86df62c
chore: Merge branch 'main' of https://github.com/arnoox/herkos into i…
Mar 6, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ docs/.ub_cache/
**/*.so
**/*.dylib
**/*.wasm
**/*.wasm.rs

# Test artifacts
*.profraw
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/herkos-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ mod ops;
pub use ops::{
i32_div_s, i32_div_u, i32_rem_s, i32_rem_u, i32_trunc_f32_s, i32_trunc_f32_u, i32_trunc_f64_s,
i32_trunc_f64_u, i64_div_s, i64_div_u, i64_rem_s, i64_rem_u, i64_trunc_f32_s, i64_trunc_f32_u,
i64_trunc_f64_s, i64_trunc_f64_u,
i64_trunc_f64_s, i64_trunc_f64_u, wasm_max_f32, wasm_max_f64, wasm_min_f32, wasm_min_f64,
wasm_nearest_f32, wasm_nearest_f64,
};

/// Wasm execution errors — no panics, no unwinding.
Expand Down
141 changes: 141 additions & 0 deletions crates/herkos-runtime/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,147 @@ pub fn i64_rem_u(lhs: i64, rhs: i64) -> WasmResult<i64> {
.ok_or(WasmTrap::DivisionByZero)
}

// ── Wasm float min/max/nearest ────────────────────────────────────────────────

/// Wasm `f32.min`: propagates NaN (unlike Rust's `f32::min` which ignores it).
/// Also preserves the Wasm rule `min(-0.0, +0.0) = -0.0`.
pub fn wasm_min_f32(a: f32, b: f32) -> f32 {
if a.is_nan() || b.is_nan() {
return f32::NAN;
}
if a == 0.0 && b == 0.0 {
return if a.is_sign_negative() { a } else { b };
}
if a <= b {
a
} else {
b
}
}

/// Wasm `f32.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
pub fn wasm_max_f32(a: f32, b: f32) -> f32 {
if a.is_nan() || b.is_nan() {
return f32::NAN;
}
if a == 0.0 && b == 0.0 {
return if a.is_sign_positive() { a } else { b };
}
if a >= b {
a
} else {
b
}
}

/// Wasm `f64.min`: propagates NaN. `min(-0.0, +0.0) = -0.0`.
pub fn wasm_min_f64(a: f64, b: f64) -> f64 {
if a.is_nan() || b.is_nan() {
return f64::NAN;
}
if a == 0.0 && b == 0.0 {
return if a.is_sign_negative() { a } else { b };
}
if a <= b {
a
} else {
b
}
}

/// Wasm `f64.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
pub fn wasm_max_f64(a: f64, b: f64) -> f64 {
if a.is_nan() || b.is_nan() {
return f64::NAN;
}
if a == 0.0 && b == 0.0 {
return if a.is_sign_positive() { a } else { b };
}
if a >= b {
a
} else {
b
}
}

/// Wasm `f32.nearest` — round to nearest even (banker's rounding).
///
/// Uses `as i32` for truncation-toward-zero (safe since we guard against values >= 2^23,
/// which have no fractional bits). Avoids `f32::round`/`f32::trunc`
/// which are not available in `no_std` without `libm`.
pub fn wasm_nearest_f32(v: f32) -> f32 {
if v.is_nan() || v.is_infinite() || v == 0.0 {
return v;
}
// Floats >= 2^23 have no fractional bits — already an integer.
const NO_FRAC: f32 = 8_388_608.0; // 2^23
if v >= NO_FRAC || v <= -NO_FRAC {
return v;
}
let trunc_i = v as i32; // truncates toward zero; safe since |v| < 2^23
let trunc_f = trunc_i as f32;
let frac = v - trunc_f; // in (-1.0, 1.0), same sign as v
if frac > 0.5 {
(trunc_i + 1) as f32
} else if frac < -0.5 {
(trunc_i - 1) as f32
} else if frac == 0.5 {
// Tie: round to even (trunc_i is the floor for positive v).
if trunc_i % 2 == 0 {
trunc_f
} else {
(trunc_i + 1) as f32
}
} else if frac == -0.5 {
// Tie: round to even. copysign preserves -0.0 when trunc_i == 0.
if trunc_i % 2 == 0 {
f32::copysign(trunc_f, v)
} else {
(trunc_i - 1) as f32
}
} else {
trunc_f
}
}

/// Wasm `f64.nearest` — round to nearest even (banker's rounding).
///
/// Uses `as i64` for truncation-toward-zero (safe since we guard against values >= 2^52,
/// which have no fractional bits). Avoids `f64::round`/`f64::trunc`
/// which are not available in `no_std` without `libm`.
pub fn wasm_nearest_f64(v: f64) -> f64 {
if v.is_nan() || v.is_infinite() || v == 0.0 {
return v;
}
// Floats >= 2^52 have no fractional bits — already an integer.
const NO_FRAC: f64 = 4_503_599_627_370_496.0; // 2^52
if v >= NO_FRAC || v <= -NO_FRAC {
return v;
}
let trunc_i = v as i64; // truncates toward zero; safe since |v| < 2^52
let trunc_f = trunc_i as f64;
let frac = v - trunc_f;
if frac > 0.5 {
(trunc_i + 1) as f64
} else if frac < -0.5 {
(trunc_i - 1) as f64
} else if frac == 0.5 {
if trunc_i % 2 == 0 {
trunc_f
} else {
(trunc_i + 1) as f64
}
} else if frac == -0.5 {
if trunc_i % 2 == 0 {
f64::copysign(trunc_f, v)
} else {
(trunc_i - 1) as f64
}
} else {
trunc_f
}
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions crates/herkos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ wasmparser = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
heck = { workspace = true }
herkos-runtime = { path = "../herkos-runtime", version = "0.1.1" }

[dev-dependencies]
wat = { workspace = true }
13 changes: 13 additions & 0 deletions crates/herkos/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ pub trait Backend {
if_false_idx: usize,
) -> String;

/// Emit Rust code for a conditional branch with an inlined comparison.
///
/// Instead of `if condition != 0`, emits the comparison directly:
/// `if lhs >= rhs { ... } else { ... }`.
fn emit_branch_cmp_to_index(
&self,
op: BinOp,
lhs: VarId,
rhs: VarId,
if_true_idx: usize,
if_false_idx: usize,
) -> String;

/// Emit Rust code for multi-way branch (br_table) using block indices.
fn emit_branch_table_to_index(
&self,
Expand Down
54 changes: 54 additions & 0 deletions crates/herkos/src/backend/safe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,21 @@ impl Backend for SafeBackend {
)
}

fn emit_branch_cmp_to_index(
&self,
op: BinOp,
lhs: VarId,
rhs: VarId,
if_true_idx: usize,
if_false_idx: usize,
) -> String {
let cmp_expr = format_cmp_expr(op, lhs, rhs);
format!(
" if {cmp_expr} {{\n __current_block = Block::B{};\n }} else {{\n __current_block = Block::B{};\n }}\n continue;",
if_true_idx, if_false_idx
)
}

fn emit_branch_table_to_index(
&self,
index: VarId,
Expand Down Expand Up @@ -597,3 +612,42 @@ impl Backend for SafeBackend {
code
}
}

/// Format a comparison expression for use in branch conditions.
fn format_cmp_expr(op: BinOp, lhs: VarId, rhs: VarId) -> String {
match op {
// Signed i32 comparisons
BinOp::I32Eq => format!("{lhs} == {rhs}"),
BinOp::I32Ne => format!("{lhs} != {rhs}"),
BinOp::I32LtS => format!("{lhs} < {rhs}"),
BinOp::I32GtS => format!("{lhs} > {rhs}"),
BinOp::I32LeS => format!("{lhs} <= {rhs}"),
BinOp::I32GeS => format!("{lhs} >= {rhs}"),
// Unsigned i32 comparisons
BinOp::I32LtU => format!("({lhs} as u32) < ({rhs} as u32)"),
BinOp::I32GtU => format!("({lhs} as u32) > ({rhs} as u32)"),
BinOp::I32LeU => format!("({lhs} as u32) <= ({rhs} as u32)"),
BinOp::I32GeU => format!("({lhs} as u32) >= ({rhs} as u32)"),
// Signed i64 comparisons
BinOp::I64Eq => format!("{lhs} == {rhs}"),
BinOp::I64Ne => format!("{lhs} != {rhs}"),
BinOp::I64LtS => format!("{lhs} < {rhs}"),
BinOp::I64GtS => format!("{lhs} > {rhs}"),
BinOp::I64LeS => format!("{lhs} <= {rhs}"),
BinOp::I64GeS => format!("{lhs} >= {rhs}"),
// Unsigned i64 comparisons
BinOp::I64LtU => format!("({lhs} as u64) < ({rhs} as u64)"),
BinOp::I64GtU => format!("({lhs} as u64) > ({rhs} as u64)"),
BinOp::I64LeU => format!("({lhs} as u64) <= ({rhs} as u64)"),
BinOp::I64GeU => format!("({lhs} as u64) >= ({rhs} as u64)"),
// Float comparisons
BinOp::F32Eq | BinOp::F64Eq => format!("{lhs} == {rhs}"),
BinOp::F32Ne | BinOp::F64Ne => format!("{lhs} != {rhs}"),
BinOp::F32Lt | BinOp::F64Lt => format!("{lhs} < {rhs}"),
BinOp::F32Gt | BinOp::F64Gt => format!("{lhs} > {rhs}"),
BinOp::F32Le | BinOp::F64Le => format!("{lhs} <= {rhs}"),
BinOp::F32Ge | BinOp::F64Ge => format!("{lhs} >= {rhs}"),
// Non-comparison ops should never reach here
_ => format!("{lhs} != 0 /* unexpected non-comparison op */"),
}
}
60 changes: 60 additions & 0 deletions crates/herkos/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,27 @@ pub fn generate_function_with_info<B: Backend>(
output.push_str(" loop {\n");
output.push_str(" match __current_block {\n");

// Build global use counts for branch condition inlining.
let global_uses = crate::optimizer::utils::build_global_use_count(ir_func);

for (idx, block) in ir_func.blocks.iter().enumerate() {
output.push_str(&format!(" Block::B{} => {{\n", idx));

// Detect if the BranchIf condition is a single-use comparison BinOp
// defined in this block — if so, skip emitting it and inline into branch.
let inlined_cmp = detect_inlined_cmp(block, &global_uses);
let skip_var = inlined_cmp.as_ref().map(|_| match &block.terminator {
IrTerminator::BranchIf { condition, .. } => *condition,
_ => unreachable!(),
});

for instr in &block.instructions {
// Skip the inlined comparison instruction
if let Some(skip) = skip_var {
if crate::optimizer::utils::instr_dest(instr) == Some(skip) {
continue;
}
}
let code =
crate::codegen::instruction::generate_instruction_with_info(backend, instr, info)?;
output.push_str(&code);
Expand All @@ -187,6 +204,7 @@ pub fn generate_function_with_info<B: Backend>(
&block.terminator,
&block_id_to_index,
ir_func.return_type,
inlined_cmp.as_ref(),
);
output.push_str(&term_code);
output.push('\n');
Expand Down Expand Up @@ -290,6 +308,48 @@ fn generate_signature_with_info<B: Backend>(
sig
}

/// Detect whether a block's `BranchIf` condition is defined by a single-use
/// comparison `BinOp` instruction within the same block. If so, return the
/// comparison info for inlining into the branch.
fn detect_inlined_cmp(
block: &IrBlock,
global_uses: &std::collections::HashMap<VarId, usize>,
) -> Option<crate::codegen::instruction::InlinedCmp> {
let condition = match &block.terminator {
IrTerminator::BranchIf { condition, .. } => *condition,
_ => return None,
};

// Condition must have exactly one use (the BranchIf itself).
if global_uses.get(&condition).copied().unwrap_or(0) != 1 {
return None;
}

// Find the defining instruction in this block.
for (i, instr) in block.instructions.iter().enumerate() {
if let IrInstr::BinOp { dest, op, lhs, rhs } = instr {
if *dest == condition && op.is_comparison() {
// Safety: verify no instruction after this one redefines
// lhs or rhs. If they do, inlining at branch-time would see
// the wrong operand values.
let operands_stable = !block.instructions[i + 1..].iter().any(|later| {
let d = crate::optimizer::utils::instr_dest(later);
d == Some(*lhs) || d == Some(*rhs)
});
if operands_stable {
return Some(crate::codegen::instruction::InlinedCmp {
op: *op,
lhs: *lhs,
rhs: *rhs,
});
}
}
}
}

None
}

/// Check if an IR function has any import calls.
fn has_import_calls(ir_func: &IrFunction) -> bool {
ir_func.blocks.iter().any(|block| {
Expand Down
Loading
Loading