Skip to content

Commit

Permalink
Implement Definition::references() (#1243)
Browse files Browse the repository at this point in the history
Closes #1216

Implements `Definition::references()` in bindings API by eagerly
resolving all remaining unresolved references and then building a
reverse mapping from definitions to references.
  • Loading branch information
ggiraldez authored Feb 7, 2025
1 parent 15b1234 commit 99d182f
Show file tree
Hide file tree
Showing 18 changed files with 257 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/famous-monkeys-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/slang": minor
---

add `definition.references()` API to find all references that resolve to a definition.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ interface bindings {

/// A giant graph that contains name binding information for all source files within the compilation unit.
/// It stores cursors to all definitions and references, and can resolve the edges between them.
/// Most cursors pointing to identifier terminals will resolve to either a definition or a reference. For example, in `contract A is B {}` the cursor to identifier `A` will resolve to a definition, and the cursor to identifier `B` will resolve to a reference.
/// There is one specific case in which a cursor to an identifier resolves to both: a non-aliased symbol import `import {X} from "library"`, where the identifier `X` is both a definition and a reference (to the symbol exported from `"library"`).
/// Also, an identifier denoting a feature in a `pragma experimental` directive will not resolve to either.
resource binding-graph {
/// Tries to resolve the identifier terminal pointed at by the provided cursor to a definition.
/// If successful, returns the definition. Otherwise, returns `undefined`.
Expand Down Expand Up @@ -31,6 +34,9 @@ interface bindings {
/// Returns the location of the definition's definiens.
/// For `contract X {}`, that is the location of the parent `ContractDefinition` node.
definiens-location: func() -> binding-location;

/// Returns a list of all references that bind to this definition.
references: func() -> list<reference>;
}

/// Represents a reference in the binding graph.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ define_wrapper! { Definition {
fn definiens_location(&self) -> ffi::BindingLocation {
self._borrow_ffi().definiens_location()._into_ffi()
}

fn references(&self) -> Vec<ffi::Reference> {
self._borrow_ffi().references().iter().cloned().map(IntoFFI::_into_ffi).collect()
}
} }

//================================================
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/metaslang/bindings/generated/public_api.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/metaslang/bindings/src/graph/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::rc::Rc;
use metaslang_cst::cursor::Cursor;
use metaslang_cst::kinds::KindTypes;

use super::{BindingGraph, BindingLocation};
use super::{BindingGraph, BindingLocation, Reference};
use crate::builder::{FileDescriptor, GraphHandle};
use crate::graph::DisplayCursor;

Expand Down Expand Up @@ -57,6 +57,10 @@ impl<KT: KindTypes + 'static> Definition<KT> {
.get_file_descriptor(self.handle)
.expect("Definition to have a valid file descriptor")
}

pub fn references(&self) -> Vec<Reference<KT>> {
self.owner.resolve_definition(self.handle)
}
}

impl<KT: KindTypes + 'static> Display for Definition<KT> {
Expand Down
13 changes: 13 additions & 0 deletions crates/metaslang/bindings/src/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ impl<KT: KindTypes + 'static> BindingGraph<KT> {
})
.collect()
}

fn resolve_definition(self: &Rc<Self>, handle: GraphHandle) -> Vec<Reference<KT>> {
let mut resolver = self.resolver.borrow_mut();
resolver.ensure_all_references_resolved(&self.graph);
let references = resolver.definition_to_references(handle);
references
.iter()
.map(|handle| Reference {
owner: Rc::clone(self),
handle: *handle,
})
.collect()
}
}

struct DisplayCursor<'a, KT: KindTypes + 'static> {
Expand Down
45 changes: 45 additions & 0 deletions crates/metaslang/bindings/src/graph/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) struct Resolver {
partials: PartialPaths,
database: Database,
references: HashMap<GraphHandle, Vec<GraphHandle>>,
definitions: Option<HashMap<GraphHandle, Vec<GraphHandle>>>,
}

impl Resolver {
Expand All @@ -27,6 +28,7 @@ impl Resolver {
partials,
database,
references: HashMap::new(),
definitions: None,
};
resolver.build(graph);
resolver
Expand Down Expand Up @@ -182,6 +184,49 @@ impl Resolver {
Vec::new()
}
}

pub(crate) fn ensure_all_references_resolved<KT: KindTypes + 'static>(
&mut self,
graph: &ExtendedStackGraph<KT>,
) {
if self.definitions.is_some() {
return;
}

// Resolve all references
for handle in graph.iter_references() {
if !self.references.contains_key(&handle)
&& graph
.get_file_descriptor(handle)
.is_some_and(|file| file.is_user())
{
let definition_handles = self.resolve_internal(graph, handle, true);
self.references.insert(handle, definition_handles);
}
}

// Build reverse mapping from definitions to reference handles
let mut definitions: HashMap<GraphHandle, Vec<GraphHandle>> = HashMap::new();
for (reference, resolved_definitions) in &self.references {
for definition in resolved_definitions {
if let Some(references) = definitions.get_mut(definition) {
references.push(*reference);
} else {
definitions.insert(*definition, vec![*reference]);
}
}
}
self.definitions = Some(definitions);
}

pub(crate) fn definition_to_references(&self, handle: GraphHandle) -> Vec<GraphHandle> {
self.definitions
.as_ref()
.expect("All references should have been resolved")
.get(&handle)
.cloned()
.unwrap_or_default()
}
}

// This is a partial paths database, but we also need to keep track of edges
Expand Down
120 changes: 120 additions & 0 deletions crates/solidity/outputs/cargo/tests/src/binding_resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::rc::Rc;

use anyhow::Result;
use semver::Version;
use slang_solidity::bindings;
use slang_solidity::cst::{Cursor, Query};
use slang_solidity::parser::Parser;

use crate::resolver::TestsPathResolver;

const TEST_VERSION: Version = Version::new(0, 8, 26);

const INPUT_FILE: &str = r##"
contract Base {}
contract Middle is Base {}
contract Test is Base, Middle {}
"##;

fn find_first_match(root_cursor: Cursor, query_string: &str, capture_name: &str) -> Result<Cursor> {
let query = Query::create(query_string)?;
let query_match = root_cursor.query(vec![query]).next();
let query_match = query_match.expect("query to succeed");
Ok(query_match.captures[capture_name]
.first()
.expect("query to capture identifier")
.clone())
}

fn find_all_matches(
root_cursor: Cursor,
query_string: &str,
capture_name: &str,
) -> Result<Vec<Cursor>> {
let query = Query::create(query_string)?;
let mut results = Vec::new();
for query_match in root_cursor.query(vec![query]) {
let cursor = query_match.captures[capture_name]
.first()
.expect("query to capture identifier");
results.push(cursor.clone());
}
Ok(results)
}

#[test]
fn test_resolve_references_from_definition() -> Result<()> {
let version = TEST_VERSION;
let parser = Parser::create(version.clone())?;
let mut builder =
bindings::create_with_resolver(version.clone(), Rc::new(TestsPathResolver {}))?;

let parse_output = parser.parse_file_contents(INPUT_FILE);
builder.add_user_file("input.sol", parse_output.create_tree_cursor());

let binding_graph = builder.build();
let root_cursor = parse_output.create_tree_cursor();

// "Base" identifier
let base_cursor = find_first_match(
root_cursor.clone(),
"[ContractDefinition @identifier [\"Base\"]]",
"identifier",
)?;
let base_definition = binding_graph
.definition_at(&base_cursor)
.expect("Base definition to be found");
let base_references = base_definition.references();
assert_eq!(2, base_references.len());

let base_ref_cursors = find_all_matches(
root_cursor.clone(),
"[IdentifierPath @identifier [\"Base\"]]",
"identifier",
)?;
for base_ref in &base_references {
assert!(base_ref_cursors.contains(base_ref.get_cursor()));
}

// "Middle" identifier
let middle_cursor = find_first_match(
root_cursor.clone(),
"[ContractDefinition @identifier [\"Middle\"]]",
"identifier",
)?;
let middle_definition = binding_graph
.definition_at(&middle_cursor)
.expect("Middle definition to be found");
let middle_references = middle_definition.references();
assert_eq!(1, middle_references.len());

let middle_ref_cursors = find_all_matches(
root_cursor.clone(),
"[IdentifierPath @identifier [\"Middle\"]]",
"identifier",
)?;
for middle_ref in &middle_references {
assert!(middle_ref_cursors.contains(middle_ref.get_cursor()));
}

// "Test" identifier
let test_cursor = find_first_match(
root_cursor.clone(),
"[ContractDefinition @identifier [\"Test\"]]",
"identifier",
)?;
let test_definition = binding_graph
.definition_at(&test_cursor)
.expect("Test definition to be found");
let test_references = test_definition.references();
assert_eq!(0, test_references.len());

let test_ref_cursors = find_all_matches(
root_cursor.clone(),
"[IdentifierPath @identifier [\"Test\"]]",
"identifier",
)?;
assert!(test_ref_cursors.is_empty());

Ok(())
}
1 change: 1 addition & 0 deletions crates/solidity/outputs/cargo/tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use metaslang_bindings as _;

mod binding_resolver;
mod binding_rules;
mod bindings_output;
mod built_ins;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 99d182f

Please sign in to comment.