Skip to content

Commit

Permalink
Inject extension scopes while running the resolution algorithm (#1170)
Browse files Browse the repository at this point in the history
This PR builds on top of #1149

This changes the way we handle extension scopes (ie. `using` directives)
in bindings. Instead of using the scope stack from the stack graph, we
hook into the resolution algorithm and inject new graph edges from nodes
designated as extension hooks (usually the source unit's lexical scope)
to extension scopes which are defined at each contract/library and
contain the definition nodes from `using` directives. This simplifies
the rules quite a bit and greatly improves performance, particularly in
the case of Solidity < 0.7.0 where `using` directives are inherited from
base contracts.

This PR also moves built-ins parsing and ingestion to `slang_solidity`
crate. Since the built-ins file needs to be pre-processed to transform
the symbols as to ensure no conflicts can occur with user code, adding
the built-ins requires a couple of manual steps that were replicated in
every construction of Solidity bindings API. By encapsulating this
functionality in the `slang_solidity` crate we remove a source of user
error and make it easier to make changes to the built-ins ingestion
code.
  • Loading branch information
ggiraldez authored Dec 17, 2024
1 parent 7a25d63 commit ced4a9a
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 342 deletions.
3 changes: 1 addition & 2 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.

117 changes: 108 additions & 9 deletions crates/metaslang/bindings/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
//! To do this, add a `source_node` attribute, whose value is a syntax node capture:
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! node def
//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition
//! }
Expand Down Expand Up @@ -161,7 +161,7 @@
//! `syntax_type` attribute, whose value is a string indicating the syntax type.
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! node def
//! ; ...
//! attr (def) syntax_type = "function"
Expand All @@ -175,7 +175,7 @@
//! `definiens_node` attribute, whose value is a syntax node that spans the definiens.
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ... @body [FunctionBody] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]] @body [FunctionBody]] {
//! node def
//! ; ...
//! attr (def) definiens_node = @body
Expand All @@ -189,7 +189,7 @@
//! To connect two stack graph nodes, use the `edge` statement to add an edge between them:
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! node def
//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition
//! node body
Expand All @@ -201,7 +201,7 @@
//! you can add a `precedence` attribute to each edge to indicate which paths are prioritized:
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! node def
//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition
//! node body
Expand All @@ -220,7 +220,7 @@
//! ``` skip
//! global ROOT_NODE
//!
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! node def
//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition
//! edge ROOT_NODE -> def
Expand All @@ -235,14 +235,80 @@
//! a scope node with a kind as follows:
//!
//! ``` skip
//! @func [FunctionDefinition ... [FunctionName @id [Identifier]] ...] {
//! @func [FunctionDefinition [FunctionName @id [Identifier]]] {
//! ; ...
//! node param_scope
//! attr (param_scope) debug_kind = "param_scope"
//! ; ...
//! }
//! ```
//!
//! ### Other node attributes introduced in Slang's usage of stack-graphs
//!
//! #### `tag` attribute
//!
//! This is used to attach a specific meaning to the node, to alter the ranking
//! algorithm used when attempting to disambiguate between multiple definitions
//! found for a reference. This is an optional string attribute.
//!
//! Possible values:
//!
//! - "alias": marks a definition node as a semantic alias of another definition
//! (eg. an import alias)
//!
//! - "c3": used to mark a function/method definition to be a candidate in
//! disambiguation using the C3 linearisation algorithm. In order for C3
//! linearisation to be possible, type hierarchy attributes need to be provided
//! as well (see `parents` attribute below).
//!
//! - "super": marks a reference as a call to super virtual call. This modifies
//! the C3 linearisation algorithm by eliminating the candidates that are at
//! or further down the hierarchy of where the reference occurs. To determine
//! where the reference occurs, we also use the `parents` attribute.
//!
//! #### `parents` attribute
//!
//! Is used to convey semantic hierarchy. Can be applied to both definitions and
//! references. It's an optional, list of graph nodes attribute.
//!
//! For references it can indicate in which language context the reference
//! occurs (eg. in which method or class). For definitions it can indicate the
//! enclosing type of the definition, or parent classes in a class hierarchy.
//! The parent handles themselves can refer to definitions or references. In the
//! later case, generally speaking they will need to be resolved at resolution
//! time in order to be useful.
//!
//! #### `export_node` and `import_nodes`
//!
//! These are used to define static fixed edges to add via `set_context()`.
//! Using `set_context()` will modify the underlying stack graph by inserting
//! edges from the `import_nodes` of all parents (resolved recursively) of the
//! given context, to the `export_node` associated with the context.
//!
//! This can be used to inject virtual method implementations defined in
//! subclasses in the scope of their parent classes, which are otherwise
//! lexically inaccessible.
//!
//! `export_node` is an optional graph node attribute, and `import_nodes` is an
//! optional list of graph nodes. Both apply only to definition nodes.
//!
//! #### `extension_hook`, `extension_scope` and `inherit_extensions`
//!
//! These attributes enable the bindings API to resolve extension methods by
//! injecting specific scopes at potentially unrelated (lexically speaking)
//! nodes in the stack graph. Availability and application of extension scopes
//! depend on the call site (ie. the reference node). Thus, the extension scope
//! to (potentially) apply when resolving a reference is computed by looking up
//! the `parents` of the reference and then querying those parent nodes for
//! their `extension_scope` (an optional scope node). Any extension providing
//! node can also have the `inherit_extensions` attribute (a boolean) which
//! indicates that the algorithm should recurse and resolve its parents to
//! further look for other extensions scopes.
//!
//! Finally, the attribute `extension_hook` defines where in the graph should
//! these extension scopes be injected. This is typically the root lexical
//! scope. This attribute applies to any scope node and is boolean.
//!
mod cancellation;
mod functions;
Expand Down Expand Up @@ -282,6 +348,9 @@ static IS_DEFINITION_ATTR: &str = "is_definition";
static IS_ENDPOINT_ATTR: &str = "is_endpoint";
static IS_EXPORTED_ATTR: &str = "is_exported";
static IS_REFERENCE_ATTR: &str = "is_reference";
static EXTENSION_HOOK_ATTR: &str = "extension_hook";
static EXTENSION_SCOPE_ATTR: &str = "extension_scope";
static INHERIT_EXTENSIONS_ATTR: &str = "inherit_extensions";
static PARENTS_ATTR: &str = "parents";
static SCOPE_ATTR: &str = "scope";
static SOURCE_NODE_ATTR: &str = "source_node";
Expand All @@ -302,6 +371,8 @@ static POP_SCOPED_SYMBOL_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
EXPORT_NODE_ATTR,
IMPORT_NODES_ATTR,
SYNTAX_TYPE_ATTR,
EXTENSION_SCOPE_ATTR,
INHERIT_EXTENSIONS_ATTR,
])
});
static POP_SYMBOL_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
Expand All @@ -315,6 +386,8 @@ static POP_SYMBOL_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
EXPORT_NODE_ATTR,
IMPORT_NODES_ATTR,
SYNTAX_TYPE_ATTR,
EXTENSION_SCOPE_ATTR,
INHERIT_EXTENSIONS_ATTR,
])
});
static PUSH_SCOPED_SYMBOL_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
Expand All @@ -336,8 +409,14 @@ static PUSH_SYMBOL_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
PARENTS_ATTR,
])
});
static SCOPE_ATTRS: Lazy<HashSet<&'static str>> =
Lazy::new(|| HashSet::from([TYPE_ATTR, IS_EXPORTED_ATTR, IS_ENDPOINT_ATTR]));
static SCOPE_ATTRS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
HashSet::from([
TYPE_ATTR,
IS_EXPORTED_ATTR,
IS_ENDPOINT_ATTR,
EXTENSION_HOOK_ATTR,
])
});

// Edge attribute names
static PRECEDENCE_ATTR: &str = "precedence";
Expand All @@ -362,6 +441,7 @@ pub(crate) struct Builder<'a, KT: KindTypes + 'static> {
cursors: HashMap<Handle<Node>, Cursor<KT>>,
definitions_info: HashMap<Handle<Node>, DefinitionBindingInfo<KT>>,
references_info: HashMap<Handle<Node>, ReferenceBindingInfo>,
extension_hooks: HashSet<Handle<Node>>,
}

pub(crate) struct BuildResult<KT: KindTypes + 'static> {
Expand All @@ -370,6 +450,8 @@ pub(crate) struct BuildResult<KT: KindTypes + 'static> {
pub cursors: HashMap<Handle<Node>, Cursor<KT>>,
pub definitions_info: HashMap<Handle<Node>, DefinitionBindingInfo<KT>>,
pub references_info: HashMap<Handle<Node>, ReferenceBindingInfo>,
// Nodes where we want to inject extensions
pub extension_hooks: HashSet<Handle<Node>>,
}

impl<'a, KT: KindTypes + 'static> Builder<'a, KT> {
Expand All @@ -392,6 +474,7 @@ impl<'a, KT: KindTypes + 'static> Builder<'a, KT> {
cursors: HashMap::new(),
definitions_info: HashMap::new(),
references_info: HashMap::new(),
extension_hooks: HashSet::new(),
}
}

Expand Down Expand Up @@ -480,6 +563,7 @@ impl<'a, KT: KindTypes + 'static> Builder<'a, KT> {
cursors: self.cursors,
definitions_info: self.definitions_info,
references_info: self.references_info,
extension_hooks: self.extension_hooks,
})
}

Expand Down Expand Up @@ -896,6 +980,15 @@ impl<'a, KT: KindTypes> Builder<'a, KT> {
None => Vec::new(),
};

let extension_scope = match node.attributes.get(EXTENSION_SCOPE_ATTR) {
Some(extension_scope) => {
Some(self.node_handle_for_graph_node(extension_scope.as_graph_node_ref()?))
}
None => None,
};

let inherit_extensions = Self::load_flag(node, INHERIT_EXTENSIONS_ATTR)?;

self.definitions_info.insert(
node_handle,
DefinitionBindingInfo {
Expand All @@ -904,13 +997,19 @@ impl<'a, KT: KindTypes> Builder<'a, KT> {
parents,
export_node,
import_nodes,
extension_scope,
inherit_extensions,
},
);
} else if stack_graph_node.is_reference() {
self.references_info
.insert(node_handle, ReferenceBindingInfo { tag, parents });
}

if Self::load_flag(node, EXTENSION_HOOK_ATTR)? {
self.extension_hooks.insert(node_handle);
}

Ok(())
}

Expand Down
48 changes: 36 additions & 12 deletions crates/metaslang/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use metaslang_cst::cursor::Cursor;
use metaslang_cst::kinds::KindTypes;
use metaslang_graph_builder::ast::File;
use metaslang_graph_builder::functions::Functions;
use resolver::Resolver;
use resolver::{ResolveOptions, Resolver};
use semver::Version;
use stack_graphs::graph::StackGraph;

Expand All @@ -36,10 +36,10 @@ pub(crate) struct DefinitionBindingInfo<KT: KindTypes + 'static> {
definiens: Option<Cursor<KT>>,
tag: Option<Tag>,
parents: Vec<GraphHandle>,
#[allow(dead_code)]
export_node: Option<GraphHandle>,
#[allow(dead_code)]
import_nodes: Vec<GraphHandle>,
extension_scope: Option<GraphHandle>,
inherit_extensions: bool,
}

pub(crate) struct ReferenceBindingInfo {
Expand All @@ -57,6 +57,7 @@ pub struct BindingGraph<KT: KindTypes + 'static> {
cursor_to_definitions: HashMap<CursorID, GraphHandle>,
cursor_to_references: HashMap<CursorID, GraphHandle>,
context: Option<GraphHandle>,
extension_hooks: HashSet<GraphHandle>,
}

pub enum FileDescriptor {
Expand Down Expand Up @@ -140,6 +141,7 @@ impl<KT: KindTypes + 'static> BindingGraph<KT> {
cursor_to_definitions: HashMap::new(),
cursor_to_references: HashMap::new(),
context: None,
extension_hooks: HashSet::new(),
}
}

Expand Down Expand Up @@ -191,6 +193,7 @@ impl<KT: KindTypes + 'static> BindingGraph<KT> {
self.definitions_info
.extend(result.definitions_info.drain());
self.references_info.extend(result.references_info.drain());
self.extension_hooks.extend(result.extension_hooks.drain());

result
}
Expand Down Expand Up @@ -258,17 +261,12 @@ impl<KT: KindTypes + 'static> BindingGraph<KT> {
if self.stack_graph[*handle].is_definition() {
self.to_definition(*handle)
} else {
self.to_reference(*handle)?.jump_to_definition().ok()
self.to_reference(*handle)?.non_recursive_resolve().ok()
}
})
.collect()
}

pub fn lookup_definition_by_name(&self, name: &str) -> Option<Definition<'_, KT>> {
self.all_definitions()
.find(|definition| definition.get_cursor().node().unparse() == name)
}

pub fn get_context(&self) -> Option<Definition<'_, KT>> {
self.context.and_then(|handle| self.to_definition(handle))
}
Expand Down Expand Up @@ -337,6 +335,10 @@ impl<KT: KindTypes + 'static> BindingGraph<KT> {
}
results
}

pub(crate) fn is_extension_hook(&self, node_handle: GraphHandle) -> bool {
self.extension_hooks.contains(&node_handle)
}
}

struct DisplayCursor<'a, KT: KindTypes + 'static> {
Expand Down Expand Up @@ -428,6 +430,20 @@ impl<'a, KT: KindTypes + 'static> Definition<'a, KT> {
.unwrap_or_default()
}

pub(crate) fn get_extension_scope(&self) -> Option<GraphHandle> {
self.owner
.definitions_info
.get(&self.handle)
.and_then(|info| info.extension_scope)
}

pub(crate) fn inherit_extensions(&self) -> bool {
self.owner
.definitions_info
.get(&self.handle)
.map_or(false, |info| info.inherit_extensions)
}

pub fn to_handle(self) -> DefinitionHandle {
DefinitionHandle(self.handle)
}
Expand Down Expand Up @@ -509,12 +525,20 @@ impl<'a, KT: KindTypes + 'static> Reference<'a, KT> {
.expect("Reference does not have a valid file descriptor")
}

pub fn jump_to_definition(&self) -> Result<Definition<'a, KT>, ResolutionError<'a, KT>> {
Resolver::build_for(self).first()
pub fn resolve_definition(&self) -> Result<Definition<'a, KT>, ResolutionError<'a, KT>> {
Resolver::build_for(self, ResolveOptions::Full).first()
}

pub fn definitions(&self) -> Vec<Definition<'a, KT>> {
Resolver::build_for(self).all()
Resolver::build_for(self, ResolveOptions::Full).all()
}

pub(crate) fn non_recursive_resolve(
&self,
) -> Result<Definition<'a, KT>, ResolutionError<'a, KT>> {
// This was likely originated from a full resolution call, so cut
// recursion here by restricting the resolution algorithm.
Resolver::build_for(self, ResolveOptions::NonRecursive).first()
}

pub(crate) fn has_tag(&self, tag: Tag) -> bool {
Expand Down
Loading

0 comments on commit ced4a9a

Please sign in to comment.