stack_graphs/
lib.rs

1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2021, stack-graphs authors.
4// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
5// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
6// ------------------------------------------------------------------------------------------------
7
8//! Stack graphs provide a single framework for performing name resolution for any programming
9//! language, while abstracting away the specific name resolution rules for each of those
10//! languages. The basic idea is to represent the _definitions_ and _references_ in a program using
11//! graph.  A _name binding_ maps a reference to all of the possible definitions that the reference
12//! could refer to.  Because we’ve represented definitions and references as a graph, bindings are
13//! represented by paths within that graph.
14//!
15//! While searching for a path in an incremental stack graph, we keep track of two stacks: a
16//! _symbol stack_ and a _scope stack_. Broadly speaking, the symbol stack keeps track of what
17//! symbols we’re trying to resolve, while the scope stack gives us control over which particular
18//! scopes we look for those symbols in.
19//!
20//! ## Relationship to scope graphs
21//!
22//! Stack graphs are based in the [scope graphs][] formalism from Eelco Visser's group at TU Delft.
23//! Scope graphs also model the name binding structure of a program using a graph, and uses paths
24//! within that graph to represent which definitions a reference might refer to.
25//!
26//! [scope graphs]: https://pl.ewi.tudelft.nl/research/projects/scope-graphs/
27//!
28//! Stack graphs add _incrementality_ to scope graphs.  An incremental analysis is one where we
29//! can reuse analysis results for source code files that haven't changed.  In a non-incremental
30//! analysis, we have to reanalyze the entire contents of a repository or package when _any_ file
31//! is changed.
32//!
33//! As one example, the [_Scopes as Types_][] paper presents some rewrite rules that let you handle
34//! field access in a record or object type — for instance, being able to resolve the `foo` part of
35//! `A.foo`. In _Scopes as Types_, we’d handle this by performing the path-finding algorithm _when
36//! constructing the graph_, to find out what `A` resolves to. Once we find its definition, we then
37//! attach the reference to `foo` to that result.
38//!
39//! [_Scopes as Types_]: https://eelcovisser.org/blog/video/paper/2018/10/27/scopes-as-types/
40//!
41//! This is not incremental because the reference to `foo` “lives” over in some unrelated part of
42//! the graph.  If `A` is defined in a different file (or worse, a different package), then we have
43//! to update that distant part of the graph with the node representing the reference.  We end up
44//! cluttering the graph for a class definition with nodes representing _all_ of its field
45//! references, which is especially problematic for popular packages with lots of downstream
46//! dependencies.
47//!
48//! Our key insight is to recognize that when resolving `A.foo`, we have to “pause” the resolution
49//! of `foo` to start resolving `A`.  Once we’ve found the binding for `A`, we can resume the
50//! original resolution of `foo`.  This describes a stack!  So instead of having our path-finding
51//! algorithm look for exactly one symbol, we can keep track of a _stack_ of things to look for.
52//! To correctly resolve `foo`, we do still have to eventually make our way over to the part of the
53//! graph where `A` is defined.  But by having a stack of path-finding targets, we can resolve the
54//! reference to `A.foo` with a _single_ execution of the path-finding algorithm.  And most
55//! importantly, each “chunk” of the overall graph only depends on “local” information from the
56//! original source file.  (a.k.a., it’s incremental!)
57
58use std::time::{Duration, Instant};
59
60use thiserror::Error;
61
62pub mod arena;
63pub mod assert;
64pub mod c;
65pub mod cycles;
66#[macro_use]
67mod debugging;
68pub mod graph;
69pub mod partial;
70pub mod paths;
71pub mod serde;
72pub mod stats;
73pub mod stitching;
74#[cfg(feature = "storage")]
75pub mod storage;
76pub(crate) mod utils;
77#[cfg(feature = "visualization")]
78pub mod visualization;
79
80/// Trait to signal that the execution is cancelled
81pub trait CancellationFlag {
82    fn check(&self, at: &'static str) -> Result<(), CancellationError>;
83}
84
85pub struct NoCancellation;
86impl CancellationFlag for NoCancellation {
87    fn check(&self, _at: &'static str) -> Result<(), CancellationError> {
88        Ok(())
89    }
90}
91
92pub struct CancelAfterDuration {
93    limit: Duration,
94    start: Instant,
95}
96
97impl CancelAfterDuration {
98    pub fn new(limit: Duration) -> Self {
99        Self {
100            limit,
101            start: Instant::now(),
102        }
103    }
104}
105
106impl CancellationFlag for CancelAfterDuration {
107    fn check(&self, at: &'static str) -> Result<(), CancellationError> {
108        if self.start.elapsed() > self.limit {
109            return Err(CancellationError(at));
110        }
111        Ok(())
112    }
113}
114
115#[derive(Clone, Debug, Error)]
116#[error("Cancelled at \"{0}\"")]
117pub struct CancellationError(pub &'static str);