Files
hakorune/src/runner/hv1_inline.rs

293 lines
15 KiB
Rust
Raw Normal View History

use serde_json::Value;
use std::collections::HashMap;
/// Minimal hv1 inline executor for tests (no Nyash parser).
/// Supports a very small subset needed by canaries:
/// - const i64
/// - mir_call(Constructor ArrayBox)
/// - mir_call(Method ArrayBox.push/size/len/length) with optional per-receiver state
/// - ret (by register id)
pub fn run_json_v1_inline(json: &str) -> i32 {
// Parse JSON
let v: Value = match serde_json::from_str(json) {
Ok(v) => v,
Err(_) => return 1,
};
// schema_version 1.x required
match v.get("schema_version").and_then(Value::as_str) {
Some(s) if s.starts_with('1') => {}
_ => return 1,
}
// Fetch first function and build block map
let functions = match v.get("functions").and_then(Value::as_array) { Some(a) => a, None => return 1 };
let func = match functions.get(0) { Some(f) => f, None => return 1 };
let blocks = match func.get("blocks").and_then(Value::as_array) { Some(a) => a, None => return 1 };
let mut bmap: HashMap<i64, &Vec<Value>> = HashMap::new();
for b in blocks {
if let (Some(id), Some(insts)) = (b.get("id").and_then(Value::as_i64), b.get("instructions").and_then(Value::as_array)) {
bmap.insert(id, insts);
}
}
// Registers and simple method state
let mut regs: HashMap<i64, i64> = HashMap::new();
let sizestate = crate::config::env::env_bool("HAKO_VM_MIRCALL_SIZESTATE");
let per_recv = crate::config::env::env_bool("HAKO_VM_MIRCALL_SIZESTATE_PER_RECV");
// Keep Array and Map length states separate to avoid crossbox interference
let mut len_global_arr: i64 = 0;
let mut len_by_recv_arr: HashMap<i64, i64> = HashMap::new();
let mut len_global_map: i64 = 0;
let mut len_by_recv_map: HashMap<i64, i64> = HashMap::new();
fn is_size_alias(name: &str) -> bool {
matches!(name, "size" | "len" | "length")
}
// Simple CFG interpreter with limited ops (const/compare/phi/branch/jump/mir_call/ret)
let mut curr: i64 = 0; // assume entry block id 0
let mut prev: i64 = -1;
let mut steps: i32 = 0;
'outer: loop {
if steps > 10000 { return 1; }
steps += 1;
let insts = match bmap.get(&curr) { Some(v) => *v, None => return 1 };
let mut ip = 0usize;
while ip < insts.len() {
let inst = &insts[ip];
ip += 1;
let op = match inst.get("op").and_then(Value::as_str) { Some(s) => s, None => return 1 };
match op {
"binop" => {
// Minimal integer binops
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let lhs = inst.get("lhs").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let rhs = inst.get("rhs").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let opn = inst.get("operation").and_then(Value::as_str).unwrap_or("");
let out = match opn {
"+" => lhs.wrapping_add(rhs),
"-" => lhs.wrapping_sub(rhs),
"*" => lhs.wrapping_mul(rhs),
"/" => {
if rhs == 0 { 0 } else { lhs.wrapping_div(rhs) }
}
"%" => {
if rhs == 0 { 0 } else { lhs.wrapping_rem(rhs) }
}
"&" => lhs & rhs,
"|" => lhs | rhs,
"^" => lhs ^ rhs,
"<<" => lhs.wrapping_shl((rhs as u32).min(63)),
">>" => ((lhs as i64) >> (rhs as u32).min(63)) as i64,
_ => 0,
};
regs.insert(dst, out);
}
"unop" => {
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let src = inst.get("src").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let kind = inst.get("kind").and_then(Value::as_str).unwrap_or("");
let out = match kind {
"neg" => src.wrapping_neg(),
"not" => if src == 0 { 1 } else { 0 },
"bitnot" => !src,
_ => 0,
};
regs.insert(dst, out);
}
"copy" => {
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let srcv = inst.get("src").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
regs.insert(dst, srcv);
}
"typeop" => {
// Minimal TypeOp support for PRIMARY reps
// Fields: operation ("check"|"is"|"cast"), src (vid), target_type (str), dst (vid)
let operation = inst
.get("operation")
.and_then(Value::as_str)
.unwrap_or("")
.to_lowercase();
let src = match inst.get("src").and_then(Value::as_i64) { Some(s) => s, None => return 1 };
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let target = inst
.get("target_type")
.and_then(Value::as_str)
.unwrap_or("")
.to_lowercase();
let sval = regs.get(&src).cloned();
let is_integer = sval.is_some(); // hv1 inline stores i64 only → integer
let mut out = 0i64;
if operation == "check" || operation == "is" {
if target == "i64" || target == "int" || target == "integer" {
out = if is_integer { 1 } else { 0 };
} else if target == "bool" {
// Inline model uses integer registers; treat 0/1 as bool when present
out = if let Some(v) = sval { if v == 0 || v == 1 { 1 } else { 0 } } else { 0 };
} else if target == "string" {
out = 0; // no string registers in inline model
} else {
out = 0;
}
regs.insert(dst, out);
} else {
// cast/as: pass-through (MVP)
regs.insert(dst, sval.unwrap_or(0));
}
}
"const" => {
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
// Prefer i64 numeric constants; otherwise stub non-numeric to 0 for inline path
if let Some(n) = inst.get("value").and_then(|vv| vv.get("value")).and_then(Value::as_i64) {
regs.insert(dst, n);
} else {
regs.insert(dst, 0);
}
}
"compare" => {
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let lhs = inst.get("lhs").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let rhs = inst.get("rhs").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let cmp = inst.get("cmp").and_then(Value::as_str).unwrap_or("");
let res = match cmp {
"Gt" => (lhs > rhs) as i64,
"Ge" => (lhs >= rhs) as i64,
"Lt" => (lhs < rhs) as i64,
"Le" => (lhs <= rhs) as i64,
"Eq" => (lhs == rhs) as i64,
"Ne" => (lhs != rhs) as i64,
_ => 0,
};
regs.insert(dst, res);
}
"mir_call" => {
// Support both nested shape {"mir_call":{"callee":...}} and flat shape {"callee":...}
let mc = inst.get("mir_call");
let callee = if let Some(m) = mc { m.get("callee") } else { inst.get("callee") };
let Some(callee) = callee else { return 1 };
let ctype = callee.get("type").and_then(Value::as_str).unwrap_or("");
match ctype {
// Constructor: just create and optionally write dst=0
"Constructor" => {
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
// ArrayBox methods we model inline
"Method" => {
let bname = callee.get("box_name").and_then(Value::as_str).unwrap_or("");
if bname == "ArrayBox" {
let method = callee.get("method").and_then(Value::as_str).unwrap_or("");
let recv = callee.get("receiver").and_then(Value::as_i64).unwrap_or(-1);
if method == "push" {
if sizestate {
if per_recv {
let e = len_by_recv_arr.entry(recv).or_insert(0);
*e += 1;
} else {
len_global_arr += 1;
}
}
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
if is_size_alias(method) {
let value = if sizestate {
if per_recv { *len_by_recv_arr.get(&recv).unwrap_or(&0) } else { len_global_arr }
} else { 0 };
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, value); }
continue;
}
// unsupported method on ArrayBox → stub 0
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
if bname == "MapBox" {
let method = callee.get("method").and_then(Value::as_str).unwrap_or("");
let recv = callee.get("receiver").and_then(Value::as_i64).unwrap_or(-1);
if method == "set" {
if sizestate {
if per_recv {
let e = len_by_recv_map.entry(recv).or_insert(0);
*e += 1;
} else {
len_global_map += 1;
}
}
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
if is_size_alias(method) {
let value = if sizestate {
if per_recv { *len_by_recv_map.get(&recv).unwrap_or(&0) } else { len_global_map }
} else { 0 };
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, value); }
continue;
}
// other methods stub 0
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
// Other box methods are not modeled; stub if dst present
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
// Extern calls: allow stub (return 0) when provider is enabled via env, else still stub to 0
"Extern" => {
let _provider_on = crate::config::env::env_bool("HAKO_V1_EXTERN_PROVIDER");
// For now, always treat extern as stub → write 0 when dst present
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); }
continue;
}
_ => {}
}
// Unsupported callee shape/type → treat as stub if possible, else error
if let Some(dst) = inst.get("dst").and_then(Value::as_i64) { regs.insert(dst, 0); continue; }
return 1;
}
"phi" => {
let dst = match inst.get("dst").and_then(Value::as_i64) { Some(d) => d, None => return 1 };
let mut val: i64 = 0;
let mut matched = false;
if let Some(incomings) = inst.get("incoming").and_then(Value::as_array) {
for pair in incomings {
if let Some(arr) = pair.as_array() {
if arr.len() >= 2 {
// v1 schema (inline minimal): [value_reg, pred_block_id]
let r = arr[0].as_i64().unwrap_or(-1);
let b = arr[1].as_i64().unwrap_or(-1);
if b == prev { val = regs.get(&r).cloned().unwrap_or(0); matched = true; break; }
}
}
}
}
if !matched { return 1; }
regs.insert(dst, val);
}
"branch" => {
let cond = inst.get("cond").and_then(Value::as_i64).and_then(|r| regs.get(&r).cloned()).unwrap_or(0);
let then_b = inst.get("then").and_then(Value::as_i64).unwrap_or(curr);
let else_b = inst.get("else").and_then(Value::as_i64).unwrap_or(curr);
prev = curr;
curr = if cond != 0 { then_b } else { else_b };
continue 'outer; // switch block
}
"jump" => {
let target = inst.get("target").and_then(Value::as_i64).unwrap_or(curr);
prev = curr;
curr = target;
continue 'outer; // switch block
}
"ret" => {
let vid = match inst.get("value").and_then(Value::as_i64) { Some(v) => v, None => return 0 };
let v = *regs.get(&vid).unwrap_or(&0);
return (v as i32) & 0xFF;
}
// ignore others for now (compare/branch not used in hv1 perrecv canaries)
_ => {}
}
// if we completed a block without control flow, stop
}
return 0;
}
}