v0.1: spawn/step/cont, state.{pc,sp,registers,memory}, prelude.js

This commit is contained in:
claude
2026-05-27 21:08:28 +00:00
commit 912f5003fe
7 changed files with 971 additions and 0 deletions
+403
View File
@@ -0,0 +1,403 @@
//! QuickJS REPL: persistent context, ptrace target lives behind an Rc<RefCell>,
//! globals are installed once at construction.
//!
//! v0.1 surface:
//! spawn(path, ...args) -> { pid, status }
//! step(n=1) -> stop-reason string
//! continue() -> stop-reason string (exposed as `continue` and `cont`)
//! print(...args) -> writes to stdout
//! ctx -> persistent plain object
//! state.pc / .sp -> BigInt
//! state.registers -> object of BigInts
//! state.memory(addr) -> { u8(), u32(), u64(), i64(), bytes(n) }
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(n=1) ----------
{
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", f)?;
}
// ---------- continue() / cont() ----------
{
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()` for v0.1.
globals.set("cont", f)?;
}
// ---------- state ----------
{
let state_obj = Object::new(c.clone())?;
// state.pc — accessor returning BigInt
{
let target = target.clone();
state_obj.prop(
"pc",
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 r = t.regs().map_err(|e| throw(&c, &e))?;
u64_to_bigint(&c, r.rip)
}),
)?;
}
// state.sp
{
let target = target.clone();
state_obj.prop(
"sp",
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 r = t.regs().map_err(|e| throw(&c, &e))?;
u64_to_bigint(&c, r.rsp)
}),
)?;
}
// state.registers — accessor returning a fresh object each time
{
let target = target.clone();
state_obj.prop(
"registers",
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 r = t.regs().map_err(|e| throw(&c, &e))?;
let obj = Object::new(c.clone())?;
let set = |name: &str, val: u64| -> rquickjs::Result<()> {
obj.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(obj.into_value())
}),
)?;
}
// 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)?;
}
globals.set("state", state_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 --------------------------------------------------------------
/// 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,))
}