From 675525e65c3dd4a7ad6436dcd215a315454d8093 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Sat, 30 May 2026 03:44:46 +0000 Subject: [PATCH] fix(math): implement f16round --- .../src/collectors/escape_check.rs | 1 + .../src/collectors/escape_news.rs | 1 + .../src/collectors/i32_locals.rs | 1 + .../src/collectors/pointer_locals.rs | 1 + crates/perry-codegen/src/collectors/refs.rs | 1 + crates/perry-codegen/src/expr/misc_methods.rs | 6 ++ crates/perry-codegen/src/expr/mod.rs | 1 + crates/perry-codegen/src/expr/property_get.rs | 24 ++++++++ .../src/runtime_decls/strings.rs | 1 + crates/perry-codegen/src/stmt/loops.rs | 3 +- crates/perry-hir/src/analysis/builtins.rs | 1 + crates/perry-hir/src/ir/expr.rs | 1 + crates/perry-hir/src/lower/array_fold.rs | 1 + .../src/lower/expr_call/module_static.rs | 7 +++ crates/perry-hir/src/lower_types.rs | 4 +- crates/perry-hir/src/stable_hash/expr.rs | 1 + crates/perry-hir/src/walker/expr_mut.rs | 1 + crates/perry-hir/src/walker/expr_ref.rs | 1 + crates/perry-runtime/src/math.rs | 54 ++++++++++++++++++ .../perry-runtime/src/object/global_this.rs | 10 ++++ .../node-suite/globals/math-f16round.ts | 57 +++++++++++++++++++ 21 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 test-parity/node-suite/globals/math-f16round.ts diff --git a/crates/perry-codegen/src/collectors/escape_check.rs b/crates/perry-codegen/src/collectors/escape_check.rs index 12acab0c7..dd93f0e2b 100644 --- a/crates/perry-codegen/src/collectors/escape_check.rs +++ b/crates/perry-codegen/src/collectors/escape_check.rs @@ -351,6 +351,7 @@ pub fn check_escapes_in_expr( | Expr::MathCeil(operand) | Expr::MathRound(operand) | Expr::MathAbs(operand) + | Expr::MathF16round(operand) | Expr::MathMinSpread(operand) | Expr::MathMaxSpread(operand) | Expr::ArrayFrom(operand) diff --git a/crates/perry-codegen/src/collectors/escape_news.rs b/crates/perry-codegen/src/collectors/escape_news.rs index c159e7bfb..fce597b9f 100644 --- a/crates/perry-codegen/src/collectors/escape_news.rs +++ b/crates/perry-codegen/src/collectors/escape_news.rs @@ -240,6 +240,7 @@ fn collect_used_new_fields_in_expr( | Expr::MathCeil(operand) | Expr::MathRound(operand) | Expr::MathAbs(operand) + | Expr::MathF16round(operand) | Expr::MathMinSpread(operand) | Expr::MathMaxSpread(operand) | Expr::ArrayFrom(operand) diff --git a/crates/perry-codegen/src/collectors/i32_locals.rs b/crates/perry-codegen/src/collectors/i32_locals.rs index 18e5d22ea..72d5ddbe2 100644 --- a/crates/perry-codegen/src/collectors/i32_locals.rs +++ b/crates/perry-codegen/src/collectors/i32_locals.rs @@ -1145,6 +1145,7 @@ pub fn collect_localset_ids_in_expr_filtered( | Expr::MathLog10(operand) | Expr::MathLog1p(operand) | Expr::MathClz32(operand) + | Expr::MathF16round(operand) | Expr::MathMinSpread(operand) | Expr::MathMaxSpread(operand) => { walk(operand, out); diff --git a/crates/perry-codegen/src/collectors/pointer_locals.rs b/crates/perry-codegen/src/collectors/pointer_locals.rs index 1a42b43f7..49bcf0eb4 100644 --- a/crates/perry-codegen/src/collectors/pointer_locals.rs +++ b/crates/perry-codegen/src/collectors/pointer_locals.rs @@ -82,6 +82,7 @@ pub fn collect_pointer_typed_locals( | Expr::MathCbrt(_) | Expr::MathHypot(_) | Expr::MathFround(_) + | Expr::MathF16round(_) | Expr::MathClz32(_) | Expr::MathExpm1(_) | Expr::MathLog1p(_) diff --git a/crates/perry-codegen/src/collectors/refs.rs b/crates/perry-codegen/src/collectors/refs.rs index f0a0da4f7..9b7013dab 100644 --- a/crates/perry-codegen/src/collectors/refs.rs +++ b/crates/perry-codegen/src/collectors/refs.rs @@ -250,6 +250,7 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet) { | Expr::MathLog10(operand) | Expr::MathLog1p(operand) | Expr::MathClz32(operand) + | Expr::MathF16round(operand) | Expr::MathMinSpread(operand) | Expr::MathMaxSpread(operand) => { walk(operand, out); diff --git a/crates/perry-codegen/src/expr/misc_methods.rs b/crates/perry-codegen/src/expr/misc_methods.rs index 95abcdc96..901e77eca 100644 --- a/crates/perry-codegen/src/expr/misc_methods.rs +++ b/crates/perry-codegen/src/expr/misc_methods.rs @@ -52,6 +52,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let v = lower_expr(ctx, operand)?; Ok(ctx.block().call(DOUBLE, "js_math_fround", &[(DOUBLE, &v)])) } + Expr::MathF16round(operand) => { + let v = lower_expr(ctx, operand)?; + Ok(ctx + .block() + .call(DOUBLE, "js_math_f16round", &[(DOUBLE, &v)])) + } // -------- new Map([[k,v], ...]) — alloc empty map, ignore source -------- Expr::MapNewFromArray(arr_expr) => { diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 061cf0389..f67ab3703 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1565,6 +1565,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } Expr::CallSpread { .. } => call_spread::lower(ctx, expr), Expr::MathFround(..) + | Expr::MathF16round(..) | Expr::MapNewFromArray(..) | Expr::DateGetTime(..) | Expr::DateGetTimezoneOffset(..) diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 5a52c7de0..c538bcf2a 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -614,6 +614,30 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(I64, &ctor_handle), (I64, &key_raw)], )); } + if property == "f16round" { + let math_idx = ctx.strings.intern("Math"); + let math_bytes_global = + format!("@{}", ctx.strings.entry(math_idx).bytes_global); + let math_len = "Math".len().to_string(); + let math_obj = ctx.block().call( + DOUBLE, + "js_get_global_this_builtin_value", + &[(PTR, &math_bytes_global), (I64, &math_len)], + ); + let key_idx = ctx.strings.intern(property); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let math_handle = unbox_to_i64(blk, &math_obj); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + return Ok(blk.call( + DOUBLE, + "js_object_get_field_by_name_f64", + &[(I64, &math_handle), (I64, &key_raw)], + )); + } if matches!( property.as_str(), "Console" diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 75b4a8681..6cfbf8c78 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1025,6 +1025,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_math_clz32", DOUBLE, &[DOUBLE]); module.declare_function("js_math_cbrt", DOUBLE, &[DOUBLE]); module.declare_function("js_math_fround", DOUBLE, &[DOUBLE]); + module.declare_function("js_math_f16round", DOUBLE, &[DOUBLE]); module.declare_function("js_math_sinh", DOUBLE, &[DOUBLE]); module.declare_function("js_math_cosh", DOUBLE, &[DOUBLE]); module.declare_function("js_math_tanh", DOUBLE, &[DOUBLE]); diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index 99bb152f3..16f70ad3b 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -965,7 +965,8 @@ pub(crate) fn expr_preserves_array_length( | Expr::MathSqrt(a) | Expr::MathFloor(a) | Expr::MathCeil(a) - | Expr::MathRound(a) => walk(a), + | Expr::MathRound(a) + | Expr::MathF16round(a) => walk(a), Expr::Array(elements) => elements.iter().all(&walk), Expr::ArraySpread(elements) => elements.iter().all(|el| match el { ArrayElement::Expr(e) | ArrayElement::Spread(e) => walk(e), diff --git a/crates/perry-hir/src/analysis/builtins.rs b/crates/perry-hir/src/analysis/builtins.rs index 114b118b0..cac1b1151 100644 --- a/crates/perry-hir/src/analysis/builtins.rs +++ b/crates/perry-hir/src/analysis/builtins.rs @@ -113,6 +113,7 @@ pub(crate) fn is_builtin_static_function_member(namespace: &str, member: &str) - | "expm1" | "floor" | "fround" + | "f16round" | "hypot" | "imul" | "log" diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index cf54393a3..e3ebf6908 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -699,6 +699,7 @@ pub enum Expr { MathCbrt(Box), // Math.cbrt(x) -> number MathHypot(Vec), // Math.hypot(...values) -> number MathFround(Box), // Math.fround(x) -> number + MathF16round(Box), // Math.f16round(x) -> number MathClz32(Box), // Math.clz32(x) -> number MathExpm1(Box), // Math.expm1(x) -> number MathLog1p(Box), // Math.log1p(x) -> number diff --git a/crates/perry-hir/src/lower/array_fold.rs b/crates/perry-hir/src/lower/array_fold.rs index 50dcf96fa..4e969fc23 100644 --- a/crates/perry-hir/src/lower/array_fold.rs +++ b/crates/perry-hir/src/lower/array_fold.rs @@ -239,6 +239,7 @@ pub(crate) fn is_known_math_static_method(name: &str) -> bool { | "expm1" | "floor" | "fround" + | "f16round" | "hypot" | "imul" | "log" diff --git a/crates/perry-hir/src/lower/expr_call/module_static.rs b/crates/perry-hir/src/lower/expr_call/module_static.rs index 4070968f8..2899643de 100644 --- a/crates/perry-hir/src/lower/expr_call/module_static.rs +++ b/crates/perry-hir/src/lower/expr_call/module_static.rs @@ -662,6 +662,13 @@ pub(super) fn try_module_static_methods( )))); } } + "f16round" => { + if !args.is_empty() { + return Ok(Ok(Expr::MathF16round(Box::new( + args.into_iter().next().unwrap(), + )))); + } + } "clz32" => { if !args.is_empty() { return Ok(Ok(Expr::MathClz32(Box::new( diff --git a/crates/perry-hir/src/lower_types.rs b/crates/perry-hir/src/lower_types.rs index a23c817a4..65addf91d 100644 --- a/crates/perry-hir/src/lower_types.rs +++ b/crates/perry-hir/src/lower_types.rs @@ -807,7 +807,9 @@ pub(crate) fn infer_call_return_type(callee: &ast::Expr, ctx: &LoweringContext) "floor" | "ceil" | "round" | "abs" | "sqrt" | "pow" | "min" | "max" | "random" | "log" | "log2" | "log10" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "atan2" | "exp" | "sign" | "trunc" - | "cbrt" | "hypot" | "fround" | "clz32" | "imul" => Type::Number, + | "cbrt" | "hypot" | "fround" | "f16round" | "clz32" | "imul" => { + Type::Number + } _ => Type::Any, }; } diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index 889d4b503..b2c68ee61 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -207,6 +207,7 @@ impl SH for Expr { Expr::MathCbrt(e) => { tag(h, 163); e.as_ref().hash(h); } Expr::MathHypot(es) => { tag(h, 164); es.hash(h); } Expr::MathFround(e) => { tag(h, 165); e.as_ref().hash(h); } + Expr::MathF16round(e) => { tag(h, 12041); e.as_ref().hash(h); } Expr::MathClz32(e) => { tag(h, 166); e.as_ref().hash(h); } Expr::MathExpm1(e) => { tag(h, 167); e.as_ref().hash(h); } Expr::MathLog1p(e) => { tag(h, 168); e.as_ref().hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index e29e6558e..2ce76c1cb 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -177,6 +177,7 @@ where | Expr::MathAtan(v) | Expr::MathCbrt(v) | Expr::MathFround(v) + | Expr::MathF16round(v) | Expr::MathExpm1(v) | Expr::MathSinh(v) | Expr::MathCosh(v) diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index be97d3e62..8230ccba8 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -178,6 +178,7 @@ where | Expr::MathAtan(v) | Expr::MathCbrt(v) | Expr::MathFround(v) + | Expr::MathF16round(v) | Expr::MathExpm1(v) | Expr::MathSinh(v) | Expr::MathCosh(v) diff --git a/crates/perry-runtime/src/math.rs b/crates/perry-runtime/src/math.rs index 423f0b6c2..84ae2e0a5 100644 --- a/crates/perry-runtime/src/math.rs +++ b/crates/perry-runtime/src/math.rs @@ -87,6 +87,60 @@ pub extern "C" fn js_math_fround(x: f64) -> f64 { x as f32 as f64 } +fn round_ties_to_even(value: f64) -> u64 { + let floor = value.floor(); + let floor_int = floor as u64; + let frac = value - floor; + if frac < 0.5 { + floor_int + } else if frac > 0.5 { + floor_int + 1 + } else if floor_int & 1 == 0 { + floor_int + } else { + floor_int + 1 + } +} + +/// Math.f16round(x) -> number — nearest IEEE-754 binary16 value +#[no_mangle] +pub extern "C" fn js_math_f16round(value: f64) -> f64 { + let x = crate::builtins::js_number_coerce(value); + if x == 0.0 || !x.is_finite() { + return x; + } + + const MIN_HALF_SUBNORMAL: f64 = 5.960464477539063e-8; // 2^-24 + const MIN_HALF_NORMAL: f64 = 0.00006103515625; // 2^-14 + const MAX_HALF_FINITE: f64 = 65504.0; + const OVERFLOW_THRESHOLD: f64 = 65520.0; + + let negative = x.is_sign_negative(); + let abs = x.abs(); + let rounded = if abs >= OVERFLOW_THRESHOLD { + f64::INFINITY + } else if abs < MIN_HALF_NORMAL { + let mantissa = round_ties_to_even(abs / MIN_HALF_SUBNORMAL); + mantissa as f64 * MIN_HALF_SUBNORMAL + } else { + let exponent = (((abs.to_bits() >> 52) & 0x7ff) as i32) - 1023; + let step = 2.0f64.powi(exponent - 10); + let significand = round_ties_to_even(abs / step); + let rounded = significand as f64 * step; + if rounded > MAX_HALF_FINITE { + f64::INFINITY + } else { + rounded + } + }; + + if negative { + -rounded + } else { + rounded + } +} + /// Math.clz32(x) -> number — count leading zeros of 32-bit integer #[no_mangle] pub extern "C" fn js_math_clz32(x: f64) -> f64 { diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 258d72c52..e7021fbd2 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -228,6 +228,13 @@ extern "C" fn global_this_btoa_thunk( crate::value::js_nanbox_string(encoded as i64) } +extern "C" fn math_f16round_thunk( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + crate::math::js_math_f16round(value) +} + extern "C" fn global_this_error_capture_stack_trace_thunk( _closure: *const crate::closure::ClosureHeader, target: f64, @@ -810,6 +817,9 @@ fn populate_global_this_builtins(singleton: *mut ObjectHeader) { if ns_obj.is_null() { continue; } + if name == "Math" { + install_proto_method(ns_obj, "f16round", math_f16round_thunk as *const u8, 1); + } crate::value::js_nanbox_pointer(ns_obj as i64) }; js_object_set_field_by_name(singleton, name_key, ns_value); diff --git a/test-parity/node-suite/globals/math-f16round.ts b/test-parity/node-suite/globals/math-f16round.ts new file mode 100644 index 000000000..273654420 --- /dev/null +++ b/test-parity/node-suite/globals/math-f16round.ts @@ -0,0 +1,57 @@ +function formatNumber(value: number): string { + if (Object.is(value, -0)) return "-0"; + if (Number.isNaN(value)) return "NaN"; + return String(value); +} + +function show(label: string, value: number): void { + console.log(label, formatNumber(value)); +} + +function showSame(label: string, value: number, expected: number): void { + console.log(label, Object.is(value, expected)); +} + +console.log("typeof:", typeof Math.f16round); +console.log("name:", Math.f16round.name); +console.log("length:", Math.f16round.length); + +show("direct 1.337:", Math.f16round(1.337)); +show("direct -0:", Math.f16round(-0)); +show("direct NaN:", Math.f16round(NaN)); +show("direct Infinity:", Math.f16round(Infinity)); +show("direct -Infinity:", Math.f16round(-Infinity)); +showSame("underflow tie:", Math.f16round(2.9802322387695312e-8), 0); +showSame( + "subnormal tie up:", + Math.f16round(8.940696716308594e-8), + 1.1920928955078125e-7, +); +showSame( + "min subnormal:", + Math.f16round(5.960464477539063e-8), + 5.960464477539063e-8, +); +showSame("min normal:", Math.f16round(0.00006103515625), 0.00006103515625); +show("max finite:", Math.f16round(65504)); +show("overflow below tie:", Math.f16round(65519)); +show("overflow tie:", Math.f16round(65520)); +showSame( + "negative underflow tie:", + Math.f16round(-2.9802322387695312e-8), + -0, +); +show("ties even down:", Math.f16round(1.00048828125)); +show("ties even up:", Math.f16round(1.00146484375)); + +const alias = Math.f16round; +console.log("alias typeof:", typeof alias); +show("alias 1.337:", alias(1.337)); + +const globalAlias = globalThis.Math.f16round; +console.log("global alias typeof:", typeof globalAlias); +showSame( + "global alias subnormal:", + globalAlias(8.940696716308594e-8), + 1.1920928955078125e-7, +);