diff --git a/src/xatu/BUILD b/src/xatu/BUILD index f60733b..2a35aeb 100644 --- a/src/xatu/BUILD +++ b/src/xatu/BUILD @@ -11,6 +11,7 @@ rust_library( "rules/manual_inherit.rs", "rules/manual_inherit_from.rs", "rules/unquoted_uri.rs", + "scope_graph.rs", "xatu.rs", ], deps = [ @@ -26,6 +27,15 @@ rust_test( deps = [":xatu_lib"], ) +rust_test( + name = "scope_graph_test", + srcs = ["scope_graph_test.rs"], + deps = [ + ":xatu_lib", + "//third_party/crates:googletest", + ], +) + rust_test( name = "xatu_test", srcs = ["xatu_test.rs"], diff --git a/src/xatu/scope_graph.rs b/src/xatu/scope_graph.rs new file mode 100644 index 0000000..8bf577d --- /dev/null +++ b/src/xatu/scope_graph.rs @@ -0,0 +1,172 @@ +#[derive(Debug)] +pub struct ScopeGraph { + scopes: Vec, + declarations: Vec, + references: Vec, +} + +impl Default for ScopeGraph { + fn default() -> Self { + Self::new() + } +} + +impl ScopeGraph { + pub fn new() -> Self { + Self { + scopes: vec![Scope::new(None)], + declarations: Vec::new(), + references: Vec::new(), + } + } + + pub fn root_scope(&self) -> ScopeId { + ScopeId(0) + } + + pub fn add_scope(&mut self, parent: ScopeId) -> ScopeId { + self.scope(parent); + let scope_id = ScopeId(self.scopes.len()); + + self.scopes.push(Scope::new(Some(parent))); + + scope_id + } + + pub fn add_declaration(&mut self, scope: ScopeId, name: impl Into) -> DeclarationId { + let declaration_id = DeclarationId(self.declarations.len()); + self.declarations.push(Declaration { + scope, + name: name.into(), + }); + self.scope_mut(scope).declarations.push(declaration_id); + + declaration_id + } + + pub fn add_reference(&mut self, scope: ScopeId, name: impl Into) -> ReferenceId { + let reference_id = ReferenceId(self.references.len()); + self.references.push(Reference { + scope, + name: name.into(), + }); + self.scope_mut(scope).references.push(reference_id); + + reference_id + } + + pub fn declaration_scope(&self, declaration: DeclarationId) -> ScopeId { + self.declaration(declaration).scope + } + + pub fn reference_scope(&self, reference: ReferenceId) -> ScopeId { + self.reference(reference).scope + } + + pub fn resolve(&self, reference: ReferenceId) -> Vec { + let reference = self.reference(reference); + let mut scope_id = reference.scope; + let mut steps = Vec::new(); + + loop { + let scope = self.scope(scope_id); + let declarations = + scope.declarations.iter().copied().filter(|declaration_id| { + self.declaration(*declaration_id).name == reference.name + }); + + let resolutions = declarations + .map(|declaration| { + let mut path = steps.clone(); + path.push(PathStep::Declaration(declaration)); + Resolution { + declaration, + path: ResolutionPath { steps: path }, + } + }) + .collect::>(); + + if !resolutions.is_empty() { + return resolutions; + } + + let Some(parent) = scope.parent else { + return Vec::new(); + }; + + steps.push(PathStep::Parent); + scope_id = parent; + } + } + + fn scope(&self, scope: ScopeId) -> &Scope { + &self.scopes[scope.0] + } + + fn scope_mut(&mut self, scope: ScopeId) -> &mut Scope { + &mut self.scopes[scope.0] + } + + fn declaration(&self, declaration: DeclarationId) -> &Declaration { + &self.declarations[declaration.0] + } + + fn reference(&self, reference: ReferenceId) -> &Reference { + &self.references[reference.0] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ScopeId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DeclarationId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ReferenceId(usize); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Resolution { + pub declaration: DeclarationId, + pub path: ResolutionPath, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolutionPath { + pub steps: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PathStep { + Parent, + Declaration(DeclarationId), +} + +#[derive(Debug)] +struct Scope { + parent: Option, + declarations: Vec, + references: Vec, +} + +impl Scope { + fn new(parent: Option) -> Self { + Self { + parent, + declarations: Vec::new(), + references: Vec::new(), + } + } +} + +#[derive(Debug)] +struct Declaration { + scope: ScopeId, + name: String, +} + +#[derive(Debug)] +struct Reference { + scope: ScopeId, + name: String, +} diff --git a/src/xatu/scope_graph_test.rs b/src/xatu/scope_graph_test.rs new file mode 100644 index 0000000..69e27f6 --- /dev/null +++ b/src/xatu/scope_graph_test.rs @@ -0,0 +1,76 @@ +use googletest::matchers::eq; +use googletest::{expect_that, gtest}; +use xatu::{PathStep, Resolution, ResolutionPath, ScopeGraph}; + +#[gtest] +fn resolves_a_local_declaration() { + let mut graph = ScopeGraph::new(); + let scope = graph.root_scope(); + let declaration = graph.add_declaration(scope, "value"); + let reference = graph.add_reference(scope, "value"); + + let expected = vec![resolution(declaration, vec![PathStep::Declaration(declaration)])]; + expect_that!(graph.resolve(reference), eq(&expected)); +} + +#[gtest] +fn resolves_a_parent_declaration() { + let mut graph = ScopeGraph::new(); + let parent = graph.root_scope(); + let declaration = graph.add_declaration(parent, "value"); + let child = graph.add_scope(parent); + let reference = graph.add_reference(child, "value"); + + let expected = vec![resolution( + declaration, + vec![PathStep::Parent, PathStep::Declaration(declaration)], + )]; + expect_that!(graph.resolve(reference), eq(&expected)); +} + +#[gtest] +fn local_declarations_shadow_parent_declarations() { + let mut graph = ScopeGraph::new(); + let parent = graph.root_scope(); + graph.add_declaration(parent, "value"); + let child = graph.add_scope(parent); + let declaration = graph.add_declaration(child, "value"); + let reference = graph.add_reference(child, "value"); + + let expected = vec![resolution(declaration, vec![PathStep::Declaration(declaration)])]; + expect_that!(graph.resolve(reference), eq(&expected)); +} + +#[gtest] +fn preserves_ambiguous_local_declarations() { + let mut graph = ScopeGraph::new(); + let scope = graph.root_scope(); + let first = graph.add_declaration(scope, "value"); + let second = graph.add_declaration(scope, "value"); + let reference = graph.add_reference(scope, "value"); + + let expected = vec![ + resolution(first, vec![PathStep::Declaration(first)]), + resolution(second, vec![PathStep::Declaration(second)]), + ]; + expect_that!(graph.resolve(reference), eq(&expected)); +} + +#[gtest] +fn leaves_unresolved_references_without_resolutions() { + let mut graph = ScopeGraph::new(); + let reference = graph.add_reference(graph.root_scope(), "value"); + + let expected = Vec::::new(); + expect_that!(graph.resolve(reference), eq(&expected)); +} + +fn resolution( + declaration: xatu::DeclarationId, + steps: Vec, +) -> Resolution { + Resolution { + declaration, + path: ResolutionPath { steps }, + } +} diff --git a/src/xatu/xatu.rs b/src/xatu/xatu.rs index bc8b40f..a906eae 100644 --- a/src/xatu/xatu.rs +++ b/src/xatu/xatu.rs @@ -2,9 +2,13 @@ mod ast; mod file_store; mod parser; mod rules; +mod scope_graph; pub use file_store::{FileId, FileStore, SourceFile}; pub use rules::{Diagnostic, Finding}; +pub use scope_graph::{ + DeclarationId, PathStep, ReferenceId, Resolution, ResolutionPath, ScopeGraph, ScopeId, +}; #[derive(Debug)] pub struct Report {