Files
llmdbg/src/js.rs
T
llmdbg-dev ce96a98829 v0.7: event-listener system (addEventListener/step/breakpoint/signal/exit)
- Native step/cont moved to __step_native/__cont_native; public step()/cont()
  defined in prelude.js wrap them and dispatch JS listeners on each stop.
- addEventListener/removeEventListener/clearListeners/listeners() registry on
  globalThis, persistent for the daemon lifetime (independent of ctx.reset()).
- Event object carries {state, prev, stop_reason} plus breakpoint_id/signal/
  exit_code as appropriate. Implements the SPEC 'Event Listeners' + 'Core Loop'.
- No-listener fast path keeps existing scripts zero-cost.
- examples/smarm_ctx_switch.js: worked session inspecting smarm's naked-asm
  context switch; README updated (status table, listener API, deferred list).
2026-05-28 05:50:09 +00:00

652 lines
27 KiB
Rust

//! QuickJS REPL: persistent context, ptrace target lives behind an Rc<RefCell>,
//! globals are installed once at construction.
//!
//! Native globals installed here:
//! spawn(path, ...args) -> { pid, status }
//! __step_native(n=1) / __cont_native() -> stop-reason string
//! (public step()/cont() are defined in prelude.js and fire listeners)
//! print(...args), ctx (persistent object)
//! state.pc, state.sp -> BigInt (live ptrace)
//! state.registers -> object of BigInts
//! state.memory(addr) -> { u8/u32/u64/i64/bytes }
//! state.disasm(n=8) -> [{addr, bytes, text, target?, target_sym?}]
//! state.stack -> { top, usable_base, guard_size }
//! prev -> register snapshot from before last resume
//! sym(addr), addr(name), symbols()
//! disasm(addr, n=8)
//! breakpoint / bp: .set, .remove, .list
//!
//! JS-side helpers (in prelude.js): hex, unhex, dump, repr, addrstr, here,
//! until, finish, show.{disasm,bp,regs}.
use crate::target::Target;
use rquickjs::function::Rest;
use rquickjs::object::Accessor;
use rquickjs::{CatchResultExt, CaughtError, Context, Ctx, Function, Object, Runtime, Value};
use std::cell::RefCell;
use std::rc::Rc;
/// Shared handle. All JS closures clone this and `.borrow_mut()` inside.
pub type TargetSlot = Rc<RefCell<Option<Target>>>;
/// Result of evaluating a script.
pub struct EvalOutput {
/// Anything the script wrote via `print()`.
pub printed: String,
/// String form of the final expression's value, or empty if `undefined` /
/// the source had no trailing expression.
pub result: String,
/// Some(msg) if the eval threw. `printed` may still contain partial output.
pub error: Option<String>,
}
pub struct Repl {
_rt: Runtime,
ctx: Context,
target: TargetSlot,
/// Accumulates print() output during one eval. Lives in an Rc<RefCell<String>>
/// captured by the `print` closure.
print_buf: Rc<RefCell<String>>,
}
impl Repl {
pub fn new() -> Result<Self, String> {
let rt = Runtime::new().map_err(|e| format!("rt: {}", e))?;
let ctx = Context::full(&rt).map_err(|e| format!("ctx: {}", e))?;
let target: TargetSlot = Rc::new(RefCell::new(None));
let print_buf: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
ctx.with(|c| -> Result<(), String> {
install_globals(c, target.clone(), print_buf.clone())
.map_err(|e| format!("install globals: {}", e))
})?;
Ok(Repl { _rt: rt, ctx, target, print_buf })
}
/// Evaluate one script. Captures `print()` output. Always returns Ok with
/// an EvalOutput; JS exceptions become `error: Some(...)`.
pub fn eval(&mut self, source: &str) -> EvalOutput {
self.print_buf.borrow_mut().clear();
let mut result = String::new();
let mut error = None;
self.ctx.with(|c| {
match c.eval::<Value, _>(source).catch(&c) {
Ok(v) => {
if !v.is_undefined() {
result = value_to_display(&c, &v).unwrap_or_else(|e| format!("<display err: {}>", e));
}
}
Err(e) => {
error = Some(format_caught(&e));
}
}
});
EvalOutput {
printed: std::mem::take(&mut *self.print_buf.borrow_mut()),
result,
error,
}
}
/// If a target exists, return its PID so we can kill on shutdown.
pub fn target_pid(&self) -> Option<i32> {
self.target.borrow().as_ref().map(|t| t.pid())
}
}
/// Render a JS value as a one-line string. We delegate to the prelude's
/// `repr()` so BigInts (and BigInts nested inside objects) render correctly.
fn value_to_display<'js>(c: &Ctx<'js>, v: &Value<'js>) -> Result<String, String> {
let globals = c.globals();
let repr: Function = globals.get("repr").map_err(|e| e.to_string())?;
let s: String = repr.call((v.clone(),)).map_err(|e| e.to_string())?;
Ok(s)
}
fn format_caught(e: &CaughtError) -> String {
match e {
CaughtError::Error(err) => format!("error: {}", err),
CaughtError::Exception(exc) => {
let msg = exc.message().unwrap_or_else(|| "<no message>".into());
if let Some(stack) = exc.stack() {
format!("error: {}\n{}", msg, stack)
} else {
format!("error: {}", msg)
}
}
CaughtError::Value(v) => format!("error: {:?}", v),
}
}
// ---------------------------------------------------------------------------
fn install_globals<'js>(
c: Ctx<'js>,
target: TargetSlot,
print_buf: Rc<RefCell<String>>,
) -> rquickjs::Result<()> {
let globals = c.globals();
// ---------- print(...) ----------
{
let buf = print_buf.clone();
let f = Function::new(c.clone(), move |rest: Rest<Value<'js>>| {
let mut s = String::new();
for (i, v) in rest.0.iter().enumerate() {
if i > 0 { s.push(' '); }
// Best-effort String() coercion using global String.
if let Ok(string_fn) = v.ctx().globals().get::<_, Function>("String") {
if let Ok(part) = string_fn.call::<_, String>((v.clone(),)) {
s.push_str(&part);
continue;
}
}
s.push_str("<?>");
}
s.push('\n');
buf.borrow_mut().push_str(&s);
})?;
globals.set("print", f)?;
}
// ---------- ctx (persistent JS object) ----------
{
// Only set if it doesn't already exist (so REPL re-init wouldn't clobber).
if globals.get::<_, Value>("ctx").map(|v| v.is_undefined()).unwrap_or(true) {
let obj = Object::new(c.clone())?;
globals.set("ctx", obj)?;
}
}
// ---------- spawn(path, ...args) ----------
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, path: String, rest: Rest<String>| -> rquickjs::Result<Value<'js>> {
let args: Vec<String> = rest.0;
match Target::spawn(&path, &args) {
Ok(t) => {
let pid = t.pid();
*target.borrow_mut() = Some(t);
let obj = Object::new(c.clone())?;
obj.set("pid", pid)?;
obj.set("status", "stopped_at_entry")?;
Ok(obj.into_value())
}
Err(e) => Err(throw(&c, &format!("spawn failed: {}", e))),
}
})?;
globals.set("spawn", f)?;
}
// ---------- __step_native(n=1) ----------
// Raw single-stepper. Steps up to n instructions, stopping early if the
// target dies. Returns the last stop-reason string. The public `step(n)`
// is defined in prelude.js: it loops one instruction at a time so it can
// fire 'step'/'breakpoint' listeners after each stop. We keep the native
// n-loop too, for the fast path when no listeners are registered.
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt<i64>| -> rquickjs::Result<String> {
let n = n.0.unwrap_or(1);
if n < 1 {
return Err(throw(&c, "step(n): n must be >= 1"));
}
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
let mut last = None;
for _ in 0..n {
match t.step() {
Ok(r) => {
let alive = r.is_alive();
last = Some(r);
if !alive { break; }
}
Err(e) => return Err(throw(&c, &e)),
}
}
Ok(last.map(|r| r.tag()).unwrap_or_else(|| "step".into()))
})?;
globals.set("__step_native", f)?;
}
// ---------- __cont_native() ----------
// Raw continue. Resumes until the next stop (breakpoint, signal, exit).
// The public `cont()` in prelude.js wraps this to fire listeners on the
// resulting stop.
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<String> {
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
match t.cont() {
Ok(r) => Ok(r.tag()),
Err(e) => Err(throw(&c, &e)),
}
})?;
// NOTE: `continue` is a reserved keyword in JS — `continue()` as an
// expression is a syntax error. We expose only `cont()`.
globals.set("__cont_native", f)?;
}
// ---------- state ----------
{
let target_for_state = target.clone();
let state_obj = build_register_view(
&c,
move |c| -> rquickjs::Result<libc::user_regs_struct> {
let slot = target_for_state.borrow();
let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?;
t.regs().map_err(|e| throw(c, &e))
},
)?;
// state.memory(addr) → object with u8/u32/u64/i64/bytes
{
let target = target.clone();
let mem_fn = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result<Value<'js>> {
let addr = coerce_addr(&c, &addr)?;
let target = target.clone();
let obj = Object::new(c.clone())?;
// u8()
{
let target = target.clone();
obj.set("u8", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<u32> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let b = t.read_mem(addr, 1).map_err(|e| throw(&c, &e))?;
Ok(b[0] as u32)
})?)?;
}
// u32()
{
let target = target.clone();
obj.set("u32", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<u32> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let b = t.read_mem(addr, 4).map_err(|e| throw(&c, &e))?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
})?)?;
}
// u64() → BigInt
{
let target = target.clone();
obj.set("u64", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?;
let mut a = [0u8; 8]; a.copy_from_slice(&b);
let v = u64::from_le_bytes(a);
u64_to_bigint(&c, v)
})?)?;
}
// i64() → BigInt
{
let target = target.clone();
obj.set("i64", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?;
let mut a = [0u8; 8]; a.copy_from_slice(&b);
let v = i64::from_le_bytes(a);
i64_to_bigint(&c, v)
})?)?;
}
// bytes(n) → hex string
{
let target = target.clone();
obj.set("bytes", Function::new(c.clone(), move |c: Ctx<'js>, n: usize| -> rquickjs::Result<String> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let b = t.read_mem(addr, n).map_err(|e| throw(&c, &e))?;
let mut s = String::with_capacity(n * 2);
for byte in &b { s.push_str(&format!("{:02x}", byte)); }
Ok(s)
})?)?;
}
Ok(obj.into_value())
})?;
state_obj.set("memory", mem_fn)?;
}
// state.disasm(n=8) — disassemble n instructions starting at pc.
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt<usize>| -> rquickjs::Result<Value<'js>> {
let n = n.0.unwrap_or(8).max(1);
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let pc = t.regs().map_err(|e| throw(&c, &e))?.rip;
let bytes = t.read_mem_pristine(pc, n.saturating_mul(15))
.map_err(|e| throw(&c, &e))?;
let decoded = crate::disasm::decode(pc, &bytes, n);
decoded_to_array(&c, &decoded, t.symbols())
})?;
state_obj.set("disasm", f)?;
}
// state.stack — accessor returning {top, usable_base, guard_size} as BigInts
{
let target = target.clone();
state_obj.prop(
"stack",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let s = t.stack().ok_or_else(|| throw(&c, "no stack info"))?;
let obj = Object::new(c.clone())?;
obj.set("top", u64_to_bigint(&c, s.high)?)?;
obj.set("usable_base", u64_to_bigint(&c, s.low)?)?;
obj.set("guard_size", u64_to_bigint(&c, s.guard_size)?)?;
Ok(obj.into_value())
}),
)?;
}
globals.set("state", state_obj)?;
}
// ---------- disasm(addr, n=8) ----------
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>, n: rquickjs::function::Opt<usize>| -> rquickjs::Result<Value<'js>> {
let addr = coerce_addr(&c, &addr)?;
let n = n.0.unwrap_or(8).max(1);
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let bytes = t.read_mem_pristine(addr, n.saturating_mul(15))
.map_err(|e| throw(&c, &e))?;
let decoded = crate::disasm::decode(addr, &bytes, n);
decoded_to_array(&c, &decoded, t.symbols())
})?;
globals.set("disasm", f)?;
}
// ---------- prev ----------
// Mirror of state for register-only data, backed by the pre-resume
// snapshot. Property accessors throw if no snapshot is available yet
// (i.e. immediately after spawn, before any step/cont).
{
let target_for_prev = target.clone();
let prev_obj = build_register_view(
&c,
move |c| -> rquickjs::Result<libc::user_regs_struct> {
let slot = target_for_prev.borrow();
let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?;
t.prev_regs().ok_or_else(|| throw(c, "no prev snapshot yet (step/cont first)"))
},
)?;
globals.set("prev", prev_obj)?;
}
// ---------- sym(addr) -> "name+0xN" | null ----------
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result<Value<'js>> {
let addr = coerce_addr(&c, &addr)?;
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
match s.name_for_addr(addr) {
Some(name) => Ok(rquickjs::String::from_str(c.clone(), &name)?.into_value()),
None => Ok(Value::new_null(c.clone())),
}
})?;
globals.set("sym", f)?;
}
// ---------- addr(name) -> BigInt | null ----------
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, name: String| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
match s.addr_for_name(&name) {
Some(a) => u64_to_bigint(&c, a),
None => Ok(Value::new_null(c.clone())),
}
})?;
globals.set("addr", f)?;
}
// ---------- symbols() -> { count, load_base, path } ----------
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
let obj = Object::new(c.clone())?;
obj.set("count", s.len() as u32)?;
obj.set("load_base", u64_to_bigint(&c, s.load_base)?)?;
obj.set("path", s.path.to_string_lossy().into_owned())?;
Ok(obj.into_value())
})?;
globals.set("symbols", f)?;
}
// ---------- breakpoint / bp ---------------------------------------------
// breakpoint.set(target, name?) → id (target = address number/BigInt or symbol name)
// breakpoint.remove(id_or_name)
// breakpoint.list() → [{id, addr, name, hits, enabled}]
{
let bp_obj = Object::new(c.clone())?;
// set(target, name?)
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, location: Value<'js>, name: rquickjs::function::Opt<String>| -> rquickjs::Result<u32> {
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
// location: string → resolve via symbols; number/BigInt → coerce_addr.
let addr = if let Some(s) = location.clone().into_string() {
let s = s.to_string()?;
let sym = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
sym.addr_for_name(&s).ok_or_else(|| throw(&c, &format!("unknown symbol: {}", s)))?
} else {
coerce_addr(&c, &location)?
};
let name = name.0;
t.breakpoint_set(addr, name).map_err(|e| throw(&c, &e))
})?;
bp_obj.set("set", f)?;
}
// remove(id_or_name)
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>, id_or_name: Value<'js>| -> rquickjs::Result<()> {
let mut slot = target.borrow_mut();
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
let id = if let Some(s) = id_or_name.clone().into_string() {
let s = s.to_string()?;
t.breakpoint_id_by_name(&s).ok_or_else(|| throw(&c, &format!("no breakpoint named '{}'", s)))?
} else if let Some(n) = id_or_name.as_number() {
if n < 0.0 || n.fract() != 0.0 {
return Err(throw(&c, "bp id must be a non-negative integer"));
}
n as u32
} else if id_or_name.is_int() {
let i: i32 = id_or_name.get()?;
if i < 0 { return Err(throw(&c, "bp id must be non-negative")); }
i as u32
} else {
return Err(throw(&c, "bp.remove expects id (number) or name (string)"));
};
t.breakpoint_remove(id).map_err(|e| throw(&c, &e))
})?;
bp_obj.set("remove", f)?;
}
// list() → array of breakpoint records
{
let target = target.clone();
let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let slot = target.borrow();
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
let arr = rquickjs::Array::new(c.clone())?;
for (i, bp) in t.breakpoints().iter().enumerate() {
let rec = Object::new(c.clone())?;
rec.set("id", bp.id)?;
rec.set("addr", u64_to_bigint(&c, bp.addr)?)?;
rec.set("hits", u64_to_bigint(&c, bp.hits)?)?;
rec.set("enabled", bp.enabled)?;
match &bp.name {
Some(n) => rec.set("name", n.as_str())?,
None => rec.set("name", Value::new_null(c.clone()))?,
};
arr.set(i, rec)?;
}
Ok(arr.into_value())
})?;
bp_obj.set("list", f)?;
}
globals.set("breakpoint", bp_obj.clone())?;
globals.set("bp", bp_obj)?;
}
// ---------- JS-side prelude ---------------------------------------------
// Loaded last, so it can reference any of the native globals above.
// Lives in src/prelude.js, embedded at compile time.
const PRELUDE: &str = include_str!("prelude.js");
c.eval::<(), _>(PRELUDE)?;
Ok(())
}
// --- helpers --------------------------------------------------------------
/// Convert a Vec<Decoded> from the disassembler into a JS Array of records:
/// [{addr: BigInt, bytes: "ffce..", text: "mov rax, ...", target?: BigInt, target_sym?: "name"}]
/// `bytes` is a hex string for compactness; `target` and `target_sym` are
/// only present for branch/call instructions.
fn decoded_to_array<'js>(
c: &Ctx<'js>,
decoded: &[crate::disasm::Decoded],
symbols: Option<&crate::symbols::SymbolTable>,
) -> rquickjs::Result<Value<'js>> {
let arr = rquickjs::Array::new(c.clone())?;
for (i, d) in decoded.iter().enumerate() {
let obj = Object::new(c.clone())?;
obj.set("addr", u64_to_bigint(c, d.addr)?)?;
let mut bytes_hex = String::with_capacity(d.bytes.len() * 2);
for b in &d.bytes { bytes_hex.push_str(&format!("{:02x}", b)); }
obj.set("bytes", bytes_hex)?;
obj.set("text", d.text.as_str())?;
if let Some(t) = d.branch_target {
obj.set("target", u64_to_bigint(c, t)?)?;
if let Some(s) = symbols.and_then(|s| s.name_for_addr(t)) {
obj.set("target_sym", s)?;
}
}
arr.set(i, obj)?;
}
Ok(arr.into_value())
}
/// Build a JS object with `pc`, `sp`, and `registers` accessor properties,
/// each backed by a closure that fetches a `user_regs_struct` on demand.
/// Used for both `state` (live regs via ptrace) and `prev` (snapshot).
fn build_register_view<'js, F>(c: &Ctx<'js>, fetch: F) -> rquickjs::Result<Object<'js>>
where
F: Fn(&Ctx<'js>) -> rquickjs::Result<libc::user_regs_struct> + Clone + 'static,
{
let obj = Object::new(c.clone())?;
{
let fetch = fetch.clone();
obj.prop(
"pc",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
u64_to_bigint(&c, r.rip)
}),
)?;
}
{
let fetch = fetch.clone();
obj.prop(
"sp",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
u64_to_bigint(&c, r.rsp)
}),
)?;
}
{
let fetch = fetch;
obj.prop(
"registers",
Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
let r = fetch(&c)?;
let inner = Object::new(c.clone())?;
let set = |name: &str, val: u64| -> rquickjs::Result<()> {
inner.set(name, u64_to_bigint(&c, val)?)
};
set("rax", r.rax)?; set("rbx", r.rbx)?; set("rcx", r.rcx)?;
set("rdx", r.rdx)?; set("rsi", r.rsi)?; set("rdi", r.rdi)?;
set("rbp", r.rbp)?; set("rsp", r.rsp)?; set("rip", r.rip)?;
set("r8", r.r8)?; set("r9", r.r9)?; set("r10", r.r10)?;
set("r11", r.r11)?; set("r12", r.r12)?; set("r13", r.r13)?;
set("r14", r.r14)?; set("r15", r.r15)?;
set("eflags", r.eflags)?;
Ok(inner.into_value())
}),
)?;
}
Ok(obj)
}
/// Create a JS Error and turn it into an rquickjs::Error::Exception. The idiom
/// in rquickjs for "throw an error from a Rust callback" is to call
/// `Exception::throw_*(ctx, msg)`.
fn throw(c: &Ctx<'_>, msg: &str) -> rquickjs::Error {
rquickjs::Exception::throw_message(c, msg)
}
/// Accept either a JS Number or BigInt and produce a u64 address.
fn coerce_addr<'js>(c: &Ctx<'js>, v: &Value<'js>) -> rquickjs::Result<u64> {
if let Some(n) = v.as_number() {
if n < 0.0 || !n.is_finite() {
return Err(throw(c, "address must be a non-negative finite number"));
}
return Ok(n as u64);
}
if v.is_int() {
let i: i32 = v.get()?;
if i < 0 { return Err(throw(c, "address must be non-negative")); }
return Ok(i as u64);
}
// Try BigInt: route through global BigInt -> toString(10) -> parse.
let globals = c.globals();
let string_fn: Function = globals.get("String")?;
let s: String = string_fn.call((v.clone(),)).map_err(|_| throw(c, "bad address"))?;
// BigInt String() form is like "1234567" (no n suffix).
s.parse::<u64>().map_err(|_| throw(c, &format!("bad address: {}", s)))
}
/// Convert a u64 to a JS BigInt by evaluating `BigInt("<dec>")` in the context.
/// Not the fastest, but completely robust and avoids hunting the rquickjs API
/// surface in v0.1.
fn u64_to_bigint<'js>(c: &Ctx<'js>, v: u64) -> rquickjs::Result<Value<'js>> {
let globals = c.globals();
let bi: Function = globals.get("BigInt")?;
let s = v.to_string();
bi.call((s,))
}
fn i64_to_bigint<'js>(c: &Ctx<'js>, v: i64) -> rquickjs::Result<Value<'js>> {
let globals = c.globals();
let bi: Function = globals.get("BigInt")?;
let s = v.to_string();
bi.call((s,))
}