feat(natu): detect single-use let bindings

This commit was merged in pull request #116.
This commit is contained in:
2026-09-30 10:43:05 -04:00
parent 2265dfc558
commit 78492739f3
7 changed files with 153 additions and 16 deletions

View File

@@ -14,7 +14,7 @@ impl ScopeGraph {
edges: Vec::new(),
lets: Vec::new(),
};
let root = graph.add_scope(None);
let root = graph.add_scope(None, ScopeKind::Root);
graph.build_expr(&file.root, root);
graph
}
@@ -36,7 +36,7 @@ impl ScopeGraph {
.flat_map(|binding| self.build_expr(&binding.value, scope))
.collect(),
ast::Expr::Lambda { parameters, body } => {
let scope = self.add_scope(Some(scope));
let scope = self.add_scope(Some(scope), ScopeKind::Lambda);
for parameter in parameters {
self.add_declaration(&parameter.name, scope);
}
@@ -62,13 +62,14 @@ impl ScopeGraph {
body: &ast::Expr,
parent: NodeId,
) -> Vec<NodeId> {
let scope = self.add_scope(Some(parent));
let scope = self.add_scope(Some(parent), ScopeKind::Let);
let mut graph_bindings = bindings
.iter()
.map(|binding| Binding {
declaration: self.add_declaration(&binding.key.name, scope),
name: binding.key.name.clone(),
references: Vec::new(),
is_lambda: matches!(binding.value, ast::Expr::Lambda { .. }),
range: binding.range,
})
.collect::<Vec<_>>();
@@ -92,6 +93,33 @@ impl ScopeGraph {
references
}
pub(crate) fn crosses_lambda_scope(&self, reference: NodeId, declaration: NodeId) -> bool {
let Some(mut scope) = self.edge_target(reference, Node::is_scope) else {
return false;
};
let Some(declaration_scope) = self
.edges
.iter()
.find(|edge| edge.to == declaration && self.nodes[edge.from.0].is_scope())
.map(|edge| edge.from)
else {
return false;
};
loop {
if scope == declaration_scope {
return false;
}
if matches!(self.nodes[scope.0], Node::Scope(ScopeKind::Lambda)) {
return true;
}
let Some(parent) = self.edge_target(scope, Node::is_scope) else {
return false;
};
scope = parent;
}
}
pub(crate) fn resolve(&self, reference: NodeId) -> Vec<NodeId> {
let Node::Reference(name) = &self.nodes[reference.0] else {
return Vec::new();
@@ -115,8 +143,8 @@ impl ScopeGraph {
}
}
fn add_scope(&mut self, parent: Option<NodeId>) -> NodeId {
let scope = self.add_node(Node::Scope);
fn add_scope(&mut self, parent: Option<NodeId>, kind: ScopeKind) -> NodeId {
let scope = self.add_node(Node::Scope(kind));
if let Some(parent) = parent {
self.add_edge(scope, parent);
}
@@ -165,17 +193,24 @@ impl ScopeGraph {
#[derive(Debug)]
enum Node {
Scope,
Scope(ScopeKind),
Declaration(Declaration),
Reference(String),
}
impl Node {
fn is_scope(&self) -> bool {
matches!(self, Self::Scope)
matches!(self, Self::Scope(_))
}
}
#[derive(Debug)]
enum ScopeKind {
Root,
Let,
Lambda,
}
#[derive(Debug)]
struct Declaration {
name: String,
@@ -202,5 +237,6 @@ pub(crate) struct Binding {
pub(crate) declaration: NodeId,
pub(crate) name: String,
pub(crate) references: Vec<NodeId>,
pub(crate) is_lambda: bool,
pub(crate) range: ast::SourceRange,
}