//! This module implements the virtual (or abstract) machine that runs
//! Tvix bytecode.
use serde_json::json;
use std::{cmp::Ordering, collections::BTreeMap, ops::DerefMut, path::PathBuf, rc::Rc};
use crate::{
chunk::Chunk,
compiler::GlobalsMap,
errors::{Error, ErrorKind, EvalResult},
io::EvalIO,
nix_search_path::NixSearchPath,
observer::RuntimeObserver,
opcode::{CodeIdx, Count, JumpOffset, OpCode, StackIdx, UpvalueIdx},
spans::LightSpan,
upvalues::Upvalues,
value::{Builtin, Closure, CoercionKind, Lambda, NixAttrs, NixList, Thunk, Value},
warnings::{EvalWarning, WarningKind},
};
/// Representation of a VM continuation;
/// see: https://en.wikipedia.org/wiki/Continuation-passing_style#CPS_in_Haskell
type Continuation = Box<dyn FnOnce(&mut VM) -> EvalResult<Trampoline>>;
/// A description of how to continue evaluation of a thunk when returned to by the VM
///
/// This struct is used when forcing thunks to avoid stack-based recursion, which for deeply nested
/// evaluation can easily overflow the stack.
#[must_use = "this `Trampoline` may be a continuation request, which should be handled"]
#[derive(Default)]
pub struct Trampoline {
/// The action to perform upon return to the trampoline
pub action: Option<TrampolineAction>,
/// The continuation to execute after the action has completed
pub continuation: Option<Continuation>,
}
impl Trampoline {
/// Add the execution of a new [`Continuation`] to the existing continuation
/// of this `Trampoline`, returning the resulting `Trampoline`.
pub fn append_to_continuation(self, f: Continuation) -> Self {
Trampoline {
action: self.action,
continuation: match self.continuation {
None => Some(f),
Some(f0) => Some(Box::new(move |vm| {
let trampoline = f0(vm)?;
Ok(trampoline.append_to_continuation(f))
})),
},
}
}
}
/// Description of an action to perform upon return to a [`Trampoline`] by the VM
pub enum TrampolineAction {
/// Enter a new stack frame
EnterFrame {
lambda: Rc<Lambda>,
upvalues: Rc<Upvalues>,
light_span: LightSpan,
arg_count: usize,
},
}
struct CallFrame {
/// The lambda currently being executed.
lambda: Rc<Lambda>,
/// Optional captured upvalues of this frame (if a thunk or
/// closure if being evaluated).
upvalues: Rc<Upvalues>,
/// Instruction pointer to the instruction currently being
/// executed.
ip: CodeIdx,
/// Stack offset, i.e. the frames "view" into the VM's full stack.
stack_offset: usize,
continuation: Option<Continuation>,
}
impl CallFrame {
/// Retrieve an upvalue from this frame at the given index.
fn upvalue(&self, idx: UpvalueIdx) -> &Value {
&self.upvalues[idx]
}
}
pub struct VM<'o> {
/// The VM call stack. One element is pushed onto this stack
/// each time a function is called or a thunk is forced.
frames: Vec<CallFrame>,
/// The VM value stack. This is actually a "stack of stacks",
/// with one stack-of-Values for each CallFrame in frames. This
/// is represented as a Vec<Value> rather than as
/// Vec<Vec<Value>> or a Vec<Value> inside CallFrame for
/// efficiency reasons: it avoids having to allocate a Vec on
/// the heap each time a CallFrame is entered.
stack: Vec<Value>,
/// Stack indices (absolute indexes into `stack`) of attribute
/// sets from which variables should be dynamically resolved
/// (`with`).
with_stack: Vec<usize>,
/// Runtime warnings collected during evaluation.
warnings: Vec<EvalWarning>,
/// Import cache, mapping absolute file paths to the value that
/// they compile to. Note that this reuses thunks, too!
// TODO: should probably be based on a file hash
pub import_cache: Box<BTreeMap<PathBuf, Value>>,
/// Parsed Nix search path, which is used to resolve `<...>`
/// references.
nix_search_path: NixSearchPath,
/// Implementation of I/O operations used for impure builtins and
/// features like `import`.
io_handle: Box<dyn EvalIO>,
/// Runtime observer which can print traces of runtime operations.
observer: &'o mut dyn RuntimeObserver,
/// Strong reference to the globals, guaranteeing that they are
/// kept alive for the duration of evaluation.
///
/// This is important because recursive builtins (specifically
/// `import`) hold a weak reference to the builtins, while the
/// original strong reference is held by the compiler which does
/// not exist anymore at runtime.
#[allow(dead_code)]
globals: Rc<GlobalsMap>,
}
/// The result of a VM's runtime evaluation.
pub struct RuntimeResult {
pub value: Value,
pub warnings: Vec<EvalWarning>,
}
/// This macro wraps a computation that returns an ErrorKind or a
/// result, and wraps the ErrorKind in an Error struct if present.
///
/// The reason for this macro's existence is that calculating spans is
/// potentially expensive, so it should be avoided to the last moment
/// (i.e. definite instantiation of a runtime error) if possible.
macro_rules! fallible {
( $self:ident, $body:expr) => {
match $body {
Ok(result) => result,
Err(kind) => return Err(Error::new(kind, $self.current_span())),
}
};
}
#[macro_export]
macro_rules! arithmetic_op {
( $self:ident, $op:tt ) => {{
let b = $self.pop();
let a = $self.pop();
let result = fallible!($self, arithmetic_op!(&a, &b, $op));
$self.push(result);
}};
( $a:expr, $b:expr, $op:tt ) => {{
match ($a, $b) {
(Value::Integer(i1), Value::Integer(i2)) => Ok(Value::Integer(i1 $op i2)),
(Value::Float(f1), Value::Float(f2)) => Ok(Value::Float(f1 $op f2)),
(Value::Integer(i1), Value::Float(f2)) => Ok(Value::Float(*i1 as f64 $op f2)),
(Value::Float(f1), Value::Integer(i2)) => Ok(Value::Float(f1 $op *i2 as f64)),
(v1, v2) => Err(ErrorKind::TypeError {
expected: "number (either int or float)",
actual: if v1.is_number() {
v2.type_of()
} else {
v1.type_of()
},
}),
}
}};
}
#[macro_export]
macro_rules! cmp_op {
( $self:ident, $op:tt ) => {{
let b = $self.pop();
let a = $self.pop();
let ordering = fallible!($self, a.nix_cmp(&b, $self));
let result = Value::Bool(cmp_op!(@order $op ordering));
$self.push(result);
}};
(@order < $ordering:expr) => {
$ordering == Some(Ordering::Less)
};
(@order > $ordering:expr) => {
$ordering == Some(Ordering::Greater)
};
(@order <= $ordering:expr) => {
!matches!($ordering, None | Some(Ordering::Greater))
};
(@order >= $ordering:expr) => {
!matches!($ordering, None | Some(Ordering::Less))
};
}
impl<'o> VM<'o> {
pub fn new(
nix_search_path: NixSearchPath,
io_handle: Box<dyn EvalIO>,
observer: &'o mut dyn RuntimeObserver,
globals: Rc<GlobalsMap>,
) -> Self {
// Backtrace-on-stack-overflow is some seriously weird voodoo and
// very unsafe. This double-guard prevents it from accidentally
// being enabled on release builds.
#[cfg(debug_assertions)]
#[cfg(feature = "backtrace_overflow")]
unsafe {
backtrace_on_stack_overflow::enable();
};
Self {
nix_search_path,
io_handle,
observer,
globals,
frames: vec![],
stack: vec![],
with_stack: vec![],
warnings: vec![],
import_cache: Default::default(),
}
}
fn frame(&self) -> &CallFrame {
&self.frames[self.frames.len() - 1]
}
fn chunk(&self) -> &Chunk {
&self.frame().lambda.chunk
}
fn frame_mut(&mut self) -> &mut CallFrame {
let idx = self.frames.len() - 1;
&mut self.frames[idx]
}
fn inc_ip(&mut self) -> OpCode {
let op = self.chunk()[self.frame().ip];
self.frame_mut().ip += 1;
op
}
pub fn pop(&mut self) -> Value {
self.stack.pop().expect("runtime stack empty")
}
pub fn pop_then_drop(&mut self, num_items: usize) {
self.stack.truncate(self.stack.len() - num_items);
}
pub fn push(&mut self, value: Value) {
self.stack.push(value)
}
fn peek(&self, offset: usize) -> &Value {
&self.stack[self.stack.len() - 1 - offset]
}
/// Returns the source span of the instruction currently being
/// executed.
pub(crate) fn current_span(&self) -> codemap::Span {
self.chunk().get_span(self.frame().ip - 1)
}
/// Returns the information needed to calculate the current span,
/// but without performing that calculation.
pub(crate) fn current_light_span(&self) -> LightSpan {
LightSpan::new_delayed(self.frame().lambda.clone(), self.frame().ip - 1)
}
/// Access the I/O handle used for filesystem access in this VM.
pub(crate) fn io(&self) -> &dyn EvalIO {
&*self.io_handle
}
/// Construct an error from the given ErrorKind and the source
/// span of the current instruction.
pub fn error(&self, kind: ErrorKind) -> Error {
Error::new(kind, self.current_span())
}
/// Push an already constructed warning.
pub fn push_warning(&mut self, warning: EvalWarning) {
self.warnings.push(warning);
}
/// Emit a warning with the given WarningKind and the source span
/// of the current instruction.
pub fn emit_warning(&mut self, kind: WarningKind) {
self.push_warning(EvalWarning {
kind,
span: self.current_span(),
});
}
/// Execute the given value in this VM's context, if it is a
/// callable.
///
/// The stack of the VM must be prepared with all required
/// arguments before calling this and the value must have already
/// been forced.
pub fn call_value(&mut self, callable: &Value) -> EvalResult<()> {
match callable {
Value::Closure(c) => self.enter_frame(c.lambda(), c.upvalues(), 1),
Value::Builtin(b) => self.call_builtin(b.clone()),
Value::Thunk(t) => {
debug_assert!(t.is_evaluated(), "call_value called with unevaluated thunk");
self.call_value(&t.value())
}
// Attribute sets with a __functor attribute are callable.
Value::Attrs(ref attrs) => match attrs.select("__functor") {
None => Err(self.error(ErrorKind::NotCallable(callable.type_of()))),
Some(functor) => {
// The functor receives the set itself as its first argument
// and needs to be called with it. However, this call is
// synthetic (i.e. there is no corresponding OpCall for the
// first call in the bytecode.)
self.push(callable.clone());
self.call_value(functor)?;
let primed = self.pop();
self.call_value(&primed)
}
},
// TODO: this isn't guaranteed to be a useful span, actually
other => Err(self.error(ErrorKind::NotCallable(other.type_of()))),
}
}
/// Call the given `callable` value with the given list of `args`
///
/// # Panics
///
/// Panics if the passed list of `args` is empty
#[track_caller]
pub fn call_with<I>(&mut self, callable: &Value, args: I) -> EvalResult<Value>
where
I: IntoIterator<Item = Value>,
I::IntoIter: DoubleEndedIterator,
{
let mut num_args = 0_usize;
for arg in args.into_iter().rev() {
num_args += 1;
self.push(arg);
}
if num_args == 0 {
panic!("call_with called with an empty list of args");
}
self.call_value(callable)?;
let mut res = self.pop();
for _ in 0..(num_args - 1) {
res.force(self).map_err(|e| self.error(e))?;
self.call_value(&res)?;
res = self.pop();
}
Ok(res)
}
fn tail_call_value(&mut self, callable: Value) -> EvalResult<()> {
match callable {
Value::Builtin(builtin) => self.call_builtin(builtin),
Value::Thunk(thunk) => self.tail_call_value(thunk.value().clone()),
Value::Closure(closure) => {
let lambda = closure.lambda();
self.observer.observe_tail_call(self.frames.len(), &lambda);
// Replace the current call frames internals with
// that of the tail-called closure.
let mut frame = self.frame_mut();
frame.lambda = lambda;
frame.upvalues = closure.upvalues();
frame.ip = CodeIdx(0); // reset instruction pointer to beginning
Ok(())
}
// Attribute sets with a __functor attribute are callable.
Value::Attrs(ref attrs) => match attrs.select("__functor") {
None => Err(self.error(ErrorKind::NotCallable(callable.type_of()))),
Some(functor) => {
// The functor receives the set itself as its first argument
// and needs to be called with it. However, this call is
// synthetic (i.e. there is no corresponding OpCall for the
// first call in the bytecode.)
self.push(callable.clone());
self.call_value(functor)?;
let primed = self.pop();
self.tail_call_value(primed)
}
},
_ => Err(self.error(ErrorKind::NotCallable(callable.type_of()))),
}
}
/// Execute the given lambda in this VM's context, leaving the
/// computed value on its stack after the frame completes.
pub fn enter_frame(
&mut self,
lambda: Rc<Lambda>,
upvalues: Rc<Upvalues>,
arg_count: usize,
) -> EvalResult<()> {
self.observer
.observe_enter_frame(arg_count, &lambda, self.frames.len() + 1);
let frame = CallFrame {
lambda,
upvalues,
ip: CodeIdx(0),
stack_offset: self.stack.len() - arg_count,
continuation: None,
};
let starting_frames_depth = self.frames.len();
self.frames.push(frame);
let result = loop {
let op = self.inc_ip();
self.observer
.observe_execute_op(self.frame().ip, &op, &self.stack);
let res = self.run_op(op);
let mut retrampoline: Option<Continuation> = None;
// we need to pop the frame before checking `res` for an
// error in order to implement `tryEval` correctly.
if self.frame().ip.0 == self.chunk().code.len() {
let frame = self.frames.pop();
retrampoline = frame.and_then(|frame| frame.continuation);
}
self.trampoline_loop(res?, retrampoline)?;
if self.frames.len() == starting_frames_depth {
break Ok(());
}
};
self.observer
.observe_exit_frame(self.frames.len() + 1, &self.stack);
result
}
fn trampoline_loop(
&mut self,
mut trampoline: Trampoline,
mut retrampoline: Option<Continuation>,
) -> EvalResult<()> {
loop {
if let Some(TrampolineAction::EnterFrame {
lambda,
upvalues,
arg_count,
light_span: _,
}) = trampoline.action
{
let frame = CallFrame {
lambda,
upvalues,
ip: CodeIdx(0),
stack_offset: self.stack.len() - arg_count,
continuation: match retrampoline {
None => trampoline.continuation,
Some(retrampoline) => match trampoline.continuation {
None => None,
Some(cont) => Some(Box::new(|vm| {
Ok(cont(vm)?.append_to_continuation(retrampoline))
})),
},
},
};
self.frames.push(frame);
break;
}
match trampoline.continuation {
None => {
if let Some(cont) = retrampoline.take() {
trampoline = cont(self)?;
} else {
break;
}
}
Some(cont) => {
trampoline = cont(self)?;
continue;
}
}
}
Ok(())
}
pub(crate) fn nix_eq(
&mut self,
v1: Value,
v2: Value,
allow_top_level_pointer_equality_on_functions_and_thunks: bool,
) -> EvalResult<bool> {
self.push(v1);
self.push(v2);
let res = self.nix_op_eq(allow_top_level_pointer_equality_on_functions_and_thunks);
self.trampoline_loop(res?, None)?;
match self.pop() {
Value::Bool(b) => Ok(b),
v => panic!("run_op(OpEqual) left a non-boolean on the stack: {v:#?}"),
}
}
pub(crate) fn nix_op_eq(
&mut self,
allow_top_level_pointer_equality_on_functions_and_thunks: bool,
) -> EvalResult<Trampoline> {
// This bit gets set to `true` (if it isn't already) as soon
// as we start comparing the contents of two
// {lists,attrsets} -- but *not* the contents of two thunks.
// See tvix/docs/value-pointer-equality.md for details.
let mut allow_pointer_equality_on_functions_and_thunks =
allow_top_level_pointer_equality_on_functions_and_thunks;
let mut numpairs: usize = 1;
let res = 'outer: loop {
if numpairs == 0 {
break true;
} else {
numpairs -= 1;
}
let v2 = self.pop();
let v1 = self.pop();
let v2 = match v2 {
Value::Thunk(thunk) => {
if allow_top_level_pointer_equality_on_functions_and_thunks {
if let Value::Thunk(t1) = &v1 {
if t1.ptr_eq(&thunk) {
continue;
}
}
}
fallible!(self, thunk.force(self));
thunk.value().clone()
}
v => v,
};
let v1 = match v1 {
Value::Thunk(thunk) => {
fallible!(self, thunk.force(self));
thunk.value().clone()
}
v => v,
};
match (v1, v2) {
(Value::List(l1), Value::List(l2)) => {
allow_pointer_equality_on_functions_and_thunks = true;
if l1.ptr_eq(&l2) {
continue;
}
if l1.len() != l2.len() {
break false;
}
for (vi1, vi2) in l1.into_iter().zip(l2.into_iter()) {
self.stack.push(vi1);
self.stack.push(vi2);
numpairs += 1;
}
}
(_, Value::List(_)) => break false,
(Value::List(_), _) => break false,
(Value::Attrs(a1), Value::Attrs(a2)) => {
if allow_pointer_equality_on_functions_and_thunks && a1.ptr_eq(&a2) {
continue;
}
allow_pointer_equality_on_functions_and_thunks = true;
match (a1.select("type"), a2.select("type")) {
(Some(v1), Some(v2))
if "derivation"
== fallible!(
self,
v1.coerce_to_string(CoercionKind::ThunksOnly, self)
)
.as_str()
&& "derivation"
== fallible!(
self,
v2.coerce_to_string(CoercionKind::ThunksOnly, self)
)
.as_str() =>
{
if fallible!(
self,
a1.select("outPath")
.expect("encountered a derivation with no `outPath` attribute!")
.coerce_to_string(CoercionKind::ThunksOnly, self)
) == fallible!(
self,
a2.select("outPath")
.expect("encountered a derivation with no `outPath` attribute!")
.coerce_to_string(CoercionKind::ThunksOnly, self)
) {
continue;
}
break false;
}
_ => {}
}
let iter1 = a1.into_iter_sorted();
let iter2 = a2.into_iter_sorted();
if iter1.len() != iter2.len() {
break false;
}
for ((k1, v1), (k2, v2)) in iter1.zip(iter2) {
if k1 != k2 {
break 'outer false;
}
self.stack.push(v1);
self.stack.push(v2);
numpairs += 1;
}
}
(Value::Attrs(_), _) => break false,
(_, Value::Attrs(_)) => break false,
(v1, v2) => {
if allow_pointer_equality_on_functions_and_thunks {
if let (Value::Closure(c1), Value::Closure(c2)) = (&v1, &v2) {
if Rc::ptr_eq(c1, c2) {
continue;
}
}
}
if !fallible!(self, v1.nix_eq(&v2, self)) {
break false;
}
}
}
};
self.pop_then_drop(numpairs * 2);
self.push(Value::Bool(res));
Ok(Trampoline::default())
}
pub(crate) fn run_op(&mut self, op: OpCode) -> EvalResult<Trampoline> {
match op {
OpCode::OpConstant(idx) => {
let c = self.chunk()[idx].clone();
self.push(c);
}
OpCode::OpPop => {
self.pop();
}
OpCode::OpAdd => {
let b = self.pop();
let a = self.pop();
let result = match (&a, &b) {
(Value::Path(p), v) => {
let mut path = p.to_string_lossy().into_owned();
path.push_str(
&v.coerce_to_string(CoercionKind::Weak, self)
.map_err(|ek| self.error(ek))?,
);
crate::value::canon_path(PathBuf::from(path)).into()
}
(Value::String(s1), Value::String(s2)) => Value::String(s1.concat(s2)),
(Value::String(s1), v) => Value::String(
s1.concat(
&v.coerce_to_string(CoercionKind::Weak, self)
.map_err(|ek| self.error(ek))?,
),
),
(v, Value::String(s2)) => Value::String(
v.coerce_to_string(CoercionKind::Weak, self)
.map_err(|ek| self.error(ek))?
.concat(s2),
),
_ => fallible!(self, arithmetic_op!(&a, &b, +)),
};
self.push(result)
}
OpCode::OpSub => arithmetic_op!(self, -),
OpCode::OpMul => arithmetic_op!(self, *),
OpCode::OpDiv => {
let b = self.peek(0);
match b {
Value::Integer(0) => return Err(self.error(ErrorKind::DivisionByZero)),
Value::Float(b) => {
if *b == 0.0_f64 {
return Err(self.error(ErrorKind::DivisionByZero));
}
arithmetic_op!(self, /)
}
_ => arithmetic_op!(self, /),
};
}
OpCode::OpInvert => {
let v = fallible!(self, self.pop().as_bool());
self.push(Value::Bool(!v));
}
OpCode::OpNegate => match self.pop() {
Value::Integer(i) => self.push(Value::Integer(-i)),
Value::Float(f) => self.push(Value::Float(-f)),
v => {
return Err(self.error(ErrorKind::TypeError {
expected: "number (either int or float)",
actual: v.type_of(),
}));
}
},
OpCode::OpEqual => return self.nix_op_eq(false),
OpCode::OpLess => cmp_op!(self, <),
OpCode::OpLessOrEq => cmp_op!(self, <=),
OpCode::OpMore => cmp_op!(self, >),
OpCode::OpMoreOrEq => cmp_op!(self, >=),
OpCode::OpNull => self.push(Value::Null),
OpCode::OpTrue => self.push(Value::Bool(true)),
OpCode::OpFalse => self.push(Value::Bool(false)),
OpCode::OpAttrs(Count(count)) => self.run_attrset(count)?,
OpCode::OpAttrsUpdate => {
let rhs = fallible!(self, self.pop().to_attrs());
let lhs = fallible!(self, self.pop().to_attrs());
self.push(Value::attrs(lhs.update(*rhs)))
}
OpCode::OpAttrsSelect => {
let key = fallible!(self, self.pop().to_str());
let attrs = fallible!(self, self.pop().to_attrs());
match attrs.select(key.as_str()) {
Some(value) => self.push(value.clone()),
None => {
return Err(self.error(ErrorKind::AttributeNotFound {
name: key.as_str().to_string(),
}))
}
}
}
OpCode::OpAttrsTrySelect => {
let key = fallible!(self, self.pop().to_str());
let value = match self.pop() {
Value::Attrs(attrs) => match attrs.select(key.as_str()) {
Some(value) => value.clone(),
None => Value::AttrNotFound,
},
_ => Value::AttrNotFound,
};
self.push(value);
}
OpCode::OpHasAttr => {
let key = fallible!(self, self.pop().to_str());
let result = match self.pop() {
Value::Attrs(attrs) => attrs.contains(key.as_str()),
// Nix allows use of `?` on non-set types, but
// always returns false in those cases.
_ => false,
};
self.push(Value::Bool(result));
}
OpCode::OpValidateClosedFormals => {
let formals = self.frame().lambda.formals.as_ref().expect(
"OpValidateClosedFormals called within the frame of a lambda without formals",
);
let args = self.peek(0).to_attrs().map_err(|err| self.error(err))?;
for arg in args.keys() {
if !formals.contains(arg) {
return Err(self.error(ErrorKind::UnexpectedArgument {
arg: arg.clone(),
formals_span: formals.span,
}));
}
}
}
OpCode::OpList(Count(count)) => {
let list =
NixList::construct(count, self.stack.split_off(self.stack.len() - count));
self.push(Value::List(list));
}
OpCode::OpConcat => {
let rhs = fallible!(self, self.pop().to_list()).into_inner();
let lhs = fallible!(self, self.pop().to_list()).into_inner();
self.push(Value::List(NixList::from(lhs + rhs)))
}
OpCode::OpInterpolate(Count(count)) => self.run_interpolate(count)?,
OpCode::OpCoerceToString => {
// TODO: handle string context, copying to store
let string = fallible!(
self,
// note that coerce_to_string also forces
self.pop().coerce_to_string(CoercionKind::Weak, self)
);
self.push(Value::String(string));
}
OpCode::OpFindFile => match self.pop() {
Value::UnresolvedPath(path) => {
let resolved = self
.nix_search_path
.resolve(path)
.map_err(|e| self.error(e))?;
self.push(resolved.into());
}
_ => panic!("tvix compiler bug: OpFindFile called on non-UnresolvedPath"),
},
OpCode::OpResolveHomePath => match self.pop() {
Value::UnresolvedPath(path) => {
match dirs::home_dir() {
None => {
return Err(self.error(ErrorKind::RelativePathResolution(
"failed to determine home directory".into(),
)));
}
Some(mut buf) => {
buf.push(path);
self.push(buf.into());
}
};
}
_ => {
panic!("tvix compiler bug: OpResolveHomePath called on non-UnresolvedPath")
}
},
OpCode::OpJump(JumpOffset(offset)) => {
debug_assert!(offset != 0);
self.frame_mut().ip += offset;
}
OpCode::OpJumpIfTrue(JumpOffset(offset)) => {
debug_assert!(offset != 0);
if fallible!(self, self.peek(0).as_bool()) {
self.frame_mut().ip += offset;
}
}
OpCode::OpJumpIfFalse(JumpOffset(offset)) => {
debug_assert!(offset != 0);
if !fallible!(self, self.peek(0).as_bool()) {
self.frame_mut().ip += offset;
}
}
OpCode::OpJumpIfNotFound(JumpOffset(offset)) => {
debug_assert!(offset != 0);
if matches!(self.peek(0), Value::AttrNotFound) {
self.pop();
self.frame_mut().ip += offset;
}
}
// These assertion operations error out if the stack
// top is not of the expected type. This is necessary
// to implement some specific behaviours of Nix
// exactly.
OpCode::OpAssertBool => {
let val = self.peek(0);
if !val.is_bool() {
return Err(self.error(ErrorKind::TypeError {
expected: "bool",
actual: val.type_of(),
}));
}
}
// Remove the given number of elements from the stack,
// but retain the top value.
OpCode::OpCloseScope(Count(count)) => {
// Immediately move the top value into the right
// position.
let target_idx = self.stack.len() - 1 - count;
self.stack[target_idx] = self.pop();
// Then drop the remaining values.
for _ in 0..(count - 1) {
self.pop();
}
}
OpCode::OpGetLocal(StackIdx(local_idx)) => {
let idx = self.frame().stack_offset + local_idx;
self.push(self.stack[idx].clone());
}
OpCode::OpPushWith(StackIdx(idx)) => {
self.with_stack.push(self.frame().stack_offset + idx)
}
OpCode::OpPopWith => {
self.with_stack.pop();
}
OpCode::OpResolveWith => {
let ident = fallible!(self, self.pop().to_str());
let value = self.resolve_with(ident.as_str())?;
self.push(value)
}
OpCode::OpAssertFail => {
return Err(self.error(ErrorKind::AssertionFailed));
}
OpCode::OpCall => {
let callable = self.pop();
self.call_value(&callable)?;
}
OpCode::OpTailCall => {
let callable = self.pop();
self.tail_call_value(callable)?;
}
OpCode::OpGetUpvalue(upv_idx) => {
let value = self.frame().upvalue(upv_idx).clone();
self.push(value);
}
OpCode::OpClosure(idx) => {
let blueprint = match &self.chunk()[idx] {
Value::Blueprint(lambda) => lambda.clone(),
_ => panic!("compiler bug: non-blueprint in blueprint slot"),
};
let upvalue_count = blueprint.upvalue_count;
debug_assert!(
upvalue_count > 0,
"OpClosure should not be called for plain lambdas"
);
let mut upvalues = Upvalues::with_capacity(blueprint.upvalue_count);
self.populate_upvalues(upvalue_count, &mut upvalues)?;
self.push(Value::Closure(Rc::new(Closure::new_with_upvalues(
Rc::new(upvalues),
blueprint,
))));
}
OpCode::OpThunkSuspended(idx) | OpCode::OpThunkClosure(idx) => {
let blueprint = match &self.chunk()[idx] {
Value::Blueprint(lambda) => lambda.clone(),
_ => panic!("compiler bug: non-blueprint in blueprint slot"),
};
let upvalue_count = blueprint.upvalue_count;
let thunk = if matches!(op, OpCode::OpThunkClosure(_)) {
debug_assert!(
upvalue_count > 0,
"OpThunkClosure should not be called for plain lambdas"
);
Thunk::new_closure(blueprint)
} else {
Thunk::new_suspended(blueprint, self.current_light_span())
};
let upvalues = thunk.upvalues_mut();
self.push(Value::Thunk(thunk.clone()));
// From this point on we internally mutate the
// upvalues. The closure (if `is_closure`) is
// already in its stack slot, which means that it
// can capture itself as an upvalue for
// self-recursion.
self.populate_upvalues(upvalue_count, upvalues)?;
}
OpCode::OpForce => {
if let Some(Value::Thunk(_)) = self.stack.last() {
let trampoline = fallible!(self, Thunk::force_trampoline(self));
return Ok(trampoline);
}
}
OpCode::OpFinalise(StackIdx(idx)) => {
match &self.stack[self.frame().stack_offset + idx] {
Value::Closure(_) => panic!("attempted to finalise a closure"),
Value::Thunk(thunk) => thunk.finalise(&self.stack[self.frame().stack_offset..]),
// In functions with "formals" attributes, it is
// possible for `OpFinalise` to be called on a
// non-capturing value, in which case it is a no-op.
//
// TODO: detect this in some phase and skip the finalise; fail here
_ => { /* TODO: panic here again to catch bugs */ }
}
}
// Data-carrying operands should never be executed,
// that is a critical error in the VM.
OpCode::DataStackIdx(_)
| OpCode::DataDeferredLocal(_)
| OpCode::DataUpvalueIdx(_)
| OpCode::DataCaptureWith => {
panic!("VM bug: attempted to execute data-carrying operand")
}
}
Ok(Trampoline::default())
}
fn run_attrset(&mut self, count: usize) -> EvalResult<()> {
let attrs = fallible!(
self,
NixAttrs::construct(count, self.stack.split_off(self.stack.len() - count * 2))
);
self.push(Value::attrs(attrs));
Ok(())
}
/// Interpolate string fragments by popping the specified number of
/// fragments of the stack, evaluating them to strings, and pushing
/// the concatenated result string back on the stack.
fn run_interpolate(&mut self, count: usize) -> EvalResult<()> {
let mut out = String::new();
for _ in 0..count {
out.push_str(fallible!(self, self.pop().to_str()).as_str());
}
self.push(Value::String(out.into()));
Ok(())
}
/// Resolve a dynamic identifier through the with-stack at runtime.
fn resolve_with(&mut self, ident: &str) -> EvalResult<Value> {
// Iterate over the with_stack manually to avoid borrowing
// self, which is required for forcing the set.
for with_stack_idx in (0..self.with_stack.len()).rev() {
let with = self.stack[self.with_stack[with_stack_idx]].clone();
if let Value::Thunk(thunk) = &with {
fallible!(self, thunk.force(self));
}
match fallible!(self, with.to_attrs()).select(ident) {
None => continue,
Some(val) => return Ok(val.clone()),
}
}
// Iterate over the captured with stack if one exists. This is
// extra tricky to do without a lot of cloning.
for idx in (0..self.frame().upvalues.with_stack_len()).rev() {
// This will not panic because having an index here guarantees
// that the stack is present.
let with = self.frame().upvalues.with_stack().unwrap()[idx].clone();
if let Value::Thunk(thunk) = &with {
fallible!(self, thunk.force(self));
}
match fallible!(self, with.to_attrs()).select(ident) {
None => continue,
Some(val) => return Ok(val.clone()),
}
}
Err(self.error(ErrorKind::UnknownDynamicVariable(ident.to_string())))
}
/// Populate the upvalue fields of a thunk or closure under construction.
fn populate_upvalues(
&mut self,
count: usize,
mut upvalues: impl DerefMut<Target = Upvalues>,
) -> EvalResult<()> {
for _ in 0..count {
match self.inc_ip() {
OpCode::DataStackIdx(StackIdx(stack_idx)) => {
let idx = self.frame().stack_offset + stack_idx;
let val = match self.stack.get(idx) {
Some(val) => val.clone(),
None => {
return Err(self.error(ErrorKind::TvixBug {
msg: "upvalue to be captured was missing on stack",
metadata: Some(Rc::new(json!({
"ip": format!("{:#x}", self.frame().ip.0 - 1),
"stack_idx(relative)": stack_idx,
"stack_idx(absolute)": idx,
}))),
}))
}
};
upvalues.deref_mut().push(val);
}
OpCode::DataUpvalueIdx(upv_idx) => {
upvalues
.deref_mut()
.push(self.frame().upvalue(upv_idx).clone());
}
OpCode::DataDeferredLocal(idx) => {
upvalues.deref_mut().push(Value::DeferredUpvalue(idx));
}
OpCode::DataCaptureWith => {
// Start the captured with_stack off of the
// current call frame's captured with_stack, ...
let mut captured_with_stack = self
.frame()
.upvalues
.with_stack()
.map(Clone::clone)
// ... or make an empty one if there isn't one already.
.unwrap_or_else(|| Vec::with_capacity(self.with_stack.len()));
for idx in &self.with_stack {
captured_with_stack.push(self.stack[*idx].clone());
}
upvalues.deref_mut().set_with_stack(captured_with_stack);
}
_ => panic!("compiler error: missing closure operand"),
}
}
Ok(())
}
pub fn call_builtin(&mut self, builtin: Builtin) -> EvalResult<()> {
let builtin_name = builtin.name();
self.observer.observe_enter_builtin(builtin_name);
let arg = self.pop();
let result = fallible!(self, builtin.apply(self, arg));
self.observer
.observe_exit_builtin(builtin_name, &self.stack);
self.push(result);
Ok(())
}
}
pub fn run_lambda(
nix_search_path: NixSearchPath,
io_handle: Box<dyn EvalIO>,
observer: &mut dyn RuntimeObserver,
globals: Rc<GlobalsMap>,
lambda: Rc<Lambda>,
) -> EvalResult<RuntimeResult> {
let mut vm = VM::new(nix_search_path, io_handle, observer, globals);
// Retain the top-level span of the expression in this lambda, as
// synthetic "calls" in deep_force will otherwise not have a span
// to fall back to.
//
// We exploit the fact that the compiler emits a final instruction
// with the span of the entire file for top-level expressions.
let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
vm.enter_frame(lambda, Rc::new(Upvalues::with_capacity(0)), 0)?;
let value = vm.pop();
value
.deep_force(&mut vm, &mut Default::default())
.map_err(|kind| Error::new(kind, root_span))?;
Ok(RuntimeResult {
value,
warnings: vm.warnings,
})
}