snix_eval/vm/
mod.rs

1//! This module implements the abstract/virtual machine that runs Snix
2//! bytecode.
3//!
4//! The operation of the VM is facilitated by the [`Frame`] type,
5//! which controls the current execution state of the VM and is
6//! processed within the VM's operating loop.
7//!
8//! A [`VM`] is used by instantiating it with an initial [`Frame`],
9//! then triggering its execution and waiting for the VM to return or
10//! yield an error.
11
12pub mod generators;
13mod macros;
14
15use bstr::{BString, ByteSlice, ByteVec};
16use codemap::Span;
17use rustc_hash::FxHashMap;
18use serde_json::json;
19use std::{cmp::Ordering, ops::DerefMut, path::PathBuf, rc::Rc};
20
21use crate::{
22    NixString, SourceCode, arithmetic_op,
23    chunk::Chunk,
24    cmp_op,
25    compiler::GlobalsMap,
26    errors::{CatchableErrorKind, Error, ErrorKind, EvalResult},
27    io::EvalIO,
28    lifted_pop,
29    nix_search_path::NixSearchPath,
30    observer::RuntimeObserver,
31    opcode::{CodeIdx, Op, Position, UpvalueIdx},
32    upvalues::Upvalues,
33    value::{
34        Builtin, BuiltinResult, Closure, CoercionKind, Lambda, NixAttrs, NixContext, NixList,
35        PointerEquality, Thunk, Value,
36    },
37    vm::generators::GenCo,
38    warnings::{EvalWarning, WarningKind},
39};
40
41use generators::{Generator, GeneratorState, call_functor};
42
43use self::generators::{VMRequest, VMResponse};
44
45/// Internal helper trait for taking a span from a variety of types, to make use
46/// of `WithSpan` (defined below) more ergonomic at call sites.
47trait GetSpan {
48    fn get_span(self) -> Span;
49}
50
51impl<IO> GetSpan for &VM<'_, IO> {
52    fn get_span(self) -> Span {
53        self.reasonable_span
54    }
55}
56
57impl GetSpan for &CallFrame {
58    fn get_span(self) -> Span {
59        self.current_span()
60    }
61}
62
63impl GetSpan for &Span {
64    fn get_span(self) -> Span {
65        *self
66    }
67}
68
69impl GetSpan for Span {
70    fn get_span(self) -> Span {
71        self
72    }
73}
74
75/// Internal helper trait for ergonomically converting from a `Result<T,
76/// ErrorKind>` to a `Result<T, Error>` using the current span of a call frame,
77/// and chaining the VM's frame stack around it for printing a cause chain.
78trait WithSpan<T, S: GetSpan, IO> {
79    fn with_span(self, top_span: S, vm: &VM<IO>) -> Result<T, Error>;
80}
81
82impl<T, S: GetSpan, IO> WithSpan<T, S, IO> for Result<T, ErrorKind> {
83    fn with_span(self, top_span: S, vm: &VM<IO>) -> Result<T, Error> {
84        match self {
85            Ok(something) => Ok(something),
86            Err(kind) => {
87                let mut error = Error::new(kind, top_span.get_span(), vm.source.clone());
88
89                // Wrap the top-level error in chaining errors for each element
90                // of the frame stack.
91                for frame in vm.frames.iter().rev() {
92                    match frame {
93                        Frame::CallFrame { span, .. } => {
94                            error = Error::new(
95                                ErrorKind::BytecodeError(Box::new(error)),
96                                *span,
97                                vm.source.clone(),
98                            );
99                        }
100                        Frame::Generator { name, span, .. } => {
101                            error = Error::new(
102                                ErrorKind::NativeError {
103                                    err: Box::new(error),
104                                    gen_type: name,
105                                },
106                                *span,
107                                vm.source.clone(),
108                            );
109                        }
110                    }
111                }
112
113                Err(error)
114            }
115        }
116    }
117}
118
119struct CallFrame {
120    /// The lambda currently being executed.
121    lambda: Rc<Lambda>,
122
123    /// Optional captured upvalues of this frame (if a thunk or
124    /// closure if being evaluated).
125    upvalues: Rc<Upvalues>,
126
127    /// Instruction pointer to the instruction currently being
128    /// executed.
129    ip: CodeIdx,
130
131    /// Stack offset, i.e. the frames "view" into the VM's full stack.
132    stack_offset: usize,
133}
134
135impl CallFrame {
136    /// Retrieve an upvalue from this frame at the given index.
137    fn upvalue(&self, idx: UpvalueIdx) -> &Value {
138        &self.upvalues[idx]
139    }
140
141    /// Borrow the chunk of this frame's lambda.
142    fn chunk(&self) -> &Chunk {
143        &self.lambda.chunk
144    }
145
146    /// Increment this frame's instruction pointer and return the operation that
147    /// the pointer moved past.
148    fn inc_ip(&mut self) -> Op {
149        debug_assert!(
150            self.ip.0 < self.chunk().code.len(),
151            "out of bounds code at IP {} in {:p}",
152            self.ip.0,
153            self.lambda
154        );
155
156        let op = self.chunk().code[self.ip.0];
157        self.ip += 1;
158        op.into()
159    }
160
161    /// Read a varint-encoded operand and return it. The frame pointer is
162    /// incremented internally.
163    fn read_uvarint(&mut self) -> u64 {
164        let (arg, size) = self.chunk().read_uvarint(self.ip.0);
165        self.ip += size;
166        arg
167    }
168
169    /// Read a fixed-size u16 and increment the frame pointer.
170    fn read_u16(&mut self) -> u16 {
171        let arg = self.chunk().read_u16(self.ip.0);
172        self.ip += 2;
173        arg
174    }
175
176    /// Construct an error result from the given ErrorKind and the source span
177    /// of the current instruction.
178    pub fn error<T, IO>(&self, vm: &VM<IO>, kind: ErrorKind) -> Result<T, Error> {
179        Err(kind).with_span(self, vm)
180    }
181
182    /// Returns the current span. This is potentially expensive and should only
183    /// be used when actually constructing an error or warning.
184    pub fn current_span(&self) -> Span {
185        self.chunk().get_span(self.ip - 1)
186    }
187}
188
189/// A frame represents an execution state of the VM. The VM has a stack of
190/// frames representing the nesting of execution inside of the VM, and operates
191/// on the frame at the top.
192///
193/// When a frame has been fully executed, it is removed from the VM's frame
194/// stack and expected to leave a result [`Value`] on the top of the stack.
195enum Frame {
196    /// CallFrame represents the execution of Snix bytecode within a thunk,
197    /// function or closure.
198    CallFrame {
199        /// The call frame itself, separated out into another type to pass it
200        /// around easily.
201        call_frame: CallFrame,
202
203        /// Span from which the call frame was launched.
204        span: Span,
205    },
206
207    /// Generator represents a frame that can yield further
208    /// instructions to the VM while its execution is being driven.
209    ///
210    /// A generator is essentially an asynchronous function that can
211    /// be suspended while waiting for the VM to do something (e.g.
212    /// thunk forcing), and resume at the same point.
213    Generator {
214        /// human-readable description of the generator,
215        name: &'static str,
216
217        /// Span from which the generator was launched.
218        span: Span,
219
220        state: GeneratorState,
221
222        /// Generator itself, which can be resumed with `.resume()`.
223        generator: Generator,
224    },
225}
226
227impl Frame {
228    pub fn span(&self) -> Span {
229        match self {
230            Frame::CallFrame { span, .. } | Frame::Generator { span, .. } => *span,
231        }
232    }
233}
234
235#[derive(Default)]
236struct ImportCache(FxHashMap<PathBuf, Value>);
237
238/// The `ImportCache` holds the `Value` resulting from `import`ing a certain
239/// file, so that the same file doesn't need to be re-evaluated multiple times.
240/// Currently the real path of the imported file (determined using
241/// [`std::fs::canonicalize()`], not to be confused with our
242/// [`crate::value::canon_path()`]) is used to identify the file,
243/// just like C++ Nix does.
244///
245/// Errors while determining the real path are currently just ignored, since we
246/// pass around some fake paths like `/__corepkgs__/fetchurl.nix`.
247///
248/// In the future, we could use something more sophisticated, like file hashes.
249/// However, a consideration is that the eval cache is observable via impurities
250/// like pointer equality and `builtins.trace`.
251impl ImportCache {
252    fn get(&self, path: PathBuf) -> Option<&Value> {
253        let path = match std::fs::canonicalize(path.as_path()).map_err(ErrorKind::from) {
254            Ok(path) => path,
255            Err(_) => path,
256        };
257        self.0.get(&path)
258    }
259
260    fn insert(&mut self, path: PathBuf, value: Value) -> Option<Value> {
261        self.0.insert(
262            match std::fs::canonicalize(path.as_path()).map_err(ErrorKind::from) {
263                Ok(path) => path,
264                Err(_) => path,
265            },
266            value,
267        )
268    }
269}
270
271struct VM<'o, IO> {
272    /// VM's frame stack, representing the execution contexts the VM is working
273    /// through. Elements are usually pushed when functions are called, or
274    /// thunks are being forced.
275    frames: Vec<Frame>,
276
277    /// The VM's top-level value stack. Within this stack, each code-executing
278    /// frame holds a "view" of the stack representing the slice of the
279    /// top-level stack that is relevant to its operation. This is done to avoid
280    /// allocating a new `Vec` for each frame's stack.
281    pub(crate) stack: Vec<Value>,
282
283    /// Stack indices (absolute indexes into `stack`) of attribute
284    /// sets from which variables should be dynamically resolved
285    /// (`with`).
286    with_stack: Vec<usize>,
287
288    /// Runtime warnings collected during evaluation.
289    warnings: Vec<EvalWarning>,
290
291    /// Import cache, mapping absolute file paths to the value that
292    /// they compile to. Note that this reuses thunks, too!
293    // TODO: should probably be based on a file hash
294    pub import_cache: ImportCache,
295
296    /// Data structure holding all source code evaluated in this VM,
297    /// used for pretty error reporting.
298    source: SourceCode,
299
300    /// Parsed Nix search path, which is used to resolve `<...>`
301    /// references.
302    nix_search_path: NixSearchPath,
303
304    /// Implementation of I/O operations used for impure builtins and
305    /// features like `import`.
306    io_handle: IO,
307
308    /// Runtime observer which can print traces of runtime operations.
309    observer: &'o mut dyn RuntimeObserver,
310
311    /// Strong reference to the globals, guaranteeing that they are
312    /// kept alive for the duration of evaluation.
313    ///
314    /// This is important because recursive builtins (specifically
315    /// `import`) hold a weak reference to the builtins, while the
316    /// original strong reference is held by the compiler which does
317    /// not exist anymore at runtime.
318    #[allow(dead_code)]
319    globals: Rc<GlobalsMap>,
320
321    /// A reasonably applicable span that can be used for errors in each
322    /// execution situation.
323    ///
324    /// The VM should update this whenever control flow changes take place (i.e.
325    /// entering or exiting a frame to yield control somewhere).
326    reasonable_span: Span,
327
328    /// This field is responsible for handling `builtins.tryEval`. When that
329    /// builtin is encountered, it sends a special message to the VM which
330    /// pushes the frame index that requested to be informed of catchable
331    /// errors in this field.
332    ///
333    /// The frame stack is then laid out like this:
334    ///
335    /// ```notrust
336    /// ┌──┬──────────────────────────┐
337    /// │ 0│ `Result`-producing frame │
338    /// ├──┼──────────────────────────┤
339    /// │-1│ `builtins.tryEval` frame │
340    /// ├──┼──────────────────────────┤
341    /// │..│ ... other frames ...     │
342    /// └──┴──────────────────────────┘
343    /// ```
344    ///
345    /// Control is yielded to the outer VM loop, which evaluates the next frame
346    /// and returns the result itself to the `builtins.tryEval` frame.
347    try_eval_frames: Vec<usize>,
348}
349
350impl<'o, IO> VM<'o, IO>
351where
352    IO: AsRef<dyn EvalIO> + 'static,
353{
354    pub fn new(
355        nix_search_path: NixSearchPath,
356        io_handle: IO,
357        observer: &'o mut dyn RuntimeObserver,
358        source: SourceCode,
359        globals: Rc<GlobalsMap>,
360        reasonable_span: Span,
361    ) -> Self {
362        Self {
363            nix_search_path,
364            io_handle,
365            observer,
366            globals,
367            reasonable_span,
368            source,
369            frames: vec![],
370            stack: vec![],
371            with_stack: vec![],
372            warnings: vec![],
373            import_cache: Default::default(),
374            try_eval_frames: vec![],
375        }
376    }
377
378    /// Push a call frame onto the frame stack.
379    fn push_call_frame(&mut self, span: Span, call_frame: CallFrame) {
380        self.frames.push(Frame::CallFrame { span, call_frame })
381    }
382
383    /// Run the VM's primary (outer) execution loop, continuing execution based
384    /// on the current frame at the top of the frame stack.
385    fn execute(mut self) -> EvalResult<RuntimeResult> {
386        while let Some(frame) = self.frames.pop() {
387            self.reasonable_span = frame.span();
388            let frame_id = self.frames.len();
389
390            match frame {
391                Frame::CallFrame { call_frame, span } => {
392                    self.observer
393                        .observe_enter_call_frame(0, &call_frame.lambda, frame_id);
394
395                    match self.execute_bytecode(span, call_frame) {
396                        Ok(true) => self.observer.observe_exit_call_frame(frame_id, &self.stack),
397                        Ok(false) => self
398                            .observer
399                            .observe_suspend_call_frame(frame_id, &self.stack),
400
401                        Err(err) => return Err(err),
402                    };
403                }
404
405                // Handle generator frames, which can request thunk forcing
406                // during their execution.
407                Frame::Generator {
408                    name,
409                    span,
410                    state,
411                    generator,
412                } => {
413                    self.observer
414                        .observe_enter_generator(frame_id, name, &self.stack);
415
416                    match self.run_generator(name, span, frame_id, state, generator, None) {
417                        Ok(true) => {
418                            self.observer
419                                .observe_exit_generator(frame_id, name, &self.stack)
420                        }
421                        Ok(false) => {
422                            self.observer
423                                .observe_suspend_generator(frame_id, name, &self.stack)
424                        }
425
426                        Err(err) => return Err(err),
427                    };
428                }
429            }
430        }
431
432        // Once no more frames are present, return the stack's top value as the
433        // result.
434        let value = self
435            .stack
436            .pop()
437            .expect("Snix bug: runtime stack empty after execution");
438        Ok(RuntimeResult {
439            value,
440            warnings: self.warnings,
441        })
442    }
443
444    /// Run the VM's inner execution loop, processing Snix bytecode from a
445    /// chunk. This function returns if:
446    ///
447    /// 1. The code has run to the end, and has left a value on the top of the
448    ///    stack. In this case, the frame is not returned to the frame stack.
449    ///
450    /// 2. The code encounters a generator, in which case the frame in its
451    ///    current state is pushed back on the stack, and the generator is left
452    ///    on top of it for the outer loop to execute.
453    ///
454    /// 3. An error is encountered.
455    ///
456    /// This function *must* ensure that it leaves the frame stack in the
457    /// correct order, especially when re-enqueuing a frame to execute.
458    ///
459    /// The return value indicates whether the bytecode has been executed to
460    /// completion, or whether it has been suspended in favour of a generator.
461    fn execute_bytecode(&mut self, span: Span, mut frame: CallFrame) -> EvalResult<bool> {
462        loop {
463            let op = frame.inc_ip();
464            self.observer.observe_execute_op(frame.ip, &op, &self.stack);
465
466            match op {
467                Op::ThunkSuspended | Op::ThunkClosure => {
468                    let idx = frame.read_uvarint() as usize;
469
470                    let blueprint = match &frame.chunk().constants[idx] {
471                        Value::Blueprint(lambda) => lambda.clone(),
472                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
473                    };
474
475                    let upvalue_count = frame.read_uvarint();
476
477                    debug_assert!(
478                        (upvalue_count >> 1) == blueprint.upvalue_count as u64,
479                        "TODO: new upvalue count not correct",
480                    );
481
482                    let thunk = if op == Op::ThunkClosure {
483                        debug_assert!(
484                            (((upvalue_count >> 1) > 0) || (upvalue_count & 0b1 == 1)),
485                            "OpThunkClosure should not be called for plain lambdas",
486                        );
487                        Thunk::new_closure(blueprint)
488                    } else {
489                        Thunk::new_suspended(blueprint, frame.current_span())
490                    };
491                    let upvalues = thunk.upvalues_mut();
492                    self.stack.push(Value::Thunk(thunk.clone()));
493
494                    // From this point on we internally mutate the
495                    // upvalues. The closure (if `is_closure`) is
496                    // already in its stack slot, which means that it
497                    // can capture itself as an upvalue for
498                    // self-recursion.
499                    self.populate_upvalues(&mut frame, upvalue_count, upvalues)?;
500                }
501
502                Op::Force => {
503                    if let Some(Value::Thunk(_)) = self.stack.last() {
504                        let thunk = match self.stack_pop() {
505                            Value::Thunk(t) => t,
506                            _ => unreachable!(),
507                        };
508
509                        let gen_span = frame.current_span();
510
511                        self.push_call_frame(span, frame);
512                        self.enqueue_generator("force", gen_span, |co| {
513                            Thunk::force(thunk, co, gen_span)
514                        });
515
516                        return Ok(false);
517                    }
518                }
519
520                Op::GetUpvalue => {
521                    let idx = UpvalueIdx(frame.read_uvarint() as usize);
522                    let value = frame.upvalue(idx).clone();
523                    self.stack.push(value);
524                }
525
526                // Discard the current frame.
527                Op::Return => {
528                    // TODO(amjoseph): I think this should assert `==` rather
529                    // than `<=` but it fails with the stricter condition.
530                    debug_assert!(self.stack.len() - 1 <= frame.stack_offset);
531                    return Ok(true);
532                }
533
534                Op::Constant => {
535                    let idx = frame.read_uvarint() as usize;
536
537                    debug_assert!(
538                        idx < frame.chunk().constants.len(),
539                        "out of bounds constant at IP {} in {:p}",
540                        frame.ip.0,
541                        frame.lambda
542                    );
543
544                    let c = frame.chunk().constants[idx].clone();
545                    self.stack.push(c);
546                }
547
548                Op::Call => {
549                    let callable = self.stack_pop();
550                    self.call_value(frame.current_span(), Some((span, frame)), callable)?;
551
552                    // exit this loop and let the outer loop enter the new call
553                    return Ok(true);
554                }
555
556                // Remove the given number of elements from the stack,
557                // but retain the top value.
558                Op::CloseScope => {
559                    let count = frame.read_uvarint() as usize;
560                    // Immediately move the top value into the right
561                    // position.
562                    let target_idx = self.stack.len() - 1 - count;
563                    self.stack[target_idx] = self.stack_pop();
564
565                    // Then drop the remaining values.
566                    for _ in 0..(count - 1) {
567                        self.stack.pop();
568                    }
569                }
570
571                Op::Closure => {
572                    let idx = frame.read_uvarint() as usize;
573                    let blueprint = match &frame.chunk().constants[idx] {
574                        Value::Blueprint(lambda) => lambda.clone(),
575                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
576                    };
577
578                    let upvalue_count = frame.read_uvarint();
579
580                    debug_assert!(
581                        (upvalue_count >> 1) == blueprint.upvalue_count as u64,
582                        "TODO: new upvalue count not correct in closure",
583                    );
584
585                    debug_assert!(
586                        ((upvalue_count >> 1) > 0 || (upvalue_count & 0b1 == 1)),
587                        "OpClosure should not be called for plain lambdas"
588                    );
589
590                    let mut upvalues = Upvalues::with_capacity(blueprint.upvalue_count);
591                    self.populate_upvalues(&mut frame, upvalue_count, &mut upvalues)?;
592                    self.stack
593                        .push(Value::Closure(Rc::new(Closure::new_with_upvalues(
594                            Rc::new(upvalues),
595                            blueprint,
596                        ))));
597                }
598
599                Op::AttrsSelect => lifted_pop! {
600                    self(key, attrs) => {
601                        let key = key.to_str().with_span(&frame, self)?;
602                        let attrs = attrs.to_attrs().with_span(&frame, self)?;
603
604                        match attrs.select(&key) {
605                            Some(value) => self.stack.push(value.clone()),
606
607                            None => {
608                                return frame.error(
609                                    self,
610                                    ErrorKind::AttributeNotFound {
611                                        name: key.to_str_lossy().into_owned()
612                                    },
613                                );
614                            }
615                        }
616                    }
617                },
618
619                Op::JumpIfFalse => {
620                    let offset = frame.read_u16() as usize;
621                    debug_assert!(offset != 0);
622                    if !self.stack_peek(0).as_bool().with_span(&frame, self)? {
623                        frame.ip += offset;
624                    }
625                }
626
627                Op::JumpIfCatchable => {
628                    let offset = frame.read_u16() as usize;
629                    debug_assert!(offset != 0);
630                    if self.stack_peek(0).is_catchable() {
631                        frame.ip += offset;
632                    }
633                }
634
635                Op::JumpIfNoFinaliseRequest => {
636                    let offset = frame.read_u16() as usize;
637                    debug_assert!(offset != 0);
638                    match self.stack_peek(0) {
639                        Value::FinaliseRequest(finalise) => {
640                            if !finalise {
641                                frame.ip += offset;
642                            }
643                        }
644                        val => panic!(
645                            "Snix bug: OpJumIfNoFinaliseRequest: expected FinaliseRequest, but got {}",
646                            val.type_of()
647                        ),
648                    }
649                }
650
651                Op::Pop => {
652                    self.stack.pop();
653                }
654
655                Op::AttrsTrySelect => {
656                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
657                    let value = match self.stack_pop() {
658                        Value::Attrs(attrs) => match attrs.select(&key) {
659                            Some(value) => value.clone(),
660                            None => Value::AttrNotFound,
661                        },
662
663                        _ => Value::AttrNotFound,
664                    };
665
666                    self.stack.push(value);
667                }
668
669                Op::GetLocal => {
670                    let local_idx = frame.read_uvarint() as usize;
671                    let idx = frame.stack_offset + local_idx;
672                    self.stack.push(self.stack[idx].clone());
673                }
674
675                Op::JumpIfNotFound => {
676                    let offset = frame.read_u16() as usize;
677                    debug_assert!(offset != 0);
678                    if matches!(self.stack_peek(0), Value::AttrNotFound) {
679                        self.stack_pop();
680                        frame.ip += offset;
681                    }
682                }
683
684                Op::Jump => {
685                    let offset = frame.read_u16() as usize;
686                    debug_assert!(offset != 0);
687                    frame.ip += offset;
688                }
689
690                Op::Equal => lifted_pop! {
691                    self(b, a) => {
692                        let gen_span = frame.current_span();
693                        self.push_call_frame(span, frame);
694                        self.enqueue_generator("nix_eq", gen_span, |co| {
695                            a.nix_eq_owned_genco(b, co, PointerEquality::ForbidAll, gen_span)
696                        });
697                        return Ok(false);
698                    }
699                },
700
701                // These assertion operations error out if the stack
702                // top is not of the expected type. This is necessary
703                // to implement some specific behaviours of Nix
704                // exactly.
705                Op::AssertBool => {
706                    let val = self.stack_peek(0);
707                    // TODO(edef): propagate this into is_bool, since bottom values *are* values of any type
708                    if !val.is_catchable() && !val.is_bool() {
709                        return frame.error(
710                            self,
711                            ErrorKind::TypeError {
712                                expected: "bool",
713                                actual: val.type_of(),
714                            },
715                        );
716                    }
717                }
718
719                Op::AssertAttrs => {
720                    let val = self.stack_peek(0);
721                    // TODO(edef): propagate this into is_attrs, since bottom values *are* values of any type
722                    if !val.is_catchable() && !val.is_attrs() {
723                        return frame.error(
724                            self,
725                            ErrorKind::TypeError {
726                                expected: "set",
727                                actual: val.type_of(),
728                            },
729                        );
730                    }
731                }
732
733                Op::Attrs => self.run_attrset(frame.read_uvarint() as usize, &frame)?,
734
735                Op::AttrsUpdate => lifted_pop! {
736                    self(rhs, lhs) => {
737                        let rhs = rhs.to_attrs().with_span(&frame, self)?;
738                        let lhs = lhs.to_attrs().with_span(&frame, self)?;
739                        self.stack.push(Value::attrs(lhs.update(*rhs)))
740                    }
741                },
742
743                Op::Invert => lifted_pop! {
744                    self(v) => {
745                        let v = v.as_bool().with_span(&frame, self)?;
746                        self.stack.push(Value::Bool(!v));
747                    }
748                },
749
750                Op::List => {
751                    let count = frame.read_uvarint() as usize;
752                    let list =
753                        NixList::construct(count, self.stack.split_off(self.stack.len() - count));
754
755                    self.stack.push(Value::List(list));
756                }
757
758                Op::JumpIfTrue => {
759                    let offset = frame.read_u16() as usize;
760                    debug_assert!(offset != 0);
761                    if self.stack_peek(0).as_bool().with_span(&frame, self)? {
762                        frame.ip += offset;
763                    }
764                }
765
766                Op::HasAttr => lifted_pop! {
767                    self(key, attrs) => {
768                        let key = key.to_str().with_span(&frame, self)?;
769                        let result = match attrs {
770                            Value::Attrs(attrs) => attrs.contains(&key),
771
772                            // Nix allows use of `?` on non-set types, but
773                            // always returns false in those cases.
774                            _ => false,
775                        };
776
777                        self.stack.push(Value::Bool(result));
778                    }
779                },
780
781                Op::Concat => lifted_pop! {
782                    self(rhs, lhs) => {
783                        let rhs = rhs.to_list().with_span(&frame, self)?.into_inner();
784                        let mut lhs = lhs.to_list().with_span(&frame, self)?.into_inner();
785                        lhs.extend(rhs.into_iter());
786                        self.stack.push(Value::List(lhs.into()))
787                    }
788                },
789
790                Op::ResolveWith => {
791                    let ident = self.stack_pop().to_str().with_span(&frame, self)?;
792
793                    // Re-enqueue this frame.
794                    let op_span = frame.current_span();
795                    self.push_call_frame(span, frame);
796
797                    // Construct a generator frame doing the lookup in constant
798                    // stack space.
799                    let with_stack_len = self.with_stack.len();
800                    let closed_with_stack_len = self
801                        .last_call_frame()
802                        .map(|frame| frame.upvalues.with_stack_len())
803                        .unwrap_or(0);
804
805                    self.enqueue_generator("resolve_with", op_span, |co| {
806                        resolve_with(co, ident.into(), with_stack_len, closed_with_stack_len)
807                    });
808
809                    return Ok(false);
810                }
811
812                Op::Finalise => {
813                    let idx = frame.read_uvarint() as usize;
814                    match &self.stack[frame.stack_offset + idx] {
815                        Value::Closure(_) => panic!("attempted to finalise a closure"),
816                        Value::Thunk(thunk) => thunk.finalise(&self.stack[frame.stack_offset..]),
817                        _ => panic!("attempted to finalise a non-thunk"),
818                    }
819                }
820
821                Op::CoerceToString => {
822                    let kind: CoercionKind = frame.chunk().code[frame.ip.0].into();
823                    frame.ip.0 += 1;
824
825                    let value = self.stack_pop();
826                    let gen_span = frame.current_span();
827                    self.push_call_frame(span, frame);
828
829                    self.enqueue_generator("coerce_to_string", gen_span, |co| {
830                        value.coerce_to_string(co, kind, gen_span)
831                    });
832
833                    return Ok(false);
834                }
835
836                Op::Interpolate => self.run_interpolate(frame.read_uvarint(), &frame)?,
837
838                Op::ValidateClosedFormals => {
839                    let formals = frame.lambda.formals.as_ref().expect(
840                        "OpValidateClosedFormals called within the frame of a lambda without formals",
841                    );
842
843                    let peeked = self.stack_peek(0);
844                    if peeked.is_catchable() {
845                        continue;
846                    }
847
848                    let args = peeked.to_attrs().with_span(&frame, self)?;
849                    for arg in args.keys() {
850                        if !formals.contains(arg) {
851                            return frame.error(
852                                self,
853                                ErrorKind::UnexpectedArgumentFormals {
854                                    arg: arg.clone(),
855                                    formals_span: formals.span,
856                                },
857                            );
858                        }
859                    }
860                }
861
862                Op::Add => lifted_pop! {
863                    self(b, a) => {
864                        let gen_span = frame.current_span();
865                        self.push_call_frame(span, frame);
866
867                        // OpAdd can add not just numbers, but also string-like
868                        // things, which requires more VM logic. This operation is
869                        // evaluated in a generator frame.
870                        self.enqueue_generator("add_values", gen_span, |co| add_values(co, a, b));
871                        return Ok(false);
872                    }
873                },
874
875                Op::Sub => lifted_pop! {
876                    self(b, a) => {
877                        let result = arithmetic_op!(&a, &b, -).with_span(&frame, self)?;
878                        self.stack.push(result);
879                    }
880                },
881
882                Op::Mul => lifted_pop! {
883                    self(b, a) => {
884                        let result = arithmetic_op!(&a, &b, *).with_span(&frame, self)?;
885                        self.stack.push(result);
886                    }
887                },
888
889                Op::Div => lifted_pop! {
890                    self(b, a) => {
891                        match b {
892                            Value::Integer(0) => return frame.error(self, ErrorKind::DivisionByZero),
893                            Value::Float(0.0_f64) => {
894                                return frame.error(self, ErrorKind::DivisionByZero)
895                            }
896                            _ => {}
897                        };
898
899                        let result = arithmetic_op!(&a, &b, /).with_span(&frame, self)?;
900                        self.stack.push(result);
901                    }
902                },
903
904                Op::Negate => match self.stack_pop() {
905                    Value::Integer(i) => self.stack.push(Value::Integer(-i)),
906                    Value::Float(f) => self.stack.push(Value::Float(-f)),
907                    Value::Catchable(cex) => self.stack.push(Value::Catchable(cex)),
908                    v => {
909                        return frame.error(
910                            self,
911                            ErrorKind::TypeError {
912                                expected: "number (either int or float)",
913                                actual: v.type_of(),
914                            },
915                        );
916                    }
917                },
918
919                Op::Less => cmp_op!(self, frame, span, <),
920                Op::LessOrEq => cmp_op!(self, frame, span, <=),
921                Op::More => cmp_op!(self, frame, span, >),
922                Op::MoreOrEq => cmp_op!(self, frame, span, >=),
923
924                Op::FindFile => match self.stack_pop() {
925                    Value::UnresolvedPath(path) => {
926                        let resolved = self
927                            .nix_search_path
928                            .resolve(&self.io_handle, *path)
929                            .with_span(&frame, self)?;
930                        self.stack.push(resolved.into());
931                    }
932
933                    _ => panic!("Snix bug: OpFindFile called on non-UnresolvedPath"),
934                },
935
936                Op::ResolveHomePath => match self.stack_pop() {
937                    Value::UnresolvedPath(path) => {
938                        match dirs::home_dir() {
939                            None => {
940                                return frame.error(
941                                    self,
942                                    ErrorKind::RelativePathResolution(
943                                        "failed to determine home directory".into(),
944                                    ),
945                                );
946                            }
947                            Some(mut buf) => {
948                                buf.push(*path);
949                                self.stack.push(buf.into());
950                            }
951                        };
952                    }
953
954                    _ => {
955                        panic!("Snix bug: OpResolveHomePath called on non-UnresolvedPath")
956                    }
957                },
958
959                Op::PushWith => self
960                    .with_stack
961                    .push(frame.stack_offset + frame.read_uvarint() as usize),
962
963                Op::PopWith => {
964                    self.with_stack.pop();
965                }
966
967                Op::AssertFail => {
968                    self.stack
969                        .push(Value::from(CatchableErrorKind::AssertionFailed));
970                }
971
972                // Encountering an invalid opcode is a critical error in the
973                // VM/compiler.
974                Op::Invalid => {
975                    panic!("Snix bug: attempted to execute invalid opcode")
976                }
977            }
978        }
979    }
980}
981
982/// Implementation of helper functions for the runtime logic above.
983impl<IO> VM<'_, IO>
984where
985    IO: AsRef<dyn EvalIO> + 'static,
986{
987    pub(crate) fn stack_pop(&mut self) -> Value {
988        self.stack.pop().expect("runtime stack empty")
989    }
990
991    fn stack_peek(&self, offset: usize) -> &Value {
992        &self.stack[self.stack.len() - 1 - offset]
993    }
994
995    fn run_attrset(&mut self, count: usize, frame: &CallFrame) -> EvalResult<()> {
996        let attrs = NixAttrs::construct(count, self.stack.split_off(self.stack.len() - count * 2))
997            .with_span(frame, self)?
998            .map(Value::attrs)
999            .into();
1000
1001        self.stack.push(attrs);
1002        Ok(())
1003    }
1004
1005    /// Access the last call frame present in the frame stack.
1006    fn last_call_frame(&self) -> Option<&CallFrame> {
1007        for frame in self.frames.iter().rev() {
1008            if let Frame::CallFrame { call_frame, .. } = frame {
1009                return Some(call_frame);
1010            }
1011        }
1012
1013        None
1014    }
1015
1016    /// Push an already constructed warning.
1017    pub fn push_warning(&mut self, warning: EvalWarning) {
1018        self.warnings.push(warning);
1019    }
1020
1021    /// Emit a warning with the given WarningKind and the source span
1022    /// of the current instruction.
1023    pub fn emit_warning(&mut self, kind: WarningKind) {
1024        self.push_warning(EvalWarning {
1025            kind,
1026            span: self.get_span(),
1027        });
1028    }
1029
1030    /// Interpolate string fragments by popping the specified number of
1031    /// fragments of the stack, evaluating them to strings, and pushing
1032    /// the concatenated result string back on the stack.
1033    fn run_interpolate(&mut self, count: u64, frame: &CallFrame) -> EvalResult<()> {
1034        let mut out = BString::default();
1035        // Interpolation propagates the context and union them.
1036        let mut context: NixContext = NixContext::new();
1037
1038        for i in 0..count {
1039            let val = self.stack_pop();
1040            if val.is_catchable() {
1041                for _ in (i + 1)..count {
1042                    self.stack.pop();
1043                }
1044                self.stack.push(val);
1045                return Ok(());
1046            }
1047            let mut nix_string = val.to_contextful_str().with_span(frame, self)?;
1048            out.push_str(nix_string.as_bstr());
1049            if let Some(nix_string_ctx) = nix_string.take_context() {
1050                context.extend(nix_string_ctx.into_iter())
1051            }
1052        }
1053
1054        self.stack
1055            .push(Value::String(NixString::new_context_from(context, out)));
1056        Ok(())
1057    }
1058
1059    /// Apply an argument from the stack to a builtin, and attempt to call it.
1060    ///
1061    /// All calls are tail-calls in Snix, as every function application is a
1062    /// separate thunk and OpCall is thus the last result in the thunk.
1063    ///
1064    /// Due to this, once control flow exits this function, the generator will
1065    /// automatically be run by the VM.
1066    fn call_builtin(&mut self, span: Span, mut builtin: Builtin) -> EvalResult<()> {
1067        let builtin_name = builtin.name();
1068        self.observer.observe_enter_builtin(builtin_name);
1069
1070        builtin.apply_arg(self.stack_pop());
1071
1072        match builtin.call() {
1073            // Partially applied builtin is just pushed back on the stack.
1074            BuiltinResult::Partial(partial) => self.stack.push(Value::Builtin(partial)),
1075
1076            // Builtin is fully applied and the generator needs to be run by the VM.
1077            BuiltinResult::Called(name, generator) => self.frames.push(Frame::Generator {
1078                generator,
1079                span,
1080                name,
1081                state: GeneratorState::Running,
1082            }),
1083        }
1084
1085        Ok(())
1086    }
1087
1088    fn call_value(
1089        &mut self,
1090        span: Span,
1091        parent: Option<(Span, CallFrame)>,
1092        callable: Value,
1093    ) -> EvalResult<()> {
1094        match callable {
1095            Value::Builtin(builtin) => self.call_builtin(span, builtin),
1096            Value::Thunk(thunk) => self.call_value(span, parent, thunk.value().clone()),
1097
1098            Value::Closure(closure) => {
1099                let lambda = closure.lambda();
1100                self.observer.observe_tail_call(self.frames.len(), &lambda);
1101
1102                // The stack offset is always `stack.len() - arg_count`, and
1103                // since this branch handles native Nix functions (which always
1104                // take only a single argument and are curried), the offset is
1105                // `stack_len - 1`.
1106                let stack_offset = self.stack.len() - 1;
1107
1108                // Reenqueue the parent frame, which should only have
1109                // `OpReturn` left. Not throwing it away leads to more
1110                // useful error traces.
1111                if let Some((parent_span, parent_frame)) = parent {
1112                    self.push_call_frame(parent_span, parent_frame);
1113                }
1114
1115                self.push_call_frame(
1116                    span,
1117                    CallFrame {
1118                        lambda,
1119                        upvalues: closure.upvalues(),
1120                        ip: CodeIdx(0),
1121                        stack_offset,
1122                    },
1123                );
1124
1125                Ok(())
1126            }
1127
1128            // Attribute sets with a __functor attribute are callable.
1129            val @ Value::Attrs(_) => {
1130                if let Some((parent_span, parent_frame)) = parent {
1131                    self.push_call_frame(parent_span, parent_frame);
1132                }
1133
1134                self.enqueue_generator("__functor call", span, |co| call_functor(co, val));
1135                Ok(())
1136            }
1137
1138            val @ Value::Catchable(_) => {
1139                // the argument that we tried to apply a catchable to
1140                self.stack.pop();
1141                // applying a `throw` to anything is still a `throw`, so we just
1142                // push it back on the stack.
1143                self.stack.push(val);
1144                Ok(())
1145            }
1146
1147            v => Err(ErrorKind::NotCallable(v.type_of())).with_span(span, self),
1148        }
1149    }
1150
1151    /// Populate the upvalue fields of a thunk or closure under construction.
1152    ///
1153    /// See the closely tied function `emit_upvalue_data` in the compiler
1154    /// implementation for details on the argument processing.
1155    fn populate_upvalues(
1156        &mut self,
1157        frame: &mut CallFrame,
1158        count: u64,
1159        mut upvalues: impl DerefMut<Target = Upvalues>,
1160    ) -> EvalResult<()> {
1161        // Determine whether to capture the with stack, and then shift the
1162        // actual count of upvalues back.
1163        let capture_with = count & 0b1 == 1;
1164        let count = count >> 1;
1165        if capture_with {
1166            // Start the captured with_stack off of the
1167            // current call frame's captured with_stack, ...
1168            let mut captured_with_stack = frame
1169                .upvalues
1170                .with_stack()
1171                .cloned()
1172                // ... or make an empty one if there isn't one already.
1173                .unwrap_or_else(|| Vec::with_capacity(self.with_stack.len()));
1174
1175            for idx in &self.with_stack {
1176                captured_with_stack.push(self.stack[*idx].clone());
1177            }
1178
1179            upvalues.deref_mut().set_with_stack(captured_with_stack);
1180        }
1181
1182        for _ in 0..count {
1183            let pos = Position(frame.read_uvarint());
1184
1185            if let Some(stack_idx) = pos.runtime_stack_index() {
1186                let idx = frame.stack_offset + stack_idx.0;
1187
1188                let val = match self.stack.get(idx) {
1189                    Some(val) => val.clone(),
1190                    None => {
1191                        return frame.error(
1192                            self,
1193                            ErrorKind::SnixBug {
1194                                msg: "upvalue to be captured was missing on stack",
1195                                metadata: Some(Rc::new(json!({
1196                                    "ip": format!("{:#x}", frame.ip.0 - 1),
1197                                    "stack_idx(relative)": stack_idx.0,
1198                                    "stack_idx(absolute)": idx,
1199                                }))),
1200                            },
1201                        );
1202                    }
1203                };
1204
1205                upvalues.deref_mut().push(val);
1206                continue;
1207            }
1208
1209            if let Some(idx) = pos.runtime_deferred_local() {
1210                upvalues.deref_mut().push(Value::DeferredUpvalue(idx));
1211                continue;
1212            }
1213
1214            if let Some(idx) = pos.runtime_upvalue_index() {
1215                upvalues.deref_mut().push(frame.upvalue(idx).clone());
1216                continue;
1217            }
1218
1219            panic!("Snix bug: invalid capture position emitted")
1220        }
1221
1222        Ok(())
1223    }
1224}
1225
1226// TODO(amjoseph): de-asyncify this
1227/// Resolve a dynamically bound identifier (through `with`) by looking
1228/// for matching values in the with-stacks carried at runtime.
1229async fn resolve_with(
1230    co: GenCo,
1231    ident: BString,
1232    vm_with_len: usize,
1233    upvalue_with_len: usize,
1234) -> Result<Value, ErrorKind> {
1235    /// Fetch and force a value on the with-stack from the VM.
1236    async fn fetch_forced_with(co: &GenCo, idx: usize) -> Value {
1237        match co.yield_(VMRequest::WithValue(idx)).await {
1238            VMResponse::Value(value) => value,
1239            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1240        }
1241    }
1242
1243    /// Fetch and force a value on the *captured* with-stack from the VM.
1244    async fn fetch_captured_with(co: &GenCo, idx: usize) -> Value {
1245        match co.yield_(VMRequest::CapturedWithValue(idx)).await {
1246            VMResponse::Value(value) => value,
1247            msg => panic!("Snix bug: VM responded with incorrect generator message: {msg}"),
1248        }
1249    }
1250
1251    for with_stack_idx in (0..vm_with_len).rev() {
1252        // TODO(tazjin): is this branch still live with the current with-thunking?
1253        let with = fetch_forced_with(&co, with_stack_idx).await;
1254
1255        if with.is_catchable() {
1256            return Ok(with);
1257        }
1258
1259        match with.to_attrs()?.select(&ident) {
1260            None => continue,
1261            Some(val) => return Ok(val.clone()),
1262        }
1263    }
1264
1265    for upvalue_with_idx in (0..upvalue_with_len).rev() {
1266        let with = fetch_captured_with(&co, upvalue_with_idx).await;
1267
1268        if with.is_catchable() {
1269            return Ok(with);
1270        }
1271
1272        match with.to_attrs()?.select(&ident) {
1273            None => continue,
1274            Some(val) => return Ok(val.clone()),
1275        }
1276    }
1277
1278    Err(ErrorKind::UnknownDynamicVariable(ident.to_string()))
1279}
1280
1281// TODO(amjoseph): de-asyncify this
1282async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
1283    // What we try to do is solely determined by the type of the first value!
1284    let result = match (a, b) {
1285        (Value::Path(p), v) => {
1286            let mut path = p.into_os_string();
1287            match generators::request_string_coerce(
1288                &co,
1289                v,
1290                CoercionKind {
1291                    strong: false,
1292
1293                    // Concatenating a Path with something else results in a
1294                    // Path, so we don't need to import any paths (paths
1295                    // imported by Nix always exist as a string, unless
1296                    // converted by the user). In C++ Nix they even may not
1297                    // contain any string context, the resulting error of such a
1298                    // case can not be replicated by us.
1299                    import_paths: false,
1300                    // FIXME(raitobezarius): per https://b.tvl.fyi/issues/364, this is a usecase
1301                    // for having a `reject_context: true` option here. This didn't occur yet in
1302                    // nixpkgs during my evaluations, therefore, I skipped it.
1303                },
1304            )
1305            .await
1306            {
1307                Ok(vs) => {
1308                    path.push(vs.to_os_str()?);
1309                    crate::value::canon_path(PathBuf::from(path)).into()
1310                }
1311                Err(c) => Value::Catchable(Box::new(c)),
1312            }
1313        }
1314        (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)),
1315        (Value::String(s1), v) => generators::request_string_coerce(
1316            &co,
1317            v,
1318            CoercionKind {
1319                strong: false,
1320                // Behaves the same as string interpolation
1321                import_paths: true,
1322            },
1323        )
1324        .await
1325        .map(|s2| Value::String(s1.concat(&s2)))
1326        .into(),
1327        (a @ Value::Integer(_), b) | (a @ Value::Float(_), b) => arithmetic_op!(&a, &b, +)?,
1328        (a, b) => {
1329            let r1 = generators::request_string_coerce(
1330                &co,
1331                a,
1332                CoercionKind {
1333                    strong: false,
1334                    import_paths: false,
1335                },
1336            )
1337            .await;
1338            let r2 = generators::request_string_coerce(
1339                &co,
1340                b,
1341                CoercionKind {
1342                    strong: false,
1343                    import_paths: false,
1344                },
1345            )
1346            .await;
1347            match (r1, r2) {
1348                (Ok(s1), Ok(s2)) => Value::String(s1.concat(&s2)),
1349                (Err(c), _) => return Ok(Value::from(c)),
1350                (_, Err(c)) => return Ok(Value::from(c)),
1351            }
1352        }
1353    };
1354
1355    Ok(result)
1356}
1357
1358/// The result of a VM's runtime evaluation.
1359pub struct RuntimeResult {
1360    pub value: Value,
1361    pub warnings: Vec<EvalWarning>,
1362}
1363
1364// TODO(amjoseph): de-asyncify this
1365/// Generator that retrieves the final value from the stack, and deep-forces it
1366/// before returning.
1367async fn final_deep_force(co: GenCo) -> Result<Value, ErrorKind> {
1368    let value = generators::request_stack_pop(&co).await;
1369    Ok(generators::request_deep_force(&co, value).await)
1370}
1371
1372/// Specification for how to handle top-level values returned by evaluation
1373#[derive(Debug, Clone, Copy, Default)]
1374pub enum EvalMode {
1375    /// The default. Values are returned from evaluations as-is, without any extra forcing or
1376    /// special handling.
1377    #[default]
1378    Lazy,
1379
1380    /// Strictly and deeply evaluate top-level values returned by evaluation.
1381    Strict,
1382}
1383
1384pub fn run_lambda<IO>(
1385    nix_search_path: NixSearchPath,
1386    io_handle: IO,
1387    observer: &mut dyn RuntimeObserver,
1388    source: SourceCode,
1389    globals: Rc<GlobalsMap>,
1390    lambda: Rc<Lambda>,
1391    mode: EvalMode,
1392) -> EvalResult<RuntimeResult>
1393where
1394    IO: AsRef<dyn EvalIO> + 'static,
1395{
1396    // Retain the top-level span of the expression in this lambda, as
1397    // synthetic "calls" in deep_force will otherwise not have a span
1398    // to fall back to.
1399    //
1400    // We exploit the fact that the compiler emits a final instruction
1401    // with the span of the entire file for top-level expressions.
1402    let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
1403
1404    let mut vm = VM::new(
1405        nix_search_path,
1406        io_handle,
1407        observer,
1408        source,
1409        globals,
1410        root_span,
1411    );
1412
1413    // When evaluating strictly, synthesise a frame that will instruct
1414    // the VM to deep-force the final value before returning it.
1415    match mode {
1416        EvalMode::Lazy => {}
1417        EvalMode::Strict => vm.enqueue_generator("final_deep_force", root_span, final_deep_force),
1418    }
1419
1420    vm.frames.push(Frame::CallFrame {
1421        span: root_span,
1422        call_frame: CallFrame {
1423            lambda,
1424            upvalues: Rc::new(Upvalues::with_capacity(0)),
1425            ip: CodeIdx(0),
1426            stack_offset: 0,
1427        },
1428    });
1429
1430    vm.execute()
1431}