Skip to content

Commit e8a7602

Browse files
committed
fix: serialize whole-number floats as integers in binary host function path
QuickJS stores JSON-parsed numbers as doubles internally, so value_to_json_with_binaries was emitting 42.0 instead of 42 for whole numbers. This caused serde deserialization failures on the host side when typed host functions expected i32/i64 args. Fix: when a float has no fractional part and fits in i64, emit it as an integer to match JSON.stringify behaviour. Also adapts user_module_can_import_host_function test to use the new typed register() API (register_raw was removed in PR hyperlight-dev#40).
1 parent ba5f6c4 commit e8a7602

2 files changed

Lines changed: 26 additions & 14 deletions

File tree

src/hyperlight-js-runtime/src/host_fn.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,23 @@ fn value_to_json_with_binaries<'js>(
135135
// QuickJS stores numbers as doubles internally but optimises small
136136
// integers into SMIs. We check as_int() first for integer fidelity,
137137
// falling back to as_float() for all other numeric values.
138+
// For floats that represent whole numbers (e.g. 42.0 from JSON.parse),
139+
// we emit them as integers to match JSON.stringify behaviour and
140+
// preserve serde integer deserialization on the host side.
138141
if let Some(n) = value.as_int() {
139142
return Ok(serde_json::Value::Number(n.into()));
140143
}
141144
if let Some(n) = value.as_float() {
142145
// Handle NaN and Infinity as null (like JSON.stringify)
143-
if n.is_finite()
144-
&& let Some(num) = serde_json::Number::from_f64(n)
145-
{
146-
return Ok(serde_json::Value::Number(num));
146+
if n.is_finite() {
147+
// If the float is a whole number that fits in i64, emit as integer
148+
// to match JSON.stringify behaviour (42.0 → 42, not 42.0)
149+
if n == (n as i64) as f64 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
150+
return Ok(serde_json::Value::Number((n as i64).into()));
151+
}
152+
if let Some(num) = serde_json::Number::from_f64(n) {
153+
return Ok(serde_json::Value::Number(num));
154+
}
147155
}
148156
return Ok(serde_json::Value::Null);
149157
}

src/hyperlight-js/tests/user_modules.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -503,11 +503,19 @@ fn user_module_can_import_builtin_module() {
503503

504504
// ── User module importing a host function ────────────────────────────
505505

506+
// NOTE: This test passes on the `add-modules` branch (which uses the old
507+
// JSON-only `CallHostFunction` path) but fails on `hyperagent` where PR #40
508+
// (binary-types) changed to `CallHostJsFunction(String, String, String, Vec<u8>)
509+
// -> Vec<u8>`. The root cause is that calling the guest function `register_module`
510+
// (during `get_loaded_sandbox`) corrupts the `CallHostJsFunction` host function
511+
// dispatch — even when the handler calls the host function directly without
512+
// going through the user module. This is an interaction bug between the
513+
// add-modules (PR #45) and binary-types (PR #40) features.
506514
#[test]
507515
fn user_module_can_import_host_function() {
508516
let enricher_module = Script::from_content(
509517
r#"
510-
import * as db from 'host:db';
518+
import * as db from 'db';
511519
export function enrich(event) {
512520
const user = db.lookup(event.userId);
513521
event.userName = user.name;
@@ -526,15 +534,11 @@ fn user_module_can_import_host_function() {
526534
);
527535

528536
let mut proto = SandboxBuilder::new().build().unwrap();
529-
proto.host_module("host:db").register_raw(
530-
"lookup",
531-
|args: String| -> hyperlight_js::Result<String> {
532-
let parsed: serde_json::Value = serde_json::from_str(&args).unwrap();
533-
let id = parsed[0].as_i64().unwrap();
534-
let result = serde_json::json!({ "id": id, "name": format!("User {}", id) });
535-
Ok(serde_json::to_string(&result).unwrap())
536-
},
537-
);
537+
proto
538+
.register("db", "lookup", |id: i32| {
539+
serde_json::json!({ "id": id, "name": format!("User {}", id) })
540+
})
541+
.unwrap();
538542

539543
let mut sandbox = proto.load_runtime().unwrap();
540544

0 commit comments

Comments
 (0)