diff --git a/Cargo.lock b/Cargo.lock index 528900d499..6284efc61a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,9 +1506,8 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lsp-positions" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa61ce94f83d24eba829bbba054b40e9996d8531c87670799fd0c43dea97be37" +version = "0.3.4" +source = "git+https://github.com/NomicFoundation/stack-graphs?branch=nomic#9631ed9c7de4443525491745d46557dc16a7355c" dependencies = [ "memchr", "unicode-segmentation", @@ -2545,9 +2544,8 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stack-graphs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af63a628b6666b3ce870cac60623d71d1575a9ccc5b2f0a2f83143c3e7da9a3a" +version = "0.14.1" +source = "git+https://github.com/NomicFoundation/stack-graphs?branch=nomic#9631ed9c7de4443525491745d46557dc16a7355c" dependencies = [ "bitvec", "controlled-option", diff --git a/Cargo.toml b/Cargo.toml index 21308200f8..dd157f1f16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,7 @@ serde = { version = "1.0.217", features = ["derive", "rc"] } serde_json = { version = "1.0.134", features = ["preserve_order"] } similar-asserts = { version = "1.6.0" } smallvec = { version = "1.7.0", features = ["union"] } -stack-graphs = { version = "0.13.0" } +stack-graphs = { git = "https://github.com/NomicFoundation/stack-graphs", branch = "nomic" } string-interner = { version = "0.17.0", features = [ "std", "inline-more", diff --git a/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs b/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs index eee8b7880f..c3f95c7400 100644 --- a/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs +++ b/crates/codegen/runtime/cargo/crate/src/extensions/bindings/mod.rs @@ -1,11 +1,11 @@ use semver::Version; -use crate::bindings::BindingGraph; +use crate::bindings::BindingGraphBuilder; use crate::parser::ParserInitializationError; #[allow(clippy::needless_pass_by_value)] pub fn add_built_ins( - _binding_graph: &mut BindingGraph, + _binding_graph_builder: &mut BindingGraphBuilder, _version: Version, ) -> Result<(), ParserInitializationError> { unreachable!("Built-ins are Solidity-specific") diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs index 4766b80e6c..052c6ba4f2 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/bindings/mod.rs @@ -10,9 +10,10 @@ use semver::Version; use crate::cst::KindTypes; +pub type BindingGraphBuilder = metaslang_bindings::BindingGraphBuilder; pub type BindingGraph = metaslang_bindings::BindingGraph; -pub type Definition<'a> = metaslang_bindings::Definition<'a, KindTypes>; -pub type Reference<'a> = metaslang_bindings::Reference<'a, KindTypes>; +pub type Definition = metaslang_bindings::Definition; +pub type Reference = metaslang_bindings::Reference; pub type BindingLocation = metaslang_bindings::BindingLocation; pub type UserFileLocation = metaslang_bindings::UserFileLocation; @@ -29,8 +30,8 @@ pub enum BindingGraphInitializationError { pub fn create_with_resolver( version: Version, resolver: Rc>, -) -> Result { - let mut binding_graph = BindingGraph::create( +) -> Result { + let mut binding_graph = BindingGraphBuilder::create( version.clone(), binding_rules::BINDING_RULES_SOURCE, resolver, diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs index aea2d3cd9c..e909b5882d 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/compilation/unit.rs @@ -43,14 +43,14 @@ impl CompilationUnit { files: self.files.clone(), }; - let mut binding_graph = + let mut builder = create_with_resolver(self.language_version.clone(), Rc::new(resolver))?; for (id, file) in &self.files { - binding_graph.add_user_file(id, file.create_tree_cursor()); + builder.add_user_file(id, file.create_tree_cursor()); } - Ok(Rc::new(binding_graph)) + Ok(builder.build()) }) } } diff --git a/crates/codegen/runtime/cargo/crate/src/runtime/cst/mod.rs b/crates/codegen/runtime/cargo/crate/src/runtime/cst/mod.rs index 53885c6a25..7aa60861d2 100644 --- a/crates/codegen/runtime/cargo/crate/src/runtime/cst/mod.rs +++ b/crates/codegen/runtime/cargo/crate/src/runtime/cst/mod.rs @@ -10,7 +10,7 @@ mod terminal_kind; pub use edge_label::EdgeLabel; pub(crate) use lexical_context::{IsLexicalContext, LexicalContext, LexicalContextType}; pub use metaslang_cst::kinds::{ - EdgeLabelExtensions, NonterminalKindExtensions, TerminalKindExtensions, + EdgeLabelExtensions, NodeKind, NonterminalKindExtensions, TerminalKindExtensions, }; pub use nonterminal_kind::NonterminalKind; pub use terminal_kind::TerminalKind; diff --git a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/bindings/mod.rs b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/bindings/mod.rs index c093fb0b7c..7d2d23d710 100644 --- a/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/bindings/mod.rs +++ b/crates/codegen/runtime/cargo/wasm/src/runtime/wrappers/bindings/mod.rs @@ -12,52 +12,8 @@ mod ffi { mod rust { pub use crate::rust_crate::bindings::{ - BindingGraph, BindingLocation, BuiltInLocation, UserFileLocation, + BindingGraph, BindingLocation, BuiltInLocation, Definition, Reference, UserFileLocation, }; - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Definition { - pub id: usize, - pub name_location: BindingLocation, - pub definiens_location: BindingLocation, - } - - impl From> for Definition { - fn from(definition: crate::rust_crate::bindings::Definition<'_>) -> Self { - Self { - id: definition.id(), - name_location: definition.name_location(), - definiens_location: definition.definiens_location(), - } - } - } - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Reference { - pub id: usize, - pub location: BindingLocation, - pub definitions: Vec, - } - - impl From> for Reference { - fn from(reference: crate::rust_crate::bindings::Reference<'_>) -> Self { - Self { - id: reference.id(), - location: reference.location(), - definitions: reference - .definitions() - .into_iter() - .map(Into::into) - .collect(), - } - } - } } impl ffi::Guest for crate::wasm_crate::World { @@ -100,15 +56,15 @@ define_rc_wrapper! { BindingGraph { define_wrapper! { Definition { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn name_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().name_location.clone()._into_ffi() + self._borrow_ffi().name_location()._into_ffi() } fn definiens_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().definiens_location.clone()._into_ffi() + self._borrow_ffi().definiens_location()._into_ffi() } } } @@ -120,15 +76,15 @@ define_wrapper! { Definition { define_wrapper! { Reference { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn location(&self) -> ffi::BindingLocation { - self._borrow_ffi().location.clone()._into_ffi() + self._borrow_ffi().location().clone()._into_ffi() } fn definitions(&self) -> Vec { - self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + self._borrow_ffi().definitions().iter().cloned().map(IntoFFI::_into_ffi).collect() } } } diff --git a/crates/metaslang/bindings/generated/public_api.txt b/crates/metaslang/bindings/generated/public_api.txt index e4b11e5bec..a029a0307a 100644 --- a/crates/metaslang/bindings/generated/public_api.txt +++ b/crates/metaslang/bindings/generated/public_api.txt @@ -11,64 +11,54 @@ impl core::c pub fn metaslang_bindings::BindingLocation::clone(&self) -> metaslang_bindings::BindingLocation impl core::fmt::Debug for metaslang_bindings::BindingLocation pub fn metaslang_bindings::BindingLocation::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -pub enum metaslang_bindings::FileDescriptor -pub metaslang_bindings::FileDescriptor::System(alloc::string::String) -pub metaslang_bindings::FileDescriptor::User(alloc::string::String) -impl metaslang_bindings::FileDescriptor -pub fn metaslang_bindings::FileDescriptor::get_path(&self) -> &str -pub fn metaslang_bindings::FileDescriptor::is_system(&self) -> bool -pub fn metaslang_bindings::FileDescriptor::is_user(&self) -> bool -pub fn metaslang_bindings::FileDescriptor::is_user_path(&self, path: &str) -> bool -pub enum metaslang_bindings::ResolutionError<'a, KT: metaslang_cst::kinds::KindTypes + 'static> -pub metaslang_bindings::ResolutionError::AmbiguousDefinitions(alloc::vec::Vec>) -pub metaslang_bindings::ResolutionError::Unresolved pub struct metaslang_bindings::BindingGraph impl metaslang_bindings::BindingGraph -pub fn metaslang_bindings::BindingGraph::add_system_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -pub fn metaslang_bindings::BindingGraph::add_user_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -pub fn metaslang_bindings::BindingGraph::add_user_file_returning_graph(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -> metaslang_graph_builder::graph::Graph -pub fn metaslang_bindings::BindingGraph::all_definitions(&self) -> impl core::iter::traits::iterator::Iterator> + '_ -pub fn metaslang_bindings::BindingGraph::all_references(&self) -> impl core::iter::traits::iterator::Iterator> + '_ -pub fn metaslang_bindings::BindingGraph::create(version: semver::Version, binding_rules: &str, path_resolver: alloc::rc::Rc>) -> Self -pub fn metaslang_bindings::BindingGraph::definition_at(&self, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> -pub fn metaslang_bindings::BindingGraph::reference_at(&self, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> +pub fn metaslang_bindings::BindingGraph::all_definitions(self: &alloc::rc::Rc) -> impl core::iter::traits::iterator::Iterator> + '_ +pub fn metaslang_bindings::BindingGraph::all_references(self: &alloc::rc::Rc) -> impl core::iter::traits::iterator::Iterator> + '_ +pub fn metaslang_bindings::BindingGraph::definition_at(self: &alloc::rc::Rc, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> +pub fn metaslang_bindings::BindingGraph::reference_at(self: &alloc::rc::Rc, cursor: &metaslang_cst::cursor::Cursor) -> core::option::Option> +pub struct metaslang_bindings::BindingGraphBuilder +impl metaslang_bindings::BindingGraphBuilder +pub fn metaslang_bindings::BindingGraphBuilder::add_system_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) +pub fn metaslang_bindings::BindingGraphBuilder::add_user_file(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) +pub fn metaslang_bindings::BindingGraphBuilder::add_user_file_returning_graph(&mut self, file_path: &str, tree_cursor: metaslang_cst::cursor::Cursor) -> metaslang_graph_builder::graph::Graph +pub fn metaslang_bindings::BindingGraphBuilder::build(self) -> alloc::rc::Rc> +pub fn metaslang_bindings::BindingGraphBuilder::create(version: semver::Version, binding_rules: &str, path_resolver: alloc::rc::Rc>) -> Self pub struct metaslang_bindings::BuiltInLocation impl core::clone::Clone for metaslang_bindings::BuiltInLocation pub fn metaslang_bindings::BuiltInLocation::clone(&self) -> metaslang_bindings::BuiltInLocation impl core::fmt::Debug for metaslang_bindings::BuiltInLocation pub fn metaslang_bindings::BuiltInLocation::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -pub struct metaslang_bindings::Definition<'a, KT: metaslang_cst::kinds::KindTypes + 'static> -impl<'a, KT: metaslang_cst::kinds::KindTypes + 'static> metaslang_bindings::Definition<'a, KT> -pub fn metaslang_bindings::Definition<'a, KT>::definiens_location(&self) -> metaslang_bindings::BindingLocation -pub fn metaslang_bindings::Definition<'a, KT>::get_cursor(&self) -> &metaslang_cst::cursor::Cursor -pub fn metaslang_bindings::Definition<'a, KT>::get_definiens_cursor(&self) -> &metaslang_cst::cursor::Cursor -pub fn metaslang_bindings::Definition<'a, KT>::get_file(&self) -> metaslang_bindings::FileDescriptor -pub fn metaslang_bindings::Definition<'a, KT>::id(&self) -> usize -pub fn metaslang_bindings::Definition<'a, KT>::name_location(&self) -> metaslang_bindings::BindingLocation -impl<'a, KT: core::clone::Clone + metaslang_cst::kinds::KindTypes + 'static> core::clone::Clone for metaslang_bindings::Definition<'a, KT> -pub fn metaslang_bindings::Definition<'a, KT>::clone(&self) -> metaslang_bindings::Definition<'a, KT> -impl core::cmp::Eq for metaslang_bindings::Definition<'_, KT> -impl core::cmp::PartialEq for metaslang_bindings::Definition<'_, KT> -pub fn metaslang_bindings::Definition<'_, KT>::eq(&self, other: &Self) -> bool -impl core::fmt::Debug for metaslang_bindings::Definition<'_, KT> -pub fn metaslang_bindings::Definition<'_, KT>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::fmt::Display for metaslang_bindings::Definition<'_, KT> -pub fn metaslang_bindings::Definition<'_, KT>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::hash::Hash for metaslang_bindings::Definition<'_, KT> -pub fn metaslang_bindings::Definition<'_, KT>::hash(&self, state: &mut H) -pub struct metaslang_bindings::Reference<'a, KT: metaslang_cst::kinds::KindTypes + 'static> -impl<'a, KT: metaslang_cst::kinds::KindTypes + 'static> metaslang_bindings::Reference<'a, KT> -pub fn metaslang_bindings::Reference<'a, KT>::definitions(&self) -> alloc::vec::Vec> -pub fn metaslang_bindings::Reference<'a, KT>::get_cursor(&self) -> &metaslang_cst::cursor::Cursor -pub fn metaslang_bindings::Reference<'a, KT>::get_file(&self) -> metaslang_bindings::FileDescriptor -pub fn metaslang_bindings::Reference<'a, KT>::id(&self) -> usize -pub fn metaslang_bindings::Reference<'a, KT>::location(&self) -> metaslang_bindings::BindingLocation -impl<'a, KT: core::clone::Clone + metaslang_cst::kinds::KindTypes + 'static> core::clone::Clone for metaslang_bindings::Reference<'a, KT> -pub fn metaslang_bindings::Reference<'a, KT>::clone(&self) -> metaslang_bindings::Reference<'a, KT> -impl core::cmp::PartialEq for metaslang_bindings::Reference<'_, KT> -pub fn metaslang_bindings::Reference<'_, KT>::eq(&self, other: &Self) -> bool -impl core::fmt::Display for metaslang_bindings::Reference<'_, KT> -pub fn metaslang_bindings::Reference<'_, KT>::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct metaslang_bindings::Definition +impl metaslang_bindings::Definition +pub fn metaslang_bindings::Definition::definiens_location(&self) -> metaslang_bindings::BindingLocation +pub fn metaslang_bindings::Definition::get_cursor(&self) -> &metaslang_cst::cursor::Cursor +pub fn metaslang_bindings::Definition::get_definiens_cursor(&self) -> &metaslang_cst::cursor::Cursor +pub fn metaslang_bindings::Definition::get_file(&self) -> FileDescriptor +pub fn metaslang_bindings::Definition::id(&self) -> usize +pub fn metaslang_bindings::Definition::name_location(&self) -> metaslang_bindings::BindingLocation +impl core::clone::Clone for metaslang_bindings::Definition +pub fn metaslang_bindings::Definition::clone(&self) -> metaslang_bindings::Definition +impl core::cmp::Eq for metaslang_bindings::Definition +impl core::cmp::PartialEq for metaslang_bindings::Definition +pub fn metaslang_bindings::Definition::eq(&self, other: &Self) -> bool +impl core::fmt::Debug for metaslang_bindings::Definition +pub fn metaslang_bindings::Definition::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for metaslang_bindings::Definition +pub fn metaslang_bindings::Definition::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct metaslang_bindings::Reference +impl metaslang_bindings::Reference +pub fn metaslang_bindings::Reference::definitions(&self) -> alloc::vec::Vec> +pub fn metaslang_bindings::Reference::get_cursor(&self) -> &metaslang_cst::cursor::Cursor +pub fn metaslang_bindings::Reference::get_file(&self) -> FileDescriptor +pub fn metaslang_bindings::Reference::id(&self) -> usize +pub fn metaslang_bindings::Reference::location(&self) -> metaslang_bindings::BindingLocation +impl core::clone::Clone for metaslang_bindings::Reference +pub fn metaslang_bindings::Reference::clone(&self) -> metaslang_bindings::Reference +impl core::cmp::PartialEq for metaslang_bindings::Reference +pub fn metaslang_bindings::Reference::eq(&self, other: &Self) -> bool +impl core::fmt::Display for metaslang_bindings::Reference +pub fn metaslang_bindings::Reference::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct metaslang_bindings::UserFileLocation impl metaslang_bindings::UserFileLocation pub fn metaslang_bindings::UserFileLocation::cursor(&self) -> &metaslang_cst::cursor::Cursor diff --git a/crates/metaslang/bindings/src/builder/cancellation.rs b/crates/metaslang/bindings/src/builder/loader/cancellation.rs similarity index 100% rename from crates/metaslang/bindings/src/builder/cancellation.rs rename to crates/metaslang/bindings/src/builder/loader/cancellation.rs diff --git a/crates/metaslang/bindings/src/builder/functions.rs b/crates/metaslang/bindings/src/builder/loader/functions.rs similarity index 98% rename from crates/metaslang/bindings/src/builder/functions.rs rename to crates/metaslang/bindings/src/builder/loader/functions.rs index 27ff5d9aa3..6219d7e2aa 100644 --- a/crates/metaslang/bindings/src/builder/functions.rs +++ b/crates/metaslang/bindings/src/builder/loader/functions.rs @@ -63,7 +63,8 @@ mod resolver { use metaslang_graph_builder::graph::{Graph, Value}; use metaslang_graph_builder::ExecutionError; - use crate::{FileDescriptor, PathResolver}; + use crate::builder::FileDescriptor; + use crate::PathResolver; pub fn add_functions( functions: &mut Functions, diff --git a/crates/metaslang/bindings/src/builder/loader/mod.rs b/crates/metaslang/bindings/src/builder/loader/mod.rs new file mode 100644 index 0000000000..56b679d0a6 --- /dev/null +++ b/crates/metaslang/bindings/src/builder/loader/mod.rs @@ -0,0 +1,988 @@ +// -*- coding: utf-8 -*- +// ------------------------------------------------------------------------------------------------ +// Copyright © 2021, stack-graphs authors. +// Copyright © 2024, slang authors. +// Licensed under MIT license +// Please see the LICENSE file in the root of this crate for license details. +// ------------------------------------------------------------------------------------------------ + +//! This module lets you construct [stack graphs][] using this crate's [graph construction DSL][]. +//! The graph DSL lets you construct arbitrary graph structures from the parsed syntax tree of a +//! source file. If you construct a graph using the vocabulary of attributes described below, then +//! the result of executing the graph DSL will be a valid stack graph, which we can then use for +//! name binding lookups. +//! +//! ## Prerequisites +//! +//! [stack graphs]: https://docs.rs/stack-graphs/*/ +//! [graph construction DSL]: https://docs.rs/metaslang_graph_builder/*/ +//! +//! To process a particular source language, you'll need to first get the CST from the source, using +//! the parser constructed from the language definition that uses the `metaslang_cst` crate. +//! +//! You will then need to create _stack graph construction rules_ for your language. These rules +//! are implemented using metaslang's [graph construction DSL][], which is based from tree-sitter's +//! graph construction DSL. They define the particular stack graph nodes and edges that should be +//! created for each part of the parsed syntax tree of a source file. +//! +//! ## Graph DSL vocabulary +//! +//! **Please note**: This documentation assumes you are already familiar with stack graphs, and how +//! to use different stack graph node types, and the connectivity between nodes, to implement the +//! name binding semantics of your language. We assume that you know what kind of stack graph you +//! want to produce; this documentation focuses only on the mechanics of _how_ to create that stack +//! graph content. +//! +//! As mentioned above, your stack graph construction rules should create stack graph nodes and +//! edges from the parsed content of a source file. You will use MSGB [stanzas][] to match on +//! different parts of the parsed syntax tree, and create stack graph content for each match. +//! +//! ### Creating stack graph nodes +//! +//! To create a stack graph node for each identifier in a Solidity file, you could use the following +//! MSGB stanza: +//! +//! ``` skip +//! [Identifier] { +//! node new_node +//! } +//! ``` +//! +//! (Here, `node` is a MSGB statement that creates a new node, and `new_node` is the name of a local +//! variable that the new node is assigned to, letting you refer to the new node in the rest of the +//! stanza.) +//! +//! [stanzas]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/index.html#high-level-structure +//! +//! By default, this new node will be a _scope node_. If you need to create a different kind of stack +//! graph node, set the `type` attribute on the new node: +//! +//! ``` skip +//! [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol" +//! } +//! ``` +//! +//! The valid `type` values are: +//! +//! - `drop_scopes`: a _drop scopes_ node +//! - `pop_symbol`: a _pop symbol_ node +//! - `pop_scoped_symbol`: a _pop scoped symbol_ node +//! - `push_symbol`: a _push symbol_ node +//! - `push_scoped_symbol`: a _push scoped symbol_ node +//! - `scope`: a _scope_ node +//! +//! A node without an explicit `type` attribute is assumed to be of type `scope`. +//! +//! Certain node types — `pop_symbol`, `pop_scoped_symbol`, `push_symbol` and `push_scoped_symbol` — +//! also require you to provide a `symbol` attribute. Its value must be a string, but will typically +//! come from the content of a parsed syntax node using the [`source-text`][] function and a syntax +//! capture: +//! +//! [`source-text`]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/functions/index.html#source-text +//! +//! ``` skip +//! @id [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol", symbol = (source-text @id) +//! } +//! ``` +//! +//! Node types `pop_symbol` and `pop_scoped_symbol` allow an optional `is_definition` attribute, +//! which marks that node as a proper definition. Node types `push_symbol` and `push_scoped_symbol` +//! allow an optional `is_reference` attribute, which marks the node as a proper reference. When +//! `is_definition` or `is_reference` are set, the `source_node` attribute is required. +//! +//! ``` skip +//! @id [Identifier] { +//! node new_node +//! attr (new_node) type = "push_symbol", symbol = (source-text @id), is_reference, source_node = @id +//! } +//! ``` +//! +//! A _push scoped symbol_ node requires a `scope` attribute. Its value must be a reference to an +//! `exported` node that you've already created. (This is the exported scope node that will be +//! pushed onto the scope stack.) For instance: +//! +//! ``` skip +//! @id [Identifier] { +//! node new_exported_scope_node +//! attr (new_exported_scope_node) is_exported +//! node new_push_scoped_symbol_node +//! attr (new_push_scoped_symbol_node) +//! type = "push_scoped_symbol", +//! symbol = (source-text @id), +//! scope = new_exported_scope_node +//! } +//! ``` +//! +//! Nodes of type `scope` allow an optional `is_exported` attribute, that is required to use the +//! scope in a `push_scoped_symbol` node. +//! +//! +//! ### Annotating nodes with location information +//! +//! You can annotate any stack graph node that you create with location information, identifying +//! the portion of the source file that the node "belongs to". This is _required_ for definition +//! and reference nodes, since the location information determines which parts of the source file +//! the user can _click on_, and the _destination_ of any code navigation queries the user makes. +//! To do this, add a `source_node` attribute, whose value is a syntax node capture: +//! +//! ``` skip +//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! } +//! ``` +//! +//! Note how in this example, we use a different syntax node for the _target_ of the definition +//! (the entirety of the function definition) and for the _name_ of the definition (the content of +//! the function's `name`). +//! +//! Adding the `empty_source_span` attribute will use an empty source span located at the start of +//! the span of the `source_node`. This can be useful when a proper reference or definition is +//! desired, and thus `source_node` is required, but the span of the available source node is too +//! large. For example, a module definition which is located at the start of the program, but does +//! span the whole program: +//! +//! ``` skip +//! @unit [SourceUnit] { +//! ; ... +//! node mod_def +//! attr mod_def type = "pop_symbol", symbol = mod_name, is_definition, source_node = @unit, empty_source_span +//! ; ... +//! } +//! ``` +//! +//! ### Annotating nodes with syntax type information +//! +//! You can annotate any stack graph node with information about its syntax type. To do this, add a +//! `syntax_type` attribute, whose value is a string indicating the syntax type. +//! +//! ``` skip +//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { +//! node def +//! ; ... +//! attr (def) syntax_type = "function" +//! } +//! ``` +//! +//! ### Annotating definitions with definiens information +//! +//! You cannot annotate definitions with a definiens, which is the thing the definition covers. For +//! example, for a function definition, the definiens would be the function body. To do this, add a +//! `definiens_node` attribute, whose value is a syntax node that spans the definiens. +//! +//! ``` skip +//! @func [FunctionDefinition [FunctionName @id [Identifier]] @body [FunctionBody]] { +//! node def +//! ; ... +//! attr (def) definiens_node = @body +//! } +//! ``` +//! +//! Definiens are optional and setting them to `#null` explicitly is allowed. +//! +//! ### Connecting stack graph nodes with edges +//! +//! To connect two stack graph nodes, use the `edge` statement to add an edge between them: +//! +//! ``` skip +//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! node body +//! edge def -> body +//! } +//! ``` +//! +//! To implement shadowing (which determines which definitions are selected when multiple are available), +//! you can add a `precedence` attribute to each edge to indicate which paths are prioritized: +//! +//! ``` skip +//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { +//! node def +//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition +//! node body +//! edge def -> body +//! attr (def -> body) precedence = 1 +//! } +//! ``` +//! +//! (If you don't specify a `precedence`, the default is 0.) +//! +//! ### Referring to the singleton nodes +//! +//! The _root node_ and _jump to scope node_ are singleton nodes that always exist for all stack +//! graphs. You can refer to them using the `ROOT_NODE` and `JUMP_TO_SCOPE_NODE` global variables: +//! +//! ``` skip +//! global ROOT_NODE +//! +//! @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 +//! } +//! ``` +//! +//! ### Attaching debug information to nodes +//! +//! It is possible to attach extra information to nodes for debugging purposes. This is done by adding +//! `debug_*` attributes to nodes. Each attribute defines a debug entry, with the key derived from the +//! attribute name, and the value the string representation of the attribute value. For example, mark +//! a scope node with a kind as follows: +//! +//! ``` skip +//! @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 +//! +//! #### `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. +//! +//! #### `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; + +use std::collections::{HashMap, HashSet}; + +pub use cancellation::{CancellationFlag, NoCancellation}; +pub use functions::default_functions; +use metaslang_cst::cursor::Cursor; +use metaslang_cst::kinds::KindTypes; +use metaslang_graph_builder::ast::File as GraphBuilderFile; +use metaslang_graph_builder::functions::Functions; +use metaslang_graph_builder::graph::{Edge, Graph, GraphNode, GraphNodeRef, Value}; +use metaslang_graph_builder::{ExecutionConfig, ExecutionError, Variables}; +use once_cell::sync::Lazy; +use stack_graphs::arena::Handle; +use stack_graphs::graph::{File, Node, NodeID, StackGraph}; +use thiserror::Error; + +use crate::builder::{DefinitionBindingInfo, ReferenceBindingInfo}; + +// Node type values +static DROP_SCOPES_TYPE: &str = "drop_scopes"; +static POP_SCOPED_SYMBOL_TYPE: &str = "pop_scoped_symbol"; +static POP_SYMBOL_TYPE: &str = "pop_symbol"; +static PUSH_SCOPED_SYMBOL_TYPE: &str = "push_scoped_symbol"; +static PUSH_SYMBOL_TYPE: &str = "push_symbol"; +static SCOPE_TYPE: &str = "scope"; + +// Node attribute names +static DEBUG_ATTR_PREFIX: &str = "debug_"; +static DEFINIENS_NODE_ATTR: &str = "definiens_node"; +static EMPTY_SOURCE_SPAN_ATTR: &str = "empty_source_span"; +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"; +static SYMBOL_ATTR: &str = "symbol"; +static SYNTAX_TYPE_ATTR: &str = "syntax_type"; +static TYPE_ATTR: &str = "type"; + +// Expected attributes per node type +static POP_SCOPED_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + SYMBOL_ATTR, + IS_DEFINITION_ATTR, + DEFINIENS_NODE_ATTR, + PARENTS_ATTR, + SYNTAX_TYPE_ATTR, + EXTENSION_SCOPE_ATTR, + INHERIT_EXTENSIONS_ATTR, + ]) +}); +static POP_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + SYMBOL_ATTR, + IS_DEFINITION_ATTR, + DEFINIENS_NODE_ATTR, + PARENTS_ATTR, + SYNTAX_TYPE_ATTR, + EXTENSION_SCOPE_ATTR, + INHERIT_EXTENSIONS_ATTR, + ]) +}); +static PUSH_SCOPED_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + SYMBOL_ATTR, + SCOPE_ATTR, + IS_REFERENCE_ATTR, + PARENTS_ATTR, + ]) +}); +static PUSH_SYMBOL_ATTRS: Lazy> = + Lazy::new(|| HashSet::from([TYPE_ATTR, SYMBOL_ATTR, IS_REFERENCE_ATTR, PARENTS_ATTR])); +static SCOPE_ATTRS: Lazy> = Lazy::new(|| { + HashSet::from([ + TYPE_ATTR, + IS_EXPORTED_ATTR, + IS_ENDPOINT_ATTR, + EXTENSION_HOOK_ATTR, + ]) +}); + +// Edge attribute names +static PRECEDENCE_ATTR: &str = "precedence"; + +// Global variables +/// Name of the variable used to pass the root node. +pub const ROOT_NODE_VAR: &str = "ROOT_NODE"; +/// Name of the variable used to pass the jump to scope node. +pub const JUMP_TO_SCOPE_NODE_VAR: &str = "JUMP_TO_SCOPE_NODE"; +/// Name of the variable used to pass the file path. +pub const FILE_PATH_VAR: &str = "FILE_PATH"; + +pub(crate) struct Loader<'a, KT: KindTypes + 'static> { + msgb: &'a GraphBuilderFile, + functions: &'a Functions, + stack_graph: &'a mut StackGraph, + file: Handle, + tree_cursor: Cursor, + graph: Graph, + remapped_nodes: HashMap, + injected_node_count: usize, + cursors: HashMap, Cursor>, + definitions_info: HashMap, DefinitionBindingInfo>, + references_info: HashMap, ReferenceBindingInfo>, + extension_hooks: HashSet>, +} + +pub(crate) struct LoadResult { + #[cfg(feature = "__private_testing_utils")] + pub graph: Graph, + pub cursors: HashMap, Cursor>, + pub definitions_info: HashMap, DefinitionBindingInfo>, + pub references_info: HashMap, ReferenceBindingInfo>, + // Nodes where we want to inject extensions + pub extension_hooks: HashSet>, +} + +impl<'a, KT: KindTypes + 'static> Loader<'a, KT> { + pub fn new( + msgb: &'a GraphBuilderFile, + functions: &'a Functions, + stack_graph: &'a mut StackGraph, + file: Handle, + tree_cursor: Cursor, + ) -> Self { + Loader { + msgb, + functions, + stack_graph, + file, + tree_cursor, + graph: Graph::new(), + remapped_nodes: HashMap::new(), + injected_node_count: 0, + cursors: HashMap::new(), + definitions_info: HashMap::new(), + references_info: HashMap::new(), + extension_hooks: HashSet::new(), + } + } + + fn build_global_variables(&mut self) -> Variables<'a> { + let mut variables = Variables::new(); + let file_path = self.stack_graph[self.file].name(); + variables + .add(FILE_PATH_VAR.into(), file_path.into()) + .expect("Failed to add FILE_PATH variable"); + + let root_node = self.inject_node(NodeID::root()); + variables + .add(ROOT_NODE_VAR.into(), root_node.into()) + .expect("Failed to set ROOT_NODE"); + + let jump_to_scope_node = self.inject_node(NodeID::jump_to()); + variables + .add(JUMP_TO_SCOPE_NODE_VAR.into(), jump_to_scope_node.into()) + .expect("Failed to set JUMP_TO_SCOPE_NODE"); + + #[cfg(feature = "__private_testing_utils")] + { + // For debugging purposes only + self.graph[root_node] + .attributes + .add( + [DEBUG_ATTR_PREFIX, "msgb_variable"] + .concat() + .as_str() + .into(), + ROOT_NODE_VAR.to_string(), + ) + .expect("Failed to set ROOT_NODE variable name for debugging"); + + self.graph[jump_to_scope_node] + .attributes + .add( + [DEBUG_ATTR_PREFIX, "msgb_variable"] + .concat() + .as_str() + .into(), + JUMP_TO_SCOPE_NODE_VAR.to_string(), + ) + .expect("Failed to set JUMP_TO_SCOPE_NODE variable name for debugging"); + } + + variables + } + + /// Executes this loader. + pub fn execute( + mut self, + cancellation_flag: &dyn CancellationFlag, + ) -> Result, BuildError> { + let variables = self.build_global_variables(); + + let config = ExecutionConfig::new(self.functions, &variables) + .lazy(true) + // .debug_attributes( + // [DEBUG_ATTR_PREFIX, "msgb_location"] + // .concat() + // .as_str() + // .into(), + // [DEBUG_ATTR_PREFIX, "msgb_variable"] + // .concat() + // .as_str() + // .into(), + // [DEBUG_ATTR_PREFIX, "msgb_match_node"] + // .concat() + // .as_str() + // .into(), + // ); + ; + + self.msgb.execute_into( + &mut self.graph, + &self.tree_cursor, + &config, + &(cancellation_flag as &dyn CancellationFlag), + )?; + + self.load(cancellation_flag)?; + + Ok(LoadResult { + #[cfg(feature = "__private_testing_utils")] + graph: self.graph, + cursors: self.cursors, + definitions_info: self.definitions_info, + references_info: self.references_info, + extension_hooks: self.extension_hooks, + }) + } + + /// Create a graph node to represent the stack graph node. It is the callers responsibility to + /// ensure the stack graph node exists. + pub fn inject_node(&mut self, id: NodeID) -> GraphNodeRef { + let node = self.graph.add_graph_node(); + self.remapped_nodes.insert(node.index(), id); + self.injected_node_count += 1; + node + } +} + +/// An error that can occur while loading a stack graph from a TSG file +#[derive(Debug, Error)] +pub enum BuildError { + #[error("{0}")] + Cancelled(&'static str), + #[error("Missing ‘symbol’ attribute on graph node")] + MissingSymbol(GraphNodeRef), + #[error("Missing ‘scope’ attribute on graph node")] + MissingScope(GraphNodeRef), + #[error("Missing ‘definiens’ attribute on graph node")] + MissingDefiniens(GraphNodeRef), + #[error("Unknown ‘{0}’ flag type {1}")] + UnknownFlagType(String, String), + #[error("Unknown node type {0}")] + UnknownNodeType(String), + #[error("Unknown symbol type {0}")] + UnknownSymbolType(String), + #[error(transparent)] + ExecutionError(ExecutionError), + #[error("Expected exported symbol scope in {0}, got {1}")] + SymbolScopeError(String, String), + #[error("Parent must be either a reference or definition")] + InvalidParent(GraphNodeRef), +} + +impl From for BuildError { + fn from(value: stack_graphs::CancellationError) -> Self { + Self::Cancelled(value.0) + } +} + +impl From for BuildError { + fn from(value: ExecutionError) -> Self { + match value { + ExecutionError::Cancelled(err) => Self::Cancelled(err.0), + err => Self::ExecutionError(err), + } + } +} + +impl<'a, KT: KindTypes + 'static> Loader<'a, KT> { + fn load(&mut self, cancellation_flag: &dyn CancellationFlag) -> Result<(), BuildError> { + let cancellation_flag: &dyn stack_graphs::CancellationFlag = &cancellation_flag; + + // By default graph ids are used for stack graph local_ids. A remapping is computed + // for local_ids that already exist in the graph---all other graph ids are mapped to + // the same local_id. See [`self.node_id_for_index`] for more details. + let mut next_local_id = u32::try_from(self.graph.node_count() - self.injected_node_count) + .expect("nodes local_id to fit in u32"); + for node in self.stack_graph.nodes_for_file(self.file) { + let local_id = self.stack_graph[node].id().local_id(); + let index = (local_id as usize) + self.injected_node_count; + // find next available local_id for which no stack graph node exists yet + while self + .stack_graph + .node_for_id(NodeID::new_in_file(self.file, next_local_id)) + .is_some() + { + next_local_id += 1; + } + // remap graph node index to the available stack graph node local_id + if self + .remapped_nodes + .insert(index, NodeID::new_in_file(self.file, next_local_id)) + .is_some() + { + panic!("index already remapped"); + } + } + + // First create a stack graph node for each MSGB node. (The skip(...) is because the first + // DSL nodes that we create are the proxies for the injected stack graph nodes.) + for node_ref in self.graph.iter_nodes().skip(self.injected_node_count) { + cancellation_flag.check("loading graph nodes")?; + let node_type = self.get_node_type(node_ref)?; + let handle = match node_type { + NodeType::DropScopes => self.load_drop_scopes(node_ref), + NodeType::PopScopedSymbol => self.load_pop_scoped_symbol(node_ref)?, + NodeType::PopSymbol => self.load_pop_symbol(node_ref)?, + NodeType::PushScopedSymbol => self.load_push_scoped_symbol(node_ref)?, + NodeType::PushSymbol => self.load_push_symbol(node_ref)?, + NodeType::Scope => self.load_scope(node_ref)?, + }; + self.load_source_info(node_ref, handle)?; + self.load_node_debug_info(node_ref, handle); + } + + // Iterate again to resolve parents attribute, which refers to other nodes in the graph + for node_ref in self.graph.iter_nodes().skip(self.injected_node_count) { + cancellation_flag.check("loading graph nodes additional info")?; + self.load_additional_info(node_ref)?; + } + + for node in self.stack_graph.nodes_for_file(self.file) { + self.verify_node(node)?; + } + + // Then add stack graph edges for each TSG edge. Note that we _don't_ skip(...) here because + // there might be outgoing nodes from the “root” node that we need to process. + // (Technically the caller could add outgoing nodes from “jump to scope” as well, but those + // are invalid according to the stack graph semantics and will never be followed. + for source_ref in self.graph.iter_nodes() { + let source = &self.graph[source_ref]; + let source_node_id = self.node_id_for_graph_node(source_ref); + let source_handle = self.stack_graph.node_for_id(source_node_id).unwrap(); + for (sink_ref, edge) in source.iter_edges() { + cancellation_flag.check("loading graph edges")?; + let precedence = match edge.attributes.get(PRECEDENCE_ATTR) { + Some(precedence) => precedence.as_integer()?, + None => 0, + } + .try_into() + .map_err(|_| ExecutionError::ExpectedInteger("integer does not fit".to_string()))?; + let sink_node_id = self.node_id_for_graph_node(sink_ref); + let sink_handle = self.stack_graph.node_for_id(sink_node_id).unwrap(); + self.stack_graph + .add_edge(source_handle, sink_handle, precedence); + Self::load_edge_debug_info(self.stack_graph, source_handle, sink_handle, edge); + } + } + + Ok(()) + } + + fn get_node_type(&self, node_ref: GraphNodeRef) -> Result { + let node = &self.graph[node_ref]; + let node_type = match node.attributes.get(TYPE_ATTR) { + Some(node_type) => node_type.as_str()?, + None => return Ok(NodeType::Scope), + }; + if node_type == DROP_SCOPES_TYPE { + Ok(NodeType::DropScopes) + } else if node_type == POP_SCOPED_SYMBOL_TYPE { + Ok(NodeType::PopScopedSymbol) + } else if node_type == POP_SYMBOL_TYPE { + Ok(NodeType::PopSymbol) + } else if node_type == PUSH_SCOPED_SYMBOL_TYPE { + Ok(NodeType::PushScopedSymbol) + } else if node_type == PUSH_SYMBOL_TYPE { + Ok(NodeType::PushSymbol) + } else if node_type == SCOPE_TYPE { + Ok(NodeType::Scope) + } else { + Err(BuildError::UnknownNodeType(node_type.to_string())) + } + } + + fn verify_node(&self, node: Handle) -> Result<(), BuildError> { + if let Node::PushScopedSymbol(node) = &self.stack_graph[node] { + let scope = &self.stack_graph[self.stack_graph.node_for_id(node.scope).unwrap()]; + if !scope.is_exported_scope() { + return Err(BuildError::SymbolScopeError( + format!("{}", node.display(self.stack_graph)), + format!("{}", scope.display(self.stack_graph)), + )); + } + } + Ok(()) + } +} + +enum NodeType { + DropScopes, + PopSymbol, + PopScopedSymbol, + PushSymbol, + PushScopedSymbol, + Scope, +} + +impl<'a, KT: KindTypes> Loader<'a, KT> { + /// Get the `NodeID` corresponding to a `Graph` node. + /// + /// By default, graph nodes get their index shifted by [`self.injected_node_count`] as their + /// `local_id`, unless they have a corresponding entry in the [`self.remapped_nodes`] map. This + /// is the case if: + /// 1. The node was injected, in which case it is mapped to the `NodeID` of the injected node. + /// 2. The node's default `local_id` clashes with a preexisting node, in which case it is mapped to + /// an available `local_id` beyond the range of default `local_ids`. + fn node_id_for_graph_node(&self, node_ref: GraphNodeRef) -> NodeID { + let index = node_ref.index(); + self.remapped_nodes.get(&index).map_or_else( + || { + NodeID::new_in_file( + self.file, + u32::try_from(index - self.injected_node_count) + .expect("local_id to fit in u32"), + ) + }, + |id| *id, + ) + } + + fn load_drop_scopes(&mut self, node_ref: GraphNodeRef) -> Handle { + let id = self.node_id_for_graph_node(node_ref); + self.stack_graph.add_drop_scopes_node(id).unwrap() + } + + fn load_pop_scoped_symbol( + &mut self, + node_ref: GraphNodeRef, + ) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; + Self::verify_attributes(node, POP_SCOPED_SYMBOL_TYPE, &POP_SCOPED_SYMBOL_ATTRS); + let node_handle = self + .stack_graph + .add_pop_scoped_symbol_node(id, symbol, is_definition) + .unwrap(); + Ok(node_handle) + } + + fn load_pop_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; + Self::verify_attributes(node, POP_SYMBOL_TYPE, &POP_SYMBOL_ATTRS); + let node_handle = self + .stack_graph + .add_pop_symbol_node(id, symbol, is_definition) + .unwrap(); + Ok(node_handle) + } + + fn load_push_scoped_symbol( + &mut self, + node_ref: GraphNodeRef, + ) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let scope = match node.attributes.get(SCOPE_ATTR) { + Some(scope) => self.node_id_for_graph_node(scope.as_graph_node_ref()?), + None => return Err(BuildError::MissingScope(node_ref)), + }; + let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; + Self::verify_attributes(node, PUSH_SCOPED_SYMBOL_TYPE, &PUSH_SCOPED_SYMBOL_ATTRS); + Ok(self + .stack_graph + .add_push_scoped_symbol_node(id, symbol, scope, is_reference) + .unwrap()) + } + + fn load_push_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let symbol = match node.attributes.get(SYMBOL_ATTR) { + Some(symbol) => Self::load_symbol(symbol)?, + None => return Err(BuildError::MissingSymbol(node_ref)), + }; + let symbol = self.stack_graph.add_symbol(&symbol); + let id = self.node_id_for_graph_node(node_ref); + let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; + Self::verify_attributes(node, PUSH_SYMBOL_TYPE, &PUSH_SYMBOL_ATTRS); + Ok(self + .stack_graph + .add_push_symbol_node(id, symbol, is_reference) + .unwrap()) + } + + fn load_scope(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { + let node = &self.graph[node_ref]; + let id = self.node_id_for_graph_node(node_ref); + let is_exported = + Self::load_flag(node, IS_EXPORTED_ATTR)? || Self::load_flag(node, IS_ENDPOINT_ATTR)?; + Self::verify_attributes(node, SCOPE_TYPE, &SCOPE_ATTRS); + Ok(self.stack_graph.add_scope_node(id, is_exported).unwrap()) + } + + fn load_symbol(value: &Value) -> Result { + match value { + Value::Integer(i) => Ok(i.to_string()), + Value::String(s) => Ok(s.clone()), + _ => Err(BuildError::UnknownSymbolType(value.to_string())), + } + } + + fn load_flag(node: &GraphNode, attribute: &str) -> Result { + match node.attributes.get(attribute) { + Some(value) => value + .as_boolean() + .map_err(|_| BuildError::UnknownFlagType(attribute.to_string(), value.to_string())), + None => Ok(false), + } + } + + fn load_source_info( + &mut self, + node_ref: GraphNodeRef, + node_handle: Handle, + ) -> Result<(), BuildError> { + let node = &self.graph[node_ref]; + + // For every added graph node which links to a corresponding source + // node, save the corresponding CST cursor so our caller can extract + // that info later. + if let Some(source_node) = node.attributes.get(SOURCE_NODE_ATTR) { + let syntax_node_ref = source_node.as_syntax_node_ref()?; + let source_node = &self.graph[syntax_node_ref]; + self.cursors.insert(node_handle, source_node.clone()); + } + + if let Some(syntax_type) = node.attributes.get(SYNTAX_TYPE_ATTR) { + let syntax_type = syntax_type.as_str()?; + let syntax_type = self.stack_graph.add_string(syntax_type); + let source_info = self.stack_graph.source_info_mut(node_handle); + source_info.syntax_type = syntax_type.into(); + } + + Ok(()) + } + + fn node_handle_for_graph_node(&self, node_ref: GraphNodeRef) -> Handle { + self.stack_graph + .node_for_id(self.node_id_for_graph_node(node_ref)) + .expect("parent node exists in the stack graph") + } + + // Saves additional binding information from the loaded graph (eg. tag, + // definiens, import/export nodes and parents). + fn load_additional_info(&mut self, node_ref: GraphNodeRef) -> Result<(), BuildError> { + let node = &self.graph[node_ref]; + let node_handle = self.node_handle_for_graph_node(node_ref); + let stack_graph_node = &self.stack_graph[node_handle]; + + let parents = match node.attributes.get(PARENTS_ATTR) { + Some(parents) => { + parents + .as_list()? + .iter() + .flat_map(|value| { + value + .as_graph_node_ref() + .map(|id| self.node_handle_for_graph_node(id)) + }) + .flat_map(|parent| { + // ensure parents are either definitions or references + let parent_node = &self.stack_graph[parent]; + if !parent_node.is_definition() && !parent_node.is_reference() { + Err(BuildError::InvalidParent(node_ref)) + } else { + Ok(parent) + } + }) + .collect() + } + None => Vec::new(), + }; + + if stack_graph_node.is_definition() { + let definiens = match node.attributes.get(DEFINIENS_NODE_ATTR) { + Some(definiens_node) => { + let syntax_node_ref = definiens_node.as_syntax_node_ref()?; + let definiens_node = &self.graph[syntax_node_ref]; + definiens_node.clone() + } + None => return Err(BuildError::MissingDefiniens(node_ref)), + }; + + 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 { + definiens, + parents, + extension_scope, + inherit_extensions, + }, + ); + } else if stack_graph_node.is_reference() { + self.references_info + .insert(node_handle, ReferenceBindingInfo { parents }); + } + + if Self::load_flag(node, EXTENSION_HOOK_ATTR)? { + self.extension_hooks.insert(node_handle); + } + + Ok(()) + } + + fn load_node_debug_info(&mut self, node_ref: GraphNodeRef, node_handle: Handle) { + let node = &self.graph[node_ref]; + for (name, value) in node.attributes.iter() { + let name = name.to_string(); + if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { + let value = match value { + Value::String(value) => value.clone(), + value => value.to_string(), + }; + let key = self.stack_graph.add_string(name_without_prefix); + let value = self.stack_graph.add_string(&value); + self.stack_graph + .node_debug_info_mut(node_handle) + .add(key, value); + } + } + } + + fn load_edge_debug_info( + stack_graph: &mut StackGraph, + source_handle: Handle, + sink_handle: Handle, + edge: &Edge, + ) { + for (name, value) in edge.attributes.iter() { + let name = name.to_string(); + if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { + let value = match value { + Value::String(value) => value.clone(), + value => value.to_string(), + }; + let key = stack_graph.add_string(name_without_prefix); + let value = stack_graph.add_string(&value); + stack_graph + .edge_debug_info_mut(source_handle, sink_handle) + .add(key, value); + } + } + } + + fn verify_attributes( + node: &GraphNode, + node_type: &str, + allowed_attributes: &HashSet<&'static str>, + ) { + for (id, _) in node.attributes.iter() { + let id = id.as_str(); + if !allowed_attributes.contains(id) + && id != SOURCE_NODE_ATTR + && id != EMPTY_SOURCE_SPAN_ATTR + && !id.starts_with(DEBUG_ATTR_PREFIX) + { + eprintln!("Unexpected attribute {id} on node of type {node_type}"); + } + } + } +} diff --git a/crates/metaslang/bindings/src/builder/mod.rs b/crates/metaslang/bindings/src/builder/mod.rs index 77943da877..21bdb1609b 100644 --- a/crates/metaslang/bindings/src/builder/mod.rs +++ b/crates/metaslang/bindings/src/builder/mod.rs @@ -1,985 +1,233 @@ -// -*- coding: utf-8 -*- -// ------------------------------------------------------------------------------------------------ -// Copyright © 2021, stack-graphs authors. -// Copyright © 2024, slang authors. -// Licensed under MIT license -// Please see the LICENSE file in the root of this crate for license details. -// ------------------------------------------------------------------------------------------------ - -//! This module lets you construct [stack graphs][] using this crate's [graph construction DSL][]. -//! The graph DSL lets you construct arbitrary graph structures from the parsed syntax tree of a -//! source file. If you construct a graph using the vocabulary of attributes described below, then -//! the result of executing the graph DSL will be a valid stack graph, which we can then use for -//! name binding lookups. -//! -//! ## Prerequisites -//! -//! [stack graphs]: https://docs.rs/stack-graphs/*/ -//! [graph construction DSL]: https://docs.rs/metaslang_graph_builder/*/ -//! -//! To process a particular source language, you'll need to first get the CST from the source, using -//! the parser constructed from the language definition that uses the `metaslang_cst` crate. -//! -//! You will then need to create _stack graph construction rules_ for your language. These rules -//! are implemented using metaslang's [graph construction DSL][], which is based from tree-sitter's -//! graph construction DSL. They define the particular stack graph nodes and edges that should be -//! created for each part of the parsed syntax tree of a source file. -//! -//! ## Graph DSL vocabulary -//! -//! **Please note**: This documentation assumes you are already familiar with stack graphs, and how -//! to use different stack graph node types, and the connectivity between nodes, to implement the -//! name binding semantics of your language. We assume that you know what kind of stack graph you -//! want to produce; this documentation focuses only on the mechanics of _how_ to create that stack -//! graph content. -//! -//! As mentioned above, your stack graph construction rules should create stack graph nodes and -//! edges from the parsed content of a source file. You will use MSGB [stanzas][] to match on -//! different parts of the parsed syntax tree, and create stack graph content for each match. -//! -//! ### Creating stack graph nodes -//! -//! To create a stack graph node for each identifier in a Solidity file, you could use the following -//! MSGB stanza: -//! -//! ``` skip -//! [Identifier] { -//! node new_node -//! } -//! ``` -//! -//! (Here, `node` is a MSGB statement that creates a new node, and `new_node` is the name of a local -//! variable that the new node is assigned to, letting you refer to the new node in the rest of the -//! stanza.) -//! -//! [stanzas]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/index.html#high-level-structure -//! -//! By default, this new node will be a _scope node_. If you need to create a different kind of stack -//! graph node, set the `type` attribute on the new node: -//! -//! ``` skip -//! [Identifier] { -//! node new_node -//! attr (new_node) type = "push_symbol" -//! } -//! ``` -//! -//! The valid `type` values are: -//! -//! - `drop_scopes`: a _drop scopes_ node -//! - `pop_symbol`: a _pop symbol_ node -//! - `pop_scoped_symbol`: a _pop scoped symbol_ node -//! - `push_symbol`: a _push symbol_ node -//! - `push_scoped_symbol`: a _push scoped symbol_ node -//! - `scope`: a _scope_ node -//! -//! A node without an explicit `type` attribute is assumed to be of type `scope`. -//! -//! Certain node types — `pop_symbol`, `pop_scoped_symbol`, `push_symbol` and `push_scoped_symbol` — -//! also require you to provide a `symbol` attribute. Its value must be a string, but will typically -//! come from the content of a parsed syntax node using the [`source-text`][] function and a syntax -//! capture: -//! -//! [`source-text`]: https://docs.rs/tree-sitter-graph/*/tree_sitter_graph/reference/functions/index.html#source-text -//! -//! ``` skip -//! @id [Identifier] { -//! node new_node -//! attr (new_node) type = "push_symbol", symbol = (source-text @id) -//! } -//! ``` -//! -//! Node types `pop_symbol` and `pop_scoped_symbol` allow an optional `is_definition` attribute, -//! which marks that node as a proper definition. Node types `push_symbol` and `push_scoped_symbol` -//! allow an optional `is_reference` attribute, which marks the node as a proper reference. When -//! `is_definition` or `is_reference` are set, the `source_node` attribute is required. -//! -//! ``` skip -//! @id [Identifier] { -//! node new_node -//! attr (new_node) type = "push_symbol", symbol = (source-text @id), is_reference, source_node = @id -//! } -//! ``` -//! -//! A _push scoped symbol_ node requires a `scope` attribute. Its value must be a reference to an -//! `exported` node that you've already created. (This is the exported scope node that will be -//! pushed onto the scope stack.) For instance: -//! -//! ``` skip -//! @id [Identifier] { -//! node new_exported_scope_node -//! attr (new_exported_scope_node) is_exported -//! node new_push_scoped_symbol_node -//! attr (new_push_scoped_symbol_node) -//! type = "push_scoped_symbol", -//! symbol = (source-text @id), -//! scope = new_exported_scope_node -//! } -//! ``` -//! -//! Nodes of type `scope` allow an optional `is_exported` attribute, that is required to use the -//! scope in a `push_scoped_symbol` node. -//! -//! -//! ### Annotating nodes with location information -//! -//! You can annotate any stack graph node that you create with location information, identifying -//! the portion of the source file that the node "belongs to". This is _required_ for definition -//! and reference nodes, since the location information determines which parts of the source file -//! the user can _click on_, and the _destination_ of any code navigation queries the user makes. -//! To do this, add a `source_node` attribute, whose value is a syntax node capture: -//! -//! ``` skip -//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { -//! node def -//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition -//! } -//! ``` -//! -//! Note how in this example, we use a different syntax node for the _target_ of the definition -//! (the entirety of the function definition) and for the _name_ of the definition (the content of -//! the function's `name`). -//! -//! Adding the `empty_source_span` attribute will use an empty source span located at the start of -//! the span of the `source_node`. This can be useful when a proper reference or definition is -//! desired, and thus `source_node` is required, but the span of the available source node is too -//! large. For example, a module definition which is located at the start of the program, but does -//! span the whole program: -//! -//! ``` skip -//! @unit [SourceUnit] { -//! ; ... -//! node mod_def -//! attr mod_def type = "pop_symbol", symbol = mod_name, is_definition, source_node = @unit, empty_source_span -//! ; ... -//! } -//! ``` -//! -//! ### Annotating nodes with syntax type information -//! -//! You can annotate any stack graph node with information about its syntax type. To do this, add a -//! `syntax_type` attribute, whose value is a string indicating the syntax type. -//! -//! ``` skip -//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { -//! node def -//! ; ... -//! attr (def) syntax_type = "function" -//! } -//! ``` -//! -//! ### Annotating definitions with definiens information -//! -//! You cannot annotate definitions with a definiens, which is the thing the definition covers. For -//! example, for a function definition, the definiens would be the function body. To do this, add a -//! `definiens_node` attribute, whose value is a syntax node that spans the definiens. -//! -//! ``` skip -//! @func [FunctionDefinition [FunctionName @id [Identifier]] @body [FunctionBody]] { -//! node def -//! ; ... -//! attr (def) definiens_node = @body -//! } -//! ``` -//! -//! Definiens are optional and setting them to `#null` explicitly is allowed. -//! -//! ### Connecting stack graph nodes with edges -//! -//! To connect two stack graph nodes, use the `edge` statement to add an edge between them: -//! -//! ``` skip -//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { -//! node def -//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition -//! node body -//! edge def -> body -//! } -//! ``` -//! -//! To implement shadowing (which determines which definitions are selected when multiple are available), -//! you can add a `precedence` attribute to each edge to indicate which paths are prioritized: -//! -//! ``` skip -//! @func [FunctionDefinition [FunctionName @id [Identifier]]] { -//! node def -//! attr (def) type = "pop_symbol", symbol = (source-text @id), source_node = @func, is_definition -//! node body -//! edge def -> body -//! attr (def -> body) precedence = 1 -//! } -//! ``` -//! -//! (If you don't specify a `precedence`, the default is 0.) -//! -//! ### Referring to the singleton nodes -//! -//! The _root node_ and _jump to scope node_ are singleton nodes that always exist for all stack -//! graphs. You can refer to them using the `ROOT_NODE` and `JUMP_TO_SCOPE_NODE` global variables: -//! -//! ``` skip -//! global ROOT_NODE -//! -//! @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 -//! } -//! ``` -//! -//! ### Attaching debug information to nodes -//! -//! It is possible to attach extra information to nodes for debugging purposes. This is done by adding -//! `debug_*` attributes to nodes. Each attribute defines a debug entry, with the key derived from the -//! attribute name, and the value the string representation of the attribute value. For example, mark -//! a scope node with a kind as follows: -//! -//! ``` skip -//! @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 -//! -//! #### `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. -//! -//! #### `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; +mod loader; +mod resolver; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::rc::Rc; -pub use cancellation::{CancellationFlag, NoCancellation}; -pub use functions::default_functions; +use loader::{LoadResult, Loader}; use metaslang_cst::cursor::Cursor; use metaslang_cst::kinds::KindTypes; -use metaslang_graph_builder::ast::File as GraphBuilderFile; +use metaslang_graph_builder::ast::File; use metaslang_graph_builder::functions::Functions; -use metaslang_graph_builder::graph::{Edge, Graph, GraphNode, GraphNodeRef, Value}; -use metaslang_graph_builder::{ExecutionConfig, ExecutionError, Variables}; -use once_cell::sync::Lazy; -use stack_graphs::arena::Handle; -use stack_graphs::graph::{File, Node, NodeID, StackGraph}; -use thiserror::Error; - -use crate::{DefinitionBindingInfo, ReferenceBindingInfo}; - -// Node type values -static DROP_SCOPES_TYPE: &str = "drop_scopes"; -static POP_SCOPED_SYMBOL_TYPE: &str = "pop_scoped_symbol"; -static POP_SYMBOL_TYPE: &str = "pop_symbol"; -static PUSH_SCOPED_SYMBOL_TYPE: &str = "push_scoped_symbol"; -static PUSH_SYMBOL_TYPE: &str = "push_symbol"; -static SCOPE_TYPE: &str = "scope"; - -// Node attribute names -static DEBUG_ATTR_PREFIX: &str = "debug_"; -static DEFINIENS_NODE_ATTR: &str = "definiens_node"; -static EMPTY_SOURCE_SPAN_ATTR: &str = "empty_source_span"; -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"; -static SYMBOL_ATTR: &str = "symbol"; -static SYNTAX_TYPE_ATTR: &str = "syntax_type"; -static TYPE_ATTR: &str = "type"; - -// Expected attributes per node type -static POP_SCOPED_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { - HashSet::from([ - TYPE_ATTR, - SYMBOL_ATTR, - IS_DEFINITION_ATTR, - DEFINIENS_NODE_ATTR, - PARENTS_ATTR, - SYNTAX_TYPE_ATTR, - EXTENSION_SCOPE_ATTR, - INHERIT_EXTENSIONS_ATTR, - ]) -}); -static POP_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { - HashSet::from([ - TYPE_ATTR, - SYMBOL_ATTR, - IS_DEFINITION_ATTR, - DEFINIENS_NODE_ATTR, - PARENTS_ATTR, - SYNTAX_TYPE_ATTR, - EXTENSION_SCOPE_ATTR, - INHERIT_EXTENSIONS_ATTR, - ]) -}); -static PUSH_SCOPED_SYMBOL_ATTRS: Lazy> = Lazy::new(|| { - HashSet::from([ - TYPE_ATTR, - SYMBOL_ATTR, - SCOPE_ATTR, - IS_REFERENCE_ATTR, - PARENTS_ATTR, - ]) -}); -static PUSH_SYMBOL_ATTRS: Lazy> = - Lazy::new(|| HashSet::from([TYPE_ATTR, SYMBOL_ATTR, IS_REFERENCE_ATTR, PARENTS_ATTR])); -static SCOPE_ATTRS: Lazy> = Lazy::new(|| { - HashSet::from([ - TYPE_ATTR, - IS_EXPORTED_ATTR, - IS_ENDPOINT_ATTR, - EXTENSION_HOOK_ATTR, - ]) -}); - -// Edge attribute names -static PRECEDENCE_ATTR: &str = "precedence"; - -// Global variables -/// Name of the variable used to pass the root node. -pub const ROOT_NODE_VAR: &str = "ROOT_NODE"; -/// Name of the variable used to pass the jump to scope node. -pub const JUMP_TO_SCOPE_NODE_VAR: &str = "JUMP_TO_SCOPE_NODE"; -/// Name of the variable used to pass the file path. -pub const FILE_PATH_VAR: &str = "FILE_PATH"; - -pub(crate) struct Builder<'a, KT: KindTypes + 'static> { - msgb: &'a GraphBuilderFile, - functions: &'a Functions, - stack_graph: &'a mut StackGraph, - file: Handle, - tree_cursor: Cursor, - graph: Graph, - remapped_nodes: HashMap, - injected_node_count: usize, - cursors: HashMap, Cursor>, - definitions_info: HashMap, DefinitionBindingInfo>, - references_info: HashMap, ReferenceBindingInfo>, - extension_hooks: HashSet>, -} - -pub(crate) struct BuildResult { - #[cfg(feature = "__private_testing_utils")] - pub graph: Graph, - pub cursors: HashMap, Cursor>, - pub definitions_info: HashMap, DefinitionBindingInfo>, - pub references_info: HashMap, ReferenceBindingInfo>, - // Nodes where we want to inject extensions - pub extension_hooks: HashSet>, -} - -impl<'a, KT: KindTypes + 'static> Builder<'a, KT> { - pub fn new( - msgb: &'a GraphBuilderFile, - functions: &'a Functions, - stack_graph: &'a mut StackGraph, - file: Handle, - tree_cursor: Cursor, - ) -> Self { - Builder { - msgb, - functions, - stack_graph, - file, - tree_cursor, - graph: Graph::new(), - remapped_nodes: HashMap::new(), - injected_node_count: 0, - cursors: HashMap::new(), - definitions_info: HashMap::new(), - references_info: HashMap::new(), - extension_hooks: HashSet::new(), - } - } +use resolver::Resolver; +use semver::Version; +use stack_graphs::graph::StackGraph; - fn build_global_variables(&mut self) -> Variables<'a> { - let mut variables = Variables::new(); - let file_path = self.stack_graph[self.file].name(); - variables - .add(FILE_PATH_VAR.into(), file_path.into()) - .expect("Failed to add FILE_PATH variable"); - - let root_node = self.inject_node(NodeID::root()); - variables - .add(ROOT_NODE_VAR.into(), root_node.into()) - .expect("Failed to set ROOT_NODE"); - - let jump_to_scope_node = self.inject_node(NodeID::jump_to()); - variables - .add(JUMP_TO_SCOPE_NODE_VAR.into(), jump_to_scope_node.into()) - .expect("Failed to set JUMP_TO_SCOPE_NODE"); - - #[cfg(feature = "__private_testing_utils")] - { - // For debugging purposes only - self.graph[root_node] - .attributes - .add( - [DEBUG_ATTR_PREFIX, "msgb_variable"] - .concat() - .as_str() - .into(), - ROOT_NODE_VAR.to_string(), - ) - .expect("Failed to set ROOT_NODE variable name for debugging"); - - self.graph[jump_to_scope_node] - .attributes - .add( - [DEBUG_ATTR_PREFIX, "msgb_variable"] - .concat() - .as_str() - .into(), - JUMP_TO_SCOPE_NODE_VAR.to_string(), - ) - .expect("Failed to set JUMP_TO_SCOPE_NODE variable name for debugging"); - } - - variables - } +pub(crate) type GraphHandle = stack_graphs::arena::Handle; +pub(crate) type FileHandle = stack_graphs::arena::Handle; +pub(crate) type CursorID = usize; - /// Executes this builder. - pub fn build( - mut self, - cancellation_flag: &dyn CancellationFlag, - ) -> Result, BuildError> { - let variables = self.build_global_variables(); - - let config = ExecutionConfig::new(self.functions, &variables) - .lazy(true) - .debug_attributes( - [DEBUG_ATTR_PREFIX, "msgb_location"] - .concat() - .as_str() - .into(), - [DEBUG_ATTR_PREFIX, "msgb_variable"] - .concat() - .as_str() - .into(), - [DEBUG_ATTR_PREFIX, "msgb_match_node"] - .concat() - .as_str() - .into(), - ); - - self.msgb.execute_into( - &mut self.graph, - &self.tree_cursor, - &config, - &(cancellation_flag as &dyn CancellationFlag), - )?; - - self.load(cancellation_flag)?; - - Ok(BuildResult { - #[cfg(feature = "__private_testing_utils")] - graph: self.graph, - cursors: self.cursors, - definitions_info: self.definitions_info, - references_info: self.references_info, - extension_hooks: self.extension_hooks, - }) - } +pub use crate::graph::BindingGraph; - /// Create a graph node to represent the stack graph node. It is the callers responsibility to - /// ensure the stack graph node exists. - pub fn inject_node(&mut self, id: NodeID) -> GraphNodeRef { - let node = self.graph.add_graph_node(); - self.remapped_nodes.insert(node.index(), id); - self.injected_node_count += 1; - node - } +pub(crate) struct DefinitionBindingInfo { + pub(crate) definiens: Cursor, + parents: Vec, + extension_scope: Option, + inherit_extensions: bool, } -/// An error that can occur while loading a stack graph from a TSG file -#[derive(Debug, Error)] -pub enum BuildError { - #[error("{0}")] - Cancelled(&'static str), - #[error("Missing ‘symbol’ attribute on graph node")] - MissingSymbol(GraphNodeRef), - #[error("Missing ‘scope’ attribute on graph node")] - MissingScope(GraphNodeRef), - #[error("Unknown ‘{0}’ flag type {1}")] - UnknownFlagType(String, String), - #[error("Unknown node type {0}")] - UnknownNodeType(String), - #[error("Unknown symbol type {0}")] - UnknownSymbolType(String), - #[error(transparent)] - ExecutionError(ExecutionError), - #[error("Expected exported symbol scope in {0}, got {1}")] - SymbolScopeError(String, String), - #[error("Parent must be either a reference or definition")] - InvalidParent(GraphNodeRef), +pub(crate) struct ReferenceBindingInfo { + parents: Vec, } -impl From for BuildError { - fn from(value: stack_graphs::CancellationError) -> Self { - Self::Cancelled(value.0) - } +pub struct BindingGraphBuilder { + graph_builder_file: File, + functions: Functions, + pub(crate) stack_graph: StackGraph, + pub(crate) cursors: HashMap>, + pub(crate) definitions_info: HashMap>, + pub(crate) references_info: HashMap, + pub(crate) cursor_to_definitions: HashMap, + pub(crate) cursor_to_references: HashMap, + extension_hooks: HashSet, } -impl From for BuildError { - fn from(value: ExecutionError) -> Self { - match value { - ExecutionError::Cancelled(err) => Self::Cancelled(err.0), - err => Self::ExecutionError(err), - } - } +#[derive(Clone)] +pub enum FileDescriptor { + User(String), + System(String), } -impl<'a, KT: KindTypes + 'static> Builder<'a, KT> { - fn load(&mut self, cancellation_flag: &dyn CancellationFlag) -> Result<(), BuildError> { - let cancellation_flag: &dyn stack_graphs::CancellationFlag = &cancellation_flag; - - // By default graph ids are used for stack graph local_ids. A remapping is computed - // for local_ids that already exist in the graph---all other graph ids are mapped to - // the same local_id. See [`self.node_id_for_index`] for more details. - let mut next_local_id = u32::try_from(self.graph.node_count() - self.injected_node_count) - .expect("nodes local_id to fit in u32"); - for node in self.stack_graph.nodes_for_file(self.file) { - let local_id = self.stack_graph[node].id().local_id(); - let index = (local_id as usize) + self.injected_node_count; - // find next available local_id for which no stack graph node exists yet - while self - .stack_graph - .node_for_id(NodeID::new_in_file(self.file, next_local_id)) - .is_some() - { - next_local_id += 1; - } - // remap graph node index to the available stack graph node local_id - if self - .remapped_nodes - .insert(index, NodeID::new_in_file(self.file, next_local_id)) - .is_some() - { - panic!("index already remapped"); - } - } - - // First create a stack graph node for each MSGB node. (The skip(...) is because the first - // DSL nodes that we create are the proxies for the injected stack graph nodes.) - for node_ref in self.graph.iter_nodes().skip(self.injected_node_count) { - cancellation_flag.check("loading graph nodes")?; - let node_type = self.get_node_type(node_ref)?; - let handle = match node_type { - NodeType::DropScopes => self.load_drop_scopes(node_ref), - NodeType::PopScopedSymbol => self.load_pop_scoped_symbol(node_ref)?, - NodeType::PopSymbol => self.load_pop_symbol(node_ref)?, - NodeType::PushScopedSymbol => self.load_push_scoped_symbol(node_ref)?, - NodeType::PushSymbol => self.load_push_symbol(node_ref)?, - NodeType::Scope => self.load_scope(node_ref)?, - }; - self.load_source_info(node_ref, handle)?; - self.load_node_debug_info(node_ref, handle); - } - - // Iterate again to resolve parents attribute, which refers to other nodes in the graph - for node_ref in self.graph.iter_nodes().skip(self.injected_node_count) { - cancellation_flag.check("loading graph nodes additional info")?; - self.load_additional_info(node_ref)?; - } +#[derive(Debug)] +pub(crate) struct FileDescriptorError; - for node in self.stack_graph.nodes_for_file(self.file) { - self.verify_node(node)?; - } +impl FileDescriptor { + // Internal functions to convert a FileDescriptor to and from a string for + // representation inside the stack graph - // Then add stack graph edges for each TSG edge. Note that we _don't_ skip(...) here because - // there might be outgoing nodes from the “root” node that we need to process. - // (Technically the caller could add outgoing nodes from “jump to scope” as well, but those - // are invalid according to the stack graph semantics and will never be followed. - for source_ref in self.graph.iter_nodes() { - let source = &self.graph[source_ref]; - let source_node_id = self.node_id_for_graph_node(source_ref); - let source_handle = self.stack_graph.node_for_id(source_node_id).unwrap(); - for (sink_ref, edge) in source.iter_edges() { - cancellation_flag.check("loading graph edges")?; - let precedence = match edge.attributes.get(PRECEDENCE_ATTR) { - Some(precedence) => precedence.as_integer()?, - None => 0, - } - .try_into() - .map_err(|_| ExecutionError::ExpectedInteger("integer does not fit".to_string()))?; - let sink_node_id = self.node_id_for_graph_node(sink_ref); - let sink_handle = self.stack_graph.node_for_id(sink_node_id).unwrap(); - self.stack_graph - .add_edge(source_handle, sink_handle, precedence); - Self::load_edge_debug_info(self.stack_graph, source_handle, sink_handle, edge); - } + pub(crate) fn as_string(&self) -> String { + match self { + Self::User(path) => format!("user:{path}"), + Self::System(path) => format!("system:{path}"), } - - Ok(()) } - fn get_node_type(&self, node_ref: GraphNodeRef) -> Result { - let node = &self.graph[node_ref]; - let node_type = match node.attributes.get(TYPE_ATTR) { - Some(node_type) => node_type.as_str()?, - None => return Ok(NodeType::Scope), - }; - if node_type == DROP_SCOPES_TYPE { - Ok(NodeType::DropScopes) - } else if node_type == POP_SCOPED_SYMBOL_TYPE { - Ok(NodeType::PopScopedSymbol) - } else if node_type == POP_SYMBOL_TYPE { - Ok(NodeType::PopSymbol) - } else if node_type == PUSH_SCOPED_SYMBOL_TYPE { - Ok(NodeType::PushScopedSymbol) - } else if node_type == PUSH_SYMBOL_TYPE { - Ok(NodeType::PushSymbol) - } else if node_type == SCOPE_TYPE { - Ok(NodeType::Scope) - } else { - Err(BuildError::UnknownNodeType(node_type.to_string())) - } + pub(crate) fn try_from(value: &str) -> Result { + value + .strip_prefix("user:") + .map(|path| FileDescriptor::User(path.into())) + .or_else(|| { + value + .strip_prefix("system:") + .map(|path| FileDescriptor::System(path.into())) + }) + .ok_or(FileDescriptorError) } - fn verify_node(&self, node: Handle) -> Result<(), BuildError> { - if let Node::PushScopedSymbol(node) = &self.stack_graph[node] { - let scope = &self.stack_graph[self.stack_graph.node_for_id(node.scope).unwrap()]; - if !scope.is_exported_scope() { - return Err(BuildError::SymbolScopeError( - format!("{}", node.display(self.stack_graph)), - format!("{}", scope.display(self.stack_graph)), - )); - } - } - Ok(()) + pub(crate) fn from(value: &str) -> Self { + Self::try_from(value) + .unwrap_or_else(|_| panic!("{value} should be a valid file descriptor")) } -} -enum NodeType { - DropScopes, - PopSymbol, - PopScopedSymbol, - PushSymbol, - PushScopedSymbol, - Scope, -} - -impl<'a, KT: KindTypes> Builder<'a, KT> { - /// Get the `NodeID` corresponding to a `Graph` node. - /// - /// By default, graph nodes get their index shifted by [`self.injected_node_count`] as their - /// `local_id`, unless they have a corresponding entry in the [`self.remapped_nodes`] map. This - /// is the case if: - /// 1. The node was injected, in which case it is mapped to the `NodeID` of the injected node. - /// 2. The node's default `local_id` clashes with a preexisting node, in which case it is mapped to - /// an available `local_id` beyond the range of default `local_ids`. - fn node_id_for_graph_node(&self, node_ref: GraphNodeRef) -> NodeID { - let index = node_ref.index(); - self.remapped_nodes.get(&index).map_or_else( - || { - NodeID::new_in_file( - self.file, - u32::try_from(index - self.injected_node_count) - .expect("local_id to fit in u32"), - ) - }, - |id| *id, - ) + pub fn get_path(&self) -> &str { + match self { + Self::User(path) => path, + Self::System(path) => path, + } } - fn load_drop_scopes(&mut self, node_ref: GraphNodeRef) -> Handle { - let id = self.node_id_for_graph_node(node_ref); - self.stack_graph.add_drop_scopes_node(id).unwrap() + pub fn is_system(&self) -> bool { + matches!(self, Self::System(_)) } - fn load_pop_scoped_symbol( - &mut self, - node_ref: GraphNodeRef, - ) -> Result, BuildError> { - let node = &self.graph[node_ref]; - let symbol = match node.attributes.get(SYMBOL_ATTR) { - Some(symbol) => Self::load_symbol(symbol)?, - None => return Err(BuildError::MissingSymbol(node_ref)), - }; - let symbol = self.stack_graph.add_symbol(&symbol); - let id = self.node_id_for_graph_node(node_ref); - let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; - Self::verify_attributes(node, POP_SCOPED_SYMBOL_TYPE, &POP_SCOPED_SYMBOL_ATTRS); - let node_handle = self - .stack_graph - .add_pop_scoped_symbol_node(id, symbol, is_definition) - .unwrap(); - Ok(node_handle) + pub fn is_user(&self) -> bool { + matches!(self, Self::User(_)) } - fn load_pop_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { - let node = &self.graph[node_ref]; - let symbol = match node.attributes.get(SYMBOL_ATTR) { - Some(symbol) => Self::load_symbol(symbol)?, - None => return Err(BuildError::MissingSymbol(node_ref)), - }; - let symbol = self.stack_graph.add_symbol(&symbol); - let id = self.node_id_for_graph_node(node_ref); - let is_definition = Self::load_flag(node, IS_DEFINITION_ATTR)?; - Self::verify_attributes(node, POP_SYMBOL_TYPE, &POP_SYMBOL_ATTRS); - let node_handle = self - .stack_graph - .add_pop_symbol_node(id, symbol, is_definition) - .unwrap(); - Ok(node_handle) + pub fn is_user_path(&self, path: &str) -> bool { + matches!(self, Self::User(user_path) if user_path == path) } +} - fn load_push_scoped_symbol( - &mut self, - node_ref: GraphNodeRef, - ) -> Result, BuildError> { - let node = &self.graph[node_ref]; - let symbol = match node.attributes.get(SYMBOL_ATTR) { - Some(symbol) => Self::load_symbol(symbol)?, - None => return Err(BuildError::MissingSymbol(node_ref)), - }; - let symbol = self.stack_graph.add_symbol(&symbol); - let id = self.node_id_for_graph_node(node_ref); - let scope = match node.attributes.get(SCOPE_ATTR) { - Some(scope) => self.node_id_for_graph_node(scope.as_graph_node_ref()?), - None => return Err(BuildError::MissingScope(node_ref)), - }; - let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; - Self::verify_attributes(node, PUSH_SCOPED_SYMBOL_TYPE, &PUSH_SCOPED_SYMBOL_ATTRS); - Ok(self - .stack_graph - .add_push_scoped_symbol_node(id, symbol, scope, is_reference) - .unwrap()) - } +pub trait PathResolver { + fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option; +} - fn load_push_symbol(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { - let node = &self.graph[node_ref]; - let symbol = match node.attributes.get(SYMBOL_ATTR) { - Some(symbol) => Self::load_symbol(symbol)?, - None => return Err(BuildError::MissingSymbol(node_ref)), - }; - let symbol = self.stack_graph.add_symbol(&symbol); - let id = self.node_id_for_graph_node(node_ref); - let is_reference = Self::load_flag(node, IS_REFERENCE_ATTR)?; - Self::verify_attributes(node, PUSH_SYMBOL_TYPE, &PUSH_SYMBOL_ATTRS); - Ok(self - .stack_graph - .add_push_symbol_node(id, symbol, is_reference) - .unwrap()) - } +impl BindingGraphBuilder { + pub fn create( + version: Version, + binding_rules: &str, + path_resolver: Rc>, + ) -> Self { + let graph_builder_file = + File::from_str(binding_rules).expect("Bindings stack graph builder parse error"); + let stack_graph = StackGraph::new(); + let functions = loader::default_functions(version, path_resolver); - fn load_scope(&mut self, node_ref: GraphNodeRef) -> Result, BuildError> { - let node = &self.graph[node_ref]; - let id = self.node_id_for_graph_node(node_ref); - let is_exported = - Self::load_flag(node, IS_EXPORTED_ATTR)? || Self::load_flag(node, IS_ENDPOINT_ATTR)?; - Self::verify_attributes(node, SCOPE_TYPE, &SCOPE_ATTRS); - Ok(self.stack_graph.add_scope_node(id, is_exported).unwrap()) + Self { + graph_builder_file, + functions, + stack_graph, + cursors: HashMap::new(), + definitions_info: HashMap::new(), + references_info: HashMap::new(), + cursor_to_definitions: HashMap::new(), + cursor_to_references: HashMap::new(), + extension_hooks: HashSet::new(), + } } - fn load_symbol(value: &Value) -> Result { - match value { - Value::Integer(i) => Ok(i.to_string()), - Value::String(s) => Ok(s.clone()), - _ => Err(BuildError::UnknownSymbolType(value.to_string())), - } + pub fn add_system_file(&mut self, file_path: &str, tree_cursor: Cursor) { + let file_kind = FileDescriptor::System(file_path.into()); + let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); + _ = self.add_file_internal(file, tree_cursor); } - fn load_flag(node: &GraphNode, attribute: &str) -> Result { - match node.attributes.get(attribute) { - Some(value) => value - .as_boolean() - .map_err(|_| BuildError::UnknownFlagType(attribute.to_string(), value.to_string())), - None => Ok(false), - } + pub fn add_user_file(&mut self, file_path: &str, tree_cursor: Cursor) { + let file_kind = FileDescriptor::User(file_path.into()); + let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); + _ = self.add_file_internal(file, tree_cursor); } - fn load_source_info( + #[cfg(feature = "__private_testing_utils")] + pub fn add_user_file_returning_graph( &mut self, - node_ref: GraphNodeRef, - node_handle: Handle, - ) -> Result<(), BuildError> { - let node = &self.graph[node_ref]; - - // For every added graph node which links to a corresponding source - // node, save the corresponding CST cursor so our caller can extract - // that info later. - if let Some(source_node) = node.attributes.get(SOURCE_NODE_ATTR) { - let syntax_node_ref = source_node.as_syntax_node_ref()?; - let source_node = &self.graph[syntax_node_ref]; - self.cursors.insert(node_handle, source_node.clone()); - } - - if let Some(syntax_type) = node.attributes.get(SYNTAX_TYPE_ATTR) { - let syntax_type = syntax_type.as_str()?; - let syntax_type = self.stack_graph.add_string(syntax_type); - let source_info = self.stack_graph.source_info_mut(node_handle); - source_info.syntax_type = syntax_type.into(); + file_path: &str, + tree_cursor: Cursor, + ) -> metaslang_graph_builder::graph::Graph { + let file_kind = FileDescriptor::User(file_path.into()); + let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); + let result = self.add_file_internal(file, tree_cursor); + result.graph + } + + fn add_file_internal(&mut self, file: FileHandle, tree_cursor: Cursor) -> LoadResult { + let loader = Loader::new( + &self.graph_builder_file, + &self.functions, + &mut self.stack_graph, + file, + tree_cursor, + ); + let mut result = loader + .execute(&loader::NoCancellation) + .expect("Internal error while building bindings"); + + for (handle, cursor) in result.cursors.drain() { + let cursor_id = cursor.node().id(); + if self.stack_graph[handle].is_definition() { + self.cursor_to_definitions.insert(cursor_id, handle); + } else { + self.cursor_to_references.insert(cursor_id, handle); + } + self.cursors.insert(handle, cursor); } + self.definitions_info + .extend(result.definitions_info.drain()); + self.references_info.extend(result.references_info.drain()); + self.extension_hooks.extend(result.extension_hooks.drain()); - Ok(()) + result } - fn node_handle_for_graph_node(&self, node_ref: GraphNodeRef) -> Handle { - self.stack_graph - .node_for_id(self.node_id_for_graph_node(node_ref)) - .expect("parent node exists in the stack graph") + pub fn build(self) -> Rc> { + let resolver = Resolver::new(&self); + let resolved_references = resolver.resolve(); + BindingGraph::build(self, resolved_references) } - // Saves additional binding information from the loaded graph (eg. tag, - // definiens, import/export nodes and parents). - fn load_additional_info(&mut self, node_ref: GraphNodeRef) -> Result<(), BuildError> { - let node = &self.graph[node_ref]; - let node_handle = self.node_handle_for_graph_node(node_ref); - let stack_graph_node = &self.stack_graph[node_handle]; - - let parents = match node.attributes.get(PARENTS_ATTR) { - Some(parents) => { - parents - .as_list()? - .iter() - .flat_map(|value| { - value - .as_graph_node_ref() - .map(|id| self.node_handle_for_graph_node(id)) - }) - .flat_map(|parent| { - // ensure parents are either definitions or references - let parent_node = &self.stack_graph[parent]; - if !parent_node.is_definition() && !parent_node.is_reference() { - Err(BuildError::InvalidParent(node_ref)) - } else { - Ok(parent) - } - }) - .collect() - } - None => Vec::new(), - }; - - if stack_graph_node.is_definition() { - let definiens = match node.attributes.get(DEFINIENS_NODE_ATTR) { - Some(definiens_node) => { - let syntax_node_ref = definiens_node.as_syntax_node_ref()?; - let definiens_node = &self.graph[syntax_node_ref]; - Some(definiens_node.clone()) - } - None => None, - }; - - 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 { - definiens, - parents, - extension_scope, - inherit_extensions, - }, - ); - } else if stack_graph_node.is_reference() { + pub(crate) fn get_parents(&self, handle: GraphHandle) -> Vec { + if self.is_definition(handle) { + self.definitions_info + .get(&handle) + .map(|info| info.parents.clone()) + .unwrap_or_default() + } else { self.references_info - .insert(node_handle, ReferenceBindingInfo { parents }); + .get(&handle) + .map(|info| info.parents.clone()) + .unwrap_or_default() } + } - if Self::load_flag(node, EXTENSION_HOOK_ATTR)? { - self.extension_hooks.insert(node_handle); - } + fn is_definition(&self, handle: GraphHandle) -> bool { + self.stack_graph[handle].is_definition() + } - Ok(()) + fn is_reference(&self, handle: GraphHandle) -> bool { + self.stack_graph[handle].is_reference() } - fn load_node_debug_info(&mut self, node_ref: GraphNodeRef, node_handle: Handle) { - let node = &self.graph[node_ref]; - for (name, value) in node.attributes.iter() { - let name = name.to_string(); - if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { - let value = match value { - Value::String(value) => value.clone(), - value => value.to_string(), - }; - let key = self.stack_graph.add_string(name_without_prefix); - let value = self.stack_graph.add_string(&value); - self.stack_graph - .node_debug_info_mut(node_handle) - .add(key, value); - } - } + fn get_extension_scope(&self, handle: GraphHandle) -> Option { + self.definitions_info + .get(&handle) + .and_then(|info| info.extension_scope) } - fn load_edge_debug_info( - stack_graph: &mut StackGraph, - source_handle: Handle, - sink_handle: Handle, - edge: &Edge, - ) { - for (name, value) in edge.attributes.iter() { - let name = name.to_string(); - if let Some(name_without_prefix) = name.strip_prefix(DEBUG_ATTR_PREFIX) { - let value = match value { - Value::String(value) => value.clone(), - value => value.to_string(), - }; - let key = stack_graph.add_string(name_without_prefix); - let value = stack_graph.add_string(&value); - stack_graph - .edge_debug_info_mut(source_handle, sink_handle) - .add(key, value); - } - } + fn inherits_extensions(&self, handle: GraphHandle) -> bool { + self.definitions_info + .get(&handle) + .is_some_and(|info| info.inherit_extensions) } - fn verify_attributes( - node: &GraphNode, - node_type: &str, - allowed_attributes: &HashSet<&'static str>, - ) { - for (id, _) in node.attributes.iter() { - let id = id.as_str(); - if !allowed_attributes.contains(id) - && id != SOURCE_NODE_ATTR - && id != EMPTY_SOURCE_SPAN_ATTR - && !id.starts_with(DEBUG_ATTR_PREFIX) - { - eprintln!("Unexpected attribute {id} on node of type {node_type}"); - } - } + fn get_file(&self, handle: GraphHandle) -> Option { + self.stack_graph[handle] + .file() + .map(|file| FileDescriptor::from(self.stack_graph[file].name())) + } + + pub(crate) fn is_extension_hook(&self, node_handle: GraphHandle) -> bool { + self.extension_hooks.contains(&node_handle) } } diff --git a/crates/metaslang/bindings/src/builder/resolver.rs b/crates/metaslang/bindings/src/builder/resolver.rs new file mode 100644 index 0000000000..85244b993c --- /dev/null +++ b/crates/metaslang/bindings/src/builder/resolver.rs @@ -0,0 +1,300 @@ +use std::collections::{HashMap, HashSet}; +use std::iter::once; + +use metaslang_cst::kinds::KindTypes; +use stack_graphs::arena::Handle; +use stack_graphs::graph::{Degree, Edge, StackGraph}; +use stack_graphs::partial::{PartialPath, PartialPaths}; +use stack_graphs::stitching::{ + Database, DatabaseCandidates, ForwardCandidates, ForwardPartialPathStitcher, StitcherConfig, + ToAppendable, +}; +use stack_graphs::{CancellationError, NoCancellation}; + +use super::{BindingGraphBuilder, GraphHandle}; + +pub(crate) struct Resolver<'a, KT: KindTypes + 'static> { + owner: &'a BindingGraphBuilder, + partials: PartialPaths, + database: Database, + references: HashMap>, +} + +impl<'a, KT: KindTypes + 'static> Resolver<'a, KT> { + pub fn new(owner: &'a BindingGraphBuilder) -> Self { + let database = Database::new(); + let partials = PartialPaths::new(); + + let mut resolver = Self { + owner, + partials, + database, + references: HashMap::new(), + }; + resolver.build(); + resolver + } + + fn build(&mut self) { + for file in self.owner.stack_graph.iter_files() { + ForwardPartialPathStitcher::find_minimal_partial_path_set_in_file( + &self.owner.stack_graph, + &mut self.partials, + file, + StitcherConfig::default(), + &NoCancellation, + |stack_graph, partials, path| { + self.database + .add_partial_path(stack_graph, partials, path.clone()); + }, + ) + .expect("Should never be cancelled"); + + self.database.ensure_both_directions(&mut self.partials); + } + } + + fn resolve_parents(&mut self, reference: GraphHandle) -> Vec { + self.owner + .get_parents(reference) + .iter() + .flat_map(|handle| { + if self.owner.is_definition(*handle) { + vec![*handle] + } else { + self.resolve_internal(*handle, false) + } + }) + .collect() + } + + fn resolve_parents_recursively(&mut self, parent: GraphHandle) -> Vec { + let mut results = HashMap::new(); + let mut resolve_queue = Vec::new(); + resolve_queue.push(parent); + while let Some(current) = resolve_queue.pop() { + let current_parents = self.resolve_parents(current); + for current_parent in ¤t_parents { + if !results.contains_key(current_parent) { + resolve_queue.push(*current_parent); + } + } + results.insert(current, current_parents); + } + results.into_values().flatten().collect() + } + + fn resolve_internal( + &mut self, + reference: GraphHandle, + allow_recursion: bool, + ) -> Vec { + if let Some(definitions) = self.references.get(&reference) { + return definitions.clone(); + } + + // Save `PartialPaths` state to restore allocations after the resolution + // is complete + let checkpoint = self.partials.save_checkpoint(); + let mut reference_paths = Vec::new(); + + if allow_recursion { + // look for extension scopes to apply to the reference + let ref_parents = self.resolve_parents(reference); + let mut extensions = HashSet::new(); + for parent in &ref_parents { + if let Some(extension_scope) = self.owner.get_extension_scope(*parent) { + extensions.insert(extension_scope); + } + + if self.owner.inherits_extensions(*parent) { + let grand_parents = self.resolve_parents_recursively(*parent); + for grand_parent in &grand_parents { + if let Some(extension_scope) = self.owner.get_extension_scope(*grand_parent) + { + extensions.insert(extension_scope); + } + } + } + } + let extensions = extensions.drain().collect::>(); + let mut database = ExtendedDatabase::new(&mut self.database); + + ForwardPartialPathStitcher::find_all_complete_partial_paths( + &mut DatabaseCandidatesExtended::new( + self.owner, + &mut self.partials, + &mut database, + extensions, + ), + once(reference), + StitcherConfig::default(), + &NoCancellation, + |_graph, _partials, path| { + reference_paths.push(path.clone()); + }, + ) + .expect("not cancelled"); + } else { + ForwardPartialPathStitcher::find_all_complete_partial_paths( + &mut DatabaseCandidates::new( + &self.owner.stack_graph, + &mut self.partials, + &mut self.database, + ), + once(reference), + StitcherConfig::default(), + &NoCancellation, + |_graph, _partials, path| { + reference_paths.push(path.clone()); + }, + ) + .expect("not cancelled"); + } + + let mut results = Vec::new(); + for reference_path in &reference_paths { + let end_node = reference_path.end_node; + + if reference_paths + .iter() + .all(|other| !other.shadows(&mut self.partials, reference_path)) + { + results.push(end_node); + } + } + + // Reclaim arena memory used for this resolution + self.partials.restore_checkpoint(checkpoint); + results + } + + pub(crate) fn resolve(mut self) -> HashMap> { + for handle in self.owner.stack_graph.iter_nodes() { + if self.owner.is_reference(handle) + && self + .owner + .get_file(handle) + .is_some_and(|file| file.is_user()) + { + let definition_handles = self.resolve_internal(handle, true); + self.references.insert(handle, definition_handles); + } + } + self.references + } +} + +// This is a partial paths database, but we also need to keep track of edges +// added to connect to extension scopes +struct ExtendedDatabase<'a> { + pub database: &'a mut Database, + pub edges: Vec, +} + +impl<'a> ExtendedDatabase<'a> { + fn new(database: &'a mut Database) -> Self { + Self { + database, + edges: Vec::new(), + } + } +} + +// These are handles to partial paths or edges in `ExtendedDatabase` +#[derive(Clone, Debug)] +enum ExtendedHandle { + Handle(Handle), + Edge(usize), +} + +impl ToAppendable for ExtendedDatabase<'_> { + fn get_appendable<'a>(&'a self, handle: &'a ExtendedHandle) -> &'a PartialPath { + match handle { + ExtendedHandle::Handle(handle) => self.database.get_appendable(handle), + ExtendedHandle::Edge(edge) => &self.edges[*edge], + } + } +} + +struct DatabaseCandidatesExtended<'a, KT: KindTypes + 'static> { + owner: &'a BindingGraphBuilder, + partials: &'a mut PartialPaths, + database: &'a mut ExtendedDatabase<'a>, + extensions: Vec, +} + +impl<'a, KT: KindTypes + 'static> DatabaseCandidatesExtended<'a, KT> { + fn new( + owner: &'a BindingGraphBuilder, + partials: &'a mut PartialPaths, + database: &'a mut ExtendedDatabase<'a>, + extensions: Vec, + ) -> Self { + Self { + owner, + partials, + database, + extensions, + } + } +} + +impl<'a, KT: KindTypes + 'static> + ForwardCandidates, CancellationError> + for DatabaseCandidatesExtended<'a, KT> +{ + // Return the forward candidates from the encapsulated `Database` and inject + // the extension edges if the given path's end is an extension hook + fn get_forward_candidates(&mut self, path: &PartialPath, result: &mut R) + where + R: std::iter::Extend, + { + let node = path.end_node; + + let mut db_candidates = Vec::new(); + self.database.database.find_candidate_partial_paths( + &self.owner.stack_graph, + self.partials, + path, + &mut db_candidates, + ); + result.extend( + db_candidates + .iter() + .map(|candidate| ExtendedHandle::Handle(*candidate)), + ); + + if self.owner.is_extension_hook(node) { + for extension in &self.extensions { + let edge = Edge { + source: node, + sink: *extension, + precedence: 0, + }; + let mut partial_path = + PartialPath::from_node(&self.owner.stack_graph, self.partials, node); + partial_path + .append(&self.owner.stack_graph, self.partials, edge) + .expect("path can be extended"); + let edge_handle = self.database.edges.len(); + self.database.edges.push(partial_path); + result.extend(once(ExtendedHandle::Edge(edge_handle))); + } + } + } + + fn get_joining_candidate_degree(&self, path: &PartialPath) -> Degree { + // TODO: this may not be correct for extension scopes, but it's only + // used for cycle detection + self.database + .database + .get_incoming_path_degree(path.end_node) + } + + fn get_graph_partials_and_db( + &mut self, + ) -> (&StackGraph, &mut PartialPaths, &ExtendedDatabase<'a>) { + (&self.owner.stack_graph, self.partials, self.database) + } +} diff --git a/crates/metaslang/bindings/src/graph/definition.rs b/crates/metaslang/bindings/src/graph/definition.rs new file mode 100644 index 0000000000..fdfb0574c6 --- /dev/null +++ b/crates/metaslang/bindings/src/graph/definition.rs @@ -0,0 +1,96 @@ +use std::fmt::{self, Debug, Display}; +use std::rc::Rc; + +use metaslang_cst::cursor::Cursor; +use metaslang_cst::kinds::KindTypes; + +use super::{BindingGraph, BindingLocation}; +use crate::builder::{FileDescriptor, GraphHandle}; +use crate::graph::DisplayCursor; + +#[derive(Clone)] +pub struct Definition { + pub(crate) owner: Rc>, + pub(crate) handle: GraphHandle, +} + +impl Definition { + pub fn id(&self) -> usize { + self.get_cursor().node().id() + } + + pub fn name_location(&self) -> BindingLocation { + match self.get_file() { + FileDescriptor::System(_) => BindingLocation::built_in(), + FileDescriptor::User(file_id) => { + BindingLocation::user_file(file_id, self.get_cursor().to_owned()) + } + } + } + + pub fn definiens_location(&self) -> BindingLocation { + match self.get_file() { + FileDescriptor::System(_) => BindingLocation::built_in(), + FileDescriptor::User(file_id) => { + BindingLocation::user_file(file_id, self.get_definiens_cursor().to_owned()) + } + } + } + + pub fn get_cursor(&self) -> &Cursor { + &self + .owner + .definitions + .get(&self.handle) + .expect("Definition handle is valid") + .cursor + } + + pub fn get_definiens_cursor(&self) -> &Cursor { + &self + .owner + .definitions + .get(&self.handle) + .expect("Definition handle is valid") + .definiens + } + + pub fn get_file(&self) -> FileDescriptor { + self.owner + .get_file( + self.owner + .definitions + .get(&self.handle) + .expect("Definition handle is valid") + .file, + ) + .expect("Definition does not have a valid file descriptor") + } +} + +impl Display for Definition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "definition {}", + DisplayCursor { + cursor: self.get_cursor(), + file: self.get_file() + } + ) + } +} + +impl Debug for Definition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + +impl PartialEq for Definition { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.owner, &other.owner) && self.handle == other.handle + } +} + +impl Eq for Definition {} diff --git a/crates/metaslang/bindings/src/location/mod.rs b/crates/metaslang/bindings/src/graph/location.rs similarity index 100% rename from crates/metaslang/bindings/src/location/mod.rs rename to crates/metaslang/bindings/src/graph/location.rs diff --git a/crates/metaslang/bindings/src/graph/mod.rs b/crates/metaslang/bindings/src/graph/mod.rs new file mode 100644 index 0000000000..935df7fe0a --- /dev/null +++ b/crates/metaslang/bindings/src/graph/mod.rs @@ -0,0 +1,159 @@ +mod definition; +mod location; +mod reference; + +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::rc::Rc; + +pub use definition::Definition; +pub use location::{BindingLocation, BuiltInLocation, UserFileLocation}; +use metaslang_cst::cursor::Cursor; +use metaslang_cst::kinds::KindTypes; +pub use reference::Reference; + +use crate::builder::{CursorID, FileDescriptor, FileHandle, GraphHandle}; +use crate::BindingGraphBuilder; + +struct DefinitionInfo { + file: FileHandle, + cursor: Cursor, + definiens: Cursor, +} + +struct ReferenceInfo { + file: FileHandle, + cursor: Cursor, +} + +pub struct BindingGraph { + files: HashMap, + definitions: BTreeMap>, + references: BTreeMap>, + cursor_to_definitions: HashMap, + cursor_to_references: HashMap, + resolved_references: HashMap>, +} + +impl BindingGraph { + pub(crate) fn build( + builder: BindingGraphBuilder, + resolved_references: HashMap>, + ) -> Rc { + let mut files = HashMap::new(); + for handle in builder.stack_graph.iter_files() { + files.insert( + handle, + FileDescriptor::from(builder.stack_graph[handle].name()), + ); + } + let mut definitions = BTreeMap::new(); + let mut references = BTreeMap::new(); + for handle in builder.stack_graph.iter_nodes() { + let graph_node = &builder.stack_graph[handle]; + let Some(file) = graph_node.file() else { + continue; + }; + if graph_node.is_definition() { + let cursor = builder + .cursors + .get(&handle) + .expect("Definition to have a valid cursor") + .clone(); + let definiens = builder.definitions_info[&handle].definiens.clone(); + definitions.insert( + handle, + DefinitionInfo { + file, + cursor, + definiens, + }, + ); + } else if graph_node.is_reference() { + let cursor = builder + .cursors + .get(&handle) + .expect("Reference to have a valid cursor") + .clone(); + references.insert(handle, ReferenceInfo { file, cursor }); + } + } + + Rc::new(Self { + files, + definitions, + references, + cursor_to_definitions: builder.cursor_to_definitions, + cursor_to_references: builder.cursor_to_references, + resolved_references, + }) + } + + pub fn all_definitions(self: &Rc) -> impl Iterator> + '_ { + self.definitions.keys().map(|handle| Definition { + owner: Rc::clone(self), + handle: *handle, + }) + } + + fn to_definition(self: &Rc, handle: GraphHandle) -> Option> { + if self.definitions.contains_key(&handle) { + Some(Definition { + owner: Rc::clone(self), + handle, + }) + } else { + None + } + } + + pub fn all_references(self: &Rc) -> impl Iterator> + '_ { + self.references.keys().map(|handle| Reference { + owner: Rc::clone(self), + handle: *handle, + }) + } + + pub fn definition_at(self: &Rc, cursor: &Cursor) -> Option> { + let cursor_id = cursor.node().id(); + self.cursor_to_definitions + .get(&cursor_id) + .map(|handle| Definition { + owner: Rc::clone(self), + handle: *handle, + }) + } + + pub fn reference_at(self: &Rc, cursor: &Cursor) -> Option> { + let cursor_id = cursor.node().id(); + self.cursor_to_references + .get(&cursor_id) + .map(|handle| Reference { + owner: Rc::clone(self), + handle: *handle, + }) + } + + fn get_file(&self, handle: FileHandle) -> Option { + self.files.get(&handle).cloned() + } +} + +struct DisplayCursor<'a, KT: KindTypes + 'static> { + cursor: &'a Cursor, + file: FileDescriptor, +} + +impl<'a, KT: KindTypes + 'static> fmt::Display for DisplayCursor<'a, KT> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let offset = self.cursor.text_offset(); + write!( + f, + "`{}` at {}:{}:{}", + self.cursor.node().unparse(), + self.file.get_path(), + offset.line + 1, + offset.column + 1, + ) + } +} diff --git a/crates/metaslang/bindings/src/graph/reference.rs b/crates/metaslang/bindings/src/graph/reference.rs new file mode 100644 index 0000000000..6131dc1e8a --- /dev/null +++ b/crates/metaslang/bindings/src/graph/reference.rs @@ -0,0 +1,81 @@ +use std::fmt::{self, Display}; +use std::rc::Rc; + +use metaslang_cst::cursor::Cursor; +use metaslang_cst::kinds::KindTypes; + +use super::{BindingGraph, BindingLocation, Definition}; +use crate::builder::{FileDescriptor, GraphHandle}; +use crate::graph::DisplayCursor; + +#[derive(Clone)] +pub struct Reference { + pub(crate) owner: Rc>, + pub(crate) handle: GraphHandle, +} + +impl Reference { + pub fn id(&self) -> usize { + self.get_cursor().node().id() + } + + pub fn location(&self) -> BindingLocation { + match self.get_file() { + FileDescriptor::System(_) => BindingLocation::built_in(), + FileDescriptor::User(file_id) => { + BindingLocation::user_file(file_id, self.get_cursor().to_owned()) + } + } + } + + pub fn get_cursor(&self) -> &Cursor { + &self + .owner + .references + .get(&self.handle) + .expect("Reference handle is valid") + .cursor + } + + pub fn get_file(&self) -> FileDescriptor { + self.owner + .get_file( + self.owner + .references + .get(&self.handle) + .expect("Reference handle is valid") + .file, + ) + .expect("Reference does not have a valid file descriptor") + } + + pub fn definitions(&self) -> Vec> { + self.owner.resolved_references[&self.handle] + .iter() + .map(|handle| { + self.owner + .to_definition(*handle) + .expect("Resolved reference handle to be a definition") + }) + .collect() + } +} + +impl Display for Reference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "reference {}", + DisplayCursor { + cursor: self.get_cursor(), + file: self.get_file() + } + ) + } +} + +impl PartialEq for Reference { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.owner, &other.owner) && self.handle == other.handle + } +} diff --git a/crates/metaslang/bindings/src/lib.rs b/crates/metaslang/bindings/src/lib.rs index 1588e48bdc..23aeeb700b 100644 --- a/crates/metaslang/bindings/src/lib.rs +++ b/crates/metaslang/bindings/src/lib.rs @@ -1,471 +1,7 @@ mod builder; -mod location; -mod resolver; +mod graph; -use std::collections::{HashMap, HashSet}; -use std::fmt::{self, Debug, Display}; -use std::hash::Hash; -use std::rc::Rc; - -use builder::BuildResult; -use metaslang_cst::cursor::Cursor; -use metaslang_cst::kinds::KindTypes; -use metaslang_graph_builder::ast::File; -use metaslang_graph_builder::functions::Functions; -use resolver::{ResolveOptions, Resolver}; -use semver::Version; -use stack_graphs::graph::StackGraph; - -type Builder<'a, KT> = builder::Builder<'a, KT>; -type GraphHandle = stack_graphs::arena::Handle; -type FileHandle = stack_graphs::arena::Handle; -type CursorID = usize; - -pub use location::{BindingLocation, BuiltInLocation, UserFileLocation}; - -pub(crate) struct DefinitionBindingInfo { - definiens: Option>, - parents: Vec, - extension_scope: Option, - inherit_extensions: bool, -} - -pub(crate) struct ReferenceBindingInfo { - parents: Vec, -} - -pub struct BindingGraph { - graph_builder_file: File, - functions: Functions, - stack_graph: StackGraph, - cursors: HashMap>, - definitions_info: HashMap>, - references_info: HashMap, - cursor_to_definitions: HashMap, - cursor_to_references: HashMap, - extension_hooks: HashSet, -} - -pub enum FileDescriptor { - User(String), - System(String), -} - -#[derive(Debug)] -pub(crate) struct FileDescriptorError; - -impl FileDescriptor { - // Internal functions to convert a FileDescriptor to and from a string for - // representation inside the stack graph - - pub(crate) fn as_string(&self) -> String { - match self { - Self::User(path) => format!("user:{path}"), - Self::System(path) => format!("system:{path}"), - } - } - - pub(crate) fn try_from(value: &str) -> Result { - value - .strip_prefix("user:") - .map(|path| FileDescriptor::User(path.into())) - .or_else(|| { - value - .strip_prefix("system:") - .map(|path| FileDescriptor::System(path.into())) - }) - .ok_or(FileDescriptorError) - } - - pub(crate) fn from(value: &str) -> Self { - Self::try_from(value) - .unwrap_or_else(|_| panic!("{value} should be a valid file descriptor")) - } - - pub fn get_path(&self) -> &str { - match self { - Self::User(path) => path, - Self::System(path) => path, - } - } - - pub fn is_system(&self) -> bool { - matches!(self, Self::System(_)) - } - - pub fn is_user(&self) -> bool { - matches!(self, Self::User(_)) - } - - pub fn is_user_path(&self, path: &str) -> bool { - matches!(self, Self::User(user_path) if user_path == path) - } -} - -pub trait PathResolver { - fn resolve_path(&self, context_path: &str, path_to_resolve: &Cursor) -> Option; -} - -impl BindingGraph { - pub fn create( - version: Version, - binding_rules: &str, - path_resolver: Rc>, - ) -> Self { - let graph_builder_file = - File::from_str(binding_rules).expect("Bindings stack graph builder parse error"); - let stack_graph = StackGraph::new(); - let functions = builder::default_functions(version, path_resolver); - - Self { - graph_builder_file, - functions, - stack_graph, - cursors: HashMap::new(), - definitions_info: HashMap::new(), - references_info: HashMap::new(), - cursor_to_definitions: HashMap::new(), - cursor_to_references: HashMap::new(), - extension_hooks: HashSet::new(), - } - } - - pub fn add_system_file(&mut self, file_path: &str, tree_cursor: Cursor) { - let file_kind = FileDescriptor::System(file_path.into()); - let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); - _ = self.add_file_internal(file, tree_cursor); - } - - pub fn add_user_file(&mut self, file_path: &str, tree_cursor: Cursor) { - let file_kind = FileDescriptor::User(file_path.into()); - let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); - _ = self.add_file_internal(file, tree_cursor); - } - - #[cfg(feature = "__private_testing_utils")] - pub fn add_user_file_returning_graph( - &mut self, - file_path: &str, - tree_cursor: Cursor, - ) -> metaslang_graph_builder::graph::Graph { - let file_kind = FileDescriptor::User(file_path.into()); - let file = self.stack_graph.get_or_create_file(&file_kind.as_string()); - let result = self.add_file_internal(file, tree_cursor); - result.graph - } - - fn add_file_internal(&mut self, file: FileHandle, tree_cursor: Cursor) -> BuildResult { - let builder = Builder::new( - &self.graph_builder_file, - &self.functions, - &mut self.stack_graph, - file, - tree_cursor, - ); - let mut result = builder - .build(&builder::NoCancellation) - .expect("Internal error while building bindings"); - - for (handle, cursor) in result.cursors.drain() { - let cursor_id = cursor.node().id(); - if self.stack_graph[handle].is_definition() { - self.cursor_to_definitions.insert(cursor_id, handle); - } else { - self.cursor_to_references.insert(cursor_id, handle); - } - self.cursors.insert(handle, cursor); - } - 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 - } - - fn to_definition(&self, handle: GraphHandle) -> Option> { - if self.stack_graph[handle].is_definition() { - Some(Definition { - owner: self, - handle, - }) - } else { - None - } - } - - pub fn all_definitions(&self) -> impl Iterator> + '_ { - self.stack_graph - .iter_nodes() - .filter_map(|handle| self.to_definition(handle)) - } - - fn to_reference(&self, handle: GraphHandle) -> Option> { - if self.stack_graph[handle].is_reference() { - Some(Reference { - owner: self, - handle, - }) - } else { - None - } - } - - pub fn all_references(&self) -> impl Iterator> + '_ { - self.stack_graph - .iter_nodes() - .filter_map(|handle| self.to_reference(handle)) - } - - pub fn definition_at(&self, cursor: &Cursor) -> Option> { - let cursor_id = cursor.node().id(); - self.cursor_to_definitions - .get(&cursor_id) - .map(|handle| Definition { - owner: self, - handle: *handle, - }) - } - - pub fn reference_at(&self, cursor: &Cursor) -> Option> { - let cursor_id = cursor.node().id(); - self.cursor_to_references - .get(&cursor_id) - .map(|handle| Reference { - owner: self, - handle: *handle, - }) - } - - fn resolve_handles(&self, handles: &[GraphHandle]) -> Vec> { - // NOTE: the Builder ensures that all handles in the parents are - // either definitions or references - handles - .iter() - .flat_map(|handle| { - if self.stack_graph[*handle].is_definition() { - vec![self.to_definition(*handle).unwrap()] - } else { - self.to_reference(*handle).unwrap().non_recursive_resolve() - } - }) - .collect() - } - - pub(crate) fn is_extension_hook(&self, node_handle: GraphHandle) -> bool { - self.extension_hooks.contains(&node_handle) - } -} - -struct DisplayCursor<'a, KT: KindTypes + 'static> { - cursor: &'a Cursor, - file: FileDescriptor, -} - -impl<'a, KT: KindTypes + 'static> fmt::Display for DisplayCursor<'a, KT> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let offset = self.cursor.text_offset(); - write!( - f, - "`{}` at {}:{}:{}", - self.cursor.node().unparse(), - self.file.get_path(), - offset.line + 1, - offset.column + 1, - ) - } -} - -#[derive(Clone)] -pub struct Definition<'a, KT: KindTypes + 'static> { - owner: &'a BindingGraph, - handle: GraphHandle, -} - -impl<'a, KT: KindTypes + 'static> Definition<'a, KT> { - pub fn id(&self) -> usize { - self.get_cursor().node().id() - } - - pub fn name_location(&self) -> BindingLocation { - match self.get_file() { - FileDescriptor::System(_) => BindingLocation::built_in(), - FileDescriptor::User(file_id) => { - BindingLocation::user_file(file_id, self.get_cursor().to_owned()) - } - } - } - - pub fn definiens_location(&self) -> BindingLocation { - match self.get_file() { - FileDescriptor::System(_) => BindingLocation::built_in(), - FileDescriptor::User(file_id) => { - BindingLocation::user_file(file_id, self.get_definiens_cursor().to_owned()) - } - } - } - - pub fn get_cursor(&self) -> &Cursor { - self.owner - .cursors - .get(&self.handle) - .expect("Definition does not have a valid cursor") - } - - pub fn get_definiens_cursor(&self) -> &Cursor { - self.owner - .definitions_info - .get(&self.handle) - .expect("Definition does not have valid binding info") - .definiens - .as_ref() - .expect("Definiens does not have a valid cursor") - } - - pub fn get_file(&self) -> FileDescriptor { - self.owner.stack_graph[self.handle] - .file() - .map(|file| FileDescriptor::from(self.owner.stack_graph[file].name())) - .expect("Definition does not have a valid file descriptor") - } - - pub(crate) fn resolve_parents(&self) -> Vec> { - self.owner - .definitions_info - .get(&self.handle) - .map(|info| &info.parents) - .map(|handles| self.owner.resolve_handles(handles)) - .unwrap_or_default() - } - - pub(crate) fn get_extension_scope(&self) -> Option { - 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) - } -} - -impl Display for Definition<'_, KT> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "definition {}", - DisplayCursor { - cursor: self.get_cursor(), - file: self.get_file() - } - ) - } -} - -impl Debug for Definition<'_, KT> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self}") - } -} - -impl PartialEq for Definition<'_, KT> { - fn eq(&self, other: &Self) -> bool { - let our_owner: *const BindingGraph = self.owner; - let other_owner: *const BindingGraph = other.owner; - our_owner == other_owner && self.handle == other.handle - } -} - -impl Eq for Definition<'_, KT> {} - -impl Hash for Definition<'_, KT> { - fn hash(&self, state: &mut H) { - let owner: *const BindingGraph = self.owner; - owner.hash(state); - self.handle.hash(state); - } -} - -#[derive(Clone)] -pub struct Reference<'a, KT: KindTypes + 'static> { - owner: &'a BindingGraph, - handle: GraphHandle, -} - -pub enum ResolutionError<'a, KT: KindTypes + 'static> { - Unresolved, - AmbiguousDefinitions(Vec>), -} - -impl<'a, KT: KindTypes + 'static> Reference<'a, KT> { - pub fn id(&self) -> usize { - self.get_cursor().node().id() - } - - pub fn location(&self) -> BindingLocation { - match self.get_file() { - FileDescriptor::System(_) => BindingLocation::built_in(), - FileDescriptor::User(file_id) => { - BindingLocation::user_file(file_id, self.get_cursor().to_owned()) - } - } - } - - pub fn get_cursor(&self) -> &Cursor { - self.owner - .cursors - .get(&self.handle) - .expect("Reference does not have a valid cursor") - } - - pub fn get_file(&self) -> FileDescriptor { - self.owner.stack_graph[self.handle] - .file() - .map(|file| FileDescriptor::from(self.owner.stack_graph[file].name())) - .expect("Reference does not have a valid file descriptor") - } - - pub fn definitions(&self) -> Vec> { - Resolver::build_for(self, ResolveOptions::Full).all() - } - - pub(crate) fn non_recursive_resolve(&self) -> Vec> { - // This was likely originated from a full resolution call, so cut - // recursion here by restricting the resolution algorithm. - Resolver::build_for(self, ResolveOptions::NonRecursive).all() - } - - pub(crate) fn resolve_parents(&self) -> Vec> { - self.owner - .references_info - .get(&self.handle) - .map(|info| &info.parents) - .map(|handles| self.owner.resolve_handles(handles)) - .unwrap_or_default() - } -} - -impl Display for Reference<'_, KT> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "reference {}", - DisplayCursor { - cursor: self.get_cursor(), - file: self.get_file() - } - ) - } -} - -impl PartialEq for Reference<'_, KT> { - fn eq(&self, other: &Self) -> bool { - let our_owner: *const BindingGraph = self.owner; - let other_owner: *const BindingGraph = other.owner; - our_owner == other_owner && self.handle == other.handle - } -} +pub use builder::{BindingGraphBuilder, PathResolver}; +pub use graph::{ + BindingGraph, BindingLocation, BuiltInLocation, Definition, Reference, UserFileLocation, +}; diff --git a/crates/metaslang/bindings/src/resolver/mod.rs b/crates/metaslang/bindings/src/resolver/mod.rs deleted file mode 100644 index 0faec8248c..0000000000 --- a/crates/metaslang/bindings/src/resolver/mod.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::iter::once; - -use metaslang_cst::kinds::KindTypes; -use stack_graphs::graph::{Degree, Edge, StackGraph}; -use stack_graphs::partial::{PartialPath, PartialPaths}; -use stack_graphs::stitching::{ - ForwardCandidates, ForwardPartialPathStitcher, GraphEdgeCandidates, GraphEdges, StitcherConfig, -}; -use stack_graphs::CancellationError; - -use crate::{BindingGraph, Definition, FileHandle, GraphHandle, Reference}; - -/// The resolver executes algorithms to resolve a reference to one or more -/// definitions. The reference may not be resolvable in the current state of the -/// bindings (eg. you may still need to add an imported file), so it may not be -/// able to find any definitions, or the definitions it finds may be an -/// incomplete set (eg. finding only an import alias, but not the actual -/// definition). The base algorithm will omit shadowed paths (ie. those -/// discarded by higher precedence edges) from the results, but there are other -/// circumstances when many definitions may be found: -/// -/// 1. Destructuring imports (with or without aliases): these are -/// represented in the graph as intermediate definition nodes along the -/// path to the actual definition; hence why this function will return -/// the longest path available. -/// -/// 2. Virtual methods: a reference should find valid paths to all available -/// definitions in a class hierarchy. -/// -pub(crate) struct Resolver<'a, KT: KindTypes + 'static> { - owner: &'a BindingGraph, - reference: Reference<'a, KT>, - partials: PartialPaths, - results: Vec>, - options: ResolveOptions, -} - -#[derive(Copy, Clone, Eq, PartialEq)] -pub(crate) enum ResolveOptions { - Full, - NonRecursive, -} - -/// Candidates for the forward stitching resolution process. This will inject -/// edges to the the given extensions scopes at extension hook nodes when asked -/// for forward candidates (ie. `get_forward_candidates`) by the resolution -/// algorithm. Other than that, it's exactly the same as `GraphEdgeCandidates`. -struct ResolverCandidates<'a, KT: KindTypes + 'static> { - owner: &'a BindingGraph, - partials: &'a mut PartialPaths, - file: Option, - edges: GraphEdges, - extensions: &'a [GraphHandle], -} - -impl<'a, KT: KindTypes + 'static> ResolverCandidates<'a, KT> { - pub fn new( - owner: &'a BindingGraph, - partials: &'a mut PartialPaths, - file: Option, - extensions: &'a [GraphHandle], - ) -> Self { - Self { - owner, - partials, - file, - edges: GraphEdges, - extensions, - } - } -} - -impl ForwardCandidates - for ResolverCandidates<'_, KT> -{ - fn get_forward_candidates(&mut self, path: &PartialPath, result: &mut R) - where - R: std::iter::Extend, - { - let node = path.end_node; - result.extend(self.owner.stack_graph.outgoing_edges(node).filter(|e| { - self.file - .map_or(true, |file| self.owner.stack_graph[e.sink].is_in_file(file)) - })); - - if self.owner.is_extension_hook(node) { - // Inject edges from the extension hook node to each extension scope - let mut extension_edges = Vec::new(); - for extension in self.extensions { - extension_edges.push(Edge { - source: node, - sink: *extension, - precedence: 0, - }); - } - result.extend(extension_edges); - } - } - - fn get_joining_candidate_degree(&self, path: &PartialPath) -> Degree { - self.owner.stack_graph.incoming_edge_degree(path.end_node) - } - - fn get_graph_partials_and_db(&mut self) -> (&StackGraph, &mut PartialPaths, &GraphEdges) { - (&self.owner.stack_graph, self.partials, &self.edges) - } -} - -impl<'a, KT: KindTypes + 'static> Resolver<'a, KT> { - pub fn build_for(reference: &Reference<'a, KT>, options: ResolveOptions) -> Self { - let mut resolver = Self { - owner: reference.owner, - reference: reference.clone(), - partials: PartialPaths::new(), - results: Vec::new(), - options, - }; - resolver.resolve(); - resolver - } - - fn resolve(&mut self) { - let mut reference_paths = Vec::new(); - if self.options == ResolveOptions::Full { - let ref_parents = self.reference.resolve_parents(); - let mut extensions = HashSet::new(); - for parent in &ref_parents { - if let Some(extension_scope) = parent.get_extension_scope() { - extensions.insert(extension_scope); - } - - if parent.inherit_extensions() { - #[allow(clippy::mutable_key_type)] - let grand_parents = Self::resolve_parents_all(parent.clone()); - for grand_parent in grand_parents.values().flatten() { - if let Some(extension_scope) = grand_parent.get_extension_scope() { - extensions.insert(extension_scope); - } - } - } - } - let extensions = extensions.drain().collect::>(); - - ForwardPartialPathStitcher::find_all_complete_partial_paths( - &mut ResolverCandidates::new(self.owner, &mut self.partials, None, &extensions), - once(self.reference.handle), - StitcherConfig::default(), - &stack_graphs::NoCancellation, - |_graph, _paths, path| { - reference_paths.push(path.clone()); - }, - ) - .expect("Should never be cancelled"); - } else { - ForwardPartialPathStitcher::find_all_complete_partial_paths( - &mut GraphEdgeCandidates::new(&self.owner.stack_graph, &mut self.partials, None), - once(self.reference.handle), - StitcherConfig::default(), - &stack_graphs::NoCancellation, - |_graph, _paths, path| { - reference_paths.push(path.clone()); - }, - ) - .expect("Should never be cancelled"); - }; - - let mut added_nodes = HashSet::new(); - for reference_path in &reference_paths { - let end_node = reference_path.end_node; - - // There may be duplicate ending nodes with different - // post-conditions in the scope stack, but we only care about the - // definition itself. Hence we need to check for uniqueness. - if !added_nodes.contains(&end_node) - && reference_paths - .iter() - .all(|other| !other.shadows(&mut self.partials, reference_path)) - { - self.results.push( - self.owner - .to_definition(end_node) - .expect("path to end in a definition node"), - ); - added_nodes.insert(end_node); - } - } - } - - pub fn all(self) -> Vec> { - self.results - } - - #[allow(clippy::mutable_key_type)] - fn resolve_parents_all( - context: Definition<'a, KT>, - ) -> HashMap, Vec>> { - let mut results = HashMap::new(); - let mut resolve_queue = Vec::new(); - resolve_queue.push(context); - while let Some(current) = resolve_queue.pop() { - let current_parents = current.resolve_parents(); - for current_parent in ¤t_parents { - if !results.contains_key(current_parent) { - resolve_queue.push(current_parent.clone()); - } - } - results.insert(current, current_parents); - } - results - } -} diff --git a/crates/metaslang/cst/generated/public_api.txt b/crates/metaslang/cst/generated/public_api.txt index 83370fcc7c..dea44ae643 100644 --- a/crates/metaslang/cst/generated/public_api.txt +++ b/crates/metaslang/cst/generated/public_api.txt @@ -51,6 +51,22 @@ impl core::iter::traits::iterator::Iterator pub type metaslang_cst::cursor::CursorIterator::Item = metaslang_cst::nodes::Edge pub fn metaslang_cst::cursor::CursorIterator::next(&mut self) -> core::option::Option pub mod metaslang_cst::kinds +pub enum metaslang_cst::kinds::NodeKind +pub metaslang_cst::kinds::NodeKind::Nonterminal(::NonterminalKind) +pub metaslang_cst::kinds::NodeKind::Terminal(::TerminalKind) +impl core::clone::Clone for metaslang_cst::kinds::NodeKind where ::NonterminalKind: core::clone::Clone, ::TerminalKind: core::clone::Clone +pub fn metaslang_cst::kinds::NodeKind::clone(&self) -> metaslang_cst::kinds::NodeKind +impl core::cmp::Eq for metaslang_cst::kinds::NodeKind where ::NonterminalKind: core::cmp::Eq, ::TerminalKind: core::cmp::Eq +impl core::cmp::PartialEq for metaslang_cst::kinds::NodeKind where ::NonterminalKind: core::cmp::PartialEq, ::TerminalKind: core::cmp::PartialEq +pub fn metaslang_cst::kinds::NodeKind::eq(&self, other: &metaslang_cst::kinds::NodeKind) -> bool +impl core::fmt::Debug for metaslang_cst::kinds::NodeKind where ::NonterminalKind: core::fmt::Debug, ::TerminalKind: core::fmt::Debug +pub fn metaslang_cst::kinds::NodeKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::Copy for metaslang_cst::kinds::NodeKind where ::NonterminalKind: core::marker::Copy, ::TerminalKind: core::marker::Copy +impl core::convert::From> for &'static str +pub fn &'static str::from(val: metaslang_cst::kinds::NodeKind) -> Self +impl core::fmt::Display for metaslang_cst::kinds::NodeKind +pub fn metaslang_cst::kinds::NodeKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for metaslang_cst::kinds::NodeKind pub trait metaslang_cst::kinds::BaseKind: core::marker::Sized + core::fmt::Debug + core::marker::Copy + core::cmp::PartialEq + core::cmp::Eq + serde::ser::Serialize pub fn metaslang_cst::kinds::BaseKind::as_static_str(&self) -> &'static str pub fn metaslang_cst::kinds::BaseKind::try_from_str(str: &str) -> core::result::Result @@ -90,7 +106,7 @@ pub fn metaslang_cst::nodes::Node::is_terminal(&self) -> bool pub fn metaslang_cst::nodes::Node::is_terminal_with_kind(&self, kind: ::TerminalKind) -> bool pub fn metaslang_cst::nodes::Node::is_terminal_with_kinds(&self, kinds: &[::TerminalKind]) -> bool pub fn metaslang_cst::nodes::Node::is_trivia(&self) -> bool -pub fn metaslang_cst::nodes::Node::kind(&self) -> metaslang_cst::nodes::NodeKind +pub fn metaslang_cst::nodes::Node::kind(&self) -> metaslang_cst::kinds::NodeKind pub fn metaslang_cst::nodes::Node::nonterminal(kind: ::NonterminalKind, children: alloc::vec::Vec>) -> Self pub fn metaslang_cst::nodes::Node::terminal(kind: ::TerminalKind, text: alloc::string::String) -> Self pub fn metaslang_cst::nodes::Node::text_len(&self) -> metaslang_cst::text_index::TextIndex @@ -109,22 +125,6 @@ pub fn metaslang_cst::nodes::Node::from(terminal: alloc::rc::Rc core::marker::StructuralPartialEq for metaslang_cst::nodes::Node impl serde::ser::Serialize for metaslang_cst::nodes::Node where T: serde::ser::Serialize + metaslang_cst::kinds::KindTypes pub fn metaslang_cst::nodes::Node::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde::ser::Serializer>::Ok, <__S as serde::ser::Serializer>::Error> where __S: serde::ser::Serializer -pub enum metaslang_cst::nodes::NodeKind -pub metaslang_cst::nodes::NodeKind::Nonterminal(::NonterminalKind) -pub metaslang_cst::nodes::NodeKind::Terminal(::TerminalKind) -impl core::clone::Clone for metaslang_cst::nodes::NodeKind where ::NonterminalKind: core::clone::Clone, ::TerminalKind: core::clone::Clone -pub fn metaslang_cst::nodes::NodeKind::clone(&self) -> metaslang_cst::nodes::NodeKind -impl core::cmp::Eq for metaslang_cst::nodes::NodeKind where ::NonterminalKind: core::cmp::Eq, ::TerminalKind: core::cmp::Eq -impl core::cmp::PartialEq for metaslang_cst::nodes::NodeKind where ::NonterminalKind: core::cmp::PartialEq, ::TerminalKind: core::cmp::PartialEq -pub fn metaslang_cst::nodes::NodeKind::eq(&self, other: &metaslang_cst::nodes::NodeKind) -> bool -impl core::fmt::Debug for metaslang_cst::nodes::NodeKind where ::NonterminalKind: core::fmt::Debug, ::TerminalKind: core::fmt::Debug -pub fn metaslang_cst::nodes::NodeKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::Copy for metaslang_cst::nodes::NodeKind where ::NonterminalKind: core::marker::Copy, ::TerminalKind: core::marker::Copy -impl core::convert::From> for &'static str -pub fn &'static str::from(val: metaslang_cst::nodes::NodeKind) -> Self -impl core::fmt::Display for metaslang_cst::nodes::NodeKind -pub fn metaslang_cst::nodes::NodeKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for metaslang_cst::nodes::NodeKind pub struct metaslang_cst::nodes::Edge pub metaslang_cst::nodes::Edge::label: core::option::Option<::EdgeLabel> pub metaslang_cst::nodes::Edge::node: metaslang_cst::nodes::Node diff --git a/crates/metaslang/cst/src/kinds.rs b/crates/metaslang/cst/src/kinds.rs index a9127773c5..efd0ab01d4 100644 --- a/crates/metaslang/cst/src/kinds.rs +++ b/crates/metaslang/cst/src/kinds.rs @@ -48,3 +48,31 @@ pub trait KindTypes: std::fmt::Debug + Clone + PartialEq { type TerminalKind: TerminalKindExtensions; type EdgeLabel: EdgeLabelExtensions; } + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NodeKind { + Nonterminal(T::NonterminalKind), + Terminal(T::TerminalKind), +} + +impl From> for &'static str { + fn from(val: NodeKind) -> Self { + match val { + NodeKind::Nonterminal(t) => t.as_static_str(), + NodeKind::Terminal(t) => t.as_static_str(), + } + } +} + +impl std::fmt::Display for NodeKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeKind::Nonterminal(t) => { + write!(f, "{}", t.as_static_str()) + } + NodeKind::Terminal(t) => { + write!(f, "{}", t.as_static_str()) + } + } + } +} diff --git a/crates/metaslang/cst/src/nodes.rs b/crates/metaslang/cst/src/nodes.rs index 8795b70a9e..90214d6a06 100644 --- a/crates/metaslang/cst/src/nodes.rs +++ b/crates/metaslang/cst/src/nodes.rs @@ -3,37 +3,9 @@ use std::rc::Rc; use serde::Serialize; use crate::cursor::{Cursor, CursorIterator}; -use crate::kinds::{BaseKind, KindTypes, TerminalKindExtensions}; +use crate::kinds::{KindTypes, NodeKind, TerminalKindExtensions}; use crate::text_index::TextIndex; -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum NodeKind { - Nonterminal(T::NonterminalKind), - Terminal(T::TerminalKind), -} - -impl From> for &'static str { - fn from(val: NodeKind) -> Self { - match val { - NodeKind::Nonterminal(t) => t.as_static_str(), - NodeKind::Terminal(t) => t.as_static_str(), - } - } -} - -impl std::fmt::Display for NodeKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NodeKind::Nonterminal(t) => { - write!(f, "{}", t.as_static_str()) - } - NodeKind::Terminal(t) => { - write!(f, "{}", t.as_static_str()) - } - } - } -} - #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct TerminalNode { pub kind: T::TerminalKind, diff --git a/crates/metaslang/cst/src/query/engine.rs b/crates/metaslang/cst/src/query/engine.rs index b758f438c5..a49681ee7c 100644 --- a/crates/metaslang/cst/src/query/engine.rs +++ b/crates/metaslang/cst/src/query/engine.rs @@ -7,8 +7,7 @@ use super::model::{ ASTNode, AlternativesASTNode, CaptureASTNode, NodeMatchASTNode, NodeSelector, OneOrMoreASTNode, OptionalASTNode, Query, SequenceASTNode, }; -use crate::kinds::{KindTypes, TerminalKindExtensions}; -use crate::nodes::NodeKind; +use crate::kinds::{KindTypes, NodeKind, TerminalKindExtensions}; use crate::query::CaptureQuantifier; impl Cursor { diff --git a/crates/metaslang/cst/src/query/model.rs b/crates/metaslang/cst/src/query/model.rs index 87da193653..c97a40b19a 100644 --- a/crates/metaslang/cst/src/query/model.rs +++ b/crates/metaslang/cst/src/query/model.rs @@ -2,8 +2,7 @@ use std::collections::BTreeMap; use std::fmt; use std::rc::Rc; -use crate::kinds::{BaseKind, KindTypes}; -use crate::nodes::NodeKind; +use crate::kinds::{BaseKind, KindTypes, NodeKind}; use crate::query::{CaptureQuantifier, QueryError}; #[derive(Clone, Debug)] diff --git a/crates/metaslang/cst/src/query/parser.rs b/crates/metaslang/cst/src/query/parser.rs index 300e985f48..e8460eb013 100644 --- a/crates/metaslang/cst/src/query/parser.rs +++ b/crates/metaslang/cst/src/query/parser.rs @@ -17,8 +17,7 @@ use super::model::{ ASTNode, AlternativesASTNode, CaptureASTNode, NodeMatchASTNode, NodeSelector, OneOrMoreASTNode, OptionalASTNode, SequenceASTNode, }; -use crate::kinds::{BaseKind, KindTypes, TerminalKindExtensions}; -use crate::nodes::NodeKind; +use crate::kinds::{BaseKind, KindTypes, NodeKind, TerminalKindExtensions}; use crate::text_index::TextIndex; // ---------------------------------------------------------------------------- diff --git a/crates/solidity/inputs/language/bindings/rules.msgb b/crates/solidity/inputs/language/bindings/rules.msgb index 9139aa190f..eeb5a00bc1 100644 --- a/crates/solidity/inputs/language/bindings/rules.msgb +++ b/crates/solidity/inputs/language/bindings/rules.msgb @@ -71,6 +71,7 @@ inherit .star_extension ; This is used to indicate the resolution algorithm that here's where it ; should inject any possible extension scopes attr (@source_unit.lexical_scope) extension_hook + attr (@source_unit.lexical_scope) is_endpoint ; Provide a default star extension sink node that gets inherited. This is ; connected to from expressions, and those can potentially happen anywhere. @@ -342,6 +343,7 @@ inherit .star_extension attr (@contract.def) definiens_node = @contract ; The .extensions node is where `using` directives will hook the definitions attr (@contract.def) extension_scope = @contract.extensions + attr (@contract.extensions) is_exported edge @contract.lexical_scope -> @contract.instance attr (@contract.lexical_scope -> @contract.instance) precedence = 1 @@ -408,6 +410,7 @@ inherit .star_extension attr (push_name) push_symbol = (source-text @name) node hook attr (hook) extension_hook + attr (hook) is_endpoint edge call -> push_typeof edge push_typeof -> push_name @@ -654,6 +657,7 @@ inherit .star_extension edge push_typeof -> push_name node hook attr (hook) extension_hook + attr (hook) is_endpoint edge push_name -> hook ; edge push_name -> JUMP_TO_SCOPE_NODE @@ -730,6 +734,7 @@ inherit .star_extension attr (@library.def) definiens_node = @library ; The .extensions node is where `using` directives will hook the definitions attr (@library.def) extension_scope = @library.extensions + attr (@library.extensions) is_exported edge @library.lexical_scope -> @library.ns diff --git a/crates/solidity/outputs/cargo/crate/generated/public_api.txt b/crates/solidity/outputs/cargo/crate/generated/public_api.txt index 48a9440e5c..ab2e8d7acd 100644 --- a/crates/solidity/outputs/cargo/crate/generated/public_api.txt +++ b/crates/solidity/outputs/cargo/crate/generated/public_api.txt @@ -16,13 +16,14 @@ impl core::fmt::Debug for slang_solidity::bindings::BindingGraphInitializationEr pub fn slang_solidity::bindings::BindingGraphInitializationError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::fmt::Display for slang_solidity::bindings::BindingGraphInitializationError pub fn slang_solidity::bindings::BindingGraphInitializationError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -pub fn slang_solidity::bindings::create_with_resolver(version: semver::Version, resolver: alloc::rc::Rc>) -> core::result::Result +pub fn slang_solidity::bindings::create_with_resolver(version: semver::Version, resolver: alloc::rc::Rc>) -> core::result::Result pub fn slang_solidity::bindings::get_binding_rules() -> &'static str -pub type slang_solidity::bindings::BindingGraph = metaslang_bindings::BindingGraph -pub type slang_solidity::bindings::BindingLocation = metaslang_bindings::location::BindingLocation -pub type slang_solidity::bindings::Definition<'a> = metaslang_bindings::Definition<'a, slang_solidity::cst::KindTypes> -pub type slang_solidity::bindings::Reference<'a> = metaslang_bindings::Reference<'a, slang_solidity::cst::KindTypes> -pub type slang_solidity::bindings::UserFileLocation = metaslang_bindings::location::UserFileLocation +pub type slang_solidity::bindings::BindingGraph = metaslang_bindings::graph::BindingGraph +pub type slang_solidity::bindings::BindingGraphBuilder = metaslang_bindings::builder::BindingGraphBuilder +pub type slang_solidity::bindings::BindingLocation = metaslang_bindings::graph::location::BindingLocation +pub type slang_solidity::bindings::Definition = metaslang_bindings::graph::definition::Definition +pub type slang_solidity::bindings::Reference = metaslang_bindings::graph::reference::Reference +pub type slang_solidity::bindings::UserFileLocation = metaslang_bindings::graph::location::UserFileLocation pub mod slang_solidity::compilation pub struct slang_solidity::compilation::AddFileResponse pub slang_solidity::compilation::AddFileResponse::import_paths: alloc::vec::Vec @@ -47,6 +48,7 @@ pub fn slang_solidity::compilation::InternalCompilationBuilder::create(language_ pub fn slang_solidity::compilation::InternalCompilationBuilder::resolve_import(&mut self, source_file_id: &str, import_path: &slang_solidity::cst::Cursor, destination_file_id: alloc::string::String) -> core::result::Result<(), ResolveImportError> pub mod slang_solidity::cst pub use slang_solidity::cst::EdgeLabelExtensions +pub use slang_solidity::cst::NodeKind pub use slang_solidity::cst::NonterminalKindExtensions pub use slang_solidity::cst::QueryError pub use slang_solidity::cst::TerminalKindExtensions diff --git a/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs b/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs index b3ec4aa052..90b69796da 100644 --- a/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs +++ b/crates/solidity/outputs/cargo/crate/src/extensions/bindings/mod.rs @@ -4,12 +4,12 @@ use metaslang_cst::text_index::TextIndex; use semver::Version; use crate::bindings::built_ins::get_built_ins_contents; -use crate::bindings::BindingGraph; +use crate::bindings::BindingGraphBuilder; use crate::cst::{Edge, Node, NonterminalNode, TerminalKind, TerminalNode}; use crate::parser::{Parser, ParserInitializationError}; pub fn add_built_ins( - binding_graph: &mut BindingGraph, + builder: &mut BindingGraphBuilder, version: Version, ) -> Result<(), ParserInitializationError> { let source = get_built_ins_contents(&version); @@ -18,7 +18,7 @@ pub fn add_built_ins( let built_ins_cursor = transform(parse_output.tree()).cursor_with_offset(TextIndex::ZERO); - binding_graph.add_system_file("built_ins.sol", built_ins_cursor); + builder.add_system_file("built_ins.sol", built_ins_cursor); Ok(()) } diff --git a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs index 7712ef9336..af5af31975 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/bindings/generated/binding_rules.rs @@ -76,6 +76,7 @@ inherit .star_extension ; This is used to indicate the resolution algorithm that here's where it ; should inject any possible extension scopes attr (@source_unit.lexical_scope) extension_hook + attr (@source_unit.lexical_scope) is_endpoint ; Provide a default star extension sink node that gets inherited. This is ; connected to from expressions, and those can potentially happen anywhere. @@ -347,6 +348,7 @@ inherit .star_extension attr (@contract.def) definiens_node = @contract ; The .extensions node is where `using` directives will hook the definitions attr (@contract.def) extension_scope = @contract.extensions + attr (@contract.extensions) is_exported edge @contract.lexical_scope -> @contract.instance attr (@contract.lexical_scope -> @contract.instance) precedence = 1 @@ -413,6 +415,7 @@ inherit .star_extension attr (push_name) push_symbol = (source-text @name) node hook attr (hook) extension_hook + attr (hook) is_endpoint edge call -> push_typeof edge push_typeof -> push_name @@ -659,6 +662,7 @@ inherit .star_extension edge push_typeof -> push_name node hook attr (hook) extension_hook + attr (hook) is_endpoint edge push_name -> hook ; edge push_name -> JUMP_TO_SCOPE_NODE @@ -735,6 +739,7 @@ inherit .star_extension attr (@library.def) definiens_node = @library ; The .extensions node is where `using` directives will hook the definitions attr (@library.def) extension_scope = @library.extensions + attr (@library.extensions) is_exported edge @library.lexical_scope -> @library.ns diff --git a/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs b/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs index 7803930461..1bca6fa67e 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/bindings/mod.rs @@ -12,9 +12,10 @@ use semver::Version; use crate::cst::KindTypes; +pub type BindingGraphBuilder = metaslang_bindings::BindingGraphBuilder; pub type BindingGraph = metaslang_bindings::BindingGraph; -pub type Definition<'a> = metaslang_bindings::Definition<'a, KindTypes>; -pub type Reference<'a> = metaslang_bindings::Reference<'a, KindTypes>; +pub type Definition = metaslang_bindings::Definition; +pub type Reference = metaslang_bindings::Reference; pub type BindingLocation = metaslang_bindings::BindingLocation; pub type UserFileLocation = metaslang_bindings::UserFileLocation; @@ -31,8 +32,8 @@ pub enum BindingGraphInitializationError { pub fn create_with_resolver( version: Version, resolver: Rc>, -) -> Result { - let mut binding_graph = BindingGraph::create( +) -> Result { + let mut binding_graph = BindingGraphBuilder::create( version.clone(), binding_rules::BINDING_RULES_SOURCE, resolver, diff --git a/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs b/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs index a15100e4df..4e92751737 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/compilation/unit.rs @@ -45,14 +45,14 @@ impl CompilationUnit { files: self.files.clone(), }; - let mut binding_graph = + let mut builder = create_with_resolver(self.language_version.clone(), Rc::new(resolver))?; for (id, file) in &self.files { - binding_graph.add_user_file(id, file.create_tree_cursor()); + builder.add_user_file(id, file.create_tree_cursor()); } - Ok(Rc::new(binding_graph)) + Ok(builder.build()) }) } } diff --git a/crates/solidity/outputs/cargo/crate/src/generated/cst/mod.rs b/crates/solidity/outputs/cargo/crate/src/generated/cst/mod.rs index f81de2d58a..a4dd3ebbd0 100644 --- a/crates/solidity/outputs/cargo/crate/src/generated/cst/mod.rs +++ b/crates/solidity/outputs/cargo/crate/src/generated/cst/mod.rs @@ -12,7 +12,7 @@ mod terminal_kind; pub use edge_label::EdgeLabel; pub(crate) use lexical_context::{IsLexicalContext, LexicalContext, LexicalContextType}; pub use metaslang_cst::kinds::{ - EdgeLabelExtensions, NonterminalKindExtensions, TerminalKindExtensions, + EdgeLabelExtensions, NodeKind, NonterminalKindExtensions, TerminalKindExtensions, }; pub use nonterminal_kind::NonterminalKind; pub use terminal_kind::TerminalKind; diff --git a/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs b/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs index ae170b2bdf..92711989d2 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_output/renderer.rs @@ -1,10 +1,11 @@ use std::iter::once; use std::ops::Range; +use std::rc::Rc; use anyhow::Result; use ariadne::{Color, Config, FnCache, Label, Report, ReportBuilder, ReportKind, Source}; use slang_solidity::bindings::{BindingGraph, Definition, Reference}; -use slang_solidity::cst::{NonterminalKind, Query}; +use slang_solidity::cst::{NonterminalKind, TerminalKind}; use slang_solidity::diagnostic; use super::runner::ParsedPart; @@ -12,7 +13,7 @@ use super::runner::ParsedPart; type ReportSpan<'a> = (&'a str, Range); pub(crate) fn render_bindings( - binding_graph: &BindingGraph, + binding_graph: &Rc, parsed_parts: &[ParsedPart<'_>], ) -> Result<(String, bool)> { let mut buffer: Vec = Vec::new(); @@ -70,8 +71,8 @@ fn write_part_report<'a>( // We collect all non built-in definitions in a vector to be able to identify // them by a numeric index -fn collect_all_definitions(binding_graph: &BindingGraph) -> Vec> { - let mut definitions: Vec> = Vec::new(); +fn collect_all_definitions(binding_graph: &Rc) -> Vec { + let mut definitions: Vec = Vec::new(); for definition in binding_graph.all_definitions() { if definition.get_file().is_user() { definitions.push(definition); @@ -93,7 +94,7 @@ impl ParsedPart<'_> { fn check_bindings_coverage<'a>( part: &'a ParsedPart<'a>, - binding_graph: &'a BindingGraph, + binding_graph: &Rc, ) -> (Report<'a, ReportSpan<'a>>, bool) { let mut all_identifiers_bound = true; let mut builder: ReportBuilder<'_, ReportSpan<'_>> = Report::build( @@ -103,24 +104,27 @@ fn check_bindings_coverage<'a>( ) .with_config(Config::default().with_color(false)); - let query = Query::parse("@identifier ([Identifier] | [YulIdentifier])").unwrap(); - let tree_cursor = part.parse_output.create_tree_cursor(); - for result in tree_cursor.query(vec![query]) { - let identifier_cursor = result.captures.get("identifier").unwrap().first().unwrap(); - let parent = { - let mut parent_cursor = identifier_cursor.spawn(); - parent_cursor.go_to_parent(); - parent_cursor.node() - }; - if parent.is_nonterminal_with_kind(NonterminalKind::ExperimentalFeature) { - // ignore identifiers in `pragma experimental` directives + let mut cursor = part.parse_output.create_tree_cursor(); + + while cursor + .go_to_next_terminal_with_kinds(&[TerminalKind::Identifier, TerminalKind::YulIdentifier]) + { + if matches!( + cursor.ancestors().next(), + Some(ancestor) + // ignore identifiers in `pragma experimental` directives, as they are unbound feature names: + if ancestor.kind == NonterminalKind::ExperimentalFeature || + // TODO(#1213): unbound named parameters in mapping types + ancestor.kind == NonterminalKind::MappingKey + ) { continue; } - if binding_graph.definition_at(identifier_cursor).is_none() - && binding_graph.reference_at(identifier_cursor).is_none() + + if binding_graph.definition_at(&cursor).is_none() + && binding_graph.reference_at(&cursor).is_none() { let range = { - let range = identifier_cursor.text_range(); + let range = cursor.text_range(); let start = part.contents[..range.start.utf8].chars().count(); let end = part.contents[..range.end.utf8].chars().count(); start..end @@ -138,8 +142,8 @@ fn check_bindings_coverage<'a>( fn build_report_for_part<'a>( part: &'a ParsedPart<'a>, - all_definitions: &'a [Definition<'a>], - part_references: impl Iterator> + 'a, + all_definitions: &'a [Definition], + part_references: impl Iterator + 'a, ) -> (Report<'a, ReportSpan<'a>>, bool) { let mut builder: ReportBuilder<'_, ReportSpan<'_>> = Report::build( ReportKind::Custom("References and definitions", Color::Unset), @@ -218,7 +222,7 @@ fn build_report_for_part<'a>( fn build_definiens_report<'a>( part: &'a ParsedPart<'a>, - all_definitions: &'a [Definition<'a>], + all_definitions: &'a [Definition], ) -> Report<'a, ReportSpan<'a>> { let mut builder: ReportBuilder<'_, ReportSpan<'_>> = Report::build(ReportKind::Custom("Definiens", Color::Unset), part.path, 0) diff --git a/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs b/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs index 218e694e4f..aeee8760b3 100644 --- a/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs +++ b/crates/solidity/outputs/cargo/tests/src/bindings_output/runner.rs @@ -44,7 +44,7 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { for version in &VERSION_BREAKS { let parser = Parser::create(version.clone())?; - let mut binding_graph = + let mut builder = bindings::create_with_resolver(version.clone(), Rc::new(TestsPathResolver {}))?; let mut parsed_parts: Vec> = Vec::new(); @@ -55,8 +55,8 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { } in &multi_part.parts { let parse_output = parser.parse(Parser::ROOT_KIND, contents); - let graph = binding_graph - .add_user_file_returning_graph(path, parse_output.create_tree_cursor()); + let graph = + builder.add_user_file_returning_graph(path, parse_output.create_tree_cursor()); parsed_parts.push(ParsedPart { path, contents, @@ -65,6 +65,7 @@ pub fn run(group_name: &str, test_name: &str) -> Result<()> { }); } + let binding_graph = builder.build(); let (bindings_output, all_resolved) = render_bindings(&binding_graph, &parsed_parts)?; let parse_success = parsed_parts.iter().all(|part| part.parse_output.is_valid()); diff --git a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs index 6163cf964f..044ca3d6eb 100644 --- a/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs +++ b/crates/solidity/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs @@ -14,52 +14,8 @@ mod ffi { mod rust { pub use crate::rust_crate::bindings::{ - BindingGraph, BindingLocation, BuiltInLocation, UserFileLocation, + BindingGraph, BindingLocation, BuiltInLocation, Definition, Reference, UserFileLocation, }; - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Definition { - pub id: usize, - pub name_location: BindingLocation, - pub definiens_location: BindingLocation, - } - - impl From> for Definition { - fn from(definition: crate::rust_crate::bindings::Definition<'_>) -> Self { - Self { - id: definition.id(), - name_location: definition.name_location(), - definiens_location: definition.definiens_location(), - } - } - } - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Reference { - pub id: usize, - pub location: BindingLocation, - pub definitions: Vec, - } - - impl From> for Reference { - fn from(reference: crate::rust_crate::bindings::Reference<'_>) -> Self { - Self { - id: reference.id(), - location: reference.location(), - definitions: reference - .definitions() - .into_iter() - .map(Into::into) - .collect(), - } - } - } } impl ffi::Guest for crate::wasm_crate::World { @@ -102,15 +58,15 @@ define_rc_wrapper! { BindingGraph { define_wrapper! { Definition { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn name_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().name_location.clone()._into_ffi() + self._borrow_ffi().name_location()._into_ffi() } fn definiens_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().definiens_location.clone()._into_ffi() + self._borrow_ffi().definiens_location()._into_ffi() } } } @@ -122,15 +78,15 @@ define_wrapper! { Definition { define_wrapper! { Reference { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn location(&self) -> ffi::BindingLocation { - self._borrow_ffi().location.clone()._into_ffi() + self._borrow_ffi().location().clone()._into_ffi() } fn definitions(&self) -> Vec { - self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + self._borrow_ffi().definitions().iter().cloned().map(IntoFFI::_into_ffi).collect() } } } diff --git a/crates/solidity/testing/perf/benches/iai/main.rs b/crates/solidity/testing/perf/benches/iai/main.rs index e9216a4f29..f36600ff96 100644 --- a/crates/solidity/testing/perf/benches/iai/main.rs +++ b/crates/solidity/testing/perf/benches/iai/main.rs @@ -7,23 +7,14 @@ use iai_callgrind::{ library_benchmark, library_benchmark_group, main, Direction, FlamegraphConfig, LibraryBenchmarkConfig, Tool, ValgrindTool, }; -use slang_solidity::bindings::BindingGraph; use solidity_testing_perf::dataset::SourceFile; -use solidity_testing_perf::tests::definitions::Dependencies; +use solidity_testing_perf::tests::bindings_resolve::BuiltBindingGraph; use solidity_testing_perf::tests::parser::ParsedFile; mod __dependencies_used_in_lib__ { - use {infra_utils as _, metaslang_bindings as _, semver as _}; + use {infra_utils as _, metaslang_bindings as _, semver as _, slang_solidity as _}; } -macro_rules! define_benchmark { - ($name:ident) => { - #[library_benchmark] - fn $name() { - black_box(solidity_testing_perf::tests::$name::run()); - } - }; -} macro_rules! define_payload_benchmark { ($name:ident, $payload:ty) => { #[library_benchmark(setup = solidity_testing_perf::tests::$name::setup)] @@ -43,15 +34,14 @@ macro_rules! define_payload_benchmark { define_payload_benchmark!(parser, Vec); define_payload_benchmark!(cursor, Vec); define_payload_benchmark!(query, Vec); -define_benchmark!(init_bindings); -define_payload_benchmark!(definitions, Dependencies); -define_payload_benchmark!(references, BindingGraph); +define_payload_benchmark!(bindings_build, Vec); +define_payload_benchmark!(bindings_resolve, BuiltBindingGraph); library_benchmark_group!( name = benchmarks; // __SLANG_INFRA_BENCHMARKS_LIST__ (keep in sync) - benchmarks = parser, cursor, query, init_bindings, definitions, references + benchmarks = parser, cursor, query, bindings_build, bindings_resolve, ); main!( diff --git a/crates/solidity/testing/perf/src/lib.rs b/crates/solidity/testing/perf/src/lib.rs index 0b25500b67..8b58eabad1 100644 --- a/crates/solidity/testing/perf/src/lib.rs +++ b/crates/solidity/testing/perf/src/lib.rs @@ -11,14 +11,6 @@ mod __dependencies_used_in_benches__ { #[cfg(test)] mod unit_tests { - macro_rules! define_test { - ($name:ident) => { - #[test] - fn $name() { - crate::tests::$name::run(); - } - }; - } macro_rules! define_payload_test { ($name:ident) => { #[test] @@ -35,7 +27,6 @@ mod unit_tests { define_payload_test!(parser); define_payload_test!(cursor); define_payload_test!(query); - define_test!(init_bindings); - define_payload_test!(definitions); - define_payload_test!(references); + define_payload_test!(bindings_build); + define_payload_test!(bindings_resolve); } diff --git a/crates/solidity/testing/perf/src/tests/bindings_build.rs b/crates/solidity/testing/perf/src/tests/bindings_build.rs new file mode 100644 index 0000000000..35a95a963f --- /dev/null +++ b/crates/solidity/testing/perf/src/tests/bindings_build.rs @@ -0,0 +1,40 @@ +use std::rc::Rc; + +use infra_utils::paths::PathExtensions; +use metaslang_bindings::PathResolver; +use slang_solidity::bindings::{create_with_resolver, BindingGraph}; +use slang_solidity::cst::{Cursor, KindTypes}; + +use crate::dataset::SOLC_VERSION; +use crate::tests::parser::ParsedFile; + +pub fn setup() -> Vec { + super::parser::run(super::parser::setup()) +} + +pub fn run(files: Vec) -> Rc { + let mut bindings_graph_builder = + create_with_resolver(SOLC_VERSION, Rc::new(Resolver {})).unwrap(); + + for file in files { + bindings_graph_builder.add_user_file( + file.path.unwrap_str(), + file.parse_output.create_tree_cursor(), + ); + } + + bindings_graph_builder.build() +} + +struct Resolver; + +impl PathResolver for Resolver { + fn resolve_path(&self, _context_path: &str, path_to_resolve: &Cursor) -> Option { + let path = path_to_resolve.node().unparse(); + let path = path + .strip_prefix(|c| matches!(c, '"' | '\''))? + .strip_suffix(|c| matches!(c, '"' | '\''))?; + + Some(path.to_owned()) + } +} diff --git a/crates/solidity/testing/perf/src/tests/bindings_resolve.rs b/crates/solidity/testing/perf/src/tests/bindings_resolve.rs new file mode 100644 index 0000000000..ec84c6c1dd --- /dev/null +++ b/crates/solidity/testing/perf/src/tests/bindings_resolve.rs @@ -0,0 +1,61 @@ +use std::rc::Rc; + +use infra_utils::paths::PathExtensions; +use slang_solidity::bindings::BindingGraph; +use slang_solidity::cst::{NonterminalKind, TerminalKind}; + +use crate::tests::parser::ParsedFile; + +pub struct BuiltBindingGraph { + files: Vec, + binding_graph: Rc, +} + +pub fn setup() -> BuiltBindingGraph { + let files = super::parser::run(super::parser::setup()); + let binding_graph = super::bindings_build::run(files.clone()); + + BuiltBindingGraph { + files, + binding_graph, + } +} + +pub fn run(dependencies: BuiltBindingGraph) { + let BuiltBindingGraph { + files, + binding_graph, + } = dependencies; + + for file in files { + let mut cursor = file.parse_output.create_tree_cursor(); + + while cursor.go_to_next_terminal_with_kinds(&[ + TerminalKind::Identifier, + TerminalKind::YulIdentifier, + ]) { + if matches!( + cursor.ancestors().next(), + Some(ancestor) + // ignore identifiers in `pragma experimental` directives, as they are unbound feature names: + if ancestor.kind == NonterminalKind::ExperimentalFeature || + // TODO(#1213): unbound named parameters in mapping types + ancestor.kind == NonterminalKind::MappingKey + ) { + continue; + } + + if binding_graph.definition_at(&cursor).is_none() + && binding_graph.reference_at(&cursor).is_none() + { + panic!( + "Unbound identifier: '{value}' in '{file_path}:{line}:{column}'.", + value = cursor.node().unparse(), + file_path = file.path.unwrap_str(), + line = cursor.text_range().start.line + 1, + column = cursor.text_range().start.column + 1, + ); + } + } + } +} diff --git a/crates/solidity/testing/perf/src/tests/definitions.rs b/crates/solidity/testing/perf/src/tests/definitions.rs deleted file mode 100644 index a41134c328..0000000000 --- a/crates/solidity/testing/perf/src/tests/definitions.rs +++ /dev/null @@ -1,43 +0,0 @@ -use slang_solidity::bindings::BindingGraph; - -use crate::tests::parser::ParsedFile; - -pub struct Dependencies { - pub binding_graph: BindingGraph, - pub files: Vec, -} - -pub fn setup() -> Dependencies { - let binding_graph = super::init_bindings::run(); - let files = super::parser::run(super::parser::setup()); - - Dependencies { - binding_graph, - files, - } -} - -pub fn run(dependencies: Dependencies) -> BindingGraph { - let mut definition_count = 0_usize; - let Dependencies { - mut binding_graph, - files, - } = dependencies; - - for ParsedFile { - path, - contents: _, - parse_output, - } in &files - { - binding_graph.add_user_file(path.to_str().unwrap(), parse_output.create_tree_cursor()); - definition_count += binding_graph - .all_definitions() - .filter(|definition| definition.get_file().is_user()) - .count(); - } - - assert_eq!(definition_count, 2322, "Failed to fetch all definitions"); - - binding_graph -} diff --git a/crates/solidity/testing/perf/src/tests/init_bindings.rs b/crates/solidity/testing/perf/src/tests/init_bindings.rs deleted file mode 100644 index f46ffa186c..0000000000 --- a/crates/solidity/testing/perf/src/tests/init_bindings.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::rc::Rc; - -use metaslang_bindings::PathResolver; -use slang_solidity::bindings::{create_with_resolver, BindingGraph}; -use slang_solidity::cst::{Cursor, KindTypes}; - -use crate::dataset::SOLC_VERSION; - -pub fn run() -> BindingGraph { - create_with_resolver(SOLC_VERSION, Rc::new(NoOpResolver {})).unwrap() -} - -struct NoOpResolver; - -impl PathResolver for NoOpResolver { - fn resolve_path(&self, _context_path: &str, path_to_resolve: &Cursor) -> Option { - let path = path_to_resolve.node().unparse(); - let path = path - .strip_prefix(|c| matches!(c, '"' | '\''))? - .strip_suffix(|c| matches!(c, '"' | '\''))?; - - Some(path.to_owned()) - } -} diff --git a/crates/solidity/testing/perf/src/tests/mod.rs b/crates/solidity/testing/perf/src/tests/mod.rs index d593675b64..c9a1088461 100644 --- a/crates/solidity/testing/perf/src/tests/mod.rs +++ b/crates/solidity/testing/perf/src/tests/mod.rs @@ -1,6 +1,5 @@ +pub mod bindings_build; +pub mod bindings_resolve; pub mod cursor; -pub mod definitions; -pub mod init_bindings; pub mod parser; pub mod query; -pub mod references; diff --git a/crates/solidity/testing/perf/src/tests/parser.rs b/crates/solidity/testing/perf/src/tests/parser.rs index 6892a0429e..1eecbec387 100644 --- a/crates/solidity/testing/perf/src/tests/parser.rs +++ b/crates/solidity/testing/perf/src/tests/parser.rs @@ -4,6 +4,7 @@ use slang_solidity::parser::{ParseOutput, Parser}; use crate::dataset::{SourceFile, SOLC_VERSION}; +#[derive(Clone)] pub struct ParsedFile { pub path: PathBuf, diff --git a/crates/solidity/testing/perf/src/tests/references.rs b/crates/solidity/testing/perf/src/tests/references.rs deleted file mode 100644 index 9ba38d9f31..0000000000 --- a/crates/solidity/testing/perf/src/tests/references.rs +++ /dev/null @@ -1,32 +0,0 @@ -use slang_solidity::bindings::BindingGraph; - -pub fn setup() -> BindingGraph { - let dependencies = super::definitions::setup(); - - super::definitions::run(dependencies) -} - -pub fn run(binding_graph: BindingGraph) { - let mut reference_count = 0_usize; - let mut resolved_references = 0_usize; - - for reference in binding_graph.all_references() { - if reference.get_file().is_system() { - // skip built-ins - continue; - } - reference_count += 1; - - let definitions = reference.definitions(); - if !definitions.is_empty() { - resolved_references += 1; - } - } - - assert_eq!(reference_count, 1652, "Failed to fetch all references"); - - assert_eq!( - resolved_references, 1490, - "Failed to resolve all references" - ); -} diff --git a/crates/solidity/testing/sanctuary/src/tests.rs b/crates/solidity/testing/sanctuary/src/tests.rs index c02c60b79f..87a59f020f 100644 --- a/crates/solidity/testing/sanctuary/src/tests.rs +++ b/crates/solidity/testing/sanctuary/src/tests.rs @@ -7,9 +7,8 @@ use infra_utils::paths::PathExtensions; use itertools::Itertools; use metaslang_bindings::PathResolver; use semver::Version; -use slang_solidity::bindings; -use slang_solidity::bindings::BindingGraph; -use slang_solidity::cst::{Cursor, KindTypes, NonterminalKind, Query, TextRange}; +use slang_solidity::bindings::{self, BindingGraph}; +use slang_solidity::cst::{Cursor, KindTypes, NonterminalKind, TerminalKind, TextRange}; use slang_solidity::diagnostic::{Diagnostic, Severity}; use slang_solidity::parser::{ParseOutput, Parser}; use slang_solidity::utils::LanguageFacts; @@ -202,27 +201,31 @@ fn run_bindings_check( } } - // Check that all `Identifier` and `YulIdentifier` nodes are bound to either - // a definition or a reference - let query = Query::parse("@identifier ([Identifier] | [YulIdentifier])").unwrap(); - let tree_cursor = output.create_tree_cursor(); - for result in tree_cursor.query(vec![query]) { - let identifier_cursor = result.captures.get("identifier").unwrap().first().unwrap(); - let parent = { - let mut parent_cursor = identifier_cursor.spawn(); - parent_cursor.go_to_parent(); - parent_cursor.node() - }; - if parent.is_nonterminal_with_kind(NonterminalKind::ExperimentalFeature) { - // ignore identifiers in `pragma experimental` directives + // Check that all `Identifier` and `YulIdentifier` nodes are bound to either a definition or a reference: + + let mut cursor = output.create_tree_cursor(); + + while cursor + .go_to_next_terminal_with_kinds(&[TerminalKind::Identifier, TerminalKind::YulIdentifier]) + { + if matches!( + cursor.ancestors().next(), + Some(ancestor) + // ignore identifiers in `pragma experimental` directives, as they are unbound feature names: + if ancestor.kind == NonterminalKind::ExperimentalFeature || + // TODO(#1213): unbound named parameters in mapping types + ancestor.kind == NonterminalKind::MappingKey + ) { continue; } - if binding_graph.definition_at(identifier_cursor).is_none() - && binding_graph.reference_at(identifier_cursor).is_none() + + if binding_graph.definition_at(&cursor).is_none() + && binding_graph.reference_at(&cursor).is_none() { - errors.push(BindingError::UnboundIdentifier(identifier_cursor.clone())); + errors.push(BindingError::UnboundIdentifier(cursor.clone())); } } + Ok(errors) } @@ -230,17 +233,17 @@ fn create_bindings( version: &Version, source_id: &str, output: &ParseOutput, -) -> Result { - let mut binding_graph = bindings::create_with_resolver( +) -> Result> { + let mut builder = bindings::create_with_resolver( version.clone(), Rc::new(SingleFileResolver { source_id: source_id.into(), }), )?; - binding_graph.add_user_file(source_id, output.create_tree_cursor()); + builder.add_user_file(source_id, output.create_tree_cursor()); - Ok(binding_graph) + Ok(builder.build()) } /// The `PathResolver` that always resolves to the given `source_id`. diff --git a/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs b/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs index eee8b7880f..c3f95c7400 100644 --- a/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs +++ b/crates/testlang/outputs/cargo/crate/src/extensions/bindings/mod.rs @@ -1,11 +1,11 @@ use semver::Version; -use crate::bindings::BindingGraph; +use crate::bindings::BindingGraphBuilder; use crate::parser::ParserInitializationError; #[allow(clippy::needless_pass_by_value)] pub fn add_built_ins( - _binding_graph: &mut BindingGraph, + _binding_graph_builder: &mut BindingGraphBuilder, _version: Version, ) -> Result<(), ParserInitializationError> { unreachable!("Built-ins are Solidity-specific") diff --git a/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs b/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs index 7803930461..1bca6fa67e 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/bindings/mod.rs @@ -12,9 +12,10 @@ use semver::Version; use crate::cst::KindTypes; +pub type BindingGraphBuilder = metaslang_bindings::BindingGraphBuilder; pub type BindingGraph = metaslang_bindings::BindingGraph; -pub type Definition<'a> = metaslang_bindings::Definition<'a, KindTypes>; -pub type Reference<'a> = metaslang_bindings::Reference<'a, KindTypes>; +pub type Definition = metaslang_bindings::Definition; +pub type Reference = metaslang_bindings::Reference; pub type BindingLocation = metaslang_bindings::BindingLocation; pub type UserFileLocation = metaslang_bindings::UserFileLocation; @@ -31,8 +32,8 @@ pub enum BindingGraphInitializationError { pub fn create_with_resolver( version: Version, resolver: Rc>, -) -> Result { - let mut binding_graph = BindingGraph::create( +) -> Result { + let mut binding_graph = BindingGraphBuilder::create( version.clone(), binding_rules::BINDING_RULES_SOURCE, resolver, diff --git a/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs b/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs index a15100e4df..4e92751737 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/compilation/unit.rs @@ -45,14 +45,14 @@ impl CompilationUnit { files: self.files.clone(), }; - let mut binding_graph = + let mut builder = create_with_resolver(self.language_version.clone(), Rc::new(resolver))?; for (id, file) in &self.files { - binding_graph.add_user_file(id, file.create_tree_cursor()); + builder.add_user_file(id, file.create_tree_cursor()); } - Ok(Rc::new(binding_graph)) + Ok(builder.build()) }) } } diff --git a/crates/testlang/outputs/cargo/crate/src/generated/cst/mod.rs b/crates/testlang/outputs/cargo/crate/src/generated/cst/mod.rs index f81de2d58a..a4dd3ebbd0 100644 --- a/crates/testlang/outputs/cargo/crate/src/generated/cst/mod.rs +++ b/crates/testlang/outputs/cargo/crate/src/generated/cst/mod.rs @@ -12,7 +12,7 @@ mod terminal_kind; pub use edge_label::EdgeLabel; pub(crate) use lexical_context::{IsLexicalContext, LexicalContext, LexicalContextType}; pub use metaslang_cst::kinds::{ - EdgeLabelExtensions, NonterminalKindExtensions, TerminalKindExtensions, + EdgeLabelExtensions, NodeKind, NonterminalKindExtensions, TerminalKindExtensions, }; pub use nonterminal_kind::NonterminalKind; pub use terminal_kind::TerminalKind; diff --git a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs index 6163cf964f..044ca3d6eb 100644 --- a/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs +++ b/crates/testlang/outputs/cargo/wasm/src/generated/wrappers/bindings/mod.rs @@ -14,52 +14,8 @@ mod ffi { mod rust { pub use crate::rust_crate::bindings::{ - BindingGraph, BindingLocation, BuiltInLocation, UserFileLocation, + BindingGraph, BindingLocation, BuiltInLocation, Definition, Reference, UserFileLocation, }; - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Definition { - pub id: usize, - pub name_location: BindingLocation, - pub definiens_location: BindingLocation, - } - - impl From> for Definition { - fn from(definition: crate::rust_crate::bindings::Definition<'_>) -> Self { - Self { - id: definition.id(), - name_location: definition.name_location(), - definiens_location: definition.definiens_location(), - } - } - } - - /// TODO: This is a work-around for the fact that `metaslang_bindings` internals (handles, locators, etc...) are exposed. - /// We should clean this when we finally publish `__experimental_bindings_api`. - /// That means removing the types below, and using the original types instead. - #[derive(Debug, Clone)] - pub struct Reference { - pub id: usize, - pub location: BindingLocation, - pub definitions: Vec, - } - - impl From> for Reference { - fn from(reference: crate::rust_crate::bindings::Reference<'_>) -> Self { - Self { - id: reference.id(), - location: reference.location(), - definitions: reference - .definitions() - .into_iter() - .map(Into::into) - .collect(), - } - } - } } impl ffi::Guest for crate::wasm_crate::World { @@ -102,15 +58,15 @@ define_rc_wrapper! { BindingGraph { define_wrapper! { Definition { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn name_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().name_location.clone()._into_ffi() + self._borrow_ffi().name_location()._into_ffi() } fn definiens_location(&self) -> ffi::BindingLocation { - self._borrow_ffi().definiens_location.clone()._into_ffi() + self._borrow_ffi().definiens_location()._into_ffi() } } } @@ -122,15 +78,15 @@ define_wrapper! { Definition { define_wrapper! { Reference { fn id(&self) -> u32 { - self._borrow_ffi().id.try_into().unwrap() + self._borrow_ffi().id().try_into().unwrap() } fn location(&self) -> ffi::BindingLocation { - self._borrow_ffi().location.clone()._into_ffi() + self._borrow_ffi().location().clone()._into_ffi() } fn definitions(&self) -> Vec { - self._borrow_ffi().definitions.iter().cloned().map(IntoFFI::_into_ffi).collect() + self._borrow_ffi().definitions().iter().cloned().map(IntoFFI::_into_ffi).collect() } } }