snix_eval/compiler/scope.rs
1//! This module implements the scope-tracking logic of the Snix
2//! compiler.
3//!
4//! Scoping in Nix is fairly complicated, there are features like
5//! mutually recursive bindings, `with`, upvalue capturing, and so
6//! on that introduce a fair bit of complexity.
7//!
8//! Snix attempts to do as much of the heavy lifting of this at
9//! compile time, and leave the runtime to mostly deal with known
10//! stack indices. To do this, the compiler simulates where locals
11//! will be at runtime using the data structures implemented here.
12
13use rustc_hash::FxHashMap;
14use std::{collections::hash_map, ops::Index};
15
16use smol_str::SmolStr;
17
18use crate::opcode::{StackIdx, UpvalueIdx};
19
20#[derive(Debug)]
21enum LocalName {
22 /// Normally declared local with a statically known name.
23 Ident(String),
24
25 /// Phantom stack value (e.g. attribute set used for `with`) that
26 /// must be accounted for to calculate correct stack offsets.
27 Phantom,
28}
29
30/// Represents a single local already known to the compiler.
31#[derive(Debug)]
32pub struct Local {
33 /// Identifier of this local. This is always a statically known
34 /// value (Nix does not allow dynamic identifier names in locals),
35 /// or a "phantom" value not accessible by users.
36 name: LocalName,
37
38 /// Source span at which this local was declared.
39 pub span: Option<codemap::Span>,
40
41 /// Scope depth of this local.
42 pub depth: usize,
43
44 /// Is this local initialised?
45 pub initialised: bool,
46
47 /// Is this local known to have been used at all?
48 pub used: bool,
49
50 /// Does this local need to be finalised after the enclosing scope
51 /// is completely constructed?
52 pub needs_finaliser: bool,
53
54 /// Does this local's upvalues contain a reference to itself?
55 pub must_thunk: bool,
56}
57
58impl Local {
59 /// Retrieve the name of the given local (if available).
60 pub fn name(&self) -> Option<SmolStr> {
61 match &self.name {
62 LocalName::Phantom => None,
63 LocalName::Ident(name) => Some(SmolStr::new(name)),
64 }
65 }
66
67 /// Is this local intentionally ignored? (i.e. name starts with `_`)
68 pub fn is_ignored(&self) -> bool {
69 match &self.name {
70 LocalName::Ident(name) => name.starts_with('_'),
71 LocalName::Phantom => false,
72 }
73 }
74
75 pub fn is_used(&self) -> bool {
76 self.depth == 0 || self.used || self.is_ignored()
77 }
78}
79
80/// Represents the current position of an identifier as resolved in a scope.
81pub enum LocalPosition {
82 /// Local is not known in this scope.
83 Unknown,
84
85 /// Local is known at the given local index.
86 Known(LocalIdx),
87
88 /// Local is known, but is being accessed recursively within its
89 /// own initialisation. Depending on context, this is either an
90 /// error or forcing a closure/thunk.
91 Recursive(LocalIdx),
92}
93
94/// Represents the different ways in which upvalues can be captured in
95/// closures or thunks.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum UpvalueKind {
98 /// This upvalue captures a local from the stack.
99 Local(LocalIdx),
100
101 /// This upvalue captures an enclosing upvalue.
102 Upvalue(UpvalueIdx),
103}
104
105#[derive(Clone, Debug)]
106pub struct Upvalue {
107 pub kind: UpvalueKind,
108}
109
110/// The index of a local in the scope's local array at compile time.
111#[repr(transparent)]
112#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
113pub struct LocalIdx(usize);
114
115/// Helper struct for indexing over `Scope::locals` by name.
116#[derive(Debug)]
117enum ByName {
118 Single(LocalIdx),
119 Shadowed(Vec<LocalIdx>),
120}
121
122impl ByName {
123 /// Add an additional index for this name.
124 fn add_idx(&mut self, new: LocalIdx) {
125 match self {
126 ByName::Shadowed(indices) => indices.push(new),
127 ByName::Single(idx) => {
128 *self = ByName::Shadowed(vec![*idx, new]);
129 }
130 }
131 }
132
133 /// Remove the most recent index for this name, unless it is a
134 /// single. Returns `true` if an entry was removed.
135 fn remove_idx(&mut self) -> bool {
136 match self {
137 ByName::Single(_) => false,
138 ByName::Shadowed(indices) => match indices[..] {
139 [fst, _snd] => {
140 *self = ByName::Single(fst);
141 true
142 }
143 _ => {
144 indices.pop();
145 true
146 }
147 },
148 }
149 }
150
151 /// Return the most recent index.
152 pub fn index(&self) -> LocalIdx {
153 match self {
154 ByName::Single(idx) => *idx,
155 ByName::Shadowed(vec) => *vec.last().unwrap(),
156 }
157 }
158}
159
160/// Represents a scope known during compilation, which can be resolved
161/// directly to stack indices.
162#[derive(Debug, Default)]
163pub struct Scope {
164 locals: Vec<Local>,
165 pub upvalues: Vec<Upvalue>,
166
167 /// Secondary by-name index over locals.
168 by_name: FxHashMap<String, ByName>,
169
170 /// How many scopes "deep" are these locals?
171 scope_depth: usize,
172
173 /// Current size of the `with`-stack at runtime.
174 with_stack_size: usize,
175}
176
177impl Index<LocalIdx> for Scope {
178 type Output = Local;
179
180 fn index(&self, index: LocalIdx) -> &Self::Output {
181 &self.locals[index.0]
182 }
183}
184
185impl Scope {
186 /// Inherit scope details from a parent scope (required for
187 /// correctly nesting scopes in lambdas and thunks when special
188 /// scope features like dynamic resolution are present).
189 pub fn inherit(&self) -> Self {
190 Self {
191 scope_depth: self.scope_depth + 1,
192 with_stack_size: self.with_stack_size,
193 ..Default::default()
194 }
195 }
196
197 /// Increase the `with`-stack size of this scope.
198 pub fn push_with(&mut self) {
199 self.with_stack_size += 1;
200 }
201
202 /// Decrease the `with`-stack size of this scope.
203 pub fn pop_with(&mut self) {
204 self.with_stack_size -= 1;
205 }
206
207 /// Does this scope currently require dynamic runtime resolution
208 /// of identifiers that could not be found?
209 pub fn has_with(&self) -> bool {
210 self.with_stack_size > 0
211 }
212
213 /// Resolve the stack index of a statically known local.
214 pub fn resolve_local(&self, name: &str) -> LocalPosition {
215 if let Some(by_name) = self.by_name.get(name) {
216 let idx = by_name.index();
217 let local = &self.locals[idx.0];
218
219 // This local is still being initialised, meaning that
220 // we know its final runtime stack position, but it is
221 // not yet on the stack.
222 if !local.initialised {
223 return LocalPosition::Recursive(idx);
224 }
225
226 return LocalPosition::Known(idx);
227 }
228
229 LocalPosition::Unknown
230 }
231
232 /// Declare a local variable that occupies a stack slot and should
233 /// be accounted for, but is not directly accessible by users
234 /// (e.g. attribute sets used for `with`).
235 pub fn declare_phantom(&mut self, span: codemap::Span, initialised: bool) -> LocalIdx {
236 let idx = self.locals.len();
237 self.locals.push(Local {
238 initialised,
239 span: Some(span),
240 name: LocalName::Phantom,
241 depth: self.scope_depth,
242 needs_finaliser: false,
243 must_thunk: false,
244 used: true,
245 });
246
247 LocalIdx(idx)
248 }
249
250 /// Declare an uninitialised, named local variable.
251 ///
252 /// Returns the `LocalIdx` of the new local, and optionally the
253 /// index of a previous local shadowed by this one.
254 pub fn declare_local(
255 &mut self,
256 name: String,
257 span: codemap::Span,
258 ) -> (LocalIdx, Option<LocalIdx>) {
259 let idx = LocalIdx(self.locals.len());
260 self.locals.push(Local {
261 name: LocalName::Ident(name.clone()),
262 span: Some(span),
263 depth: self.scope_depth,
264 initialised: false,
265 needs_finaliser: false,
266 must_thunk: false,
267 used: false,
268 });
269
270 let mut shadowed = None;
271 match self.by_name.entry(name) {
272 hash_map::Entry::Occupied(mut entry) => {
273 let existing = entry.get_mut();
274 shadowed = Some(existing.index());
275 existing.add_idx(idx);
276 }
277 hash_map::Entry::Vacant(entry) => {
278 entry.insert(ByName::Single(idx));
279 }
280 }
281
282 (idx, shadowed)
283 }
284
285 pub fn declare_constant(&mut self, name: String) -> LocalIdx {
286 let idx = LocalIdx(self.locals.len());
287 self.locals.push(Local {
288 name: LocalName::Ident(name.clone()),
289 span: None,
290 depth: 0,
291 initialised: true,
292 used: false,
293 needs_finaliser: false,
294 must_thunk: false,
295 });
296 // We don't need to worry about shadowing for constants; they're defined at the toplevel
297 // always
298 self.by_name.insert(name, ByName::Single(idx));
299 idx
300 }
301
302 /// Mark local as initialised after compiling its expression.
303 pub fn mark_initialised(&mut self, idx: LocalIdx) {
304 self.locals[idx.0].initialised = true;
305 }
306
307 pub fn mark_used(&mut self, idx: LocalIdx) {
308 self.locals[idx.0].used = true;
309 }
310
311 /// Mark local as needing a finaliser.
312 pub fn mark_needs_finaliser(&mut self, idx: LocalIdx) {
313 self.locals[idx.0].needs_finaliser = true;
314 }
315
316 /// Mark local as must be wrapped in a thunk. This happens if
317 /// the local has a reference to itself in its upvalues.
318 pub fn mark_must_thunk(&mut self, idx: LocalIdx) {
319 self.locals[idx.0].must_thunk = true;
320 }
321
322 /// Compute the runtime stack index for a given local by
323 /// accounting for uninitialised variables at scopes below this
324 /// one.
325 pub fn stack_index(&self, idx: LocalIdx) -> StackIdx {
326 let uninitialised_count = self.locals[..(idx.0)]
327 .iter()
328 .filter(|l| !l.initialised && self[idx].depth > l.depth)
329 .count();
330
331 StackIdx(idx.0 - uninitialised_count)
332 }
333
334 /// Increase the current scope depth (e.g. within a new bindings
335 /// block, or `with`-scope). Pass a short descriptive string as
336 /// `name`. This will be used in debug builds to aid debugging
337 /// scoping issues. The returned [ScopeGuard] must be passed back
338 /// to `end_scope`.
339 pub fn begin_scope(&mut self, name: &'static str) -> ScopeGuard {
340 self.scope_depth += 1;
341 ScopeGuard::new(self.scope_depth, name)
342 }
343
344 /// Decrease the scope depth and remove all locals still tracked
345 /// for the current scope.
346 /// Pass in the [ScopeGuard] instance you obtained when opening
347 /// this scope via [Scope::begin_scope]. The [ScopeGuard]
348 /// instances have to be passed on the correct order. In other
349 /// words: the last scope opened must be the first one closed. If
350 /// the order mismatched this will panic in debug builds.
351 ///
352 /// Returns the count of locals that were dropped while marked as
353 /// initialised (used by the compiler to determine whether to emit
354 /// scope cleanup operations), as well as the spans of the
355 /// definitions of unused locals (used by the compiler to emit
356 /// unused binding warnings).
357 pub fn end_scope(&mut self, guard: ScopeGuard) -> (usize, Vec<codemap::Span>) {
358 debug_assert!(self.scope_depth != 0, "can not end top scope");
359
360 guard.close(self.scope_depth);
361
362 let mut pops = 0;
363 let mut unused_spans = vec![];
364
365 // TL;DR - iterate from the back while things belonging to the
366 // ended scope still exist.
367 while self.locals.last().unwrap().depth == self.scope_depth {
368 if let Some(local) = self.locals.pop() {
369 // pop the local from the stack if it was actually
370 // initialised
371 if local.initialised {
372 pops += 1;
373 }
374
375 // analyse whether the local was accessed during its
376 // lifetime, and emit a warning otherwise (unless the
377 // user explicitly chose to ignore it by prefixing the
378 // identifier with `_`)
379 if !local.is_used() {
380 unused_spans.extend(local.span);
381 }
382
383 // remove the by-name index if this was a named local
384 if let LocalName::Ident(name) = local.name
385 && let hash_map::Entry::Occupied(mut entry) = self.by_name.entry(name)
386 {
387 // If no removal occurred through `remove_idx`
388 // (i.e. there was no shadowing going on),
389 // nuke the whole entry.
390 if !entry.get_mut().remove_idx() {
391 entry.remove();
392 }
393 }
394 }
395 }
396
397 self.scope_depth -= 1;
398
399 (pops, unused_spans)
400 }
401
402 /// Access the current scope depth.
403 pub fn scope_depth(&self) -> usize {
404 self.scope_depth
405 }
406}
407
408#[must_use = "ScopeGuards are there to ensure a given scope is closed and not simply forgotten about. Return it via the end_scope function."]
409/// Guard that helps uphold the scope depth correctness invariant.
410///
411/// Scopes should be closed and they should be closed in the right
412/// order. If that isn't the case the compiler has somehow lost track
413/// of scopes and the resulting program will not be correct.
414pub(crate) struct ScopeGuard {
415 #[cfg(debug_assertions)]
416 depth: usize,
417
418 #[cfg(debug_assertions)]
419 name: &'static str,
420}
421
422impl ScopeGuard {
423 fn new(depth: usize, name: &'static str) -> Self {
424 #[cfg(not(debug_assertions))]
425 let _ = (depth, name);
426
427 Self {
428 #[cfg(debug_assertions)]
429 depth,
430 #[cfg(debug_assertions)]
431 name,
432 }
433 }
434
435 fn close(self, depth: usize) {
436 #[cfg(debug_assertions)]
437 assert_eq!(
438 self.depth, depth,
439 "Snix bug: the scope `{}` was closed out of order. The guard is for depth {} but was used to close {}.",
440 self.name, self.depth, depth
441 );
442
443 let _ = depth;
444
445 // don't run drop on this; drop panics in case someone didn't
446 // close the scope e.g. didn't call this function
447 std::mem::forget(self);
448 }
449}
450
451impl Drop for ScopeGuard {
452 fn drop(&mut self) {
453 // the impl is always there as otherwise clippy will complain
454 // about the `std::mem::forget(…)` in the happy path.
455 #[cfg(debug_assertions)]
456 if !std::thread::panicking() {
457 panic!(
458 "Snix bug: the scope `{}` for level {} was opened but never closed.",
459 self.name, self.depth
460 );
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::ScopeGuard;
468
469 #[test]
470 #[should_panic(
471 expected = "Snix bug: the scope `test-invalid-depth` was closed out of order. The guard is for depth 1 but was used to close 2."
472 )]
473 fn scope_guard_panics_with_invalid_depth() {
474 let level = 1;
475 let guard = ScopeGuard::new(level, "test-invalid-depth");
476 guard.close(level + 1);
477 }
478
479 #[test]
480 fn scope_guard_doest_not_panics_with_valid_depth() {
481 let level = 1;
482 let guard = ScopeGuard::new(level, "test-valid-depth");
483 guard.close(level);
484 }
485
486 #[test]
487 #[should_panic(
488 expected = "Snix bug: the scope `test-panic-on-drop` for level 1 was opened but never closed."
489 )]
490 fn scope_guard_panics_when_dropped() {
491 let level = 1;
492 let guard = ScopeGuard::new(level, "test-panic-on-drop");
493 drop(guard);
494 }
495}