snix_eval/value/
thunk.rs

1//! This module implements the runtime representation of Thunks.
2//!
3//! Thunks are a special kind of Nix value, similar to a 0-argument
4//! closure that yields some value. Thunks are used to implement the
5//! lazy evaluation behaviour of Nix:
6//!
7//! Whenever the compiler determines that an expression should be
8//! evaluated lazily, it creates a thunk instead of compiling the
9//! expression value directly. At any point in the runtime where the
10//! actual value of a thunk is required, it is "forced", meaning that
11//! the encompassing computation takes place and the thunk takes on
12//! its new value.
13//!
14//! Thunks have interior mutability to be able to memoise their
15//! computation. Once a thunk is evaluated, its internal
16//! representation becomes the result of the expression. It is legal
17//! for the runtime to replace a thunk object directly with its value
18//! object, but when forcing a thunk, the runtime *must* mutate the
19//! memoisable slot.
20
21use rustc_hash::FxHashSet;
22use std::{
23    cell::{Ref, RefCell, RefMut},
24    fmt::Debug,
25    rc::Rc,
26};
27
28use crate::{
29    errors::ErrorKind,
30    opcode::Op,
31    upvalues::Upvalues,
32    value::Closure,
33    vm::generators::{self, GenCo},
34    Value,
35};
36
37use super::{Lambda, TotalDisplay};
38use codemap::Span;
39
40/// Internal representation of a suspended native thunk.
41struct SuspendedNative(Box<dyn Fn() -> Result<Value, ErrorKind>>);
42
43impl Debug for SuspendedNative {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "SuspendedNative({:p})", self.0)
46    }
47}
48
49/// Internal representation of the different states of a thunk.
50///
51/// Upvalues must be finalised before leaving the initial state
52/// (Suspended or RecursiveClosure).  The [`Thunk::value()`] function may
53/// not be called until the thunk is in the final state (Evaluated).
54#[derive(Debug)]
55enum ThunkRepr {
56    /// Thunk is closed over some values, suspended and awaiting
57    /// execution.
58    Suspended {
59        lambda: Rc<Lambda>,
60        upvalues: Rc<Upvalues>,
61        span: Span,
62    },
63
64    /// Thunk is a suspended native computation.
65    Native(SuspendedNative),
66
67    /// Thunk currently under-evaluation; encountering a blackhole
68    /// value means that infinite recursion has occured.
69    Blackhole {
70        /// Span at which the thunk was first forced.
71        forced_at: Span,
72
73        /// Span at which the thunk was originally suspended.
74        suspended_at: Option<Span>,
75
76        /// Span of the first instruction of the actual code inside
77        /// the thunk.
78        content_span: Option<Span>,
79    },
80
81    // TODO(amjoseph): consider changing `Value` to `Rc<Value>` to avoid
82    // expensive clone()s in Thunk::force().
83    /// Fully evaluated thunk.
84    Evaluated(Value),
85}
86
87impl ThunkRepr {
88    fn debug_repr(&self) -> String {
89        match self {
90            ThunkRepr::Evaluated(v) => format!("thunk(val|{})", v),
91            ThunkRepr::Blackhole { .. } => "thunk(blackhole)".to_string(),
92            ThunkRepr::Native(_) => "thunk(native)".to_string(),
93            ThunkRepr::Suspended { lambda, .. } => format!("thunk({:p})", *lambda),
94        }
95    }
96
97    /// Return the Value within a fully-evaluated ThunkRepr; panics
98    /// if the thunk is not fully-evaluated.
99    fn expect(self) -> Value {
100        match self {
101            ThunkRepr::Evaluated(value) => value,
102            ThunkRepr::Blackhole { .. } => panic!("Thunk::expect() called on a black-holed thunk"),
103            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
104                panic!("Thunk::expect() called on a suspended thunk")
105            }
106        }
107    }
108
109    fn expect_ref(&self) -> &Value {
110        match self {
111            ThunkRepr::Evaluated(value) => value,
112            ThunkRepr::Blackhole { .. } => panic!("Thunk::expect() called on a black-holed thunk"),
113            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
114                panic!("Thunk::expect() called on a suspended thunk")
115            }
116        }
117    }
118
119    pub fn is_forced(&self) -> bool {
120        match self {
121            ThunkRepr::Evaluated(Value::Thunk(_)) => false,
122            ThunkRepr::Evaluated(_) => true,
123            _ => false,
124        }
125    }
126}
127
128/// A thunk is created for any value which requires non-strict
129/// evaluation due to self-reference or lazy semantics (or both).
130/// Every reference cycle involving `Value`s will contain at least
131/// one `Thunk`.
132#[derive(Clone, Debug)]
133pub struct Thunk(Rc<RefCell<ThunkRepr>>);
134
135impl Thunk {
136    pub fn new_closure(lambda: Rc<Lambda>) -> Self {
137        Thunk(Rc::new(RefCell::new(ThunkRepr::Evaluated(Value::Closure(
138            Rc::new(Closure {
139                upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
140                lambda: lambda.clone(),
141            }),
142        )))))
143    }
144
145    pub fn new_suspended(lambda: Rc<Lambda>, span: Span) -> Self {
146        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
147            upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
148            lambda: lambda.clone(),
149            span,
150        })))
151    }
152
153    pub fn new_suspended_native(native: Box<dyn Fn() -> Result<Value, ErrorKind>>) -> Self {
154        Thunk(Rc::new(RefCell::new(ThunkRepr::Native(SuspendedNative(
155            native,
156        )))))
157    }
158
159    /// Helper function to create a [`Thunk`] that calls a function given as the
160    /// [`Value`] `callee` with the argument `arg` when it is forced. This is
161    /// particularly useful in builtin implementations if the result of calling
162    /// a function does not need to be forced immediately, because e.g. it is
163    /// stored in an attribute set.
164    pub fn new_suspended_call(callee: Value, arg: Value, span: Span) -> Self {
165        let mut lambda = Lambda::default();
166
167        let arg_idx = lambda.chunk().push_constant(arg);
168        let f_idx = lambda.chunk().push_constant(callee);
169
170        // This is basically a recreation of compile_apply():
171        // We need to push the argument onto the stack and then the function.
172        // The function (not the argument) needs to be forced before calling.
173        lambda.chunk.push_op(Op::Constant, span);
174        lambda.chunk.push_uvarint(arg_idx.0 as u64);
175        lambda.chunk.push_op(Op::Constant, span);
176        lambda.chunk.push_uvarint(f_idx.0 as u64);
177        lambda.chunk.push_op(Op::Force, span);
178        lambda.chunk.push_op(Op::Call, span);
179
180        // Inform the VM that the chunk has ended
181        lambda.chunk.push_op(Op::Return, span);
182
183        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
184            upvalues: Rc::new(Upvalues::with_capacity(0)),
185            lambda: Rc::new(lambda),
186            span,
187        })))
188    }
189
190    fn prepare_blackhole(&self, forced_at: Span) -> ThunkRepr {
191        match &*self.0.borrow() {
192            ThunkRepr::Suspended { span, lambda, .. } => ThunkRepr::Blackhole {
193                forced_at,
194                suspended_at: Some(*span),
195                content_span: Some(lambda.chunk.first_span()),
196            },
197
198            _ => ThunkRepr::Blackhole {
199                forced_at,
200                suspended_at: None,
201                content_span: None,
202            },
203        }
204    }
205
206    pub async fn force(myself: Thunk, co: GenCo, span: Span) -> Result<Value, ErrorKind> {
207        Self::force_(myself, &co, span).await
208    }
209    pub async fn force_(mut myself: Thunk, co: &GenCo, span: Span) -> Result<Value, ErrorKind> {
210        // This vector of "thunks which point to the thunk-being-forced", to
211        // be updated along with it, is necessary in order to write this
212        // function in iterative (and later, mutual-tail-call) form.
213        let mut also_update: Vec<Rc<RefCell<ThunkRepr>>> = vec![];
214
215        loop {
216            // If the current thunk is already fully evaluated, return its evaluated
217            // value. The VM will continue running the code that landed us here.
218            if myself.is_forced() {
219                let val = myself.unwrap_or_clone();
220                for other_thunk in also_update.into_iter() {
221                    other_thunk.replace(ThunkRepr::Evaluated(val.clone()));
222                }
223                return Ok(val);
224            }
225
226            // Begin evaluation of this thunk by marking it as a blackhole, meaning
227            // that any other forcing frame encountering this thunk before its
228            // evaluation is completed detected an evaluation cycle.
229            let inner = myself.0.replace(myself.prepare_blackhole(span));
230
231            match inner {
232                // If there was already a blackhole in the thunk, this is an
233                // evaluation cycle.
234                ThunkRepr::Blackhole {
235                    forced_at,
236                    suspended_at,
237                    content_span,
238                } => {
239                    return Err(ErrorKind::InfiniteRecursion {
240                        first_force: forced_at,
241                        suspended_at,
242                        content_span,
243                    })
244                }
245
246                // If there is a native function stored in the thunk, evaluate it
247                // and replace this thunk's representation with the result.
248                ThunkRepr::Native(native) => {
249                    let value = native.0()?;
250                    myself.0.replace(ThunkRepr::Evaluated(value));
251                    continue;
252                }
253
254                // When encountering a suspended thunk, request that the VM enters
255                // it and produces the result.
256                ThunkRepr::Suspended {
257                    lambda,
258                    upvalues,
259                    span,
260                } => {
261                    // TODO(amjoseph): use #[tailcall::mutual] here.  This can
262                    // be turned into a tailcall to vm::execute_bytecode() by
263                    // passing `also_update` to it.
264                    let value = generators::request_enter_lambda(co, lambda, upvalues, span).await;
265                    myself.0.replace(ThunkRepr::Evaluated(value));
266                    continue;
267                }
268
269                // nested thunks -- try to flatten before forcing
270                ThunkRepr::Evaluated(Value::Thunk(inner_thunk)) => {
271                    match Rc::try_unwrap(inner_thunk.0) {
272                        Ok(refcell) => {
273                            // we are the only reference to the inner thunk,
274                            // so steal it
275                            myself.0.replace(refcell.into_inner());
276                            continue;
277                        }
278                        Err(rc) => {
279                            let inner_thunk = Thunk(rc);
280                            if inner_thunk.is_forced() {
281                                // tail call to force the inner thunk; note that
282                                // this means the outer thunk remains unforced
283                                // even after calling force() on it; however the
284                                // next time it is forced we will be one
285                                // thunk-forcing closer to it being
286                                // fully-evaluated.
287                                myself
288                                    .0
289                                    .replace(ThunkRepr::Evaluated(inner_thunk.value().clone()));
290                                continue;
291                            }
292                            also_update.push(myself.0.clone());
293                            myself = inner_thunk;
294                            continue;
295                        }
296                    }
297                }
298
299                ThunkRepr::Evaluated(val) => {
300                    return Ok(val);
301                }
302            }
303        }
304    }
305
306    pub fn finalise(&self, stack: &[Value]) {
307        self.upvalues_mut().resolve_deferred_upvalues(stack);
308    }
309
310    pub fn is_evaluated(&self) -> bool {
311        matches!(*self.0.borrow(), ThunkRepr::Evaluated(_))
312    }
313
314    pub fn is_suspended(&self) -> bool {
315        matches!(
316            *self.0.borrow(),
317            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_)
318        )
319    }
320
321    /// Returns true if forcing this thunk will not change it.
322    pub fn is_forced(&self) -> bool {
323        self.0.borrow().is_forced()
324    }
325
326    /// Returns a reference to the inner evaluated value of a thunk.
327    /// It is an error to call this on a thunk that has not been
328    /// forced, or is not otherwise known to be fully evaluated.
329    // Note: Due to the interior mutability of thunks this is
330    // difficult to represent in the type system without impacting the
331    // API too much.
332    pub fn value(&self) -> Ref<Value> {
333        Ref::map(self.0.borrow(), |thunk| match thunk {
334            ThunkRepr::Evaluated(value) => value,
335            ThunkRepr::Blackhole { .. } => panic!("Thunk::value called on a black-holed thunk"),
336            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
337                panic!("Thunk::value called on a suspended thunk")
338            }
339        })
340    }
341
342    /// Returns the inner evaluated value of a thunk, cloning it if
343    /// the Rc has more than one strong reference.  It is an error
344    /// to call this on a thunk that has not been forced, or is not
345    /// otherwise known to be fully evaluated.
346    fn unwrap_or_clone(self) -> Value {
347        match Rc::try_unwrap(self.0) {
348            Ok(refcell) => refcell.into_inner().expect(),
349            Err(rc) => Ref::map(rc.borrow(), |thunkrepr| thunkrepr.expect_ref()).clone(),
350        }
351    }
352
353    pub fn upvalues(&self) -> Ref<'_, Upvalues> {
354        Ref::map(self.0.borrow(), |thunk| match thunk {
355            ThunkRepr::Suspended { upvalues, .. } => upvalues.as_ref(),
356            ThunkRepr::Evaluated(Value::Closure(c)) => &c.upvalues,
357            _ => panic!("upvalues() on non-suspended thunk"),
358        })
359    }
360
361    pub fn upvalues_mut(&self) -> RefMut<'_, Upvalues> {
362        RefMut::map(self.0.borrow_mut(), |thunk| match thunk {
363            ThunkRepr::Suspended { upvalues, .. } => Rc::get_mut(upvalues).unwrap(),
364            ThunkRepr::Evaluated(Value::Closure(c)) => Rc::get_mut(
365                &mut Rc::get_mut(c).unwrap().upvalues,
366            )
367            .expect(
368                "upvalues_mut() was called on a thunk which already had multiple references to it",
369            ),
370            thunk => panic!("upvalues() on non-suspended thunk: {thunk:?}"),
371        })
372    }
373
374    /// Do not use this without first reading and understanding
375    /// `snix/docs/value-pointer-equality.md`.
376    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
377        if Rc::ptr_eq(&self.0, &other.0) {
378            return true;
379        }
380        match &*self.0.borrow() {
381            ThunkRepr::Evaluated(Value::Closure(c1)) => match &*other.0.borrow() {
382                ThunkRepr::Evaluated(Value::Closure(c2)) => Rc::ptr_eq(c1, c2),
383                _ => false,
384            },
385            _ => false,
386        }
387    }
388
389    /// Helper function to format thunks in observer output.
390    pub(crate) fn debug_repr(&self) -> String {
391        self.0.borrow().debug_repr()
392    }
393}
394
395impl TotalDisplay for Thunk {
396    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
397        if !set.insert(self) {
398            return f.write_str("<CYCLE>");
399        }
400
401        match &*self.0.borrow() {
402            ThunkRepr::Evaluated(v) => v.total_fmt(f, set),
403            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => f.write_str("<CODE>"),
404            other => write!(f, "internal[{}]", other.debug_repr()),
405        }
406    }
407}
408
409/// A wrapper type for tracking which thunks have already been seen
410/// in a context. This is necessary for printing and deeply forcing
411/// cyclic non-diverging data structures like `rec { f = [ f ]; }`.
412/// This is separate from the ThunkRepr::Blackhole mechanism, which
413/// detects diverging data structures like `(rec { f = f; }).f`.
414///
415/// The inner `HashSet` is not available on the outside, as it would be
416/// potentially unsafe to interact with the pointers in the set.
417#[derive(Default)]
418pub struct ThunkSet(FxHashSet<*const ThunkRepr>);
419
420impl ThunkSet {
421    /// Check whether the given thunk has already been seen. Will mark the thunk
422    /// as seen otherwise.
423    pub fn insert(&mut self, thunk: &Thunk) -> bool {
424        let ptr: *const ThunkRepr = thunk.0.as_ptr();
425        self.0.insert(ptr)
426    }
427}