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}