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}