use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use anyhow::Result;
use super::bytecode::{Chunk, Op};
use super::scalar_fold::fold_moves;
use super::scalar_loop::{
LOp, LTo, MAX_CALL_ARGS, NO_SLOT, OpOut, Region, eval_op, slot, translate,
};
use super::scalar_reads::chunk_reads;
use super::scalar_val::{SVal, s_value};
use super::value::Value;
use super::vm::{MAX_CALL_DEPTH, Vm};
use super::vm_step::StepCtx;
const FN_POLL: u32 = 65_536;
const MAX_FAILS: u32 = 32;
pub struct FnPlan {
ops: Vec<LOp>,
num_slots: usize,
num_params: usize,
fails: AtomicU32,
}
fn build(vm: &Vm, chunk: &Arc<Chunk>) -> Option<FnPlan> {
if chunk.path_forwarder
|| !chunk.generics.is_empty()
|| chunk.num_params > MAX_CALL_ARGS
|| chunk.code.is_empty()
{
return None;
}
let mut regs: Vec<u16> = (0..u16::try_from(chunk.num_params).ok()?).collect();
let region = Region {
head: usize::MAX,
body: 0,
exit: chunk.code.len(),
};
let mut recursive = false;
let mut try_mask = 0u64;
let mut ops = Vec::with_capacity(chunk.code.len());
for op in &chunk.code {
let lop = match op {
Op::CallFn {
dst,
func,
base,
argc,
targ,
} => {
let callee = vm.functions.get(*func as usize)?;
if *targ != u32::MAX
|| !Arc::ptr_eq(callee, chunk)
|| usize::from(*argc) != chunk.num_params
{
return None;
}
let mut args = [0u16; MAX_CALL_ARGS];
for (arg, reg) in args.iter_mut().zip(*base..base.saturating_add(*argc)) {
*arg = slot(&mut regs, reg)?;
}
recursive = true;
LOp::CallSelf {
dst: slot(&mut regs, *dst)?,
args,
argc: u8::try_from(*argc).ok()?,
}
}
Op::Ret { src } => LOp::Ret {
src: slot(&mut regs, *src)?,
},
other => translate(vm, chunk, ®ion, &mut regs, None, &mut try_mask, other)?,
};
ops.push(lop);
}
if !recursive {
return None;
}
fold_moves(&mut ops, NO_SLOT, &chunk_reads(chunk), ®s);
Some(FnPlan {
ops,
num_slots: regs.len(),
num_params: chunk.num_params,
fails: AtomicU32::new(0),
})
}
struct Frame {
base: usize,
ret_ip: usize,
dst: u16,
}
fn run(vm: &Arc<Vm>, plan: &FnPlan, args: &[SVal], depth_budget: usize) -> Result<Option<SVal>> {
let slots = plan.num_slots;
let mut stack: Vec<SVal> = vec![SVal::Unit; slots];
stack[..args.len()].copy_from_slice(args);
let mut frames: Vec<Frame> = Vec::new();
let mut base = 0usize;
let mut ip = 0usize;
let mut work = 0u32;
loop {
if work >= FN_POLL {
vm.run_pending_ctrlc()?;
work = 0;
}
let returned = match plan.ops.get(ip) {
None => Some(SVal::Unit),
Some(LOp::Ret { src }) => Some(stack[base + usize::from(*src)]),
Some(LOp::CallSelf { dst, args, argc }) => {
if frames.len() >= depth_budget {
return Ok(None);
}
let callee = stack.len();
stack.resize(callee + slots, SVal::Unit);
for (i, arg) in args[..usize::from(*argc)].iter().enumerate() {
stack[callee + i] = stack[base + usize::from(*arg)];
}
frames.push(Frame {
base,
ret_ip: ip + 1,
dst: *dst,
});
base = callee;
ip = 0;
work += 1;
None
}
Some(other) => match eval_op(other, &mut stack[base..base + slots]) {
OpOut::Fall => {
ip += 1;
None
}
OpOut::Fail | OpOut::Jump(LTo::Next) => return Ok(None),
OpOut::Jump(LTo::Exit) => Some(SVal::Unit),
OpOut::Jump(LTo::Op(t)) => {
let t = t as usize;
if t <= ip {
work += 1;
}
ip = t;
None
}
},
};
if let Some(v) = returned {
let Some(frame) = frames.pop() else {
return Ok(Some(v));
};
stack.truncate(base);
base = frame.base;
ip = frame.ret_ip;
stack[base + usize::from(frame.dst)] = v;
}
}
}
fn note_fail(plan: &FnPlan, chunk: &Chunk) {
if plan.fails.fetch_add(1, Ordering::Relaxed) + 1 >= MAX_FAILS {
chunk.fn_rejected.store(1, Ordering::Relaxed);
}
}
pub(super) fn try_call(
ctx: &StepCtx,
callee: &Arc<Chunk>,
abase: u16,
argc: u16,
) -> Result<Option<Value>> {
if callee.fn_rejected.load(Ordering::Relaxed) != 0 {
return Ok(None);
}
let plan = {
let mut cached = callee.fn_plan.lock();
if let Some(plan) = &*cached {
plan.clone()
} else if let Some(plan) = build(ctx.vm, callee).map(Arc::new) {
*cached = Some(plan.clone());
plan
} else {
callee.fn_rejected.store(1, Ordering::Relaxed);
return Ok(None);
}
};
if usize::from(argc) != plan.num_params || ctx.depth >= MAX_CALL_DEPTH {
return Ok(None);
}
let mut vals = [SVal::Unit; MAX_CALL_ARGS];
for (val, reg) in vals.iter_mut().zip(abase..abase.saturating_add(argc)) {
*val = SVal::of(ctx.get(reg));
if matches!(*val, SVal::Opaque) {
note_fail(&plan, callee);
return Ok(None);
}
}
let budget = MAX_CALL_DEPTH - ctx.depth - 1;
if let Some(v) = run(ctx.vm, &plan, &vals[..usize::from(argc)], budget)? {
plan.fails.store(0, Ordering::Relaxed);
Ok(s_value(v))
} else {
note_fail(&plan, callee);
Ok(None)
}
}