Skip to content

Commit 077e1b2

Browse files
benchclaude
andcommitted
feat: add wasm float min/max/nearest operations to runtime
Add implementations of Wasm float operations: - f32.min, f32.max (with NaN propagation and ±0.0 handling) - f64.min, f64.max (with NaN propagation and ±0.0 handling) - f32.nearest (banker's rounding without libm dependency) - f64.nearest (banker's rounding without libm dependency) All operations follow Wasm spec semantics: - NaN input → NaN output - min(-0.0, +0.0) = -0.0; max(-0.0, +0.0) = +0.0 - Nearest uses banker's rounding (round-to-even) These are no_std compatible and require no heap allocation. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 136373b commit 077e1b2

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ docs/.ub_cache/
2323
**/*.so
2424
**/*.dylib
2525
**/*.wasm
26+
**/*.wasm.rs
2627

2728
# Test artifacts
2829
*.profraw

crates/herkos-runtime/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ mod ops;
2626
pub use ops::{
2727
i32_div_s, i32_div_u, i32_rem_s, i32_rem_u, i32_trunc_f32_s, i32_trunc_f32_u, i32_trunc_f64_s,
2828
i32_trunc_f64_u, i64_div_s, i64_div_u, i64_rem_s, i64_rem_u, i64_trunc_f32_s, i64_trunc_f32_u,
29-
i64_trunc_f64_s, i64_trunc_f64_u,
29+
i64_trunc_f64_s, i64_trunc_f64_u, wasm_max_f32, wasm_max_f64, wasm_min_f32, wasm_min_f64,
30+
wasm_nearest_f32, wasm_nearest_f64,
3031
};
3132

3233
/// Wasm execution errors — no panics, no unwinding.

crates/herkos-runtime/src/ops.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,147 @@ pub fn i64_rem_u(lhs: i64, rhs: i64) -> WasmResult<i64> {
194194
.ok_or(WasmTrap::DivisionByZero)
195195
}
196196

197+
// ── Wasm float min/max/nearest ────────────────────────────────────────────────
198+
199+
/// Wasm `f32.min`: propagates NaN (unlike Rust's `f32::min` which ignores it).
200+
/// Also preserves the Wasm rule `min(-0.0, +0.0) = -0.0`.
201+
pub fn wasm_min_f32(a: f32, b: f32) -> f32 {
202+
if a.is_nan() || b.is_nan() {
203+
return f32::NAN;
204+
}
205+
if a == 0.0 && b == 0.0 {
206+
return if a.is_sign_negative() { a } else { b };
207+
}
208+
if a <= b {
209+
a
210+
} else {
211+
b
212+
}
213+
}
214+
215+
/// Wasm `f32.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
216+
pub fn wasm_max_f32(a: f32, b: f32) -> f32 {
217+
if a.is_nan() || b.is_nan() {
218+
return f32::NAN;
219+
}
220+
if a == 0.0 && b == 0.0 {
221+
return if a.is_sign_positive() { a } else { b };
222+
}
223+
if a >= b {
224+
a
225+
} else {
226+
b
227+
}
228+
}
229+
230+
/// Wasm `f64.min`: propagates NaN. `min(-0.0, +0.0) = -0.0`.
231+
pub fn wasm_min_f64(a: f64, b: f64) -> f64 {
232+
if a.is_nan() || b.is_nan() {
233+
return f64::NAN;
234+
}
235+
if a == 0.0 && b == 0.0 {
236+
return if a.is_sign_negative() { a } else { b };
237+
}
238+
if a <= b {
239+
a
240+
} else {
241+
b
242+
}
243+
}
244+
245+
/// Wasm `f64.max`: propagates NaN. `max(-0.0, +0.0) = +0.0`.
246+
pub fn wasm_max_f64(a: f64, b: f64) -> f64 {
247+
if a.is_nan() || b.is_nan() {
248+
return f64::NAN;
249+
}
250+
if a == 0.0 && b == 0.0 {
251+
return if a.is_sign_positive() { a } else { b };
252+
}
253+
if a >= b {
254+
a
255+
} else {
256+
b
257+
}
258+
}
259+
260+
/// Wasm `f32.nearest` — round to nearest even (banker's rounding).
261+
///
262+
/// Uses `as i32` for truncation-toward-zero (safe since we guard against values >= 2^23,
263+
/// which have no fractional bits). Avoids `f32::round`/`f32::trunc`
264+
/// which are not available in `no_std` without `libm`.
265+
pub fn wasm_nearest_f32(v: f32) -> f32 {
266+
if v.is_nan() || v.is_infinite() || v == 0.0 {
267+
return v;
268+
}
269+
// Floats >= 2^23 have no fractional bits — already an integer.
270+
const NO_FRAC: f32 = 8_388_608.0; // 2^23
271+
if v >= NO_FRAC || v <= -NO_FRAC {
272+
return v;
273+
}
274+
let trunc_i = v as i32; // truncates toward zero; safe since |v| < 2^23
275+
let trunc_f = trunc_i as f32;
276+
let frac = v - trunc_f; // in (-1.0, 1.0), same sign as v
277+
if frac > 0.5 {
278+
(trunc_i + 1) as f32
279+
} else if frac < -0.5 {
280+
(trunc_i - 1) as f32
281+
} else if frac == 0.5 {
282+
// Tie: round to even (trunc_i is the floor for positive v).
283+
if trunc_i % 2 == 0 {
284+
trunc_f
285+
} else {
286+
(trunc_i + 1) as f32
287+
}
288+
} else if frac == -0.5 {
289+
// Tie: round to even. copysign preserves -0.0 when trunc_i == 0.
290+
if trunc_i % 2 == 0 {
291+
f32::copysign(trunc_f, v)
292+
} else {
293+
(trunc_i - 1) as f32
294+
}
295+
} else {
296+
trunc_f
297+
}
298+
}
299+
300+
/// Wasm `f64.nearest` — round to nearest even (banker's rounding).
301+
///
302+
/// Uses `as i64` for truncation-toward-zero (safe since we guard against values >= 2^52,
303+
/// which have no fractional bits). Avoids `f64::round`/`f64::trunc`
304+
/// which are not available in `no_std` without `libm`.
305+
pub fn wasm_nearest_f64(v: f64) -> f64 {
306+
if v.is_nan() || v.is_infinite() || v == 0.0 {
307+
return v;
308+
}
309+
// Floats >= 2^52 have no fractional bits — already an integer.
310+
const NO_FRAC: f64 = 4_503_599_627_370_496.0; // 2^52
311+
if v >= NO_FRAC || v <= -NO_FRAC {
312+
return v;
313+
}
314+
let trunc_i = v as i64; // truncates toward zero; safe since |v| < 2^52
315+
let trunc_f = trunc_i as f64;
316+
let frac = v - trunc_f;
317+
if frac > 0.5 {
318+
(trunc_i + 1) as f64
319+
} else if frac < -0.5 {
320+
(trunc_i - 1) as f64
321+
} else if frac == 0.5 {
322+
if trunc_i % 2 == 0 {
323+
trunc_f
324+
} else {
325+
(trunc_i + 1) as f64
326+
}
327+
} else if frac == -0.5 {
328+
if trunc_i % 2 == 0 {
329+
f64::copysign(trunc_f, v)
330+
} else {
331+
(trunc_i - 1) as f64
332+
}
333+
} else {
334+
trunc_f
335+
}
336+
}
337+
197338
// ── Tests ─────────────────────────────────────────────────────────────────────
198339

199340
#[cfg(test)]

0 commit comments

Comments
 (0)