1297 lines
49 KiB
Rust
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")),
|
|
}
|
|
}
|