Files
nix-js/fix-vm/src/lib.rs
T

1297 lines
49 KiB
Rust

use std::path::PathBuf;
use fix_builtins::{BUILTINS, BuiltinId};
use fix_codegen::{AttrKeyType, InstructionPtr, OperandType};
use fix_common::StringId;
use fix_error::{Error, Result, Source};
use gc_arena::arena::CollectionPhase;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
use string_interner::Symbol as _;
mod boxing;
mod value;
use value::*;
mod helpers;
use helpers::*;
pub use value::StaticValue;
type VmResult<T> = std::result::Result<T, VmError>;
enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
pub trait VmContext {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn bytecode(&self) -> &[u8];
fn get_const(&self, id: u32) -> StaticValue;
fn compile(&mut self, source: Source);
}
trait VmContextExt: VmContext {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmContext> VmContextExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
fn convert_value(&self, val: Value) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value(val);
map.insert(fix_common::Symbol::from(key), converted);
}
Value::AttrSet(fix_common::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() {
let items: Vec<_> = list
.inner
.iter()
.copied()
.map(|v| self.convert_value(v))
.collect();
Value::List(fix_common::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if val.is::<Thunk>() {
Value::Thunk
} else if val.as_inline::<PrimOp>().is_some() {
Value::PrimOp("primop".into())
} else if val.is::<PrimOpApp>() {
Value::PrimOpApp("primop-app".into())
} else {
Value::Null
}
}
}
pub struct Vm<C: VmContext> {
arena: Arena<Rootable![GcRoot<'_>]>,
error_context: Vec<ErrorFrame>,
ctx: C,
pc: usize,
force_mode: ForceMode,
}
#[derive(Collect)]
#[collect(no_drop)]
struct GcRoot<'gc> {
stack: Vec<Value<'gc>>,
call_stack: Vec<CallFrame<'gc>>,
call_depth: usize,
with_env: Option<Gc<'gc, WithEnv<'gc>>>,
builtins: Value<'gc>,
empty_list: Value<'gc>,
empty_attrs: Value<'gc>,
import_cache: HashMap<PathBuf, Value<'gc>>,
current_env: Option<GcEnv<'gc>>,
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Value<'gc> {
let mut entries = SmallVec::with_capacity(BUILTINS.len());
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name);
entries.push((name, Value::new_inline(PrimOp { id, arity })));
}
let consts = [
(
"__currentSystem",
Value::new_inline(ctx.intern_string("x86_64-linux")),
),
("__langVersion", Value::new_inline(6i32)),
(
"__nixVersion",
Value::new_inline(ctx.intern_string("2.24.0")),
),
(
"__storeDir",
Value::new_inline(ctx.intern_string("/nix/store")),
),
(
"__nixPath",
Value::new_gc(Gc::new(
mc,
List {
inner: SmallVec::new(),
},
)),
),
("null", Value::new_inline(Null)),
("true", Value::new_inline(true)),
("false", Value::new_inline(false)),
];
for (name, val) in consts {
let name = name.strip_prefix("__").unwrap_or(name);
let name = ctx.intern_string(name);
entries.push((name, val));
}
let self_ref_thunk: Gc<'gc, Thunk<'gc>> = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
let sym = ctx.intern_string("builtins");
entries.push((sym, Value::new_gc(self_ref_thunk)));
entries.sort_by_key(|(k, _)| *k);
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
Value::new_gc(builtins_set)
}
impl<'gc> GcRoot<'gc> {
fn new(mc: &Mutation<'gc>, ctx: &mut impl VmContext) -> Self {
let builtins = init_builtins(mc, ctx);
GcRoot {
stack: Vec::with_capacity(8192),
call_stack: Vec::with_capacity(1024),
call_depth: 0,
with_env: None,
builtins,
empty_list: Value::new_gc(Gc::new(mc, List::default())),
empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())),
import_cache: HashMap::new(),
current_env: None,
}
}
#[inline(always)]
fn env(&self) -> Gc<'gc, RefLock<Env<'gc>>> {
self.current_env.expect("no current env")
}
#[inline(always)]
fn push_stack(&mut self, val: Value<'gc>) {
self.stack.push(val);
}
#[inline(always)]
fn pop_stack(&mut self) -> Value<'gc> {
self.stack.pop().expect("stack underflow")
}
#[inline(always)]
fn peek_stack(&mut self, depth: usize) -> Value<'gc> {
*self
.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
}
#[inline(always)]
fn replace_stack(&mut self, depth: usize, val: Value<'gc>) {
let len = self.stack.len();
*self
.stack
.get_mut(len - depth - 1)
.expect("stack underflow") = val;
}
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn pop_stack_forced(&mut self) -> StrictValue<'gc> {
self.stack
.pop()
.expect("stack underflow")
.restrict()
.expect("forced")
}
}
struct ErrorFrame {
span_id: u32,
message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
struct CallFrame<'gc> {
pc: usize,
stack_depth: usize,
thunk: Option<Gc<'gc, Thunk<'gc>>>,
env: Gc<'gc, RefLock<Env<'gc>>>,
with_env: Option<Gc<'gc, WithEnv<'gc>>>,
}
pub(crate) enum Action {
Continue,
Done(Result<fix_common::Value>),
}
enum NixNum {
Int(i64),
Float(f64),
}
enum OperandData {
Const(StaticValue),
Local { layer: u8, idx: u32 },
Builtins,
BigInt(i64),
}
impl OperandData {
fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &GcRoot<'gc>) -> Value<'gc> {
match *self {
OperandData::Const(sv) => sv.into(),
OperandData::Local { layer, idx } => {
let mut cur = root.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
OperandData::Builtins => root.builtins,
OperandData::BigInt(val) => Value::new_gc(Gc::new(mc, val)),
}
}
}
enum AttrKeyData {
Static(StringId),
Dynamic(OperandData),
}
struct AttrEntry {
key: AttrKeyData,
val: OperandData,
}
enum SelectKeyData {
Static(StringId),
Dynamic,
}
macro_rules! try_vm {
($self:ident; $expr:expr) => {
match $expr {
Ok(v) => v,
Err(e) => return GcRoot::handle_vm_error($self, e),
}
};
}
impl<C: VmContext> Vm<C> {
pub fn new(mut ctx: C, ip: InstructionPtr, force_mode: ForceMode) -> Self {
Self {
arena: Arena::new(|mc| GcRoot::new(mc, &mut ctx)),
ctx,
force_mode,
pc: ip.0,
error_context: Vec::new(),
}
}
pub fn run(mut self) -> Result<fix_common::Value> {
const COLLECTOR_GRANULARITY: f64 = 1024.0;
self.arena.mutate_root(|mc, root| {
if root.current_env.is_none() {
root.current_env = Some(Gc::new(mc, RefLock::new(Env::empty())));
}
});
let mut pc = self.pc;
loop {
match self
.arena
.mutate_root(|mc, root| root.execute_batch(&mut self.ctx, &mut pc, mc))
{
Action::Continue => {
if self.arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
if self.arena.collection_phase() == CollectionPhase::Sweeping {
self.arena.collect_debt();
} else if let Some(marked) = self.arena.mark_debt() {
marked.start_sweeping();
}
}
}
Action::Done(done) => break done,
}
}
}
}
impl<'gc> GcRoot<'gc> {
#[inline(always)]
fn execute_batch(
&mut self,
ctx: &mut impl VmContext,
pc: &mut usize,
mc: &Mutation<'gc>,
) -> Action {
use fix_codegen::Op::{self, *};
const DEFAULT_FUEL_AMOUNT: usize = 1024;
#[inline(always)]
#[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(bytecode: &[u8], pc: &mut usize) -> [u8; N] {
let ret = bytecode[*pc..*pc + N]
.try_into()
.expect("read_array failed");
*pc += N;
ret
}
macro_rules! read {
(StringId) => {{
let raw = read!(u32);
StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap())
}};
(OperandData) => {{
let tag = read!(u8);
let Ok(ty) = OperandType::try_from_primitive(tag)
.map_err(|err| panic!("unknown operand tag: {:#04x}", err.number));
match ty {
OperandType::Const => {
let id = read!(u32);
#[allow(clippy::unwrap_used)]
OperandData::Const(ctx.get_const(id))
}
OperandType::Local => {
let layer = read!(u8);
let idx = read!(u32);
OperandData::Local { layer, idx }
}
OperandType::Builtins => OperandData::Builtins,
OperandType::BigInt => {
let val = read!(i64);
OperandData::BigInt(val)
}
}
}};
(SelectKeyData, $n:expr) => {{
let mut keys = SmallVec::<[SelectKeyData; 4]>::with_capacity($n as usize);
for _ in 0..$n {
let tag = read!(u8);
let Ok(ty) = AttrKeyType::try_from_primitive(tag)
.map_err(|err| panic!("unknown key tag: {:#04x}", err.number));
match ty {
AttrKeyType::Static => keys.push(SelectKeyData::Static(read!(StringId))),
AttrKeyType::Dynamic => keys.push(SelectKeyData::Dynamic),
};
}
keys
}};
($type:ty) => {
<$type>::from_le_bytes(read_array(ctx.bytecode(), pc))
};
}
let mut fuel = DEFAULT_FUEL_AMOUNT;
'dispatch: loop {
macro_rules! try_force {
($depth:expr, $inst_start:expr) => {{
let val = self.peek_stack($depth);
if let Some(thunk) = val.as_gc::<Thunk>() {
let mut state = thunk.borrow_mut(mc);
match *state {
ThunkState::Pending { ip, env, with_env } => {
// retry
self.call_stack.push(CallFrame {
thunk: Some(thunk),
stack_depth: $depth,
pc: $inst_start,
env: self.env(),
with_env: self.with_env,
});
*pc = ip;
self.current_env = Some(env);
self.with_env = with_env;
*state = ThunkState::Blackhole;
continue 'dispatch;
}
ThunkState::Evaluated(v) => {
self.replace_stack($depth, v.relax());
}
ThunkState::Apply { .. } => todo!("force apply"),
ThunkState::Blackhole => {
return Action::Done(Err(Error::eval_error(
"infinite recursion encountered",
)));
}
}
}
}};
}
if fuel == 0 {
return Action::Continue;
}
fuel -= 1;
// Save PC for Instruction Retry
let inst_start_pc = *pc;
let byte = ctx.bytecode()[*pc];
if !likely_stable::likely((0..Op::Illegal as u8).contains(&byte)) {
panic!("unknown opcode: {byte:#04x}")
}
let op = unsafe { std::mem::transmute::<u8, Op>(byte) };
*pc += 1;
match op {
PushSmi => {
let val = read!(i32);
self.push_stack(Value::new_inline(val));
}
PushBigInt => {
let val = read!(i64);
self.push_stack(Value::new_gc(Gc::new(mc, val)));
}
PushFloat => {
let val = read!(f64);
self.push_stack(Value::new_float(val));
}
PushString => {
let sid = read!(StringId);
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 = read!(u32) as usize;
self.push_stack(self.env().borrow().locals[idx]);
}
LoadOuter => {
let layer = read!(u8);
let idx = read!(u32) as usize;
let mut cur = self.env();
for _ in 0..layer {
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
cur = prev;
}
let val = cur.borrow().locals[idx];
self.push_stack(val);
}
StoreLocal => {
let idx = read!(u32) as usize;
let val = self.pop_stack();
self.env().borrow_mut(mc).locals[idx] = val;
}
AllocLocals => {
let count = read!(u32) as usize;
self.env()
.borrow_mut(mc)
.locals
.extend(std::iter::repeat_n(Value::default(), count));
}
MakeThunk => {
let entry_point = read!(u32);
let thunk = Gc::new(
mc,
RefLock::new(ThunkState::Pending {
ip: entry_point as usize,
env: self.env(),
with_env: self.with_env,
}),
);
self.push_stack(Value::new_gc(thunk));
}
MakeClosure => {
let entry_point = read!(u32);
let n_locals = read!(u32);
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 = read!(u32);
let n_locals = read!(u32);
let req_count = read!(u16) as usize;
let opt_count = read!(u16) as usize;
let has_ellipsis = read!(u8) != 0;
let mut required = SmallVec::new();
for _ in 0..req_count {
required.push(read!(StringId));
}
let mut optional = SmallVec::new();
for _ in 0..opt_count {
optional.push(read!(StringId));
}
let total = req_count + opt_count;
let mut param_spans = Vec::with_capacity(total);
for _ in 0..total {
let name = read!(StringId);
let span_id = read!(u32);
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 => {
try_force!(0, inst_start_pc);
if self.call_depth > 10000 {
return Action::Done(Err(Error::eval_error(
"stack overflow; max-call-depth exceeded",
)));
}
self.call_depth += 1;
let func = self.pop_stack();
let arg = read!(OperandData).resolve(mc, self);
if let Some(closure) = func.as_gc::<Closure>() {
let ip = closure.ip;
let n_locals = closure.n_locals;
let env = closure.env;
if let Some(ref _pattern) = closure.pattern {
todo!("pattern call")
} else {
let new_env =
Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
self.call_stack.push(CallFrame {
pc: *pc,
stack_depth: 0,
thunk: None,
env: self.env(),
with_env: self.with_env,
});
*pc = ip as usize;
self.current_env = Some(new_env);
}
} else {
todo!("call other types: {func:?}")
}
}
MakeAttrs => {
let count = read!(u32) as usize;
let mut entries: SmallVec<[AttrEntry; 4]> = SmallVec::with_capacity(count);
for _ in 0..count {
let key_tag = read!(u8);
let Ok(ty) = AttrKeyType::try_from_primitive(key_tag)
.map_err(|err| panic!("unknown key tag: {:#04x}", err.number));
let key = match ty {
AttrKeyType::Static => AttrKeyData::Static(read!(StringId)),
AttrKeyType::Dynamic => AttrKeyData::Dynamic(read!(OperandData)),
};
let val = read!(OperandData);
let _span_id = read!(u32);
entries.push(AttrEntry { key, val });
}
let mut kv: SmallVec<[(StringId, Value); 4]> = SmallVec::with_capacity(count);
for entry in &entries {
let key_sid = match &entry.key {
&AttrKeyData::Static(sid) => sid,
AttrKeyData::Dynamic(op) => {
let v = op.resolve(mc, self);
v.as_inline::<StringId>()
.expect("dynamic attr key must be a string")
}
};
let val = entry.val.resolve(mc, self);
kv.push((key_sid, val));
}
kv.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
self.push_stack(Value::new_gc(attrs));
}
MakeEmptyAttrs => {
self.push_stack(self.empty_attrs);
}
Select => {
let n = read!(u16) as usize;
let _span_id = read!(u32);
let keys = read!(SelectKeyData, n);
let dyn_count = keys
.iter()
.filter(|k| matches!(k, SelectKeyData::Dynamic))
.count();
// Force target (at depth `dyn_count`) and all dynamic keys on top of it.
for i in 0..=dyn_count {
try_force!(i, inst_start_pc);
}
// Stack Layout: [..., target, dyn1, dyn2, ..., dyn_m]
let target_idx = self.stack.len() - dyn_count - 1;
let mut current_dyn_key_idx = target_idx + 1;
let mut current_val = self.stack[target_idx].restrict().expect("forced");
let mut result_val = None;
let mut error = None;
for (i, key) in keys.iter().enumerate() {
let key_sid = match key {
SelectKeyData::Static(sid) => *sid,
SelectKeyData::Dynamic => {
let v = self.stack[current_dyn_key_idx]
.restrict()
.expect("dynamic key must be forced");
current_dyn_key_idx += 1;
if let Some(sid) = v.as_inline::<StringId>() {
sid
} else if let Some(ns) = v.as_gc::<NixString>() {
ctx.intern_string(ns.as_str())
} else {
panic!("dynamic select key must be a string")
}
}
};
let Some(attrset) = current_val.as_gc::<AttrSet>() else {
error = Some(vm_err("value is not a set while a set was expected"));
break;
};
match attrset.lookup(key_sid) {
Some(v) => {
if i < n - 1 {
// FIXME: Proper async force hook inside select chain for nested thunks
current_val = v.restrict().unwrap_or_else(|_| {
panic!("intermediate select values must be forced")
});
} else {
result_val = Some(v);
}
}
None => {
let name = ctx.resolve_string(key_sid);
error = Some(vm_err(format!("attribute '{name}' missing")));
break;
}
}
}
// Clean up the target and all dynamic keys
self.stack.truncate(target_idx);
if let Some(e) = error {
return self.handle_vm_error(e);
}
if let Some(v) = result_val {
self.push_stack(v);
}
}
SelectDefault => {
let n = read!(u16) as usize;
let _span_id = read!(u32);
let keys = read!(SelectKeyData, n);
let dyn_count = keys
.iter()
.filter(|k| matches!(k, SelectKeyData::Dynamic))
.count();
// Stack layout: [..., target, default_val, dyn1, dyn2, ..., dyn_m]
for i in 0..=dyn_count + 1 {
try_force!(i, inst_start_pc);
}
let target_idx = self.stack.len() - dyn_count - 2;
let default_idx = target_idx + 1;
let mut current_dyn_key_idx = default_idx + 1;
let mut current_val = self.stack[target_idx].restrict().expect("forced");
let mut result_val = None;
let mut use_default = false;
for (i, key) in keys.iter().enumerate() {
let key_sid = match key {
SelectKeyData::Static(sid) => *sid,
SelectKeyData::Dynamic => {
let v = self.stack[current_dyn_key_idx]
.restrict()
.expect("dynamic key must be forced");
current_dyn_key_idx += 1;
if let Some(sid) = v.as_inline::<StringId>() {
sid
} else if let Some(ns) = v.as_gc::<NixString>() {
ctx.intern_string(ns.as_str())
} else {
panic!("dynamic select key must be a string")
}
}
};
if let Some(attrset) = current_val.as_gc::<AttrSet>()
&& let Some(v) = attrset.lookup(key_sid)
{
if i < n - 1 {
current_val = v.restrict().unwrap_or_else(|_| {
panic!("intermediate select values must be forced")
});
} else {
result_val = Some(v);
}
continue;
}
use_default = true;
break;
}
let def = self.stack[default_idx];
self.stack.truncate(target_idx);
if use_default {
self.push_stack(def);
} else if let Some(v) = result_val {
self.push_stack(v);
}
}
HasAttr => {
let _n = read!(u16) as usize;
todo!("HasAttr");
}
MakeList => {
let count = read!(u32) as usize;
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
for _ in 0..count {
items.push(read!(OperandData).resolve(mc, self));
}
let list = Gc::new(mc, List { inner: items });
self.push_stack(Value::new_gc(list));
}
MakeEmptyList => {
self.push_stack(self.empty_list);
}
OpAdd => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let res = (|| {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
// FIXME: path & string context
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
let ns = Gc::new(mc, NixString::new(format!("{ls}{rs}")));
self.push_stack(Value::new_gc(ns));
return Ok(());
}
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b)?;
self.push_stack(res);
VmResult::Ok(())
})();
try_vm!(self; res);
}
OpSub | OpMul => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
#[allow(clippy::type_complexity)]
let func: (fn(i64, i64) -> i64, fn(f64, f64) -> f64) = match op {
OpSub => (i64::wrapping_sub, |a, b| a - b),
OpMul => (i64::wrapping_mul, |a, b| a * b),
_ => unreachable!(),
};
let res = (|| {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
let res = numeric_binop(lhs, rhs, mc, func.0, func.1)?;
self.push_stack(res);
VmResult::Ok(())
})();
try_vm!(self; res);
}
OpDiv => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let res = (|| {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
match (get_num(lhs), get_num(rhs)) {
(_, Some(NixNum::Int(0))) => Err(vm_err("division by zero")),
(_, Some(NixNum::Float(0.))) => Err(vm_err("division by zero")),
_ => Ok(()),
}?;
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b)?;
self.push_stack(res);
VmResult::Ok(())
})();
try_vm!(self; res);
}
OpEq | OpNeq => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let map: fn(bool) -> bool = match op {
OpEq => |a| a,
OpNeq => |a| !a,
_ => unreachable!(),
};
let eq = try_vm!(self; self.values_equal(ctx));
self.push_stack(Value::new_inline(map(eq)));
}
OpLt | OpGt | OpLeq | OpGeq => {
use std::cmp::Ordering;
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let pred: fn(Ordering) -> bool = match op {
OpLt => Ordering::is_lt,
OpGt => Ordering::is_gt,
OpLeq => Ordering::is_le,
OpGeq => Ordering::is_ge,
_ => unreachable!(),
};
try_vm!(self; self.compare_values(ctx, pred));
}
OpConcat => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let res = (|| {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
// TODO: better type-assert ergonomic
let Some(l) = lhs.as_gc::<List>() else {
return Err(vm_err("cannot concatenate: left operand is not a list"));
};
let Some(r) = rhs.as_gc::<List>() else {
return Err(vm_err("cannot concatenate: right operand is not a list"));
};
let mut items = SmallVec::new();
items.extend_from_slice(&l);
items.extend_from_slice(&r);
self.push_stack(Value::new_gc(Gc::new(mc, List { inner: items })));
VmResult::Ok(())
})();
try_vm!(self; res);
}
OpUpdate => {
try_force!(1, inst_start_pc);
try_force!(0, inst_start_pc);
let res = (|| {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
// TODO: better type-assert ergonomic
let Some(l) = lhs.as_gc::<AttrSet>() else {
return Err(vm_err("cannot update: left operand is not a set"));
};
let Some(r) = rhs.as_gc::<AttrSet>() else {
return Err(vm_err("cannot update: right operand is not a set"));
};
self.push_stack(Value::new_gc(l.merge(&r, mc)));
VmResult::Ok(())
})();
try_vm!(self; res);
}
OpNeg => {
todo!("implement unary operation");
}
OpNot => {
todo!("implement unary operation");
}
JumpIfFalse => {
let offset = read!(i32);
try_force!(0, inst_start_pc);
let cond = self.pop_stack();
if cond.as_inline::<bool>() == Some(false) {
*pc = ((*pc as isize) + (offset as isize)) as usize;
}
}
JumpIfTrue => {
let offset = read!(i32);
try_force!(0, inst_start_pc);
let cond = self.pop_stack();
if cond.as_inline::<bool>() == Some(true) {
*pc = ((*pc as isize) + (offset as isize)) as usize;
}
}
Jump => {
let offset = read!(i32);
*pc = ((*pc as isize) + (offset as isize)) as usize;
}
ConcatStrings => {
let parts_count = read!(u16) as usize;
let _force_string = read!(u8) != 0;
let mut operands: SmallVec<[OperandData; 4]> =
SmallVec::with_capacity(parts_count);
for _ in 0..parts_count {
operands.push(read!(OperandData));
}
todo!("implement ConcatStrings (force parts, coerce to string, concatenate)");
}
ResolvePath => {
todo!("implement ResolvePath");
}
Assert => {
let _raw_idx = read!(u32);
let _span_id = read!(u32);
todo!("implement Assert (force TOS)");
}
PushWith => {
let env = self.pop_stack();
let scope = Gc::new(
mc,
WithEnv {
env,
prev: self.with_env,
},
);
self.with_env = Some(scope);
}
PopWith => {
let Some(scope) = self.with_env else {
unreachable!("no with_scope to pop");
};
self.with_env = scope.prev;
}
WithLookup => {
let name = read!(StringId);
let mut cur = self.with_env;
let mut found_val = None;
while let Some(scope) = cur {
// Using restrict() to force extraction sync due to CPS structure.
let env_val = scope
.env
.restrict()
.unwrap_or_else(|_| panic!("with scope env must be forced"));
let Some(attrs) = env_val.as_gc::<AttrSet>() else {
return self
.handle_vm_error(vm_err("value in 'with' scope must be a set"));
};
if let Some(v) = attrs.lookup(name) {
found_val = Some(v);
break;
}
cur = scope.prev;
}
if let Some(v) = found_val {
self.push_stack(v);
} else {
let name_str = ctx.resolve_string(name);
return self
.handle_vm_error(vm_err(format!("undefined variable '{name_str}'")));
}
}
LoadBuiltins => {
self.push_stack(self.builtins);
}
LoadBuiltin => {
let Ok(id) = BuiltinId::try_from_primitive(read!(u8))
.map_err(|err| panic!("unknown builtin id: {}", err.number));
self.push_stack(Value::new_inline(PrimOp {
id,
arity: BUILTINS[id as usize].1,
}));
}
MkPos => {
let _span_id = read!(u32);
todo!("MkPos")
}
LoadReplBinding => {
let _name = read!(StringId);
todo!("LoadReplBinding")
}
LoadScopedBinding => {
let _name = read!(StringId);
todo!("LoadScopedBinding")
}
Return => {
if let Some(result) = self.handle_return(pc, ctx, mc) {
return Action::Done(result);
}
}
Illegal => unreachable!(),
}
}
}
#[inline]
fn values_equal(&mut self, ctx: &impl VmContext) -> VmResult<bool> {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
return Ok(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 Ok(a == b);
}
if lhs.is::<Null>() && rhs.is::<Null>() {
return Ok(true);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return Ok(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<List>(), rhs.as_gc::<List>()) {
if a.inner.len() != b.inner.len() {
return Ok(false);
}
let len = a.inner.len();
for (x, y) in a.inner.iter().zip(b.inner.iter()).rev() {
self.push_stack(*x);
self.push_stack(*y);
}
for i in 0..len {
let eq = self.values_equal(ctx)?;
if !eq {
let rem = len - 1 - i;
self.stack.truncate(self.stack.len() - rem * 2);
return Ok(false);
}
}
return Ok(true);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<AttrSet>(), rhs.as_gc::<AttrSet>()) {
if a.len() != b.len() {
return Ok(false);
}
let len = a.len();
for ((k1, v1), (k2, v2)) in a.iter().zip(b.iter()).rev() {
if k1 != k2 {
return Ok(false);
}
self.push_stack(*v1);
self.push_stack(*v2);
}
for i in 0..len {
let eq = self.values_equal(ctx)?;
if !eq {
let rem = len - 1 - i;
self.stack.truncate(self.stack.len() - rem * 2);
return Ok(false);
}
}
return Ok(true);
}
Ok(false)
}
#[inline]
fn compare_values(
&mut self,
ctx: &impl VmContext,
pred: fn(std::cmp::Ordering) -> bool,
) -> VmResult<()> {
let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced();
if let (Some(a), Some(b)) = (get_num(lhs), get_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),
};
self.push_stack(Value::new_inline(pred(ord)));
return Ok(());
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
self.push_stack(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
Err(vm_err("cannot compare these types"))
}
#[inline(always)]
fn handle_return(
&mut self,
pc: &mut usize,
ctx: &impl VmContext,
mc: &Mutation<'gc>,
) -> Option<Result<fix_common::Value>> {
let ret_inst_pc = *pc - 1;
#[deny(unused_variables)]
if let Some(CallFrame {
pc: ret_pc,
stack_depth,
thunk,
env,
with_env,
}) = self.call_stack.pop()
{
*pc = ret_pc;
if let Some(outer_thunk) = thunk {
let val = self.pop_stack();
match val.restrict() {
Ok(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if ctx.bytecode().get(ret_pc).copied()
== Some(fix_codegen::Op::Return as u8)
{
self.push_stack(val.relax());
}
}
Err(inner_thunk) => {
let mut state = inner_thunk.borrow_mut(mc);
match *state {
ThunkState::Pending {
ip: inner_ip,
env: inner_env,
with_env: inner_with_env,
} => {
self.call_stack.push(CallFrame {
pc: ret_pc,
stack_depth,
thunk: Some(outer_thunk),
env,
with_env,
});
self.call_stack.push(CallFrame {
pc: ret_inst_pc,
stack_depth: 0,
thunk: Some(inner_thunk),
env: inner_env,
with_env: inner_with_env,
});
*state = ThunkState::Blackhole;
*pc = inner_ip;
self.current_env = Some(inner_env);
self.with_env = inner_with_env;
return None;
}
ThunkState::Evaluated(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if ctx.bytecode().get(ret_pc).copied()
== Some(fix_codegen::Op::Return as u8)
{
self.push_stack(val.relax());
}
}
ThunkState::Apply { func: _, arg: _ } => todo!("force Apply thunk"),
ThunkState::Blackhole => {
return Some(Err(Error::eval_error(
"infinite recursion encountered",
)));
}
}
}
}
} else {
self.call_depth -= 1;
}
self.current_env = Some(env);
self.with_env = with_env;
return None;
}
// FIXME: ForceMode
self.current_env = None;
self.with_env = None;
let val = self.pop_stack();
Some(Ok(ctx.convert_value(val)))
}
fn handle_vm_error(&mut self, e: VmError) -> Action {
match e {
VmError::Catchable(_) => {
todo!("Check for tryEval catch frames");
}
VmError::Uncatchable(e) => Action::Done(Err(e)),
}
}
}
#[inline]
fn numeric_binop<'gc>(
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 (get_num(lhs), get_num(rhs)) {
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Value::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(vm_err("cannot perform arithmetic on non-numbers")),
}
}