2275 lines
80 KiB
Rust
2275 lines
80 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use gc_arena::{Collect, Gc, Mutation, RefLock};
|
|
use hashbrown::HashMap;
|
|
use num_enum::TryFromPrimitive;
|
|
use smallvec::SmallVec;
|
|
use string_interner::{DefaultStringInterner, Symbol as _};
|
|
|
|
use super::builtins::{BUILTINS, BuiltinId, PrimOpArgs, PrimOpStrictArgs, is_lazy_builtin};
|
|
use super::primops::{BuiltinResult, BuiltinState, PrimOpCtx, dispatch_lazy_builtin, dispatch_strict_builtin};
|
|
use super::stack::Stack;
|
|
use super::value::*;
|
|
use crate::error::{Error, Result};
|
|
use crate::ir::StringId;
|
|
|
|
pub(super) type VmResult<T> = std::result::Result<T, VmError>;
|
|
|
|
pub(super) enum VmError {
|
|
Catchable(String),
|
|
Uncatchable(Box<Error>),
|
|
}
|
|
|
|
impl From<Box<Error>> for VmError {
|
|
fn from(e: Box<Error>) -> Self {
|
|
VmError::Uncatchable(e)
|
|
}
|
|
}
|
|
|
|
#[derive(Collect)]
|
|
#[collect(no_drop)]
|
|
pub(super) struct VM<'gc> {
|
|
stack: Stack<65536, Value<'gc>>,
|
|
frames: Stack<8192, CallFrame<'gc>>,
|
|
with_scope: Option<Gc<'gc, WithScope<'gc>>>,
|
|
error_contexts: Stack<8192, ErrorFrame>,
|
|
globals: GlobalState<'gc>,
|
|
import_cache: HashMap<PathBuf, Value<'gc>>,
|
|
pc: usize,
|
|
current_env: Option<Gc<'gc, RefLock<Env<'gc>>>>,
|
|
started: bool,
|
|
}
|
|
|
|
#[derive(Collect)]
|
|
#[collect(no_drop)]
|
|
struct GlobalState<'gc> {
|
|
builtins: Value<'gc>,
|
|
builtin_lookup: HashMap<StringId, PrimOp>,
|
|
empty_list: Value<'gc>,
|
|
empty_attrs: Value<'gc>,
|
|
}
|
|
|
|
#[derive(Collect)]
|
|
#[collect(require_static)]
|
|
struct ErrorFrame {
|
|
span_id: u32,
|
|
message: Option<String>,
|
|
}
|
|
|
|
#[derive(Collect, Debug)]
|
|
#[collect(no_drop)]
|
|
pub(super) struct WithScope<'gc> {
|
|
env: Value<'gc>,
|
|
prev: Option<Gc<'gc, WithScope<'gc>>>,
|
|
}
|
|
|
|
#[derive(Collect, Debug)]
|
|
#[collect(no_drop)]
|
|
struct CallFrame<'gc> {
|
|
pc: usize,
|
|
env: Gc<'gc, RefLock<Env<'gc>>>,
|
|
continuation: Continuation<'gc>,
|
|
span: Option<u32>,
|
|
}
|
|
|
|
#[derive(Collect, Debug)]
|
|
#[collect(no_drop)]
|
|
enum Continuation<'gc> {
|
|
Return,
|
|
ForceThunk {
|
|
thunk: Gc<'gc, Thunk<'gc>>,
|
|
after: AfterForce<'gc>,
|
|
},
|
|
BuiltinReturn(BuiltinState<'gc>),
|
|
BuiltinCallAndForce(BuiltinState<'gc>),
|
|
}
|
|
|
|
#[derive(Collect, Debug)]
|
|
#[collect(no_drop)]
|
|
pub(super) enum AfterForce<'gc> {
|
|
Identity,
|
|
ForceBool,
|
|
BinOpLhs {
|
|
rhs: Value<'gc>,
|
|
#[collect(require_static)]
|
|
op: BinOpTag,
|
|
},
|
|
BinOpRhs {
|
|
lhs_forced: StrictValue<'gc>,
|
|
#[collect(require_static)]
|
|
op: BinOpTag,
|
|
},
|
|
UnNeg,
|
|
UnNot,
|
|
Call {
|
|
arg: Value<'gc>,
|
|
span: Option<u32>,
|
|
},
|
|
PatternCallArgForce {
|
|
func: StrictValue<'gc>,
|
|
span: Option<u32>,
|
|
},
|
|
Select {
|
|
keys: SmallVec<[Value<'gc>; 4]>,
|
|
remaining: u16,
|
|
span: u32,
|
|
default: Option<Value<'gc>>,
|
|
},
|
|
HasAttr {
|
|
keys: SmallVec<[Value<'gc>; 4]>,
|
|
remaining: u16,
|
|
},
|
|
Assert {
|
|
expr: Value<'gc>,
|
|
raw_idx: u32,
|
|
span_id: u32,
|
|
},
|
|
ConcatStrings {
|
|
forced: SmallVec<[StrictValue<'gc>; 8]>,
|
|
remaining: SmallVec<[Value<'gc>; 8]>,
|
|
force_string: bool,
|
|
},
|
|
PushWith,
|
|
WithLookup {
|
|
#[collect(require_static)]
|
|
name: StringId,
|
|
next: Option<Gc<'gc, WithScope<'gc>>>,
|
|
},
|
|
TopLevelForce,
|
|
DiscardAndPush {
|
|
value: Value<'gc>,
|
|
},
|
|
Builtin(BuiltinState<'gc>),
|
|
BuiltinArgForce {
|
|
#[collect(require_static)]
|
|
id: BuiltinId,
|
|
#[collect(require_static)]
|
|
arity: u8,
|
|
#[collect(require_static)]
|
|
forced_count: u8,
|
|
args: PrimOpStrictArgs<'gc>,
|
|
remaining: PrimOpArgs<'gc>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(super) enum BinOpTag {
|
|
Add,
|
|
Sub,
|
|
Mul,
|
|
Div,
|
|
Eq,
|
|
Neq,
|
|
Lt,
|
|
Gt,
|
|
Leq,
|
|
Geq,
|
|
Concat,
|
|
Update,
|
|
}
|
|
|
|
pub(super) enum ForceResult<'gc> {
|
|
Ready(StrictValue<'gc>),
|
|
NeedEval {
|
|
ip: u32,
|
|
env: Gc<'gc, RefLock<Env<'gc>>>,
|
|
thunk: Gc<'gc, Thunk<'gc>>,
|
|
},
|
|
NeedApply(Gc<'gc, Thunk<'gc>>),
|
|
}
|
|
|
|
pub(crate) enum Action {
|
|
Continue,
|
|
Done(Result<crate::value::Value>),
|
|
NeedGc,
|
|
IoRequest(()),
|
|
}
|
|
|
|
pub(super) enum NixNum {
|
|
Int(i64),
|
|
Float(f64),
|
|
}
|
|
|
|
macro_rules! try_vm {
|
|
($self:ident, $expr:expr) => {
|
|
match $expr {
|
|
Ok(v) => v,
|
|
Err(e) => return VM::handle_vm_error($self, e),
|
|
}
|
|
};
|
|
}
|
|
|
|
impl<'gc> VM<'gc> {
|
|
pub(super) fn new(mc: &Mutation<'gc>, strings: &mut DefaultStringInterner) -> Self {
|
|
let (builtins, builtin_lookup) = Self::init_builtins(mc, strings);
|
|
Self {
|
|
stack: Stack::new(),
|
|
frames: Stack::new(),
|
|
with_scope: None,
|
|
error_contexts: Stack::new(),
|
|
globals: GlobalState {
|
|
builtins,
|
|
builtin_lookup,
|
|
empty_list: Value::new_gc(Gc::new(mc, List::default())),
|
|
empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())),
|
|
},
|
|
import_cache: HashMap::new(),
|
|
pc: 0,
|
|
current_env: None,
|
|
started: false,
|
|
}
|
|
}
|
|
|
|
fn init_builtins(mc: &Mutation<'gc>, strings: &mut DefaultStringInterner) -> (Value<'gc>, HashMap<StringId, PrimOp>) {
|
|
let mut builtin_lookup = HashMap::new();
|
|
let mut entries = SmallVec::new();
|
|
|
|
for (id, &(name, arity)) in BUILTINS.iter().enumerate() {
|
|
let Some(sym) = strings.get(name) else {
|
|
continue;
|
|
};
|
|
let sid = StringId(sym);
|
|
let primop = PrimOp {
|
|
id: BuiltinId::try_from_primitive(id as u8).expect("invalid BuiltinId??"),
|
|
arity,
|
|
};
|
|
builtin_lookup.insert(sid, primop);
|
|
|
|
if arity == 0 {
|
|
// "null" constant
|
|
entries.push((sid, Value::new_inline(Null)));
|
|
} else {
|
|
entries.push((sid, Value::new_inline(primop)));
|
|
}
|
|
}
|
|
|
|
// Add constant entries
|
|
macro_rules! add_const {
|
|
($name:expr, $val:expr) => {{
|
|
let sym = strings.get_or_intern($name);
|
|
entries.push((StringId(sym), $val));
|
|
}};
|
|
}
|
|
add_const!(
|
|
"currentSystem",
|
|
Value::new_gc(Gc::new(mc, NixString::new("x86_64-linux")))
|
|
);
|
|
add_const!("langVersion", Value::new_inline(6i32));
|
|
add_const!(
|
|
"nixVersion",
|
|
Value::new_gc(Gc::new(mc, NixString::new("2.24.0")))
|
|
);
|
|
add_const!(
|
|
"storeDir",
|
|
Value::new_gc(Gc::new(mc, NixString::new("/nix/store")))
|
|
);
|
|
add_const!(
|
|
"nixPath",
|
|
Value::new_gc(Gc::new(
|
|
mc,
|
|
List {
|
|
inner: SmallVec::new()
|
|
}
|
|
))
|
|
);
|
|
add_const!("true", Value::new_inline(true));
|
|
add_const!("false", Value::new_inline(false));
|
|
|
|
// Self-reference thunk for builtins.builtins
|
|
let self_ref_thunk: Gc<'gc, Thunk<'gc>> = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
|
|
let sym = strings.get_or_intern("builtins");
|
|
entries.push((StringId(sym), Value::new_gc(self_ref_thunk)));
|
|
|
|
entries.sort_by_key(|(k, _)| *k);
|
|
|
|
let builtins_set = Gc::new(mc, unsafe {
|
|
AttrSet::from_sorted_unchecked(entries)
|
|
});
|
|
let builtins_val = Value::new_gc(builtins_set);
|
|
|
|
// Populate the self-reference
|
|
*self_ref_thunk.borrow_mut(mc) = ThunkState::Evaluated(builtins_val);
|
|
|
|
(builtins_val, builtin_lookup)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_array<const N: usize>(&mut self, bc: &[u8]) -> [u8; N] {
|
|
#[cfg(debug_assertions)]
|
|
let ret = bc[self.pc..self.pc + N]
|
|
.try_into()
|
|
.expect("read_array failed");
|
|
#[cfg(not(debug_assertions))]
|
|
let ret = unsafe { bc[self.pc..self.pc + N].try_into().unwrap_unchecked() };
|
|
self.pc += N;
|
|
ret
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_u8(&mut self, bc: &[u8]) -> u8 {
|
|
u8::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_u16(&mut self, bc: &[u8]) -> u16 {
|
|
u16::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_u32(&mut self, bc: &[u8]) -> u32 {
|
|
u32::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_i32(&mut self, bc: &[u8]) -> i32 {
|
|
i32::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_i64(&mut self, bc: &[u8]) -> i64 {
|
|
i64::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_f64(&mut self, bc: &[u8]) -> f64 {
|
|
f64::from_le_bytes(self.read_array(bc))
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn read_string_id(&mut self, bc: &[u8]) -> StringId {
|
|
let raw = self.read_u32(bc);
|
|
StringId(unsafe {
|
|
string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap_unchecked()
|
|
})
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn env(&self) -> Gc<'gc, RefLock<Env<'gc>>> {
|
|
self.current_env.expect("no current env")
|
|
}
|
|
|
|
pub(super) fn force_inline(&self, val: Value<'gc>) -> VmResult<ForceResult<'gc>> {
|
|
let mut current = val;
|
|
loop {
|
|
let Some(thunk) = current.as_gc::<Thunk<'gc>>() else {
|
|
return Ok(ForceResult::Ready(unsafe {
|
|
StrictValue::try_from_forced(current).unwrap_unchecked()
|
|
}));
|
|
};
|
|
let thunk_ref = thunk.borrow();
|
|
match &*thunk_ref {
|
|
ThunkState::Evaluated(v) => {
|
|
current = *v;
|
|
drop(thunk_ref);
|
|
}
|
|
&ThunkState::Pending { ip, env } => {
|
|
return Ok(ForceResult::NeedEval { ip, env, thunk });
|
|
}
|
|
ThunkState::Apply { .. } => {
|
|
return Ok(ForceResult::NeedApply(thunk));
|
|
}
|
|
ThunkState::Blackhole => {
|
|
return Err(VmError::Uncatchable(Error::eval_error(
|
|
"infinite recursion encountered",
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn push_force_frame(
|
|
&mut self,
|
|
thunk: Gc<'gc, Thunk<'gc>>,
|
|
after: AfterForce<'gc>,
|
|
ip: u32,
|
|
env: Gc<'gc, RefLock<Env<'gc>>>,
|
|
mc: &Mutation<'gc>,
|
|
) {
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::ForceThunk { thunk, after },
|
|
span: None,
|
|
})
|
|
.expect("frame stack overflow");
|
|
*thunk.borrow_mut(mc) = ThunkState::Blackhole;
|
|
self.pc = ip as usize;
|
|
self.current_env = Some(env);
|
|
}
|
|
|
|
fn push_apply_force_frame(
|
|
&mut self,
|
|
thunk: Gc<'gc, Thunk<'gc>>,
|
|
after: AfterForce<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
let (func, arg) = match &*thunk.borrow() {
|
|
&ThunkState::Apply { func, arg } => (func, arg),
|
|
_ => unreachable!(),
|
|
};
|
|
*thunk.borrow_mut(mc) = ThunkState::Blackhole;
|
|
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::ForceThunk { thunk, after },
|
|
span: None,
|
|
})
|
|
.expect("frame stack overflow");
|
|
|
|
match self.force_inline(func)? {
|
|
ForceResult::Ready(f) => {
|
|
self.do_call(f, arg, None, mc, strings)?;
|
|
}
|
|
ForceResult::NeedEval {
|
|
ip,
|
|
env,
|
|
thunk: func_thunk,
|
|
} => {
|
|
self.push_force_frame(
|
|
func_thunk,
|
|
AfterForce::Call { arg, span: None },
|
|
ip,
|
|
env,
|
|
mc,
|
|
);
|
|
}
|
|
ForceResult::NeedApply(func_thunk) => {
|
|
self.push_apply_force_frame(
|
|
func_thunk,
|
|
AfterForce::Call { arg, span: None },
|
|
mc,
|
|
strings,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn setup_force(
|
|
&mut self,
|
|
result: ForceResult<'gc>,
|
|
after: AfterForce<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
match result {
|
|
ForceResult::Ready(_) => unreachable!(),
|
|
ForceResult::NeedEval { ip, env, thunk } => {
|
|
self.push_force_frame(thunk, after, ip, env, mc);
|
|
Ok(())
|
|
}
|
|
ForceResult::NeedApply(thunk) => self.push_apply_force_frame(thunk, after, mc, strings),
|
|
}
|
|
}
|
|
|
|
pub(super) fn make_int(val: i64, mc: &Mutation<'gc>) -> Value<'gc> {
|
|
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
|
|
Value::new_inline(val as i32)
|
|
} else {
|
|
Value::new_gc(Gc::new(mc, val))
|
|
}
|
|
}
|
|
|
|
pub(super) fn as_num(val: StrictValue<'gc>) -> Option<NixNum> {
|
|
if let Some(i) = val.as_inline::<i32>() {
|
|
Some(NixNum::Int(i as i64))
|
|
} else if let Some(gc_i) = val.as_gc::<i64>() {
|
|
Some(NixNum::Int(*gc_i))
|
|
} else {
|
|
val.as_float().map(NixNum::Float)
|
|
}
|
|
}
|
|
|
|
pub(super) fn get_string_id(val: &Value<'gc>) -> Option<StringId> {
|
|
val.as_inline::<StringId>()
|
|
}
|
|
|
|
pub(super) fn get_string<'a, 'gc1: 'gc + 'a>(
|
|
val: StrictValue<'gc1>,
|
|
strings: &'a DefaultStringInterner,
|
|
) -> Option<&'a str> {
|
|
if let Some(sid) = val.as_inline::<StringId>() {
|
|
Some(strings.resolve(sid.0)?)
|
|
} else {
|
|
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
|
|
}
|
|
}
|
|
|
|
pub(super) fn err(msg: impl Into<String>) -> VmError {
|
|
VmError::Uncatchable(Error::eval_error(msg.into()))
|
|
}
|
|
|
|
pub(super) fn handle_return(
|
|
&mut self,
|
|
ret_val: Value<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> Action {
|
|
let Some(frame) = self.frames.pop() else {
|
|
return match self.force_inline(ret_val) {
|
|
Ok(ForceResult::Ready(v)) => Action::Done(Ok(self.convert_value(&v, strings))),
|
|
Ok(other) => {
|
|
try_vm!(self, self.setup_force(other, AfterForce::TopLevelForce, mc, strings));
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
};
|
|
};
|
|
|
|
self.pc = frame.pc;
|
|
self.current_env = Some(frame.env);
|
|
|
|
match frame.continuation {
|
|
Continuation::Return => {
|
|
self.push_stack(ret_val);
|
|
Action::Continue
|
|
}
|
|
Continuation::ForceThunk { thunk, after } => {
|
|
*thunk.borrow_mut(mc) = ThunkState::Evaluated(ret_val);
|
|
match self.force_inline(ret_val) {
|
|
Ok(ForceResult::Ready(strict)) => {
|
|
self.resume_after_force(strict, after, mc, strings)
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(self, self.setup_force(other, after, mc, strings));
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
Continuation::BuiltinReturn(state) => {
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
match ctx.vm.force_inline(ret_val) {
|
|
Ok(ForceResult::Ready(strict)) => {
|
|
let result = state.resume(strict, &ctx);
|
|
self.process_builtin_result(result, mc, strings)
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(other, AfterForce::Builtin(state), mc, strings)
|
|
);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
Continuation::BuiltinCallAndForce(state) => {
|
|
match self.force_inline(ret_val) {
|
|
Ok(ForceResult::Ready(strict)) => {
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
let result = state.resume(strict, &ctx);
|
|
self.process_builtin_result(result, mc, strings)
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(other, AfterForce::Builtin(state), mc, strings)
|
|
);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn resume_after_force(
|
|
&mut self,
|
|
val: StrictValue<'gc>,
|
|
after: AfterForce<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> Action {
|
|
match after {
|
|
AfterForce::Identity => {
|
|
self.push_stack(val.relax());
|
|
Action::Continue
|
|
}
|
|
AfterForce::ForceBool => {
|
|
if val.as_inline::<bool>().is_none() {
|
|
return Action::Done(Err(Error::eval_error("value is not a boolean")));
|
|
}
|
|
self.push_stack(val.relax());
|
|
Action::Continue
|
|
}
|
|
AfterForce::BinOpLhs { rhs, op } => match self.force_inline(rhs) {
|
|
Ok(ForceResult::Ready(rhs_forced)) => {
|
|
match self.compute_binop(op, val, rhs_forced, mc, strings) {
|
|
Ok(result) => {
|
|
self.push_stack(result);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::BinOpRhs {
|
|
lhs_forced: val,
|
|
op,
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
},
|
|
AfterForce::BinOpRhs { lhs_forced, op } => {
|
|
match self.compute_binop(op, lhs_forced, val, mc, strings) {
|
|
Ok(result) => {
|
|
self.push_stack(result);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
AfterForce::UnNeg => match Self::as_num(val) {
|
|
Some(NixNum::Int(i)) => {
|
|
self.push_stack(Self::make_int(-i, mc));
|
|
Action::Continue
|
|
}
|
|
Some(NixNum::Float(f)) => {
|
|
self.push_stack(Value::new_float(-f));
|
|
Action::Continue
|
|
}
|
|
None => Action::Done(Err(Error::eval_error("cannot negate non-number"))),
|
|
},
|
|
AfterForce::UnNot => match val.as_inline::<bool>() {
|
|
Some(b) => {
|
|
self.push_stack(Value::new_inline(!b));
|
|
Action::Continue
|
|
}
|
|
None => Action::Done(Err(Error::eval_error("value is not a boolean"))),
|
|
},
|
|
AfterForce::Call { arg, span } => match self.do_call(val, arg, span, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
},
|
|
AfterForce::PatternCallArgForce { func, span } => {
|
|
if let Some(closure_gc) = func.as_gc::<Closure<'gc>>() {
|
|
let pattern = closure_gc
|
|
.pattern
|
|
.as_ref()
|
|
.expect("internal: pattern call on non-pattern-closure");
|
|
match self.setup_pattern_call(
|
|
closure_gc.ip,
|
|
closure_gc.n_locals,
|
|
closure_gc.env,
|
|
pattern,
|
|
val,
|
|
span,
|
|
mc,
|
|
) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
} else {
|
|
Action::Done(Err(Error::eval_error(
|
|
"internal: pattern call on non-closure",
|
|
)))
|
|
}
|
|
}
|
|
AfterForce::Select {
|
|
keys,
|
|
remaining,
|
|
span,
|
|
default,
|
|
} => match self.do_select_step(val, keys, remaining, span, default, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
},
|
|
AfterForce::HasAttr { keys, remaining } => {
|
|
match self.do_has_attr_step(val, keys, remaining, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
AfterForce::Assert {
|
|
expr,
|
|
raw_idx,
|
|
span_id: _,
|
|
} => match val.as_inline::<bool>() {
|
|
Some(true) => {
|
|
self.push_stack(expr);
|
|
Action::Continue
|
|
}
|
|
Some(false) => {
|
|
let sym = string_interner::symbol::SymbolU32::try_from_usize(raw_idx as usize);
|
|
let msg = sym.and_then(|s| strings.resolve(s)).unwrap_or("<unknown>");
|
|
Action::Done(Err(Error::eval_error(format!("assertion '{msg}' failed"))))
|
|
}
|
|
None => Action::Done(Err(Error::eval_error(
|
|
"assertion condition must be a boolean",
|
|
))),
|
|
},
|
|
AfterForce::ConcatStrings {
|
|
mut forced,
|
|
remaining,
|
|
force_string,
|
|
} => {
|
|
forced.push(val);
|
|
match self.concat_strings_continue(forced, remaining, force_string, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
AfterForce::PushWith => {
|
|
let scope = Gc::new(
|
|
mc,
|
|
WithScope {
|
|
env: val.relax(),
|
|
prev: self.with_scope,
|
|
},
|
|
);
|
|
self.with_scope = Some(scope);
|
|
Action::Continue
|
|
}
|
|
AfterForce::WithLookup { name, next } => {
|
|
match self.do_with_lookup_step(val, name, next, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
AfterForce::TopLevelForce => Action::Done(Ok(self.convert_value(&val, strings))),
|
|
AfterForce::DiscardAndPush { value } => {
|
|
self.push_stack(value);
|
|
Action::Continue
|
|
}
|
|
AfterForce::Builtin(state) => {
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
let result = state.resume(val, &ctx);
|
|
self.process_builtin_result(result, mc, strings)
|
|
}
|
|
AfterForce::BuiltinArgForce {
|
|
id,
|
|
arity,
|
|
mut forced_count,
|
|
mut args,
|
|
remaining,
|
|
} => {
|
|
args[forced_count as usize] = val;
|
|
forced_count += 1;
|
|
|
|
while forced_count < arity {
|
|
let next = remaining[forced_count as usize];
|
|
match self.force_inline(next) {
|
|
Ok(ForceResult::Ready(v)) => {
|
|
args[forced_count as usize] = v;
|
|
forced_count += 1;
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::BuiltinArgForce {
|
|
id,
|
|
arity,
|
|
forced_count,
|
|
args,
|
|
remaining,
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
return Action::Continue;
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
let result = dispatch_strict_builtin(id, args, arity, &ctx);
|
|
self.process_builtin_result(result, mc, strings)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_builtin_result(
|
|
&mut self,
|
|
result: BuiltinResult<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> Action {
|
|
match result {
|
|
BuiltinResult::Done(v) => {
|
|
self.push_stack(v);
|
|
Action::Continue
|
|
}
|
|
BuiltinResult::Force(state, val) => {
|
|
match self.force_inline(val) {
|
|
Ok(ForceResult::Ready(strict)) => {
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
let result = state.resume(strict, &ctx);
|
|
self.process_builtin_result(result, mc, strings)
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(other, AfterForce::Builtin(state), mc, strings)
|
|
);
|
|
Action::Continue
|
|
}
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
BuiltinResult::Call(state, func, arg) => {
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::BuiltinReturn(state),
|
|
span: None,
|
|
})
|
|
.expect("frame stack overflow");
|
|
match self.do_call(func, arg, None, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
BuiltinResult::CallAndForce(state, func, arg) => {
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::BuiltinCallAndForce(state),
|
|
span: None,
|
|
})
|
|
.expect("frame stack overflow");
|
|
match self.do_call(func, arg, None, mc, strings) {
|
|
Ok(()) => Action::Continue,
|
|
Err(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
BuiltinResult::Error(e) => self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
fn dispatch_primop(
|
|
&mut self,
|
|
id: BuiltinId,
|
|
arity: u8,
|
|
args: PrimOpArgs<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
|
|
let result = if is_lazy_builtin(id) {
|
|
dispatch_lazy_builtin(id, &args, arity, &ctx)
|
|
} else {
|
|
let mut strict_args: PrimOpStrictArgs<'gc> = [StrictValue::default(); 3];
|
|
for i in 0..arity as usize {
|
|
match self.force_inline(args[i])? {
|
|
ForceResult::Ready(v) => {
|
|
strict_args[i] = v;
|
|
}
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::BuiltinArgForce {
|
|
id,
|
|
arity,
|
|
forced_count: i as u8,
|
|
args: strict_args,
|
|
remaining: args,
|
|
},
|
|
mc,
|
|
strings,
|
|
)?;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
let ctx = PrimOpCtx {
|
|
vm: self,
|
|
mc,
|
|
strings,
|
|
};
|
|
dispatch_strict_builtin(id, strict_args, arity, &ctx)
|
|
};
|
|
|
|
match result {
|
|
BuiltinResult::Done(v) => {
|
|
self.push_stack(v);
|
|
Ok(())
|
|
}
|
|
other => {
|
|
let action = self.process_builtin_result(other, mc, strings);
|
|
match action {
|
|
Action::Continue => Ok(()),
|
|
Action::Done(Err(e)) => Err(VmError::Uncatchable(e)),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn do_call(
|
|
&mut self,
|
|
func: StrictValue<'gc>,
|
|
arg: Value<'gc>,
|
|
span: Option<u32>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
if let Some(closure_gc) = func.as_gc::<Closure<'gc>>() {
|
|
let ip = closure_gc.ip;
|
|
let n_locals = closure_gc.n_locals;
|
|
let closure_env = closure_gc.env;
|
|
|
|
if let Some(ref pattern) = closure_gc.pattern {
|
|
match self.force_inline(arg)? {
|
|
ForceResult::Ready(forced_arg) => {
|
|
self.setup_pattern_call(
|
|
ip,
|
|
n_locals,
|
|
closure_env,
|
|
pattern,
|
|
forced_arg,
|
|
span,
|
|
mc,
|
|
)?;
|
|
}
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::PatternCallArgForce { func, span },
|
|
mc,
|
|
strings,
|
|
)?;
|
|
}
|
|
}
|
|
} else {
|
|
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, closure_env)));
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::Return,
|
|
span,
|
|
})
|
|
.expect("frame stack overflow");
|
|
self.pc = ip as usize;
|
|
self.current_env = Some(new_env);
|
|
}
|
|
Ok(())
|
|
} else if let Some(po) = func.as_inline::<PrimOp>() {
|
|
if po.arity <= 1 {
|
|
let mut primop_args: PrimOpArgs<'gc> = [Value::default(); 3];
|
|
if po.arity == 1 {
|
|
primop_args[0] = arg;
|
|
}
|
|
self.dispatch_primop(po.id, po.arity, primop_args, mc, strings)
|
|
} else {
|
|
let app = Gc::new(
|
|
mc,
|
|
PrimOpApp {
|
|
primop: po,
|
|
args: SmallVec::from_elem(arg, 1),
|
|
},
|
|
);
|
|
self.push_stack(Value::new_gc(app));
|
|
Ok(())
|
|
}
|
|
} else if let Some(poa) = func.as_gc::<PrimOpApp<'gc>>() {
|
|
let mut args = poa.args.clone();
|
|
args.push(arg);
|
|
if args.len() >= poa.primop.arity as usize {
|
|
let mut primop_args: PrimOpArgs<'gc> = [Value::default(); 3];
|
|
for (i, a) in args.iter().enumerate() {
|
|
if i < 3 {
|
|
primop_args[i] = *a;
|
|
}
|
|
}
|
|
self.dispatch_primop(poa.primop.id, poa.primop.arity, primop_args, mc, strings)
|
|
} else {
|
|
let app = Gc::new(
|
|
mc,
|
|
PrimOpApp {
|
|
primop: poa.primop,
|
|
args,
|
|
},
|
|
);
|
|
self.push_stack(Value::new_gc(app));
|
|
Ok(())
|
|
}
|
|
} else {
|
|
Err(Self::err(
|
|
"attempt to call something which is not a function",
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(super) fn setup_pattern_call(
|
|
&mut self,
|
|
ip: u32,
|
|
n_locals: u32,
|
|
closure_env: Gc<'gc, RefLock<Env<'gc>>>,
|
|
pattern: &PatternInfo,
|
|
arg: StrictValue<'gc>,
|
|
span: Option<u32>,
|
|
mc: &Mutation<'gc>,
|
|
) -> VmResult<()> {
|
|
let Some(attrs) = arg.as_gc::<AttrSet<'gc>>() else {
|
|
return Err(Self::err(
|
|
"function that expected a set received a non-set argument",
|
|
));
|
|
};
|
|
|
|
for &req in &pattern.required {
|
|
if !attrs.has(req) {
|
|
return Err(Self::err("function argument missing required attribute"));
|
|
}
|
|
}
|
|
|
|
if !pattern.ellipsis {
|
|
for (key, _) in attrs.iter() {
|
|
let is_known = pattern.required.contains(key) || pattern.optional.contains(key);
|
|
if !is_known {
|
|
return Err(Self::err("function received unexpected attribute"));
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_env = Gc::new(
|
|
mc,
|
|
RefLock::new(Env::with_arg(arg.relax(), n_locals, closure_env)),
|
|
);
|
|
|
|
self.frames
|
|
.push(CallFrame {
|
|
pc: self.pc,
|
|
env: self.env(),
|
|
continuation: Continuation::Return,
|
|
span,
|
|
})
|
|
.expect("frame stack overflow");
|
|
self.pc = ip as usize;
|
|
self.current_env = Some(new_env);
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn compute_binop(
|
|
&self,
|
|
op: BinOpTag,
|
|
lhs: StrictValue<'gc>,
|
|
rhs: StrictValue<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<Value<'gc>> {
|
|
match op {
|
|
BinOpTag::Add => {
|
|
if let (Some(ls), Some(rs)) = (
|
|
Self::get_string(lhs, strings),
|
|
Self::get_string(rhs, strings),
|
|
) {
|
|
let ns = Gc::new(mc, NixString::new(format!("{ls}{rs}")));
|
|
return Ok(Value::new_gc(ns));
|
|
}
|
|
self.numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b)
|
|
}
|
|
BinOpTag::Sub => self.numeric_binop(lhs, rhs, mc, i64::wrapping_sub, |a, b| a - b),
|
|
BinOpTag::Mul => self.numeric_binop(lhs, rhs, mc, i64::wrapping_mul, |a, b| a * b),
|
|
BinOpTag::Div => match (Self::as_num(lhs), Self::as_num(rhs)) {
|
|
(_, Some(NixNum::Int(0))) => Err(Self::err("division by zero")),
|
|
(_, Some(NixNum::Float(0.))) => Err(Self::err("division by zero")),
|
|
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => {
|
|
Ok(Self::make_int(a.wrapping_div(b), mc))
|
|
}
|
|
(Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(a / b)),
|
|
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => {
|
|
Ok(Value::new_float(a as f64 / b))
|
|
}
|
|
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
|
|
Ok(Value::new_float(a / b as f64))
|
|
}
|
|
_ => Err(Self::err("cannot divide non-numbers")),
|
|
},
|
|
BinOpTag::Eq => Ok(Value::new_inline(self.values_equal(lhs, rhs, strings))),
|
|
BinOpTag::Neq => Ok(Value::new_inline(!self.values_equal(lhs, rhs, strings))),
|
|
BinOpTag::Lt => self.compare_values(lhs, rhs, strings, |o| o.is_lt()),
|
|
BinOpTag::Gt => self.compare_values(lhs, rhs, strings, |o| o.is_gt()),
|
|
BinOpTag::Leq => self.compare_values(lhs, rhs, strings, |o| o.is_le()),
|
|
BinOpTag::Geq => self.compare_values(lhs, rhs, strings, |o| o.is_ge()),
|
|
BinOpTag::Concat => {
|
|
let Some(l) = lhs.as_gc::<List<'gc>>() else {
|
|
return Err(Self::err("cannot concatenate: left operand is not a list"));
|
|
};
|
|
let Some(r) = rhs.as_gc::<List<'gc>>() else {
|
|
return Err(Self::err("cannot concatenate: right operand is not a list"));
|
|
};
|
|
let mut items = SmallVec::new();
|
|
items.extend(l.inner.iter().cloned());
|
|
items.extend(r.inner.iter().cloned());
|
|
Ok(Value::new_gc(Gc::new(mc, List { inner: items })))
|
|
}
|
|
BinOpTag::Update => {
|
|
let Some(l) = lhs.as_gc::<AttrSet<'gc>>() else {
|
|
return Err(Self::err("cannot update: left operand is not a set"));
|
|
};
|
|
let Some(r) = rhs.as_gc::<AttrSet<'gc>>() else {
|
|
return Err(Self::err("cannot update: right operand is not a set"));
|
|
};
|
|
Ok(Value::new_gc(l.merge(&r, mc)))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn numeric_binop(
|
|
&self,
|
|
lhs: StrictValue<'gc>,
|
|
rhs: StrictValue<'gc>,
|
|
mc: &Mutation<'gc>,
|
|
int_op: fn(i64, i64) -> i64,
|
|
float_op: fn(f64, f64) -> f64,
|
|
) -> VmResult<Value<'gc>> {
|
|
match (Self::as_num(lhs), Self::as_num(rhs)) {
|
|
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Self::make_int(int_op(a, b), mc)),
|
|
(Some(NixNum::Float(a)), Some(NixNum::Float(b))) => {
|
|
Ok(Value::new_float(float_op(a, b)))
|
|
}
|
|
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => {
|
|
Ok(Value::new_float(float_op(a as f64, b)))
|
|
}
|
|
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
|
|
Ok(Value::new_float(float_op(a, b as f64)))
|
|
}
|
|
_ => Err(Self::err("cannot perform arithmetic on non-numbers")),
|
|
}
|
|
}
|
|
|
|
pub(super) fn values_equal(
|
|
&self,
|
|
lhs: StrictValue<'gc>,
|
|
rhs: StrictValue<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> bool {
|
|
if let (Some(a), Some(b)) = (Self::as_num(lhs), Self::as_num(rhs)) {
|
|
return match (a, b) {
|
|
(NixNum::Int(a), NixNum::Int(b)) => a == b,
|
|
(NixNum::Float(a), NixNum::Float(b)) => a == b,
|
|
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
|
|
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
|
|
};
|
|
}
|
|
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
|
|
return a == b;
|
|
}
|
|
if lhs.is::<Null>() && rhs.is::<Null>() {
|
|
return true;
|
|
}
|
|
if let (Some(a), Some(b)) = (
|
|
Self::get_string(lhs, strings),
|
|
Self::get_string(rhs, strings),
|
|
) {
|
|
return a == b;
|
|
}
|
|
if let (Some(a), Some(b)) = (lhs.as_gc::<List<'gc>>(), rhs.as_gc::<List<'gc>>()) {
|
|
if a.inner.len() != b.inner.len() {
|
|
return false;
|
|
}
|
|
return a.inner.iter().zip(b.inner.iter()).all(|(x, y)| {
|
|
let (Ok(ForceResult::Ready(x)), Ok(ForceResult::Ready(y))) =
|
|
(self.force_inline(*x), self.force_inline(*y))
|
|
else {
|
|
return false;
|
|
};
|
|
self.values_equal(x, y, strings)
|
|
});
|
|
}
|
|
if let (Some(a), Some(b)) = (lhs.as_gc::<AttrSet<'gc>>(), rhs.as_gc::<AttrSet<'gc>>()) {
|
|
if a.len() != b.len() {
|
|
return false;
|
|
}
|
|
return a
|
|
.iter()
|
|
.zip(b.iter())
|
|
.all(|((k1, v1), (k2, v2))| {
|
|
if k1 != k2 {
|
|
return false;
|
|
}
|
|
let (Ok(ForceResult::Ready(v1)), Ok(ForceResult::Ready(v2))) =
|
|
(self.force_inline(*v1), self.force_inline(*v2))
|
|
else {
|
|
return false;
|
|
};
|
|
self.values_equal(v1, v2, strings)
|
|
});
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(super) fn compare_values(
|
|
&self,
|
|
lhs: StrictValue<'gc>,
|
|
rhs: StrictValue<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
pred: impl FnOnce(std::cmp::Ordering) -> bool,
|
|
) -> VmResult<Value<'gc>> {
|
|
if let (Some(a), Some(b)) = (Self::as_num(lhs), Self::as_num(rhs)) {
|
|
let ord = match (a, b) {
|
|
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
|
|
(NixNum::Float(a), NixNum::Float(b)) => {
|
|
a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Less)
|
|
}
|
|
(NixNum::Int(a), NixNum::Float(b)) => (a as f64)
|
|
.partial_cmp(&b)
|
|
.unwrap_or(std::cmp::Ordering::Less),
|
|
(NixNum::Float(a), NixNum::Int(b)) => a
|
|
.partial_cmp(&(b as f64))
|
|
.unwrap_or(std::cmp::Ordering::Less),
|
|
};
|
|
return Ok(Value::new_inline(pred(ord)));
|
|
}
|
|
if let (Some(a), Some(b)) = (
|
|
Self::get_string(lhs, strings),
|
|
Self::get_string(rhs, strings),
|
|
) {
|
|
return Ok(Value::new_inline(pred(a.cmp(b))));
|
|
}
|
|
Err(Self::err("cannot compare these types"))
|
|
}
|
|
|
|
pub(super) fn do_select_step(
|
|
&mut self,
|
|
set_val: StrictValue<'gc>,
|
|
keys: SmallVec<[Value<'gc>; 4]>,
|
|
remaining: u16,
|
|
span: u32,
|
|
default: Option<Value<'gc>>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
let Some(attrs) = set_val.as_gc::<AttrSet<'gc>>() else {
|
|
if let Some(def) = default {
|
|
self.push_stack(def);
|
|
return Ok(());
|
|
}
|
|
return Err(Self::err("cannot select from non-set"));
|
|
};
|
|
|
|
let key_idx = keys.len() - remaining as usize;
|
|
let key = &keys[key_idx];
|
|
let Some(key_sid) = Self::get_string_id(key) else {
|
|
return Err(Self::err("attribute name must be a string"));
|
|
};
|
|
|
|
let found = attrs.lookup(key_sid);
|
|
|
|
if remaining <= 1 {
|
|
match (found, default) {
|
|
(Some(v), _) => {
|
|
self.push_stack(v);
|
|
Ok(())
|
|
}
|
|
(None, Some(default)) => {
|
|
self.push_stack(default);
|
|
Ok(())
|
|
}
|
|
(None, None) => {
|
|
let name = strings.resolve(key_sid.0).unwrap_or("<unknown>");
|
|
Err(Self::err(format!("attribute '{name}' missing")))
|
|
}
|
|
}
|
|
} else {
|
|
match (found, default) {
|
|
(Some(v), default) => match self.force_inline(v)? {
|
|
ForceResult::Ready(forced) => {
|
|
self.do_select_step(forced, keys, remaining - 1, span, default, mc, strings)
|
|
}
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::Select {
|
|
keys,
|
|
remaining: remaining - 1,
|
|
span,
|
|
default,
|
|
},
|
|
mc,
|
|
strings,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
},
|
|
(None, Some(default)) => {
|
|
self.push_stack(default);
|
|
Ok(())
|
|
}
|
|
(None, None) => {
|
|
let name = strings.resolve(key_sid.0).unwrap_or("<unknown>");
|
|
Err(Self::err(format!("attribute '{name}' missing")))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn do_has_attr_step(
|
|
&mut self,
|
|
set_val: StrictValue<'gc>,
|
|
keys: SmallVec<[Value<'gc>; 4]>,
|
|
remaining: u16,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
let Some(attrs) = set_val.as_gc::<AttrSet<'gc>>() else {
|
|
self.push_stack(Value::new_inline(false));
|
|
return Ok(());
|
|
};
|
|
|
|
let key_idx = keys.len() - remaining as usize;
|
|
let Some(key_sid) = Self::get_string_id(&keys[key_idx]) else {
|
|
self.push_stack(Value::new_inline(false));
|
|
return Ok(());
|
|
};
|
|
|
|
if remaining <= 1 {
|
|
self.push_stack(Value::new_inline(attrs.has(key_sid)));
|
|
Ok(())
|
|
} else {
|
|
match attrs.lookup(key_sid) {
|
|
Some(v) => match self.force_inline(v)? {
|
|
ForceResult::Ready(forced) => {
|
|
self.do_has_attr_step(forced, keys, remaining - 1, mc, strings)
|
|
}
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::HasAttr {
|
|
keys,
|
|
remaining: remaining - 1,
|
|
},
|
|
mc,
|
|
strings,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
},
|
|
None => {
|
|
self.push_stack(Value::new_inline(false));
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn concat_strings_continue(
|
|
&mut self,
|
|
mut forced: SmallVec<[StrictValue<'gc>; 8]>,
|
|
mut remaining: SmallVec<[Value<'gc>; 8]>,
|
|
force_string: bool,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
while let Some(part) = remaining.pop() {
|
|
match self.force_inline(part)? {
|
|
ForceResult::Ready(v) => forced.push(v),
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::ConcatStrings {
|
|
forced,
|
|
remaining,
|
|
force_string,
|
|
},
|
|
mc,
|
|
strings,
|
|
)?;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut result = String::new();
|
|
for part in &forced {
|
|
if let Some(s) = Self::get_string(*part, strings) {
|
|
result.push_str(s);
|
|
} else if let Some(n) = Self::as_num(*part) {
|
|
match n {
|
|
NixNum::Int(i) => result.push_str(&i.to_string()),
|
|
NixNum::Float(f) => result.push_str(&format!("{f}")),
|
|
}
|
|
} else if part.is::<Null>() {
|
|
} else if let Some(b) = part.as_inline::<bool>() {
|
|
if force_string {
|
|
result.push_str(if b { "1" } else { "" });
|
|
} else {
|
|
return Err(Self::err("cannot coerce a boolean to a string"));
|
|
}
|
|
} else {
|
|
return Err(Self::err("cannot coerce value to string"));
|
|
}
|
|
}
|
|
|
|
let ns = Gc::new(mc, NixString::new(result));
|
|
self.push_stack(Value::new_gc(ns));
|
|
Ok(())
|
|
}
|
|
|
|
fn do_with_lookup_step(
|
|
&mut self,
|
|
scope_val: StrictValue<'gc>,
|
|
name: StringId,
|
|
next: Option<Gc<'gc, WithScope<'gc>>>,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> VmResult<()> {
|
|
if let Some(attrs) = scope_val.as_gc::<AttrSet<'gc>>()
|
|
&& let Some(v) = attrs.lookup(name)
|
|
{
|
|
self.push_stack(v);
|
|
return Ok(());
|
|
}
|
|
|
|
match next {
|
|
Some(scope) => {
|
|
let env_val = scope.env;
|
|
let next_prev = scope.prev;
|
|
match self.force_inline(env_val)? {
|
|
ForceResult::Ready(forced) => {
|
|
self.do_with_lookup_step(forced, name, next_prev, mc, strings)
|
|
}
|
|
other => {
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::WithLookup {
|
|
name,
|
|
next: next_prev,
|
|
},
|
|
mc,
|
|
strings,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
let name_str = strings.resolve(name.0).unwrap_or("<unknown>");
|
|
Err(Self::err(format!("undefined variable '{name_str}'")))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn convert_value(
|
|
&self,
|
|
val: &Value<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> crate::value::Value {
|
|
if let Some(i) = val.as_inline::<i32>() {
|
|
crate::value::Value::Int(i as i64)
|
|
} else if let Some(gc_i) = val.as_gc::<i64>() {
|
|
crate::value::Value::Int(*gc_i)
|
|
} else if let Some(f) = val.as_float() {
|
|
crate::value::Value::Float(f)
|
|
} else if let Some(b) = val.as_inline::<bool>() {
|
|
crate::value::Value::Bool(b)
|
|
} else if val.is::<Null>() {
|
|
crate::value::Value::Null
|
|
} else if let Some(sid) = val.as_inline::<StringId>() {
|
|
let s = strings.resolve(sid.0).unwrap_or("").to_owned();
|
|
crate::value::Value::String(s)
|
|
} else if let Some(ns) = val.as_gc::<NixString>() {
|
|
crate::value::Value::String(ns.as_str().to_owned())
|
|
} else if let Some(attrs) = val.as_gc::<AttrSet<'gc>>() {
|
|
let mut map = std::collections::BTreeMap::new();
|
|
for (key, val) in attrs.iter() {
|
|
let key_str = strings.resolve(key.0).unwrap_or("").to_owned();
|
|
let converted = self.convert_value(val, strings);
|
|
map.insert(crate::value::Symbol::from(key_str), converted);
|
|
}
|
|
crate::value::Value::AttrSet(crate::value::AttrSet::new(map))
|
|
} else if let Some(list) = val.as_gc::<List<'gc>>() {
|
|
let items: Vec<_> = list
|
|
.inner
|
|
.iter()
|
|
.map(|v| self.convert_value(v, strings))
|
|
.collect();
|
|
crate::value::Value::List(crate::value::List::new(items))
|
|
} else if val.is::<Closure<'gc>>() {
|
|
crate::value::Value::Func
|
|
} else if val.is::<Thunk<'gc>>() {
|
|
crate::value::Value::Thunk
|
|
} else if val.as_inline::<PrimOp>().is_some() {
|
|
crate::value::Value::PrimOp("primop".into())
|
|
} else if val.is::<PrimOpApp<'gc>>() {
|
|
crate::value::Value::PrimOpApp("primop-app".into())
|
|
} else {
|
|
crate::value::Value::Null
|
|
}
|
|
}
|
|
|
|
fn execute_one(
|
|
&mut self,
|
|
bc: &[u8],
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> Action {
|
|
use crate::codegen::Op::{self, *};
|
|
|
|
#[cfg(debug_assertions)]
|
|
let opcode_byte = bc[self.pc];
|
|
#[cfg(not(debug_assertions))]
|
|
let opcode_byte = unsafe { *bc.get_unchecked(self.pc) };
|
|
self.pc += 1;
|
|
|
|
#[cfg(debug_assertions)]
|
|
let Ok(op) = Op::try_from_primitive(opcode_byte) else {
|
|
return Action::Done(Err(Error::eval_error(format!(
|
|
"unknown opcode: {opcode_byte:#04x}"
|
|
))));
|
|
};
|
|
#[cfg(not(debug_assertions))]
|
|
let op = unsafe { Op::try_from_primitive(opcode_byte).unwrap_unchecked() };
|
|
|
|
match op {
|
|
PushSmi => {
|
|
let val = self.read_i32(bc);
|
|
self.push_stack(Value::new_inline(val));
|
|
}
|
|
PushBigInt => {
|
|
let val = self.read_i64(bc);
|
|
self.push_stack(Value::new_gc(Gc::new(mc, val)));
|
|
}
|
|
PushFloat => {
|
|
let val = self.read_f64(bc);
|
|
self.push_stack(Value::new_float(val));
|
|
}
|
|
PushString => {
|
|
let sid = self.read_string_id(bc);
|
|
self.push_stack(Value::new_inline(sid));
|
|
}
|
|
PushNull => self.push_stack(Value::new_inline(Null)),
|
|
PushTrue => self.push_stack(Value::new_inline(true)),
|
|
PushFalse => self.push_stack(Value::new_inline(false)),
|
|
|
|
LoadLocal => {
|
|
let idx = self.read_u32(bc) as usize;
|
|
let val = self.env().borrow().locals[idx];
|
|
self.push_stack(val);
|
|
}
|
|
LoadOuter => {
|
|
let layer = self.read_u8(bc);
|
|
let idx = self.read_u32(bc) as usize;
|
|
let mut env = self.env();
|
|
for _ in 0..layer {
|
|
let prev = env.borrow().prev.expect("LoadOuter: env chain too short");
|
|
env = prev;
|
|
}
|
|
let val = env.borrow().locals[idx];
|
|
self.push_stack(val);
|
|
}
|
|
StoreLocal => {
|
|
let idx = self.read_u32(bc) as usize;
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
self.env().borrow_mut(mc).locals[idx] = val;
|
|
}
|
|
AllocLocals => {
|
|
let count = self.read_u32(bc) as usize;
|
|
self.env()
|
|
.borrow_mut(mc)
|
|
.locals
|
|
.resize(count, Value::default());
|
|
}
|
|
|
|
MakeThunk => {
|
|
let entry_point = self.read_u32(bc);
|
|
let _label = self.read_string_id(bc);
|
|
let thunk = Gc::new(
|
|
mc,
|
|
RefLock::new(ThunkState::Pending {
|
|
ip: entry_point,
|
|
env: self.env(),
|
|
}),
|
|
);
|
|
self.push_stack(Value::new_gc(thunk));
|
|
}
|
|
MakeClosure => {
|
|
let entry_point = self.read_u32(bc);
|
|
let n_locals = self.read_u32(bc);
|
|
let closure = Gc::new(
|
|
mc,
|
|
Closure {
|
|
ip: entry_point,
|
|
n_locals,
|
|
env: self.env(),
|
|
pattern: None,
|
|
},
|
|
);
|
|
self.push_stack(Value::new_gc(closure));
|
|
}
|
|
MakePatternClosure => {
|
|
let entry_point = self.read_u32(bc);
|
|
let n_locals = self.read_u32(bc);
|
|
let req_count = self.read_u16(bc) as usize;
|
|
let opt_count = self.read_u16(bc) as usize;
|
|
let has_ellipsis = self.read_u8(bc) != 0;
|
|
|
|
let mut required = SmallVec::new();
|
|
for _ in 0..req_count {
|
|
required.push(self.read_string_id(bc));
|
|
}
|
|
let mut optional = SmallVec::new();
|
|
for _ in 0..opt_count {
|
|
optional.push(self.read_string_id(bc));
|
|
}
|
|
let total = req_count + opt_count;
|
|
let mut param_spans = Vec::with_capacity(total);
|
|
for _ in 0..total {
|
|
let name = self.read_string_id(bc);
|
|
let span_id = self.read_u32(bc);
|
|
param_spans.push((name, span_id));
|
|
}
|
|
|
|
let pattern = Gc::new(
|
|
mc,
|
|
PatternInfo {
|
|
required,
|
|
optional,
|
|
ellipsis: has_ellipsis,
|
|
param_spans: param_spans.into_boxed_slice(),
|
|
},
|
|
);
|
|
let closure = Gc::new(
|
|
mc,
|
|
Closure {
|
|
ip: entry_point,
|
|
n_locals,
|
|
env: self.env(),
|
|
pattern: Some(pattern),
|
|
},
|
|
);
|
|
self.push_stack(Value::new_gc(closure));
|
|
}
|
|
|
|
Call => {
|
|
let span_id = self.read_u32(bc);
|
|
let arg = self.stack.pop().expect("stack underflow");
|
|
let func = self.stack.pop().expect("stack underflow");
|
|
match self.force_inline(func) {
|
|
Ok(ForceResult::Ready(f)) => {
|
|
try_vm!(self, self.do_call(f, arg, Some(span_id), mc, strings))
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::Call {
|
|
arg,
|
|
span: Some(span_id),
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
CallNoSpan => {
|
|
let arg = self.stack.pop().expect("stack underflow");
|
|
let func = self.stack.pop().expect("stack underflow");
|
|
match self.force_inline(func) {
|
|
Ok(ForceResult::Ready(f)) => {
|
|
try_vm!(self, self.do_call(f, arg, None, mc, strings))
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(other, AfterForce::Call { arg, span: None }, mc, strings)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
MakeAttrs => {
|
|
let count = self.read_u32(bc) as usize;
|
|
let total = 3 * count;
|
|
let items = self.stack.pop_n::<16>(total);
|
|
|
|
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
|
|
for i in 0..count {
|
|
let key = &items[2 * i];
|
|
let val = items[2 * i + 1];
|
|
let key_sid =
|
|
Self::get_string_id(key).expect("MakeAttrs: key must be StringId");
|
|
entries.push((key_sid, val));
|
|
}
|
|
entries.sort_by_key(|(k, _)| *k);
|
|
|
|
let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
|
|
self.push_stack(Value::new_gc(attrs));
|
|
}
|
|
MakeAttrsDyn => {
|
|
let static_count = self.read_u32(bc) as usize;
|
|
let dynamic_count = self.read_u32(bc) as usize;
|
|
let total = 3 * static_count + 3 * dynamic_count;
|
|
let items = self.stack.pop_n::<16>(total);
|
|
|
|
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
|
|
|
|
for i in 0..static_count {
|
|
let key_sid = Self::get_string_id(&items[2 * i])
|
|
.expect("MakeAttrsDyn: static key must be StringId");
|
|
entries.push((key_sid, items[2 * i + 1]));
|
|
}
|
|
|
|
let dyn_base = 3 * static_count;
|
|
for i in 0..dynamic_count {
|
|
let key = &items[dyn_base + 3 * i];
|
|
let val = items[dyn_base + 3 * i + 1];
|
|
if key.is::<Null>() {
|
|
continue;
|
|
}
|
|
let Some(key_sid) = Self::get_string_id(key) else {
|
|
return Action::Done(Err(Error::eval_error(
|
|
"dynamic attribute name must be a string",
|
|
)));
|
|
};
|
|
entries.push((key_sid, val));
|
|
}
|
|
|
|
entries.sort_by_key(|(k, _)| *k);
|
|
// FIXME: incorrect!!!
|
|
entries.dedup_by_key(|(k, _)| *k);
|
|
|
|
let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
|
|
self.push_stack(Value::new_gc(attrs));
|
|
}
|
|
MakeEmptyAttrs => {
|
|
let attrs = Gc::new(mc, unsafe {
|
|
AttrSet::from_sorted_unchecked(SmallVec::new())
|
|
});
|
|
self.push_stack(Value::new_gc(attrs));
|
|
}
|
|
|
|
Select => {
|
|
let n = self.read_u16(bc) as usize;
|
|
let span_id = self.read_u32(bc);
|
|
let mut keys = self.stack.pop_n::<4>(n);
|
|
let expr = self.stack.pop().expect("stack underflow");
|
|
keys.reverse();
|
|
|
|
let remaining = keys.len() as u16;
|
|
match self.force_inline(expr) {
|
|
Ok(ForceResult::Ready(forced)) => {
|
|
try_vm!(
|
|
self,
|
|
self.do_select_step(
|
|
forced, keys, remaining, span_id, None, mc, strings,
|
|
)
|
|
);
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::Select {
|
|
keys,
|
|
remaining,
|
|
span: span_id,
|
|
default: None,
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
SelectDefault => {
|
|
let n = self.read_u16(bc) as usize;
|
|
let span_id = self.read_u32(bc);
|
|
let default = self.stack.pop().expect("stack underflow");
|
|
let mut keys = self.stack.pop_n::<4>(n);
|
|
let expr = self.stack.pop().expect("stack underflow");
|
|
keys.reverse();
|
|
|
|
let remaining = keys.len() as u16;
|
|
match self.force_inline(expr) {
|
|
Ok(ForceResult::Ready(forced)) => {
|
|
try_vm!(
|
|
self,
|
|
self.do_select_step(
|
|
forced,
|
|
keys,
|
|
remaining,
|
|
span_id,
|
|
Some(default),
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::Select {
|
|
keys,
|
|
remaining,
|
|
span: span_id,
|
|
default: Some(default),
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
HasAttr => {
|
|
let n = self.read_u16(bc) as usize;
|
|
let mut keys = self.stack.pop_n::<4>(n);
|
|
let expr = self.stack.pop().expect("stack underflow");
|
|
keys.reverse();
|
|
|
|
let remaining = keys.len() as u16;
|
|
match self.force_inline(expr) {
|
|
Ok(ForceResult::Ready(forced)) => {
|
|
try_vm!(self, self.do_has_attr_step(forced, keys, remaining, mc, strings));
|
|
}
|
|
Ok(ForceResult::NeedEval { ip, env, thunk }) => {
|
|
self.push_force_frame(
|
|
thunk,
|
|
AfterForce::HasAttr { keys, remaining },
|
|
ip,
|
|
env,
|
|
mc,
|
|
);
|
|
}
|
|
Ok(ForceResult::NeedApply(thunk)) => {
|
|
try_vm!(
|
|
self,
|
|
self.push_apply_force_frame(
|
|
thunk,
|
|
AfterForce::HasAttr { keys, remaining },
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
MakeList => {
|
|
let count = self.read_u32(bc) as usize;
|
|
let items = self.stack.pop_n::<4>(count);
|
|
let list = Gc::new(mc, List { inner: items });
|
|
self.push_stack(Value::new_gc(list));
|
|
}
|
|
|
|
OpAdd | OpSub | OpMul | OpDiv | OpEq | OpNeq | OpLt | OpGt | OpLeq | OpGeq
|
|
| OpConcat | OpUpdate => {
|
|
let tag = match op {
|
|
OpAdd => BinOpTag::Add,
|
|
OpSub => BinOpTag::Sub,
|
|
OpMul => BinOpTag::Mul,
|
|
OpDiv => BinOpTag::Div,
|
|
OpEq => BinOpTag::Eq,
|
|
OpNeq => BinOpTag::Neq,
|
|
OpLt => BinOpTag::Lt,
|
|
OpGt => BinOpTag::Gt,
|
|
OpLeq => BinOpTag::Leq,
|
|
OpGeq => BinOpTag::Geq,
|
|
OpConcat => BinOpTag::Concat,
|
|
OpUpdate => BinOpTag::Update,
|
|
_ => unreachable!(),
|
|
};
|
|
let rhs = self.stack.pop().expect("stack underflow");
|
|
let lhs = self.stack.pop().expect("stack underflow");
|
|
|
|
match self.force_inline(lhs) {
|
|
Ok(ForceResult::Ready(lhs_f)) => match self.force_inline(rhs) {
|
|
Ok(ForceResult::Ready(rhs_f)) => {
|
|
match self.compute_binop(tag, lhs_f, rhs_f, mc, strings) {
|
|
Ok(r) => self.push_stack(r),
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::BinOpRhs {
|
|
lhs_forced: lhs_f,
|
|
op: tag,
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
},
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(other, AfterForce::BinOpLhs { rhs, op: tag }, mc, strings)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
OpNeg => {
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
match self.force_inline(val) {
|
|
Ok(ForceResult::Ready(f)) => match Self::as_num(f) {
|
|
Some(NixNum::Int(i)) => self
|
|
.push_stack(Self::make_int(-i, mc)),
|
|
Some(NixNum::Float(fl)) => self
|
|
.push_stack(Value::new_float(-fl)),
|
|
None => {
|
|
return Action::Done(Err(Error::eval_error(
|
|
"cannot negate non-number",
|
|
)));
|
|
}
|
|
},
|
|
Ok(other) => {
|
|
try_vm!(self, self.setup_force(other, AfterForce::UnNeg, mc, strings));
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
OpNot => {
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
match self.force_inline(val) {
|
|
Ok(ForceResult::Ready(f)) => match f.as_inline::<bool>() {
|
|
Some(b) => self
|
|
.push_stack(Value::new_inline(!b)),
|
|
None => {
|
|
return Action::Done(Err(Error::eval_error("value is not a boolean")));
|
|
}
|
|
},
|
|
Ok(other) => {
|
|
try_vm!(self, self.setup_force(other, AfterForce::UnNot, mc, strings));
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
ForceBool => {
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
match self.force_inline(val) {
|
|
Ok(ForceResult::Ready(f)) => {
|
|
if f.as_inline::<bool>().is_none() {
|
|
return Action::Done(Err(Error::eval_error("value is not a boolean")));
|
|
}
|
|
self.push_stack(f.relax());
|
|
}
|
|
Ok(ForceResult::NeedEval { ip, env, thunk }) => {
|
|
self.push_force_frame(thunk, AfterForce::ForceBool, ip, env, mc);
|
|
}
|
|
Ok(ForceResult::NeedApply(thunk)) => {
|
|
try_vm!(
|
|
self,
|
|
self.push_apply_force_frame(thunk, AfterForce::ForceBool, mc, strings)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
JumpIfFalse => {
|
|
let offset = self.read_i32(bc);
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
if let Some(false) = val.as_inline::<bool>() {
|
|
self.pc = ((self.pc as isize) + (offset as isize)) as usize;
|
|
}
|
|
}
|
|
JumpIfTrue => {
|
|
let offset = self.read_i32(bc);
|
|
let val = self.stack.pop().expect("stack underflow");
|
|
if let Some(true) = val.as_inline::<bool>() {
|
|
self.pc = ((self.pc as isize) + (offset as isize)) as usize;
|
|
}
|
|
}
|
|
Jump => {
|
|
let offset = self.read_i32(bc);
|
|
self.pc = ((self.pc as isize) + (offset as isize)) as usize;
|
|
}
|
|
|
|
ConcatStrings => {
|
|
let parts_count = self.read_u16(bc) as usize;
|
|
let force_string = self.read_u8(bc) != 0;
|
|
let parts = self.stack.pop_n::<8>(parts_count);
|
|
let mut remaining = parts;
|
|
remaining.reverse();
|
|
let forced = SmallVec::new();
|
|
try_vm!(
|
|
self,
|
|
self.concat_strings_continue(forced, remaining, force_string, mc, strings,)
|
|
);
|
|
}
|
|
ResolvePath => {
|
|
let _val = self.stack.pop().expect("stack underflow");
|
|
self.push_stack(Value::new_inline(Null));
|
|
}
|
|
|
|
Assert => {
|
|
let raw_idx = self.read_u32(bc);
|
|
let span_id = self.read_u32(bc);
|
|
let expr = self.stack.pop().expect("stack underflow");
|
|
let assertion = self.stack.pop().expect("stack underflow");
|
|
|
|
match self.force_inline(assertion) {
|
|
Ok(ForceResult::Ready(f)) => match f.as_inline::<bool>() {
|
|
Some(true) => self.push_stack(expr),
|
|
Some(false) => {
|
|
let sym = string_interner::symbol::SymbolU32::try_from_usize(
|
|
raw_idx as usize,
|
|
);
|
|
let msg = sym.and_then(|s| strings.resolve(s)).unwrap_or("<unknown>");
|
|
return self.handle_vm_error(VmError::Uncatchable(Error::eval_error(
|
|
format!("assertion '{msg}' failed"),
|
|
)));
|
|
}
|
|
None => {
|
|
return self.handle_vm_error(Self::err(
|
|
"assertion condition must be a boolean",
|
|
));
|
|
}
|
|
},
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::Assert {
|
|
expr,
|
|
raw_idx,
|
|
span_id,
|
|
},
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
|
|
PushWith => {
|
|
let ns = self.stack.pop().expect("stack underflow");
|
|
let scope = Gc::new(
|
|
mc,
|
|
WithScope {
|
|
env: ns,
|
|
prev: self.with_scope,
|
|
},
|
|
);
|
|
self.with_scope = Some(scope);
|
|
}
|
|
PopWith => {
|
|
if let Some(scope) = self.with_scope {
|
|
self.with_scope = scope.prev;
|
|
}
|
|
}
|
|
WithLookup => {
|
|
let name = self.read_string_id(bc);
|
|
match self.with_scope {
|
|
Some(scope_gc) => {
|
|
let env_val = scope_gc.env;
|
|
let next = scope_gc.prev;
|
|
match self.force_inline(env_val) {
|
|
Ok(ForceResult::Ready(forced)) => {
|
|
try_vm!(
|
|
self,
|
|
self.do_with_lookup_step(forced, name, next, mc, strings)
|
|
);
|
|
}
|
|
Ok(other) => {
|
|
try_vm!(
|
|
self,
|
|
self.setup_force(
|
|
other,
|
|
AfterForce::WithLookup { name, next },
|
|
mc,
|
|
strings,
|
|
)
|
|
);
|
|
}
|
|
Err(e) => return self.handle_vm_error(e),
|
|
}
|
|
}
|
|
None => {
|
|
let name_str = strings.resolve(name.0).unwrap_or("<unknown>");
|
|
return self.handle_vm_error(Self::err(format!(
|
|
"undefined variable '{name_str}'"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
LoadBuiltins => {
|
|
self.push_stack(self.globals.builtins);
|
|
}
|
|
LoadBuiltin => {
|
|
let name_raw = self.read_string_id(bc);
|
|
if let Some(&primop) = self.globals.builtin_lookup.get(&name_raw) {
|
|
self.push_stack(Value::new_inline(primop));
|
|
} else {
|
|
return Action::Done(Err(Error::eval_error(format!(
|
|
"unknown builtin (id {:?})",
|
|
name_raw
|
|
))));
|
|
}
|
|
}
|
|
|
|
MkPos => {
|
|
let _span_id = self.read_u32(bc);
|
|
todo!("MkPos")
|
|
}
|
|
|
|
LoadReplBinding => {
|
|
let _name = self.read_string_id(bc);
|
|
todo!("LoadReplBinding")
|
|
}
|
|
LoadScopedBinding => {
|
|
let _name = self.read_string_id(bc);
|
|
todo!("LoadScopedBinding")
|
|
}
|
|
|
|
Return => {
|
|
let ret_val = self.stack.pop().expect("stack underflow");
|
|
return self.handle_return(ret_val, mc, strings);
|
|
}
|
|
}
|
|
|
|
Action::Continue
|
|
}
|
|
|
|
pub(super) fn run_batch(
|
|
&mut self,
|
|
bytecode: &[u8],
|
|
pc: &mut usize,
|
|
mc: &Mutation<'gc>,
|
|
strings: &DefaultStringInterner,
|
|
) -> Action {
|
|
const COLLECTOR_GRANULARITY: f64 = 1024.0;
|
|
const BATCH_SIZE: usize = 1024;
|
|
|
|
if !self.started {
|
|
self.pc = *pc;
|
|
self.current_env = Some(Gc::new(mc, RefLock::new(Env::empty())));
|
|
self.started = true;
|
|
}
|
|
|
|
for _ in 0..BATCH_SIZE {
|
|
let action = self.execute_one(bytecode, mc, strings);
|
|
match action {
|
|
Action::Continue => {}
|
|
other => {
|
|
*pc = self.pc;
|
|
if matches!(other, Action::Done(_)) {
|
|
self.started = false;
|
|
}
|
|
return other;
|
|
}
|
|
}
|
|
}
|
|
|
|
*pc = self.pc;
|
|
|
|
if mc.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
|
|
Action::NeedGc
|
|
} else {
|
|
Action::Continue
|
|
}
|
|
}
|
|
|
|
fn vm_try<T>(&mut self, result: VmResult<T>) -> std::result::Result<T, Action> {
|
|
match result {
|
|
Ok(ok) => Ok(ok),
|
|
Err(err) => Err(self.handle_vm_error(err)),
|
|
}
|
|
}
|
|
|
|
fn handle_vm_error(&mut self, e: VmError) -> Action {
|
|
match e {
|
|
VmError::Catchable(msg) => {
|
|
// Check for tryEval catch frames
|
|
if let Some(catch) = self.find_catch_frame() {
|
|
self.restore_catch_frame(catch);
|
|
return Action::Continue;
|
|
}
|
|
Action::Done(Err(Error::catchable(msg)))
|
|
}
|
|
VmError::Uncatchable(e) => Action::Done(Err(e)),
|
|
}
|
|
}
|
|
|
|
fn find_catch_frame(&mut self) -> Option<CatchFrameInfo> {
|
|
todo!("find catch frame")
|
|
}
|
|
|
|
fn restore_catch_frame(&mut self, _catch: CatchFrameInfo) {
|
|
todo!("restore catch frame")
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(super) fn push_stack(&mut self, val: Value<'gc>) {
|
|
#[cfg(debug_assertions)]
|
|
self.stack.push(val).expect("stack overflow");
|
|
#[cfg(not(debug_assertions))]
|
|
unsafe {
|
|
self.stack.push_unchecked(val);
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(super) fn push_empty_list(&mut self) {
|
|
self.push_stack(self.globals.empty_list);
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(super) fn push_empty_attrs(&mut self) {
|
|
self.push_stack(self.globals.empty_attrs);
|
|
}
|
|
}
|
|
|
|
struct CatchFrameInfo;
|