diff options
Diffstat (limited to 'tvix/eval/src/vm.rs')
-rw-r--r-- | tvix/eval/src/vm.rs | 1218 |
1 files changed, 0 insertions, 1218 deletions
diff --git a/tvix/eval/src/vm.rs b/tvix/eval/src/vm.rs deleted file mode 100644 index f5107f9ed738..000000000000 --- a/tvix/eval/src/vm.rs +++ /dev/null @@ -1,1218 +0,0 @@ -//! This module implements the virtual (or abstract) machine that runs -//! Tvix bytecode. - -pub mod generators; - -use serde_json::json; -use std::{cmp::Ordering, collections::HashMap, 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<HashMap<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) => { - if let Value::Thunk(thunk) = &functor { - fallible!(self, thunk.force(self)); - } - - // 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_call_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_call_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::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.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 value = self.pop(); - let trampoline = fallible!(self, Thunk::force_trampoline(self, value)); - 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, - }) -} |